commit e519cac94ed2217c2baa23818da899c76cc8f7cb Author: database-mysql Date: Wed Feb 11 23:32:56 2026 +0800 pull file diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..ecaad82 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "1.0.3", + "commands": [ + "csharpier" + ] + }, + "husky": { + "version": "0.7.2", + "commands": [ + "husky" + ] + } + } +} \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d38e442 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +# .dockerignore + +.git +.gitattributes +.gitignore +.github + +.editorconfig + +README.md + +Dockerfile + +[b|B]in +[O|o]bj diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c97755a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,260 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Default settings: +# A newline ending every file +# Use CRLF line break +# Use 4 spaces as indentation +[*] +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +# Json config files +[*.json] +indent_size = 2 + +# Yaml config files +[*.{yml,yaml}] +indent_size = 2 + +# Xml project files +[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# c# 文件 +[*.cs] + +#### Core EditorConfig 选项 #### + +# 缩进和间距 +indent_size = 4 +indent_style = space +tab_width = 4 + +# 新行首选项 +end_of_line = lf +insert_final_newline = false + +#### .NET 编码约定 #### + +# 组织 Using +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. 和 Me. 首选项 +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# 语言关键字与 bcl 类型首选项 +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# 括号首选项 +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# 修饰符首选项 +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# 表达式级首选项 +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# 字段首选项 +dotnet_style_readonly_field = true + +# 参数首选项 +dotnet_code_quality_unused_parameters = all + +# 禁止显示首选项 +dotnet_remove_unnecessary_suppression_exclusions = none + +# 新行首选项 +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### c# 编码约定 #### + +# var 首选项 +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = true + +# Expression-bodied 成员 +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = true +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# 模式匹配首选项 +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null 检查首选项 +csharp_style_conditional_delegate_call = true + +# 修饰符首选项 +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# 代码块首选项 +csharp_prefer_braces = when_multiline +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = file_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_top_level_statements = true + +# 表达式级首选项 +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# "using" 指令首选项 +csharp_using_directive_placement = outside_namespace + +# 新行首选项 +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# 格式规则 #### + +# 新行首选项 +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# 缩进首选项 +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# 空格键首选项 +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# 包装首选项 +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### 命名样式 #### + +# 命名规则 + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# 符号规范 + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# 命名样式 + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case diff --git a/.github/ISSUE_TEMPLATE/bug-report----.md b/.github/ISSUE_TEMPLATE/bug-report----.md new file mode 100644 index 0000000..378e969 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report----.md @@ -0,0 +1,63 @@ +--- +name: Bug report(缺陷) +about: 缺陷或异常 +title: "【Bug】<请在标题中清晰地概述你要反馈的异常或缺陷>" +labels: '' +assignees: '' + +--- + + + + +### 版本 + +BiliTool版本号:`x.x.x` + +### 确认 + +- [ ] 是的,我已搜索并确认,没有其他相同的议题 +- [ ] 是的,我确认,已尝试升级到最新版,但未解决 + +### 服务器架构 + +- [ ] x64 +- [ ] arm64 +- [ ] arm +- [ ] 其他(请在下面补充) + +### 服务器系统 + +- [ ] Windows +- [ ] macOS +- [ ] Linux + - [ ] Debian + - [ ] Ubuntu + - [ ] Windows + - [ ] Alpine + - [ ] Centos + - [ ] 其他(请在下面补充) + +### 选择的BiliTool运行模式 + +- [ ] docker +- [ ] podman +- [ ] 下载的Release包 +- [ ] 其他(请在下面补充) + +### 问题描述 + + +<这里> + +### 日志信息 + + + +
+ +``` +<这里> +``` + +
diff --git a/.github/ISSUE_TEMPLATE/bug-report-qinglong----.md b/.github/ISSUE_TEMPLATE/bug-report-qinglong----.md new file mode 100644 index 0000000..c3264a0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-qinglong----.md @@ -0,0 +1,93 @@ +--- +name: 【QingLong】Bug report(缺陷) +about: 缺陷或异常(青龙专属) +title: "【Bug】【青龙】<请在标题中清晰地概述你要反馈的异常或缺陷>" +labels: '' +assignees: '' + +--- + + + + +### 版本 + +BiliTool版本号:`x.x.x` + +青龙版本号:`x.x.x` + +### 确认 + +- [ ] 是的,我已搜索并确认,没有其他相同的议题 +- [ ] 是的,我确认,已尝试升级bilitool到最新版,但未解决 +- [ ] 是的,我确认,已尝试升级青龙到最新版,但未解决 + +### 服务器架构 + +- [ ] x64 +- [ ] arm64 +- [ ] arm +- [ ] 其他(请在下面补充) + +### 服务器系统 + +- [ ] Windows +- [ ] macOS +- [ ] Linux + - [ ] Debian + - [ ] Ubuntu + - [ ] Windows + - [ ] Alpine + - [ ] Centos + - [ ] 其他(请在下面补充) + +### 青龙容器类型 + +- [ ] Docker +- [ ] Podman +- [ ] 其他(请在下面补充) + +### 青龙镜像 + +- [ ] whyour/qinglong:latest(Alpine) +- [ ] whyour/qinglong:debian(Debian) + +### 选择的BiliTool运行模式 + +- [ ] dotnet +- [ ] bilitool + +### 如果是青龙拉库相关bug,请贴出拉库方式截图 + +- [ ] 否 +- [ ] 是,截图如下 + +### 如果是缺失文件相关bug,请贴出容器内文件路径信息 + +- [ ] 否 +- [ ] 是,信息如下 + +查看方式参考文档:[提示文件不存在或路径异常怎么排查](https://github.com/RayWangQvQ/BiliBiliToolPro/blob/main/qinglong/README.md#43-提示文件不存在或路径异常怎么排查) + +BiliTool仓库文件路径:`<粘贴路径>` + +脚本文件路径:`<粘贴路径>` + +<这里贴截图> + +### 问题描述 + + +<这里> + +### 日志信息 + + + +
+ +``` +<这里> +``` + +
diff --git a/.github/ISSUE_TEMPLATE/feature-request----.md b/.github/ISSUE_TEMPLATE/feature-request----.md new file mode 100644 index 0000000..fda2f77 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request----.md @@ -0,0 +1,21 @@ +--- +name: Feature request(建议) +about: 建议或需求 +title: "【建议】<请在标题中清晰地概述你的建议>" +labels: 建议/enhancement +assignees: '' + +--- + + + + +### 确认 + +- [ ] 是的,我已搜索并确认,没有其他相同的议题 + +### 建议内容 + + + +<这里> diff --git a/.github/ISSUE_TEMPLATE/other----.md b/.github/ISSUE_TEMPLATE/other----.md new file mode 100644 index 0000000..2632385 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/other----.md @@ -0,0 +1,18 @@ +--- +name: Other(其他) +about: 既不是Bug也不是建议的或不能确定的其他议题 +title: "【其他】<请在标题中清晰地概述内容>" +labels: '' +assignees: '' + +--- + +### 确认 + +- [ ] 是的,我确认要选Other,因为我的内容既不是Bug,也不是Feature +- [ ] 是的,我已搜索并确认,没有其他相同的议题 + +### 描述 + + +<这里> diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6295e66 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ + + +### 内容 + +<请描述您将贡献的内容> diff --git a/.github/pull.yml b/.github/pull.yml new file mode 100644 index 0000000..f2387fd --- /dev/null +++ b/.github/pull.yml @@ -0,0 +1,6 @@ +version: "1" +rules: # Array of rules + - base: main # Required. Target branch + upstream: RayWangQvQ:main # Required. Must be in the same fork network. + mergeMethod: hardreset # Optional, one of [none, merge, squash, rebase, hardreset], Default: hardreset. + mergeUnstable: true # Optional, merge pull request even when the mergeable_state is not clean. Default: true diff --git a/.github/workflows/auto-deploy-tencent-scf.yml b/.github/workflows/auto-deploy-tencent-scf.yml new file mode 100644 index 0000000..98d9747 --- /dev/null +++ b/.github/workflows/auto-deploy-tencent-scf.yml @@ -0,0 +1,54 @@ +# https://github.com/June1991/serverless-express + +name: auto-deploy-tencent-scf + +on: + workflow_dispatch: + schedule: + - cron: "0 2 * * 1,3,5" # 每周一、三、五的10点 + +env: + IsAutoDeployTencentScf: ${{ secrets.IS_AUTO_DEPLOY_TENCENT_SCF }} # 是否开启自动部署云函数 + +jobs: + pre-check: + runs-on: ubuntu-latest + outputs: + result: ${{ steps.check.outputs.result }} # 不能直接传递secrets的值,否则会被skip,需要转一下 + steps: + - id: check + run: | + [ ${{ github.event_name }} == 'workflow_dispatch' -o true == "${{ env.IsAutoDeployTencentScf }}" ] && echo "result=开启" >> $GITHUB_OUTPUT || echo "result=关闭" >> $GITHUB_OUTPUT + + deploy: + name: deploy serverless + runs-on: ubuntu-latest + needs: pre-check + # if: env.IsAutoDeployTencentScf=='true' # 这里job.if读取不到env或secrets,很坑...但是发现可以读到needs的outputs值 + if: needs.pre-check.outputs.result=='开启' + steps: + - name: clone local repository + uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: install serverless + run: npm i -g serverless-cloud-framework + - name: deploy serverless + run: | + cd ./tencentScf + echo "开始配置云函数:" + echo "$Tencent_Serverless_Yml" + [ -z "$Tencent_Serverless_Yml" ] && echo "未配置serverless.yml,使用默认值" || echo "$Tencent_Serverless_Yml" > serverless.yml + echo "开始发布项目" + chmod +x publish.sh + ./publish.sh + echo "开始部署到云函数" + scf deploy + env: # 环境变量 + STAGE: dev #您的部署环境 + SERVERLESS_PLATFORM_VENDOR: tencent # serverless海外默认为aws部署,配置为腾讯部署 + TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }} # 您的腾讯云账号sercret ID + TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }} # 您的腾讯云账号sercret key + Tencent_Serverless_Yml: ${{ secrets.TENCENT_SERVERLESS_YML }} # 云函数配置(区域、环境变量、触发器等) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..9c3088c --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,78 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main, develop, release/* ] + paths: + - '**/*.js' + - '**/*.cs' + - '**/*.cshtml' + - '**/*.csproj' + pull_request: + # The branches below must be a subset of the branches above + branches: [ main, develop ] + paths: + - '**/*.js' + - '**/*.cs' + - '**/*.cshtml' + - '**/*.csproj' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/no-toxic-comments.yml b/.github/workflows/no-toxic-comments.yml new file mode 100644 index 0000000..5844c80 --- /dev/null +++ b/.github/workflows/no-toxic-comments.yml @@ -0,0 +1,13 @@ +name: Check Toxic Comments +on: [issue_comment, pull_request_review] + +jobs: + toxic_check: + runs-on: ubuntu-latest + name: Safe space + steps: + - uses: actions/checkout@v2 + - name: Safe space - action step + uses: charliegerard/safe-space@master + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml new file mode 100644 index 0000000..039a5ca --- /dev/null +++ b/.github/workflows/publish-image.yml @@ -0,0 +1,87 @@ +name: Publish image + +on: + workflow_dispatch: + inputs: + autoWithLatestTag: + description: 'Auto Add Latest Tag' + required: true + default: true + type: boolean + release: + types: [created] + +env: + DOCKERHUB_USERNAME : ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_PASSWORD : ${{ secrets.DOCKERHUB_PASSWORD }} + GHCR_USERNAME: ${{ github.repository_owner }} + GHCR_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + DOCKER_IMG_NAME: "zai7lou/bili_tool_web" + GHC_IMG_NAME: "ghcr.io/raywangqvq/bili_tool_web" + +jobs: + PublishImage: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: GetTargetVersion + id: getTargetVersion + run: | + TargetVersion="" + if [ "${{ github.event.release.tag_name }}" ] ; then + TargetVersion=${{ github.event.release.tag_name }} + else + TargetVersion=$(grep -oP '(?<=).*?(?=<\/Version>)' ./common.props) + fi + echo "TargetVersion: $TargetVersion" + echo "TargetVersion=$TargetVersion" >> $GITHUB_OUTPUT + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ env.DOCKERHUB_USERNAME }} + password: ${{ env.DOCKERHUB_PASSWORD }} + + - name: Log in to ghcr + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ env.GHCR_USERNAME }} + password: ${{ env.GHCR_PASSWORD }} + + - name: Generate tags + id: tags + run: | + targetVersion="${{ steps.getTargetVersion.outputs.TargetVersion }}" + dockerTagWithVersion="${{ env.DOCKER_IMG_NAME }}:$targetVersion" + ghcrTagWithVersion="${{ env.GHC_IMG_NAME }}:$targetVersion" + dockerTagWithLatest="" + ghcrTagWithLatest="" + if [ "${{ github.event.inputs.autoWithLatestTag }}" == "true" ] || [ ${{ github.event.release.created_at }} ]; then + dockerTagWithLatest="${{ env.DOCKER_IMG_NAME }}:latest" + ghcrTagWithLatest="${{ env.GHC_IMG_NAME }}:latest" + fi + echo "dockerTagWithVersion=$dockerTagWithVersion" >> $GITHUB_OUTPUT + echo "ghcrTagWithVersion=$ghcrTagWithVersion" >> $GITHUB_OUTPUT + echo "dockerTagWithLatest=$dockerTagWithLatest" >> $GITHUB_OUTPUT + echo "ghcrTagWithLatest=$ghcrTagWithLatest" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ steps.tags.outputs.dockerTagWithVersion }} + ${{ steps.tags.outputs.ghcrTagWithVersion }} + ${{ steps.tags.outputs.dockerTagWithLatest }} + ${{ steps.tags.outputs.ghcrTagWithLatest }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..9683177 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,45 @@ +name: Publish release + +on: + workflow_dispatch: + +permissions: + contents: write + discussions: write + +jobs: + build: + name: Publish Release + if: ${{ github.repository == 'RayWangQvQ/BiliBiliToolPro' }} + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' + + - name: Publish and Zip Release + run: | + cd ./scripts + chmod +x ./publish.sh + ./publish.sh --runtime all + + - name: Read Version + id: version + run: echo "version=$(cat ./src/Ray.BiliBiliTool.Console/bin/Publish/version.txt)" >> $GITHUB_OUTPUT + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: './src/Ray.BiliBiliTool.Console/bin/Publish/*.zip' + token: ${{ secrets.GITHUB_TOKEN }} + name: "BiliBiliToolPro-V${{ steps.version.outputs.version }}" + tag_name: ${{ steps.version.outputs.version }} + body_path: './src/Ray.BiliBiliTool.Console/bin/Publish/release_notes.md' + discussion_category_name: Announcements + generate_release_notes: true + fail_on_unmatched_files: true diff --git a/.github/workflows/repo-sync.yml b/.github/workflows/repo-sync.yml new file mode 100644 index 0000000..85b5a3d --- /dev/null +++ b/.github/workflows/repo-sync.yml @@ -0,0 +1,28 @@ +# 自动同步上游仓库 + +name: repo-sync + +on: + workflow_dispatch: + schedule: + - cron: '0 1 * * 1,3,5' + # UTC时区,比我们东八区早8小时,上面示例为:每周一、三、五的9点。 + +jobs: + repo-sync: + if: ${{ github.repository != 'RayWangQvQ/BiliBiliToolPro' }} + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + persist-credentials: false + + - name: repo-sync + uses: repo-sync/github-sync@v2 + with: + source_repo: "https://github.com/RayWangQvQ/BiliBiliToolPro.git" + source_branch: "main" + destination_branch: "main" + sync_tags: "true" + github_token: ${{ secrets.PAT }} diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml new file mode 100644 index 0000000..0ecc388 --- /dev/null +++ b/.github/workflows/stale-issues.yml @@ -0,0 +1,25 @@ +name: Close Stale Issues +on: + schedule: + - cron: "0 8 * * *" # 每天的 00:00 运行 + workflow_dispatch: + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + close_stale_issues: + runs-on: ubuntu-latest + steps: + - name: Close Stale Issues + uses: actions/stale@v5 + with: + days-before-stale: 3 # 3 天不活跃后标记Stale + days-before-close: 3 # 标记Stale后3天不活跃则关闭问题 + stale-issue-label: "Stale" # 标记为 "Stale" 的问题 + stale-issue-message: "🕸️ This has been inactive for 3 days, please confirm if it still needs attention~~" # Comment added + close-issue-message: "🚫 This has been inactive for too long and is now closed, feel free to reopen it if needed!" # Comment added + only-labels: "needs-more-info" # 只处理标签为 "help wanted" 的问题 + repo-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml new file mode 100644 index 0000000..1a48b98 --- /dev/null +++ b/.github/workflows/tag.yml @@ -0,0 +1,30 @@ +name: Tag + +on: + pull_request: + types: + - closed + branches: + - main + +jobs: + tag: + name: add tag + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Get current version + id: current_version + run: | + version=$(grep -oP '(?<=).*?(?=<\/Version>)' ./common.props) + echo "Curent version: $version" + echo "current_version=$version" >> $GITHUB_OUTPUT + + - name: Tag and push + run: | + new_tag="${{ steps.current_version.outputs.current_version }}" + git tag -f "$new_tag" + git push -f origin "$new_tag" diff --git a/.github/workflows/verify-pr.yml b/.github/workflows/verify-pr.yml new file mode 100644 index 0000000..3436e1e --- /dev/null +++ b/.github/workflows/verify-pr.yml @@ -0,0 +1,19 @@ +name: VerifyPR +on: + pull_request_target: + types: [opened, edited] + +jobs: + checkTargetBranch: + runs-on: ubuntu-latest + steps: + - uses: Vankka/pr-target-branch-action@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + target: main + change-to: develop + exclude: RayWangQvQ/BiliBiliToolPro:develop + comment: | + Your PR was set to `main`, but PRs should be sent to `develop` + The base branch of this PR has been automatically changed to `develop`, please check that there are no merge conflicts. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3458752 --- /dev/null +++ b/.gitignore @@ -0,0 +1,382 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# +.idea/ +tencentScf/.env +.obsidian + +# vs code +.vscode + +# krew +krew/bilipro +krew/cmd/kubectl-bilipro +krew/kustomization.yaml +bilipro +krew/pkg/utils/fixtures +kustomization.yaml + +# cookie config +**/Ray.BiliBiliTool.Console/cookies.json +**/Ray.BiliBiliTool.Web/config/cookies.json + +# ut +coveragereport + +# bruno +bruno/.env + +# db +src/**/BiliBiliTool.db* diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..b57b217 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +echo "This is pre commit" +dotnet husky run --group pre-commit \ No newline at end of file diff --git a/.husky/task-runner.json b/.husky/task-runner.json new file mode 100644 index 0000000..c509430 --- /dev/null +++ b/.husky/task-runner.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://alirezanet.github.io/Husky.Net/schema.json", + "tasks": [ + { + "name": "welcome-message-example", + "command": "bash", + "args": [ "-c", "echo Husky.Net is awesome!" ], + "windows": { + "command": "cmd", + "args": ["/c", "echo Husky.Net is awesome!" ] + } + }, + { + "name": "csharpier-install", + "group": "pre-commit", + "command": "dotnet", + "args": [ "tool", "install", "csharpier" ] + }, + { + "name": "csharpier-fotmat", + "group": "pre-commit", + "command": "dotnet", + "args": [ "csharpier", "format", "${staged}" ], + "include": [ "**/*.cs" ] + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..76358bc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,218 @@ +## 3.8.2 +- Fix[#1026]: 更新文档 +## 3.8.1 +- Fix[#1005]: 更新文档 +## 3.8.0 +- Feature: 使推送的更版本号信息更简洁 +- Fix[#998]: 修复企业微信 App 推送缺少 access_token 问题 +- Fix[#996]: 修复 Server 酱推送标题不能为空的问题 +- Fix[#669]: 企业微信默认消息类型从 markdown 改为 text +## 3.7.0 +- Fix[#989]: 修复钉钉推送标题不能为空的问题 +## 3.6.0 +- Feature[#961]: Web 项目新增推送功能 +- Feature[#961]: 升级 Serilog Pkg +- Feature: 使用中心化包管理 +## 3.5.0 +- Feature[#924]: 新增 Sqlite 配置源 +- Feature[#924]: 新增在线配置页 +- Feature[#924]: 根据任务拆分配置 +- Feature[#924]: 实现开启、关闭任务功能 +- Feature[#924]: 实现修改 Cron 定时时间功能 +- Feature: 定时任务页改为默认50条 +- Feature: 更新配置文档 +## 3.4.0 +- Feature: 优化登录失败时的提示信息 +- Feature: 更新推送的文档说明 +## 3.3.0 +- Feature[#935]: Web 新增登录功能 +- Feature[#935]: Web 新增修改密码功能 +- Feature[#935]: 更新文档 +- Feature: 更新开源协议为 GNU GPLv3 +- Feature: 拆分原本的 Daily 任务 +- Doc: 更新文档 +- Feature: 升级 csharpier +- Feature: 变更默认数据库文件位置到 /app/config 下 +## 3.2.0 +- Fix: 修复大会员大积分签到任务 +- Fix: 修复大会员大积分的签到和浏览追番频道任务 +- Feature[#901]: 实现大会员大积分的浏览影视频道页任务 +- Feature[#921]: 新增大会员大积分的观看剧集 bruno 信息 +- Feature: 鉴权不再兼容老版本青龙(老版本需要手动添加 bili cookie) +- Feature: 修复 warnings +- Feature: 移除无用的using +- Fix: 修复 VerifyPR CI/CD 流水线 +- Feature: README 添加 Trending 信息 +## 3.1.0 +- Feature[#842]: 对接青龙新的 OpenAPI,实现青龙版 Bili 登录后自动存储 Cookie +- Feature[#842]: 兼容老版本青龙的文件鉴权方式 +- Feature[#842]: 新增新版青龙添加鉴权的说明文档 +- Feature[#820]: 更新文档解决配置文件边缘场景下的刷新问题 +- Fix[#863]: 修复青龙尝试修复异常任务 +- Fix[#879]: 移除文档内过期的加速器地址 +- Feature: 开启 Nullable 特性,在编译阶段检查潜在的 NullReferenceException 问题 +- Feature: 临时取消 Null warning +- Fix: Bruno 脚本错误 +- Fix: 尝试修复发布包时 CI/CD 丢失 change log 问题 +## 3.0.0 +- Feature[#884]: 上线 bili_tool_web +- Fix[#875]: 青龙检测 dotnet 版本只需要大于等于 8.0 +- Fix[#876]: 升级 VipBigPoint 接口,解决风控 +- Fix[#881]: 升级 LiveLottery 接口,解决风控 +## 2.2.2 +- Code refactor +- Integration Husky.Net and CSharpier +## 2.2.1 +- Fix[#847]: DefaultRequestHeaders can not be null or empty with dotnet 8 +- Fix[#849]: Temporary disable PublishTrimmed +## 2.2.0 +- Migrate from dotnet 6.0 to dotnet 8.0 +- Add Bruno to document the APIs +- Fix[#824]: Log cookie when qinglong save env failed +- Fix[#648]: Set DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 in qinglong to ignore random "Couldn't find a valid ICU package" issue +## 2.1.3 +- Code refactor +- Fix[#791]:修复VipBigPoint任务异常导致终止的问题 +## 2.1.2 +- Feature: enhancement CICD scripts +- Fix[#728]: compatible with qinglong history versions +## 2.1.1 +- Feature: listen ctrl+c at the very beginning +- Fix: fix qinglong read cron error +## 2.1.0 +- Feature[#691]: 重构并优化基于qinglong的部署方式,尝试解决偶发的安装失败的问题 +- Feature[#670]: 新增针对App的AppUserAgent配置项,用于解决大会员大积分异常问题 +- Fix: 修复CICD发布脚本错误 +## 2.0.5 +- Fix[#260]: 再次尝试修复大会员大积分“账号风险”异常 +## 2.0.4 +- Fix: 尝试修复大会员大积分“账号风险”异常 +- Feature:为agent api创建集成测试 +## 2.0.3 +- PR[#641]:实现浏览会员购页面与观看正片内容功能 +- PR[#685]:部分修复大积分功能 +- Fix:更新过于老旧的UserAgent +- Fix:更新排行榜api +## 2.0.2 +- PR[#617]:增加专栏投币功能与领取大会员经验的功能 +## 2.0.1 +- PR[#539]:更新文档 +- PR[#557]:修复直播接口权限不足问题 +## 2.0.0 +- Feature[#513]:将基础组件和抽象迁移到nuget包中 +- Fix[#543]:修复部分Api调用报“访问权限不足” +## 1.0.3 +- Fix #486 : fix release zip error +## 1.0.2 +- Fix #484 : fix read dic config error +- Merge PR #472 : add reverse proxy host for telegram notification +- Merge PR #483 : update login field for entrypoint +## 1.0.1 +- Fix #463 : do not trust user's ck config +- Feature #460 : publish single file when release +- Feature: use new scripts for gh actions's release +- Feature #473 : let user input when there is no target task in configs +## 1.0.0 +- Feature: Enable asynchronous +- Fix #344 : Support `Ctrl + C` to trigger exit event +- Fix #451 : Rebuild cookie factory pattern and fix bug of donating coin +- Featur: Replace AOP from MethodBoundaryAspect.Fody to Rougamo.Fody, to fix async exception +- Merge PR #448 : Fix typo +- Fix #446 : Change id type from int to long +## 0.4.6 +- Fix: ck list init empty error +- Feature #440 : use 'apk add' to install dotnet in qinglong +## 0.4.5 +- Fix #423 : Change int to string to avoid overflow exception +## 0.4.4 +- Fix #228 : Try to fix sharing video error +- Feature: Change default docker image from dockerhub to github +## 0.4.3 +- Feature #419 : Add a auto shell script for installing with docker +- Feature #396 : Publish docker image to GitHub pkg +## 0.4.2 +- Merfe PRs #425 #426 #427 : Enhancement docker things, thx @zclkkk +## 0.4.1 +- Merge PR #418 : Fix search video api's error, thx @catlair +## 0.4.0 +- 合并PR( #381 #383 ),新增直播间挂机功能,感谢@bakapiano +## 0.3.2 +- Fix( #358 ),获取auth时兼容老版青龙文件路径 +- Fix( #364 ),兼容青龙异形response数据类型 +- Fix( #366 #361 ),修复一些低级bug +- Feature( #359 ),兼容读取不到`$QL_DIR`的情况 +## 0.3.1 +- Fix( #260 ),在需要的时候encode cookie +- 更新文档 +## 0.3.0 +- hotfix docker build error +- 合并PR(#341),新增krew部署,感谢@chenliu1993 +- 合并PR(##348),更新文档,感谢@jexjws +- 合并PR(#350),修改请求header错误的bug,感谢@catlair +- 合并PR(#353),新增python扫码登录的feature(仅针对青龙),感谢@AFUL1991 +- Feature(#351):重构并新增了扫码登录功能,使之适用于各种部署平台 +## 0.2.2 +- 新增`podman`部署教程 +- 合并PR(#264),腾讯云定时任务补充新增的大会员大积分任务,感谢@layui0320 +- 合并PR(#262),更新docker的entry.sh,感谢@syrinka +- 合并PR(#308 #312),新增Chart部署,感谢@chenliu1993 +- 合并PR(#309)新增lv6后开启白嫖模式的配置(多账号时可以实现不足lv6的继续投币,达到lv6的开始白嫖),感谢@cluom +- 优化青龙安装dotnet的脚本,改为使用官方`dotnet-install.sh`脚本安装(之前测试网络不通,后发现--no-cdn可以) +- 优化青龙的执行脚本,提取公共部分,并且在执行前会尝试安装一次dotnet,会清理一次缓存 +## 0.2.1 +- 合并PR(#253、#257),更新文档(@layui0320) +- 合并PR(#256),重构docker运行是cron构建方式,并优化读取环境变量的方式(@syrinka) +- Feature(#65):新增TG推送配置并使用代理功能 +- Feature(#240):新增gotify推送 +- Feature(#259):大会员状态改为枚举类型,当非会员时自动跳过大积分任务 +- Feature:更新、优化docker部署文档 +## 0.2.0 +- 新增大会员大积分任务 +## 0.1.2 +- 修复`auto-close-pr.yml`分支错误的bug +- 【#107】新增自动检测并关闭长时无状态issues的actions:no-response.yml +- 【#73】【#105】【#108】更新、纠正文档内容 +- HostConfiguration,删除了CommandLine配置源,推荐只使用环境变量,同时更新青龙shell脚本内配置 +- 【#169】领取大会员福利任务更改为每日都尝试执行 +- 青龙拉库兼容大小写问题 +- 【#197】合并PR,新增了阅读漫画功能到每日任务中(@ChanceLuo) +## 0.1.1 +- 【#54】优化青龙shell脚本读取仓库目录方式,解决青龙新老版本切换导致出现多个repo目录的bug +- 【#82】【#85】合并外部PR,更新了文档 +- 感谢`JetBrain`提供免费的证书支持 +## 0.1.0 +- 【#62】`codeql-analysis.yml`可以指定检查的文件类型 +- 【#61】`publish-image.yml`手动打镜像时支持指定是否打latest的tag +- 【#32】新增企业微信的应用推送,实现微信接受推送消息 +- 优化日志格式 +## 0.0.9 +- 【#47】青龙安装`dotnet`环境,支持arm架构服务器 +## 0.0.8 +- 【#55】新增日志推送端:`Microsoft Teams` +- 【#27】更新README +## 0.0.7 +- 【#44】兼容青龙最新版本(v2.12.0),修复因青龙调整目录结构导致的bug +- 更新`publish-image.yml`,只有`release`时才打`latest tag`,手动运行时不打`latest tag` +## 0.0.6 +- 更新docker镜像的构建 +- 【#12】新增配置`Notification:IsSingleAccountSingleNotify`,支持开启每个账号单独推送消息 +- publish-release.yml新增手动输入tag功能 +## 0.0.5 +- 优化推送日志,在标题中显示运行的任务名称 +- 新增`CodeQL`workflows,用于检测代码 +- 新增`Publish image`workflows,用于发布镜像 +- 新增`no-toxic-comments.yml`,用于检测评论 +- 更新`auto-close-pr.yml`,用于修正PR的目标到`develop` +## 0.0.4 +- 【#15】修复`Actions`部署到腾讯云函数时的偶发异常 +## 0.0.3 +- 【#16】修复银瓜子兑换硬币bug +- 【#18】修改[青龙面板](https://github.com/whyour/qinglong)以`Production`环境运行 +- [青龙面板](https://github.com/whyour/qinglong)新增拉取dev先行版功能 +## 0.0.2 +- 更新文档 +- 天选抽奖新增黑名单功能 +- 批量取关新增白名单功能 +## 0.0.1 +- 重启项目 +- 支持[青龙面板](https://github.com/whyour/qinglong)部署 diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..77c7fe6 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,80 @@ + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..acecb3a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +#See https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/docker/building-net-docker-images +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 8080 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /code + +COPY ["Directory.Packages.props", "./"] +COPY ["src/Ray.BiliBiliTool.Web/Ray.BiliBiliTool.Web.csproj", "src/Ray.BiliBiliTool.Web/"] +COPY ["src/Ray.BiliBiliTool.Web.Client/Ray.BiliBiliTool.Web.Client.csproj", "src/Ray.BiliBiliTool.Web.Client/"] +COPY ["src/Ray.BiliBiliTool.Application/Ray.BiliBiliTool.Application.csproj", "src/Ray.BiliBiliTool.Application/"] +COPY ["src/Ray.BiliBiliTool.Application.Contracts/Ray.BiliBiliTool.Application.Contracts.csproj", "src/Ray.BiliBiliTool.Application.Contracts/"] +COPY ["src/Ray.BiliBiliTool.Domain/Ray.BiliBiliTool.Domain.csproj", "src/Ray.BiliBiliTool.Domain/"] +COPY ["src/Ray.BiliBiliTool.DomainService/Ray.BiliBiliTool.DomainService.csproj", "src/Ray.BiliBiliTool.DomainService/"] +COPY ["src/Ray.BiliBiliTool.Config/Ray.BiliBiliTool.Config.csproj", "src/Ray.BiliBiliTool.Config/"] +COPY ["src/Ray.BiliBiliTool.Agent/Ray.BiliBiliTool.Agent.csproj", "src/Ray.BiliBiliTool.Agent/"] +COPY ["src/Ray.BiliBiliTool.Infrastructure/Ray.BiliBiliTool.Infrastructure.csproj", "src/Ray.BiliBiliTool.Infrastructure/"] +COPY ["src/Ray.BiliBiliTool.Infrastructure.EF/Ray.BiliBiliTool.Infrastructure.EF.csproj", "src/Ray.BiliBiliTool.Infrastructure.EF/"] +COPY ["src/BlazingQuartz.Core/BlazingQuartz.Core.csproj", "src/BlazingQuartz.Core/"] +COPY ["src/BlazingQuartz.Jobs/BlazingQuartz.Jobs.csproj", "src/BlazingQuartz.Jobs/"] +COPY ["src/BlazingQuartz.Jobs.Abstractions/BlazingQuartz.Jobs.Abstractions.csproj", "src/BlazingQuartz.Jobs.Abstractions/"] + +RUN dotnet restore "src/Ray.BiliBiliTool.Web/Ray.BiliBiliTool.Web.csproj" +COPY . . +WORKDIR "/code/src/Ray.BiliBiliTool.Web" +RUN dotnet build "Ray.BiliBiliTool.Web.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Ray.BiliBiliTool.Web.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +COPY docker/entrypoint.sh /app/entrypoint.sh +RUN rm -rf /var/lib/apt/lists/* \ + && chmod +x /app/entrypoint.sh +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..12cf93d --- /dev/null +++ b/README.md @@ -0,0 +1,276 @@ +![2233](docs/imgs/2233.png) + +
+ +

+ +BiliTool + +

+ +[![GitHub Stars](https://img.shields.io/github/stars/RayWangQvQ/BiliBiliToolPro?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/stargazers) +[![GitHub Forks](https://img.shields.io/github/forks/RayWangQvQ/BiliBiliToolPro?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/network) +[![GitHub Issues](https://img.shields.io/github/issues/RayWangQvQ/BiliBiliToolPro?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/issues) +[![GitHub Contributors](https://img.shields.io/github/contributors/RayWangQvQ/BiliBiliToolPro?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/graphs/contributors) +[![GitHub All Releases](https://img.shields.io/github/downloads/RayWangQvQ/BiliBiliToolPro/total?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/releases) +[![GitHub Release (latest SemVer)](https://img.shields.io/github/v/release/RayWangQvQ/BiliBiliToolPro?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/releases) +[![GitHub License](https://img.shields.io/github/license/RayWangQvQ/BiliBiliToolPro?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/blob/main/LICENSE) + + + RayWangQvQ%2FBiliBiliToolPro | Trendshift + + +
+ +**BiliTool 是一个自动执行任务的工具,当我们忘记做某项任务时,它会像一个贴心小助手,按照我们预先吩咐它的命令,在指定频率、时间范围内帮助我们完成计划的任务。** + +**BiliTool is an automated task execution tool that acts as a helpful assistant, following pre-configured commands to complete planned tasks within specified frequencies and timeframes when we forget to do them.** + +主要功能如下: + +- **扫码登录,自动更新cookie** +- **每日获取满额升级经验(登录、投币、点赞、分享视频)(支持指定up主)** +- **直播间挂机** +- **每天漫画签到** +- **每天直播签到** +- **直播中心银瓜子兑换为硬币** +- **每月领取大会员赠送的 5 张 B 币券和福利(忘记或者不领就浪费了哦)** +- **每月领取大会员漫画福利** +- **月底在 B 币券过期前进行充电(支持指定想要支持的up主,如果没有喜欢的up,也可以为自己充个电啊,做个用爱为自己发电的人~)** +- **直播中心天选时刻自动参与抽奖** +- **批量取关** +- **大会员大积分任务** +- **支持多账号** +- **理论上支持所有远端的日志推送(默认支持推送到Telegram、企业微信、钉钉、PushPlus、Server酱、酷推,另外也支持自定义推送到任意api)** +--- +[目录] + + + +- [1. 如何使用](#1-如何使用) + - [1.1. 部署 BiliTool](#11-部署-bilitool) + - [1.1.1. 方案一:免费在线容器](#111-方案一免费在线容器) + - [1.1.2. 方式二:青龙](#112-方式二青龙) + - [1.1.3. 方式三:Docker 或 Podman 运行](#113-方式三docker-或-podman-运行) + - [1.1.4. 方式四:下载程序包到本地或服务器运行](#114-方式四下载程序包到本地或服务器运行) + - [1.1.5. 方式五:Chart部署](#115-方式五chart部署) + - [1.2. 消息推送(可选)](#12-消息推送可选) +- [2. 功能任务说明](#2-功能任务说明) +- [3. 个性化自定义配置](#3-个性化自定义配置) +- [4. 多账号支持](#4-多账号支持) +- [5. 常见问题](#5-常见问题) +- [6. 版本发布及更新](#6-版本发布及更新) +- [7. 成为开源贡献成员](#7-成为开源贡献成员) + - [7.1. 贡献代码](#71-贡献代码) + - [7.2. 贡献文档](#72-贡献文档) +- [8. 捐赠支持](#8-捐赠支持) +- [9. 其他](#9-其他) + + + +--- +**Github 仓库地址:[RayWangQvQ/BiliBiliToolPro](https://github.com/RayWangQvQ/BiliBiliToolPro)** + +**注意:** + +- **本应用仅用于学习和测试,作者本人并不对其负责,请于运行测试完成后自行删除,请勿滥用!** +- **所有代码都是开源且透明的,任何人均可查看,程序不会保存或滥用任何用户的个人信息** +- **应用内几乎所有功能都开放了配置(如任务开关、日期、id等),详细信息可阅读配置文档** + +运行图示: + +

+ 运行图示 +
+ 运行日志 +
+ 运行日志 +
+

+ +## 1. 如何使用 + +BiliTool 实现自动完成任务的原理,是通过调用一系列开放的api实现的。 + +**要使用 BiliTool,很简单,按照下面教程部署完成,运行后扫码登录即可。** + +### 1.1. 部署 BiliTool + +支持多种部署方式,以下选择任一适合自己的方式即可。 + +#### 1.1.1. 方案一:免费在线容器 + +有很多平台会提供一定免费额度的在线容器,基于官方镜像,部署 BiliTool 很容易。 + +以下以 ClawCloud 为例,其他平台操作类似: + +[>>ClawCloud 部署教程](docs/claw-cloud.md) + +#### 1.1.2. 方式二:青龙 + +[>>青龙部署教程](qinglong/README.md) + +#### 1.1.3. 方式三:Docker 或 Podman 运行 + +[>>Docker 部署说明](docker/README.md) + +[>>Podman 部署说明](podman/README.md) + +#### 1.1.4. 方式四:下载程序包到本地或服务器运行 + +[>>本地部署说明](docs/runInLocal.md) + +#### 1.1.5. 方式五:Chart部署 + +[>>Chart部署说明](helm/README.md) + +### 1.2. 消息推送(可选) + +如果配置了推送,执行成功后,指定的接收端会收到推送消息,推送效果如下所示: + +

+ Telegram推送图示 +

+ +目前默认支持**Telegram推送、PushPlus推送、企业微信应用推送、企业微信推送、钉钉推送、Microsoft Teams推送、Server酱推送和酷推QQ推送**(以上顺序即为个人推荐的排序),如果需要推送到其他端,也可以配置为任意的可以接受消息的Api地址,关于如何配置推送请详见下面的**个性化自定义配置**章节。 + +推送配置见:[confifuration](/docs/configuration.md) + +## 2. 功能任务说明 + +这里的**任务**是指一组功能的集合,是工具每次运行的最小单位。 + +任务列表如下: + + +|    任务名     |      Code       |                                                功能                                                 | 推荐运行频率 | +| :--------: | :-------------: | :-----------------------------------------------------------------------------------------------: | :--------------: | +| 扫码登录 | Login | 使用app扫码登录,用于第一次运行时初始化cookie,或cookie过期时的更新。不同平台会将cookie存储到不同地方 |       手动         | +| 每日任务 | Daily | 完成每日任务获取满额65点经验(登录、观看视频、分享视频、投币),快速升级Lv6 | 每天一次 | +| 天选时刻抽奖 | LiveLottery | 直播中心天选时刻抽奖,大部分抽奖都需要关注主播,介意的不要开启 | 每天0-4次 | +| 批量取关 | UnfollowBatched | 批量取关指定分组下的所有关注(主要用于清理天选抽奖而产生的关注) | 手动运行 | +| 大会员大积分 |   VipBigPoint   | 大会员大积分任务(签到、浏览、观看) | 每天一次,建议凌晨 | +| 直播间挂机 | LiveFansMedal | 直播间挂机 | 每天一次 | +| 漫画任务 | Manga | 漫画签到、阅读 | 每天一次 | +| 领取大会员漫画权益 | MangaPrivilege | 领取大会员的漫画权益 | 每天一次 | +| 银瓜子兑换硬币 | Silver2Coin | 使用银瓜子换取硬币 | 每天一次 | +| 免费B币券充电 | Charge | 大会员每31天可免费领取一张5B币券,可用于给除自己以外的UP充电 | 每天一次 | +| 领取大会员福利 | VipPrivilege | 领取大会员福利 | 每天一次 | +| 测试Cookie | Test | 测试Cookie是否正常 | 手动运行 | + + +## 3. 个性化自定义配置 + +[>>点击查看配置说明文档](docs/configuration.md) + +## 4. 多账号支持 + +部署成功后,直接去运行扫码登录任务,扫码成功后,应用会自动更新或添加cookie。 + +青龙平台会添加环境变量里,Key 为 `Ray_BiliBiliCookies__0`、`Ray_BiliBiliCookies__1`、`Ray_BiliBiliCookies__2`... + +其他平台默认会添加到名为cookies.json的账号配置文件中: +``` +{ + "BiliBiliCookies": [ + "cookie1", + "cookie2", + "...", + ], +} + +``` + +## 5. 常见问题 + +[>>点击查看常见问题文档](docs/questions.md) + +[Issues(议题)](https://github.com/RayWangQvQ/BiliBiliToolPro/issues)板块可以用来提交**Bug**和**建议**; + +[Discussions(讨论)](https://github.com/RayWangQvQ/BiliBiliToolPro/discussions)板块可以用来**提问**和**讨论**。 + +大部分问题其实都可以在文档、议题和讨论中找到答案。 + +所以如果你有疑问, + +* 请先确认是否可以通过升级到最新版本解决 +* 然后搜索文档(特别是配置说明文档和常见问题文档)、议题和讨论,查看是否已有其他人遇到相同问题、是否已有解决方案 + +如果确认还未解决,可以自己提交 Issue,或发布 Discussions 与大家一起探讨,我会尽快确认并解决。 + +(关于如何正确的提交Issue,请详见**常见问题文档**)。 + +## 6. 版本发布及更新 + +当前正处于稳定的迭代开发中,详细待更新和计划内容可参见 [Projects](https://github.com/RayWangQvQ/BiliBiliToolPro/projects) 和 [Issues](https://github.com/RayWangQvQ/BiliBiliToolPro/issues) 。 + +想要有重要更新时收到通知的话,可以把仓库右上角的`Star`按钮点亮。 + +## 7. 成为开源贡献成员 + +### 7.1. 贡献代码 + +如果你有好的想法,欢迎向仓库贡献你的代码,贡献步骤: + +* 搜索查看 Issue,确定是否已有人提过同类问题 +* 对于不确定的主题,为避免code结束后PR不被接受,可以先新建 Issue,描述问题或建议,讨论清楚后再动手编码 +* 如果确认自己可以解决,请 Fork 仓库后,在**develop 分支**进行编码开发,完成后**提交 PR 到 develop 分支** + +我会尽快进行代码审核,测试成功后会合并入 main 主分支,提前感谢您的贡献。 + +### 7.2. 贡献文档 + +文档部分由于我个人精力有限(写文档比写代码累多了),所以有些地方写的很简略,甚至有遗漏和错别字,不能贡献代码的朋友也欢迎来一起维护文档,欢迎 PR 来纠正我,一样都算是对开源做贡献了。 + +## 8. 捐赠支持 + +个人维护开源不易 + +如果觉得我写的程序对你小有帮助 + +或者,就是单纯的想集资给我买瓶霸王增发液 + +那么下面的赞赏码可以扫一扫啦 + +(赞赏时记得留下【昵称】和【留言】~ 另外我发现很多留言想要进群或者加好友的,一定一定要记得留下微信号哈,微信赞赏页面是看不到微信号的) + +**☟☟☟ 扫码自动赞赏 1 元:☟☟☟** + +![赞赏码](docs/imgs/donate.jpg) + +> 项目中的优先支持的UP主的配置项,默认是作者的 UpId (只是作为了 JSON 配置文件的默认值,代码是干净的),需要更改的话,直接修改相应配置即可(secrets或环境变量等各种方式都行)。 +当然,不改的话,也算是另一种捐赠支持作者的方式啦。 + +感谢支持~ + +## 9. 其他 + +`API`参考: + +- [www.bilibili.com](https://www.bilibili.com/) + +- [SocialSisterYi/bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect) + +- [JunzhouLiu/BILIBILI-HELPER](https://github.com/JunzhouLiu/BILIBILI-HELPER) + +❤️Thanks to `JetBrains` for the free certificate support: + +

+ ReSharper logo +

+ +❤️Thanks to [YxVM](https://yxvm.com/aff.php?aff=668) & [NodeSeekDev](https://github.com/NodeSeekDev/NodeSupport) for sponsoring the server for testing support: + +

+ + YxVm logo + +

+ +❤️Thanks to [DartNode](https://dartnode.com?aff=FriskyGopher833) for sponsoring the server for testing support: + +[![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source") + +❤️Thank you for your star to this project: + +[![Star History Chart](https://api.star-history.com/svg?repos=RayWangQvQ/BiliBiliToolPro&type=Date)](https://www.star-history.com/#RayWangQvQ/BiliBiliToolPro&Date) diff --git a/Ray.BiliBiliTool.sln b/Ray.BiliBiliTool.sln new file mode 100644 index 0000000..a1b104c --- /dev/null +++ b/Ray.BiliBiliTool.sln @@ -0,0 +1,294 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32112.339 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UI", "UI", "{38736647-2196-417E-8519-C48A012A63D9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{98051127-2868-4F5C-9B2C-2150975E05F3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{120917DC-474C-448B-9EBB-1B3BA3A20B9D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AF21E067-3307-4E7F-8CE8-C695E6B61876}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E9BDDCBE-A57D-4E3B-8252-708088386ADF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ray.BiliBiliTool.Console", "src\Ray.BiliBiliTool.Console\Ray.BiliBiliTool.Console.csproj", "{DB227D60-0737-45C2-8CEA-F55FDA711301}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigTest", "test\ConfigTest\ConfigTest.csproj", "{114D45C8-E4BB-47EE-89AC-BD1DC6FA3BAD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogTest", "test\LogTest\LogTest.csproj", "{2039BF6A-5EC4-439C-8D2E-77313075843A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{110D3D21-8E9B-45AB-9667-6DA1DB3862AC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ray.BiliBiliTool.Infrastructure", "src\Ray.BiliBiliTool.Infrastructure\Ray.BiliBiliTool.Infrastructure.csproj", "{7188698C-0A9A-43B2-B3E2-5136B14FDE13}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ray.BiliBiliTool.Application", "src\Ray.BiliBiliTool.Application\Ray.BiliBiliTool.Application.csproj", "{3388A58D-91CC-4875-A29F-3E6FC3B44BF5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ray.BiliBiliTool.Application.Contracts", "src\Ray.BiliBiliTool.Application.Contracts\Ray.BiliBiliTool.Application.Contracts.csproj", "{B6AEDD60-9C06-4391-9171-65EBD5E9D77A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ray.BiliBiliTool.Agent", "src\Ray.BiliBiliTool.Agent\Ray.BiliBiliTool.Agent.csproj", "{D5F9FBCE-31BE-4E80-93E2-183CF029431F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ray.BiliBiliTool.Config", "src\Ray.BiliBiliTool.Config\Ray.BiliBiliTool.Config.csproj", "{191C48BD-5CB5-42CA-B394-7A4A9BFA6275}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ray.BiliBiliTool.DomainService", "src\Ray.BiliBiliTool.DomainService\Ray.BiliBiliTool.DomainService.csproj", "{7105652A-B1C1-4F9E-BA38-8034BC8B26B4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F3DE0D72-426B-4AD9-B3ED-3343CF4223F1}" + ProjectSection(SolutionItems) = preProject + .dockerignore = .dockerignore + .editorconfig = .editorconfig + .gitignore = .gitignore + CHANGELOG.md = CHANGELOG.md + common.props = common.props + Dockerfile = Dockerfile + LICENSE = LICENSE + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{73DD457B-E06E-45ED-A6BA-7E3C02F8BDF1}" + ProjectSection(SolutionItems) = preProject + .github\pull.yml = .github\pull.yml + .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{61613EF4-3644-42D4-A620-70547291FB38}" + ProjectSection(SolutionItems) = preProject + .github\workflows\auto-close-pr.yml = .github\workflows\auto-close-pr.yml + .github\workflows\auto-deploy-tencent-scf.yml = .github\workflows\auto-deploy-tencent-scf.yml + .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml + .github\workflows\publish-image.yml = .github\workflows\publish-image.yml + .github\workflows\publish-release.yml = .github\workflows\publish-release.yml + .github\workflows\repo-sync.yml = .github\workflows\repo-sync.yml + .github\workflows\tag.yml = .github\workflows\tag.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{A93210FD-27B6-40E4-B08D-391F96CA2754}" + ProjectSection(SolutionItems) = preProject + docker\README.md = docker\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{2F1CB892-336C-4672-8A0A-FBAEB4B9EA8A}" + ProjectSection(SolutionItems) = preProject + docker\sample\docker-compose.yml = docker\sample\docker-compose.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{C0173851-1515-4BE1-A018-84E0B64A6877}" + ProjectSection(SolutionItems) = preProject + docs\configuration.md = docs\configuration.md + docs\donate-list.md = docs\donate-list.md + docs\questions.md = docs\questions.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tencentScf", "tencentScf", "{DD86F293-AE70-46CF-837C-8870D8F51237}" + ProjectSection(SolutionItems) = preProject + tencentScf\bootstrap = tencentScf\bootstrap + tencentScf\index.sh = tencentScf\index.sh + tencentScf\publish.bat = tencentScf\publish.bat + tencentScf\publish.sh = tencentScf\publish.sh + tencentScf\README.md = tencentScf\README.md + tencentScf\serverless.yml = tencentScf\serverless.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "qinglong", "qinglong", "{1C6CC38A-A5D5-41EF-9072-70AEEEA211F7}" + ProjectSection(SolutionItems) = preProject + qinglong\dotnet-install.sh = qinglong\dotnet-install.sh + qinglong\extra.sh = qinglong\extra.sh + qinglong\ray-dotnet-install.sh = qinglong\ray-dotnet-install.sh + qinglong\README.md = qinglong\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DefaultTasks", "DefaultTasks", "{DE60A16C-CA3B-45E9-8A9D-0E91ACEBDEE0}" + ProjectSection(SolutionItems) = preProject + qinglong\DefaultTasks\bili_dev_task_daily.sh = qinglong\DefaultTasks\bili_dev_task_daily.sh + qinglong\DefaultTasks\bili_dev_task_liveLottery.sh = qinglong\DefaultTasks\bili_dev_task_liveLottery.sh + qinglong\DefaultTasks\bili_dev_task_test.sh = qinglong\DefaultTasks\bili_dev_task_test.sh + qinglong\DefaultTasks\bili_dev_task_unfollowBatched.sh = qinglong\DefaultTasks\bili_dev_task_unfollowBatched.sh + qinglong\DefaultTasks\bili_task_daily.sh = qinglong\DefaultTasks\bili_task_daily.sh + qinglong\DefaultTasks\bili_task_liveLottery.sh = qinglong\DefaultTasks\bili_task_liveLottery.sh + qinglong\DefaultTasks\bili_task_test.sh = qinglong\DefaultTasks\bili_task_test.sh + qinglong\DefaultTasks\bili_task_unfollowBatched.sh = qinglong\DefaultTasks\bili_task_unfollowBatched.sh + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{830361B7-BCC1-4853-879A-761B0FD86826}" + ProjectSection(SolutionItems) = preProject + .github\ISSUE_TEMPLATE\bug-report----.md = .github\ISSUE_TEMPLATE\bug-report----.md + .github\ISSUE_TEMPLATE\bug-report-qinglong----.md = .github\ISSUE_TEMPLATE\bug-report-qinglong----.md + .github\ISSUE_TEMPLATE\feature-request----.md = .github\ISSUE_TEMPLATE\feature-request----.md + .github\ISSUE_TEMPLATE\other----.md = .github\ISSUE_TEMPLATE\other----.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BiliAgentTest", "test\BiliAgentTest\BiliAgentTest.csproj", "{F6B8ED3A-5428-4D26-8172-8B41FBF0C4CF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{75A9CC5C-DF92-4D72-A14C-625AA902855B}" + ProjectSection(SolutionItems) = preProject + docker\build\buildImage.cmd = docker\build\buildImage.cmd + docker\build\buildImage_amd64.cmd = docker\build\buildImage_amd64.cmd + docker\build\buildImage_arm64.cmd = docker\build\buildImage_arm64.cmd + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppServiceTest", "test\AppServiceTest\AppServiceTest.csproj", "{1D9DFF34-71EA-44AE-85B0-1F10C9BA0D81}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DomainServiceTest", "test\DomainServiceTest\DomainServiceTest.csproj", "{26B21C30-7358-4E7B-A73E-2272F10A6CA8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InfrastructureTest", "test\InfrastructureTest\InfrastructureTest.csproj", "{90C1DB73-B3DB-4BE5-AD1A-5248FE47860E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ray.BiliBiliTool.Agent.FunctionalTests", "test\Ray.BiliBiliTool.Agent.FunctionalTests\Ray.BiliBiliTool.Agent.FunctionalTests.csproj", "{16F315CF-056A-4B08-8C3C-A3177EA3CBB9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{2B5FD099-CC28-4FBC-9F20-F20300C5DFD2}" + ProjectSection(SolutionItems) = preProject + scripts\clean.cmd = scripts\clean.cmd + scripts\publish.bat = scripts\publish.bat + scripts\publish.ps1 = scripts\publish.ps1 + scripts\publish.sh = scripts\publish.sh + scripts\ut.ps1 = scripts\ut.ps1 + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ray.BiliBiliTool.Web.Client", "src\Ray.BiliBiliTool.Web.Client\Ray.BiliBiliTool.Web.Client.csproj", "{5772E00F-271F-4B25-8B10-A3C24D8D3E10}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ray.BiliBiliTool.Web", "src\Ray.BiliBiliTool.Web\Ray.BiliBiliTool.Web.csproj", "{189F1FF4-2BFA-4F91-A6B2-00D00AC2910C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazingQuartz.Core", "src\BlazingQuartz.Core\BlazingQuartz.Core.csproj", "{C947CA59-157C-47F0-A842-7912FACDADA7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazingQuartz.Jobs.Abstractions", "src\BlazingQuartz.Jobs.Abstractions\BlazingQuartz.Jobs.Abstractions.csproj", "{5792B0A0-A3F5-461D-AD44-8E8778298BE4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazingQuartz.Jobs", "src\BlazingQuartz.Jobs\BlazingQuartz.Jobs.csproj", "{1F93A755-60A6-4B14-A522-633A732F3B91}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ray.BiliBiliTool.Domain", "src\Ray.BiliBiliTool.Domain\Ray.BiliBiliTool.Domain.csproj", "{C1334E67-CCF9-4F5B-9C1F-B9516A8270AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ray.BiliBiliTool.Infrastructure.EF", "src\Ray.BiliBiliTool.Infrastructure.EF\Ray.BiliBiliTool.Infrastructure.EF.csproj", "{59583288-CE93-42A4-AC9F-67DE347A02A1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DB227D60-0737-45C2-8CEA-F55FDA711301}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB227D60-0737-45C2-8CEA-F55FDA711301}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB227D60-0737-45C2-8CEA-F55FDA711301}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB227D60-0737-45C2-8CEA-F55FDA711301}.Release|Any CPU.Build.0 = Release|Any CPU + {114D45C8-E4BB-47EE-89AC-BD1DC6FA3BAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {114D45C8-E4BB-47EE-89AC-BD1DC6FA3BAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {114D45C8-E4BB-47EE-89AC-BD1DC6FA3BAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {114D45C8-E4BB-47EE-89AC-BD1DC6FA3BAD}.Release|Any CPU.Build.0 = Release|Any CPU + {2039BF6A-5EC4-439C-8D2E-77313075843A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2039BF6A-5EC4-439C-8D2E-77313075843A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2039BF6A-5EC4-439C-8D2E-77313075843A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2039BF6A-5EC4-439C-8D2E-77313075843A}.Release|Any CPU.Build.0 = Release|Any CPU + {7188698C-0A9A-43B2-B3E2-5136B14FDE13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7188698C-0A9A-43B2-B3E2-5136B14FDE13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7188698C-0A9A-43B2-B3E2-5136B14FDE13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7188698C-0A9A-43B2-B3E2-5136B14FDE13}.Release|Any CPU.Build.0 = Release|Any CPU + {3388A58D-91CC-4875-A29F-3E6FC3B44BF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3388A58D-91CC-4875-A29F-3E6FC3B44BF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3388A58D-91CC-4875-A29F-3E6FC3B44BF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3388A58D-91CC-4875-A29F-3E6FC3B44BF5}.Release|Any CPU.Build.0 = Release|Any CPU + {B6AEDD60-9C06-4391-9171-65EBD5E9D77A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6AEDD60-9C06-4391-9171-65EBD5E9D77A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6AEDD60-9C06-4391-9171-65EBD5E9D77A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6AEDD60-9C06-4391-9171-65EBD5E9D77A}.Release|Any CPU.Build.0 = Release|Any CPU + {D5F9FBCE-31BE-4E80-93E2-183CF029431F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5F9FBCE-31BE-4E80-93E2-183CF029431F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5F9FBCE-31BE-4E80-93E2-183CF029431F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5F9FBCE-31BE-4E80-93E2-183CF029431F}.Release|Any CPU.Build.0 = Release|Any CPU + {191C48BD-5CB5-42CA-B394-7A4A9BFA6275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {191C48BD-5CB5-42CA-B394-7A4A9BFA6275}.Debug|Any CPU.Build.0 = Debug|Any CPU + {191C48BD-5CB5-42CA-B394-7A4A9BFA6275}.Release|Any CPU.ActiveCfg = Release|Any CPU + {191C48BD-5CB5-42CA-B394-7A4A9BFA6275}.Release|Any CPU.Build.0 = Release|Any CPU + {7105652A-B1C1-4F9E-BA38-8034BC8B26B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7105652A-B1C1-4F9E-BA38-8034BC8B26B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7105652A-B1C1-4F9E-BA38-8034BC8B26B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7105652A-B1C1-4F9E-BA38-8034BC8B26B4}.Release|Any CPU.Build.0 = Release|Any CPU + {F6B8ED3A-5428-4D26-8172-8B41FBF0C4CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6B8ED3A-5428-4D26-8172-8B41FBF0C4CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6B8ED3A-5428-4D26-8172-8B41FBF0C4CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6B8ED3A-5428-4D26-8172-8B41FBF0C4CF}.Release|Any CPU.Build.0 = Release|Any CPU + {1D9DFF34-71EA-44AE-85B0-1F10C9BA0D81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D9DFF34-71EA-44AE-85B0-1F10C9BA0D81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D9DFF34-71EA-44AE-85B0-1F10C9BA0D81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D9DFF34-71EA-44AE-85B0-1F10C9BA0D81}.Release|Any CPU.Build.0 = Release|Any CPU + {26B21C30-7358-4E7B-A73E-2272F10A6CA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26B21C30-7358-4E7B-A73E-2272F10A6CA8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26B21C30-7358-4E7B-A73E-2272F10A6CA8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26B21C30-7358-4E7B-A73E-2272F10A6CA8}.Release|Any CPU.Build.0 = Release|Any CPU + {90C1DB73-B3DB-4BE5-AD1A-5248FE47860E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90C1DB73-B3DB-4BE5-AD1A-5248FE47860E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90C1DB73-B3DB-4BE5-AD1A-5248FE47860E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90C1DB73-B3DB-4BE5-AD1A-5248FE47860E}.Release|Any CPU.Build.0 = Release|Any CPU + {16F315CF-056A-4B08-8C3C-A3177EA3CBB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16F315CF-056A-4B08-8C3C-A3177EA3CBB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16F315CF-056A-4B08-8C3C-A3177EA3CBB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16F315CF-056A-4B08-8C3C-A3177EA3CBB9}.Release|Any CPU.Build.0 = Release|Any CPU + {5772E00F-271F-4B25-8B10-A3C24D8D3E10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5772E00F-271F-4B25-8B10-A3C24D8D3E10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5772E00F-271F-4B25-8B10-A3C24D8D3E10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5772E00F-271F-4B25-8B10-A3C24D8D3E10}.Release|Any CPU.Build.0 = Release|Any CPU + {189F1FF4-2BFA-4F91-A6B2-00D00AC2910C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {189F1FF4-2BFA-4F91-A6B2-00D00AC2910C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {189F1FF4-2BFA-4F91-A6B2-00D00AC2910C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {189F1FF4-2BFA-4F91-A6B2-00D00AC2910C}.Release|Any CPU.Build.0 = Release|Any CPU + {C947CA59-157C-47F0-A842-7912FACDADA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C947CA59-157C-47F0-A842-7912FACDADA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C947CA59-157C-47F0-A842-7912FACDADA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C947CA59-157C-47F0-A842-7912FACDADA7}.Release|Any CPU.Build.0 = Release|Any CPU + {5792B0A0-A3F5-461D-AD44-8E8778298BE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5792B0A0-A3F5-461D-AD44-8E8778298BE4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5792B0A0-A3F5-461D-AD44-8E8778298BE4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5792B0A0-A3F5-461D-AD44-8E8778298BE4}.Release|Any CPU.Build.0 = Release|Any CPU + {1F93A755-60A6-4B14-A522-633A732F3B91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F93A755-60A6-4B14-A522-633A732F3B91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F93A755-60A6-4B14-A522-633A732F3B91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F93A755-60A6-4B14-A522-633A732F3B91}.Release|Any CPU.Build.0 = Release|Any CPU + {C1334E67-CCF9-4F5B-9C1F-B9516A8270AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1334E67-CCF9-4F5B-9C1F-B9516A8270AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1334E67-CCF9-4F5B-9C1F-B9516A8270AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1334E67-CCF9-4F5B-9C1F-B9516A8270AB}.Release|Any CPU.Build.0 = Release|Any CPU + {59583288-CE93-42A4-AC9F-67DE347A02A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59583288-CE93-42A4-AC9F-67DE347A02A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59583288-CE93-42A4-AC9F-67DE347A02A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59583288-CE93-42A4-AC9F-67DE347A02A1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {38736647-2196-417E-8519-C48A012A63D9} = {AF21E067-3307-4E7F-8CE8-C695E6B61876} + {98051127-2868-4F5C-9B2C-2150975E05F3} = {AF21E067-3307-4E7F-8CE8-C695E6B61876} + {120917DC-474C-448B-9EBB-1B3BA3A20B9D} = {AF21E067-3307-4E7F-8CE8-C695E6B61876} + {DB227D60-0737-45C2-8CEA-F55FDA711301} = {38736647-2196-417E-8519-C48A012A63D9} + {114D45C8-E4BB-47EE-89AC-BD1DC6FA3BAD} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF} + {2039BF6A-5EC4-439C-8D2E-77313075843A} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF} + {110D3D21-8E9B-45AB-9667-6DA1DB3862AC} = {AF21E067-3307-4E7F-8CE8-C695E6B61876} + {7188698C-0A9A-43B2-B3E2-5136B14FDE13} = {110D3D21-8E9B-45AB-9667-6DA1DB3862AC} + {3388A58D-91CC-4875-A29F-3E6FC3B44BF5} = {98051127-2868-4F5C-9B2C-2150975E05F3} + {B6AEDD60-9C06-4391-9171-65EBD5E9D77A} = {98051127-2868-4F5C-9B2C-2150975E05F3} + {D5F9FBCE-31BE-4E80-93E2-183CF029431F} = {120917DC-474C-448B-9EBB-1B3BA3A20B9D} + {191C48BD-5CB5-42CA-B394-7A4A9BFA6275} = {120917DC-474C-448B-9EBB-1B3BA3A20B9D} + {7105652A-B1C1-4F9E-BA38-8034BC8B26B4} = {120917DC-474C-448B-9EBB-1B3BA3A20B9D} + {73DD457B-E06E-45ED-A6BA-7E3C02F8BDF1} = {F3DE0D72-426B-4AD9-B3ED-3343CF4223F1} + {61613EF4-3644-42D4-A620-70547291FB38} = {73DD457B-E06E-45ED-A6BA-7E3C02F8BDF1} + {A93210FD-27B6-40E4-B08D-391F96CA2754} = {F3DE0D72-426B-4AD9-B3ED-3343CF4223F1} + {2F1CB892-336C-4672-8A0A-FBAEB4B9EA8A} = {A93210FD-27B6-40E4-B08D-391F96CA2754} + {DD86F293-AE70-46CF-837C-8870D8F51237} = {F3DE0D72-426B-4AD9-B3ED-3343CF4223F1} + {1C6CC38A-A5D5-41EF-9072-70AEEEA211F7} = {F3DE0D72-426B-4AD9-B3ED-3343CF4223F1} + {DE60A16C-CA3B-45E9-8A9D-0E91ACEBDEE0} = {1C6CC38A-A5D5-41EF-9072-70AEEEA211F7} + {830361B7-BCC1-4853-879A-761B0FD86826} = {73DD457B-E06E-45ED-A6BA-7E3C02F8BDF1} + {F6B8ED3A-5428-4D26-8172-8B41FBF0C4CF} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF} + {75A9CC5C-DF92-4D72-A14C-625AA902855B} = {A93210FD-27B6-40E4-B08D-391F96CA2754} + {1D9DFF34-71EA-44AE-85B0-1F10C9BA0D81} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF} + {26B21C30-7358-4E7B-A73E-2272F10A6CA8} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF} + {90C1DB73-B3DB-4BE5-AD1A-5248FE47860E} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF} + {16F315CF-056A-4B08-8C3C-A3177EA3CBB9} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF} + {2B5FD099-CC28-4FBC-9F20-F20300C5DFD2} = {F3DE0D72-426B-4AD9-B3ED-3343CF4223F1} + {5772E00F-271F-4B25-8B10-A3C24D8D3E10} = {38736647-2196-417E-8519-C48A012A63D9} + {189F1FF4-2BFA-4F91-A6B2-00D00AC2910C} = {38736647-2196-417E-8519-C48A012A63D9} + {C947CA59-157C-47F0-A842-7912FACDADA7} = {98051127-2868-4F5C-9B2C-2150975E05F3} + {5792B0A0-A3F5-461D-AD44-8E8778298BE4} = {98051127-2868-4F5C-9B2C-2150975E05F3} + {1F93A755-60A6-4B14-A522-633A732F3B91} = {98051127-2868-4F5C-9B2C-2150975E05F3} + {C1334E67-CCF9-4F5B-9C1F-B9516A8270AB} = {120917DC-474C-448B-9EBB-1B3BA3A20B9D} + {59583288-CE93-42A4-AC9F-67DE347A02A1} = {110D3D21-8E9B-45AB-9667-6DA1DB3862AC} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {197319DA-1148-4A99-847C-8B270B6A29AB} + EndGlobalSection +EndGlobal diff --git a/bruno/.env.sample b/bruno/.env.sample new file mode 100644 index 0000000..054e96b --- /dev/null +++ b/bruno/.env.sample @@ -0,0 +1,8 @@ +phone= +pwd= +mid= +buvid= +csrf= +access_key= +cookieStr= +device_id= \ No newline at end of file diff --git a/bruno/api.bilibili.com/x/space/folder.bru b/bruno/api.bilibili.com/x/space/folder.bru new file mode 100644 index 0000000..42ab525 --- /dev/null +++ b/bruno/api.bilibili.com/x/space/folder.bru @@ -0,0 +1,3 @@ +meta { + name: space +} diff --git a/bruno/api.bilibili.com/x/space/wbi-acc-info.bru b/bruno/api.bilibili.com/x/space/wbi-acc-info.bru new file mode 100644 index 0000000..b7254de --- /dev/null +++ b/bruno/api.bilibili.com/x/space/wbi-acc-info.bru @@ -0,0 +1,42 @@ +meta { + name: wbi-acc-info + type: http + seq: 1 +} + +get { + url: https://api.bilibili.com/x/space/wbi/acc/info?mid=23947287&token&platform=web&web_location=1550101&dm_img_list=[]&dm_img_str=V2ViR0wgMS4wIChPcGVuR0wgRVMgMi4wIENocm9taXVtKQ&dm_cover_img_str=QU5HTEUgKEFNRCwgQU1EIFJhZGVvbihUTSkgR3JhcGhpY3MgKDB4MDAwMDE2MzgpIERpcmVjdDNEMTEgdnNfNV8wIHBzXzVfMCwgRDNEMTEpR29vZ2xlIEluYy4gKEFNRC&dm_img_inter={"ds":[],"wh":[5563,4171,107],"of":[66,132,66]}&w_webid=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzcG1faWQiOiIzMzMuMTM4NyIsImJ1dmlkIjoiQ0VGODNDQjAtMzY2NC0zNDU4LTI5RTctRTJFOENCQjY0NzlCNjU2MjFpbmZvYyIsInVzZXJfYWdlbnQiOiJNb3ppbGxhLzUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW42NDsgeDY0KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvMTM3LjAuMC4wIFNhZmFyaS81MzcuMzYgRWRnLzEzNy4wLjAuMCIsImJ1dmlkX2ZwIjoiMDNjNGYyNWQyZTlmMDZkNjU4NzJlMmVhNTdiMjJmMzkiLCJiaWxpX3RpY2tldCI6ImV5SmhiR2NpT2lKSVV6STFOaUlzSW10cFpDSTZJbk13TXlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKbGVIQWlPakUzTkRjM01qYzVORGdzSW1saGRDSTZNVGMwTnpRMk9EWTRPQ3dpY0d4MElqb3RNWDAuQmVkeFFPX3VZMllldFVFb0FvWE5hellBNzFTTkdhc0JtSkNnaFFsVy1iTSIsImNyZWF0ZWRfYXQiOjE3NDc0Njg4NDEsInR0bCI6ODY0MDAsInVybCI6Ii8yMzk0NzI4NyIsInJlc3VsdCI6MCwiaXNzIjoiZ2FpYSIsImlhdCI6MTc0NzQ2ODg0MX0.IhLw1v9Auwys4SFZ7PFaTXgkVqodmz67ZdVPORbnmcw3rxirilDqngVFpr3AYjUQ8hx-gmGTrMgeus12QE4zn9ql-wjTRKzbi9G2WEzDKEwHMNBKVIepaSZqHYIbKBwWsjqdEL9paDeRDpPiZ37YJ4YMKNsFfEP2yC_4ke2_KS2cgfSaaOhaXtXfEZortr_KvKWhY3gtJKC77HqEerZEID5hda8oqCTxZbb7gV7DDuncJ803K9H5ezWn-8a-y3eSdpbBB8Td5-u8At9mhHVgrKcODM7gi-Lhbnb86m7p4yyBoPs_iucDQJSGvs9Lijio57bL60Qm6_mIz0p49rzFWA&w_rid=160dcf0623b0733688e8940d2f6763e3&wts=1747468845 + body: none + auth: inherit +} + +params:query { + mid: 23947287 + token: + platform: web + web_location: 1550101 + dm_img_list: [] + dm_img_str: V2ViR0wgMS4wIChPcGVuR0wgRVMgMi4wIENocm9taXVtKQ + dm_cover_img_str: QU5HTEUgKEFNRCwgQU1EIFJhZGVvbihUTSkgR3JhcGhpY3MgKDB4MDAwMDE2MzgpIERpcmVjdDNEMTEgdnNfNV8wIHBzXzVfMCwgRDNEMTEpR29vZ2xlIEluYy4gKEFNRC + dm_img_inter: {"ds":[],"wh":[5563,4171,107],"of":[66,132,66]} + w_webid: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzcG1faWQiOiIzMzMuMTM4NyIsImJ1dmlkIjoiQ0VGODNDQjAtMzY2NC0zNDU4LTI5RTctRTJFOENCQjY0NzlCNjU2MjFpbmZvYyIsInVzZXJfYWdlbnQiOiJNb3ppbGxhLzUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW42NDsgeDY0KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvMTM3LjAuMC4wIFNhZmFyaS81MzcuMzYgRWRnLzEzNy4wLjAuMCIsImJ1dmlkX2ZwIjoiMDNjNGYyNWQyZTlmMDZkNjU4NzJlMmVhNTdiMjJmMzkiLCJiaWxpX3RpY2tldCI6ImV5SmhiR2NpT2lKSVV6STFOaUlzSW10cFpDSTZJbk13TXlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKbGVIQWlPakUzTkRjM01qYzVORGdzSW1saGRDSTZNVGMwTnpRMk9EWTRPQ3dpY0d4MElqb3RNWDAuQmVkeFFPX3VZMllldFVFb0FvWE5hellBNzFTTkdhc0JtSkNnaFFsVy1iTSIsImNyZWF0ZWRfYXQiOjE3NDc0Njg4NDEsInR0bCI6ODY0MDAsInVybCI6Ii8yMzk0NzI4NyIsInJlc3VsdCI6MCwiaXNzIjoiZ2FpYSIsImlhdCI6MTc0NzQ2ODg0MX0.IhLw1v9Auwys4SFZ7PFaTXgkVqodmz67ZdVPORbnmcw3rxirilDqngVFpr3AYjUQ8hx-gmGTrMgeus12QE4zn9ql-wjTRKzbi9G2WEzDKEwHMNBKVIepaSZqHYIbKBwWsjqdEL9paDeRDpPiZ37YJ4YMKNsFfEP2yC_4ke2_KS2cgfSaaOhaXtXfEZortr_KvKWhY3gtJKC77HqEerZEID5hda8oqCTxZbb7gV7DDuncJ803K9H5ezWn-8a-y3eSdpbBB8Td5-u8At9mhHVgrKcODM7gi-Lhbnb86m7p4yyBoPs_iucDQJSGvs9Lijio57bL60Qm6_mIz0p49rzFWA + w_rid: 160dcf0623b0733688e8940d2f6763e3 + wts: 1747468845 +} + +headers { + accept: */* + accept-language: en + dnt: 1 + origin: https://space.bilibili.com + priority: u=1, i + referer: https://space.bilibili.com/23947287 + sec-ch-ua: "Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24" + sec-ch-ua-mobile: ?0 + sec-ch-ua-platform: "Windows" + sec-fetch-dest: empty + sec-fetch-mode: cors + sec-fetch-site: same-site + user-agent: {{user-agent}} + Cookie: {{cookieStr}} +} diff --git a/bruno/api.bilibili.com/x/vip/folder.bru b/bruno/api.bilibili.com/x/vip/folder.bru new file mode 100644 index 0000000..ac73e48 --- /dev/null +++ b/bruno/api.bilibili.com/x/vip/folder.bru @@ -0,0 +1,3 @@ +meta { + name: vip +} diff --git a/bruno/api.bilibili.com/x/vip/vip_center/folder.bru b/bruno/api.bilibili.com/x/vip/vip_center/folder.bru new file mode 100644 index 0000000..0f54197 --- /dev/null +++ b/bruno/api.bilibili.com/x/vip/vip_center/folder.bru @@ -0,0 +1,3 @@ +meta { + name: vip_center +} diff --git a/bruno/api.bilibili.com/x/vip/vip_center/sign_in/folder.bru b/bruno/api.bilibili.com/x/vip/vip_center/sign_in/folder.bru new file mode 100644 index 0000000..40cbc12 --- /dev/null +++ b/bruno/api.bilibili.com/x/vip/vip_center/sign_in/folder.bru @@ -0,0 +1,3 @@ +meta { + name: sign_in +} diff --git a/bruno/api.bilibili.com/x/vip/vip_center/sign_in/three_days_sign.bru b/bruno/api.bilibili.com/x/vip/vip_center/sign_in/three_days_sign.bru new file mode 100644 index 0000000..22dbdb1 --- /dev/null +++ b/bruno/api.bilibili.com/x/vip/vip_center/sign_in/three_days_sign.bru @@ -0,0 +1,88 @@ +meta { + name: three_days_sign + type: http + seq: 1 +} + +get { + url: https://api.bilibili.com/x/vip/vip_center/sign_in/three_days_sign?access_key={{access_key}}&appkey={{appKey}}&build=8451100&csrf={{csrf}}&device=phone&disable_rcmd=0&mobi_app=android&platform=android&statistics={"appId":1,"platform":3,"version":"8.45.1","abtest":""}&t=1748747431084&ts=1748747431&web_location=666.146&sign=e95c76f976aed346f84cdc7098f8c35e + body: none + auth: inherit +} + +params:query { + access_key: {{access_key}} + appkey: {{appKey}} + build: 8451100 + csrf: {{csrf}} + device: phone + disable_rcmd: 0 + mobi_app: android + platform: android + statistics: {"appId":1,"platform":3,"version":"8.45.1","abtest":""} + t: 1748747431084 + ts: 1748747431 + web_location: 666.146 + sign: e95c76f976aed346f84cdc7098f8c35e +} + +headers { + Host: api.bilibili.com + Cookie: {{cookieStr}} + accept: application/json, text/plain, */* + bili-http-engine: ignet + buvid: {{buvid}} + native_api_from: h5 + referer: https://big.bilibili.com/mobile/index + user-agent: {{user-agent}} + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-metadata-legal-region: CN + x-bili-mid: {{mid}} + x-bili-net-bin: DQAAgL8gAQ + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDg3NzU1OTksImlhdCI6MTc0ODc0NjQ5OSwiYnV2aWQiOiJYVUE1NjUxQTlFREY3Mzg3MTUzQTk0NUNERTk2Q0FEQ0I2MDAwIn0.k0x2o3e2Q3W-6Wzc56IhbLgSjDKTaAuUV9om7K213fI + x-bili-trace-id: 25ad3d0de58f794c18d1a884fc683bc4:18d1a884fc683bc4:0:0 +} + +docs { + Response sample: + + ```json + { + "code": 0, + "message": "0", + "ttl": 1, + "data": { + "big_point": { + "point": 405, + "expire_point": 0, + "expire_time": 0, + "expire_days": 0 + }, + "three_day_sign": { + "previous_vip_status": 0, + "vip_status": 1, + "day": 5, + "signed": false, + "count": 2, + "has_coupon": false, + "countdown": 0, + "icon": "", + "score": 5, + "vip_score": 5, + "explain": "", + "exp_value": 3, + "received_coupon": false, + "day1_icon": "https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day1_icon.png", + "day2_icon": "https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day2_icon.png", + "day3_icon": "https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_icon.png", + "day3_icon_vip": "https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_icon_vip.png", + "day3_win_img": "https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_win_img.png", + "day3_win_img_vip": "https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_win_img_vip.png", + "day3_icon_received": "https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_icon_received.png", + "day3_icon_vip_received": "https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_icon_vip_received.png", + "duration": 7 + } + } + } + ``` +} diff --git a/bruno/api.bilibili.com/x/vip/web/folder.bru b/bruno/api.bilibili.com/x/vip/web/folder.bru new file mode 100644 index 0000000..667ca52 --- /dev/null +++ b/bruno/api.bilibili.com/x/vip/web/folder.bru @@ -0,0 +1,3 @@ +meta { + name: web +} diff --git a/bruno/api.bilibili.com/x/vip/web/vip_center/folder.bru b/bruno/api.bilibili.com/x/vip/web/vip_center/folder.bru new file mode 100644 index 0000000..0f54197 --- /dev/null +++ b/bruno/api.bilibili.com/x/vip/web/vip_center/folder.bru @@ -0,0 +1,3 @@ +meta { + name: vip_center +} diff --git a/bruno/api.bilibili.com/x/vip/web/vip_center/modules.bru b/bruno/api.bilibili.com/x/vip/web/vip_center/modules.bru new file mode 100644 index 0000000..4f85fa0 --- /dev/null +++ b/bruno/api.bilibili.com/x/vip/web/vip_center/modules.bru @@ -0,0 +1,2805 @@ +meta { + name: modules + type: http + seq: 2 +} + +get { + url: https://api.bilibili.com/x/vip/web/vip_center/modules?access_key={{access_key}}&act_id=872&appkey={{appKey}}&build=8451100&csrf={{csrf}}&device=phone&disable_rcmd=0&is_selected=true&mobi_app=android&platform=android&select_modules=VipExclusive,BigPoint&statistics={"appId":1,"platform":3,"version":"8.45.1","abtest":""}&ts=1748751549&web_location=666.146&sign=4b1610076d36f3eaf400307591fce0d0 + body: none + auth: inherit +} + +params:query { + access_key: {{access_key}} + act_id: 872 + appkey: {{appKey}} + build: 8451100 + csrf: {{csrf}} + device: phone + disable_rcmd: 0 + is_selected: true + mobi_app: android + platform: android + select_modules: VipExclusive,BigPoint + statistics: {"appId":1,"platform":3,"version":"8.45.1","abtest":""} + ts: 1748751549 + web_location: 666.146 + sign: 4b1610076d36f3eaf400307591fce0d0 +} + +headers { + Host: api.bilibili.com + Cookie: {{cookieStr}} + accept: application/json, text/plain, */* + bili-http-engine: ignet + buvid: {{buvid}} + native_api_from: h5 + referer: https://big.bilibili.com/mobile/index + user-agent: {{user-agent}} + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-metadata-legal-region: CN + x-bili-mid: {{mid}} + x-bili-net-bin: DQAAgL8gAQ + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDg3NzU1OTksImlhdCI6MTc0ODc0NjQ5OSwiYnV2aWQiOiJYVUE1NjUxQTlFREY3Mzg3MTUzQTk0NUNERTk2Q0FEQ0I2MDAwIn0.k0x2o3e2Q3W-6Wzc56IhbLgSjDKTaAuUV9om7K213fI + x-bili-trace-id: 32b84b9079d40a0946714475fc683bd4:46714475fc683bd4:0:0 +} + +docs { + Response sample: + + ```json + { + "code": 0, + "message": "0", + "ttl": 1, + "data": { + "privileges": { + "list": [], + "privileges_covers": [], + "num": 0 + }, + "banner": [], + "union_vip": { + "union_vips": [] + }, + "other_open_info": { + "open_infos": [] + }, + "benefits": [], + "big_point": { + "point_info": { + "point": 405, + "expire_point": 0, + "expire_time": 0, + "expire_days": 0 + }, + "sign_info": { + "sign_remind": true, + "benefit": 10, + "bonus_benefit": 0, + "normal_remind": true, + "muggle_task": false, + "exp_value": 3 + }, + "sku_info": { + "skus": [{ + "base": { + "token": "1216669149548369495", + "title": "东航国内机票立减50元优惠券", + "picture": "https://i0.hdslb.com/bfs/activity-plat/58e67cefd0a194b6380d09749ea6a9b6bbd18a84.png", + "rotation_pictures": ["https://i0.hdslb.com/bfs/activity-plat/58e67cefd0a194b6380d09749ea6a9b6bbd18a84.png"], + "price": { + "origin": 12999, + "promotion": { + "price": 399, + "type": 2, + "discount": 0, + "label": "秒杀" + }, + "sale": 12999 + }, + "inventory": { + "available_num": 2980, + "used_num": 2150, + "surplus_num": 830, + "total_num": 5980, + "is_sold_out": false, + "next_sold_time": 1748836800, + "status": 1 + }, + "user_type": 2, + "exchange_limit_type": 2, + "exchange_limit_num": 1, + "start_time": 1748593800, + "end_time": 1750262400, + "state": 2, + "priority": 97 + } + }, { + "base": { + "token": "1214802504886365529", + "title": "BEMOE 初音未来 樱花未来 可爱体UWA系列 毛绒4wa", + "picture": "https://i0.hdslb.com/bfs/activity-plat/b782d7228e8a58d2562d26f33448a50519ce4ec5.png", + "rotation_pictures": ["https://i0.hdslb.com/bfs/activity-plat/b782d7228e8a58d2562d26f33448a50519ce4ec5.png"], + "price": { + "origin": 33600, + "promotion": null, + "sale": 33600 + }, + "inventory": { + "available_num": 1, + "used_num": 0, + "surplus_num": 1, + "total_num": 20, + "is_sold_out": false, + "next_sold_time": -62135596800, + "status": 1 + }, + "user_type": 2, + "exchange_limit_type": 2, + "exchange_limit_num": 1, + "start_time": 1748404800, + "end_time": 1751256000, + "state": 2, + "priority": 96 + } + }, { + "base": { + "token": "1214800503834258777", + "title": "BEMOE 初音未来 UWA可爱体系列 亚克力立牌 呜哇满足", + "picture": "https://i0.hdslb.com/bfs/activity-plat/186fa29b28e671422010bf9d8bd0f5e6b46556b9.png", + "rotation_pictures": ["https://i0.hdslb.com/bfs/activity-plat/186fa29b28e671422010bf9d8bd0f5e6b46556b9.png"], + "price": { + "origin": 23333, + "promotion": null, + "sale": 23333 + }, + "inventory": { + "available_num": 3, + "used_num": 2, + "surplus_num": 1, + "total_num": 35, + "is_sold_out": false, + "next_sold_time": -62135596800, + "status": 1 + }, + "user_type": 2, + "exchange_limit_type": 2, + "exchange_limit_num": 1, + "start_time": 1748404800, + "end_time": 1751256000, + "state": 2, + "priority": 96 + } + }, { + "base": { + "token": "1214791752167304537", + "title": "暴蒙《蓝色监狱》烫金KV徽章洁世一", + "picture": "https://i0.hdslb.com/bfs/activity-plat/a23b188dcc430c4348b8cccb7ab056bc5527a67d.png", + "rotation_pictures": ["https://i0.hdslb.com/bfs/activity-plat/a23b188dcc430c4348b8cccb7ab056bc5527a67d.png"], + "price": { + "origin": 3900, + "promotion": null, + "sale": 3900 + }, + "inventory": { + "available_num": 5, + "used_num": 4, + "surplus_num": 1, + "total_num": 10, + "is_sold_out": false, + "next_sold_time": -62135596800, + "status": 1 + }, + "user_type": 2, + "exchange_limit_type": 2, + "exchange_limit_num": 1, + "start_time": 1748404800, + "end_time": 1751256000, + "state": 2, + "priority": 96 + } + }] + }, + "point_switch_off": false, + "tips": [{ + "content": "今日签到10大积分" + }, { + "content": "今天的任务还没有做完哦" + }], + "button_text": "赚大积分", + "sku_price_hidden": false + }, + "welfare": {}, + "experience": { + "level": 0, + "cur_exp": 0, + "next_exp": 0, + "is_senior_member": 0, + "is_get_exp": false, + "is_task_complete": false, + "state": 0 + }, + "vip_exclusive": [{ + "tap_title": "猜你喜欢", + "tap_type": 7, + "seasons": [{ + "season_id": 48006, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "电影", + "title": "逆行人生", + "sub_title": "徐峥现实主义喜剧", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/9d2e5c77a7f9a665ca8349d45d283d1bc68d805b.png", + "link": "https://www.bilibili.com/bangumi/play/ss48006?theme=movie", + "track_params": null + }, { + "season_id": 39533, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.3", + "left_subscript": "电影", + "title": "007:大破量子危机", + "sub_title": "邦德为爱复仇", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/742a56d34b61e5d29ea413a78d0540d32681ac73.png", + "link": "https://www.bilibili.com/bangumi/play/ss39533?theme=movie", + "track_params": null + }, { + "season_id": 43361, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.8", + "left_subscript": "纪录片", + "title": "挤痘大师 第2季", + "sub_title": "皮肤疑难杂症的解决", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/1d6fe011a5d474a32eeb1ddb250e17de3b69c459.png", + "link": "https://www.bilibili.com/bangumi/play/ss43361", + "track_params": null + }, { + "season_id": 44560, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "电影", + "title": "太极张三丰", + "sub_title": "少年练就武林奇功", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/e8e77dc7bbc716a5159358d311cde5c2cdfddba9.png", + "link": "https://www.bilibili.com/bangumi/play/ss44560?theme=movie", + "track_params": null + }, { + "season_id": 44175, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.5", + "left_subscript": "纪录片", + "title": "挤痘大师 第3季", + "sub_title": "皮肤疑难杂症的解决", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/f8df75ad1d7553a4fd26c1b0d4548053c4d3e617.png", + "link": "https://www.bilibili.com/bangumi/play/ss44175", + "track_params": null + }, { + "season_id": 39247, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "纪录片", + "title": "人间世 第一季", + "sub_title": "生死考验,人间世态", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/32575c2a7feef1a94c3ec1b0d229cfcfd0f01549.png", + "link": "https://www.bilibili.com/bangumi/play/ss39247", + "track_params": null + }, { + "season_id": 47463, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.4", + "left_subscript": "电影", + "title": "恶世之子", + "sub_title": "美国无差别狙击案件", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/5a18f11ddecbde05d3cba47502d3102dc1eda995.png", + "link": "https://www.bilibili.com/bangumi/play/ss47463?theme=movie", + "track_params": null + }, { + "season_id": 48826, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.9", + "left_subscript": "纪录片", + "title": "生命奇观", + "sub_title": "无穷小亮自然纪录片", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/0ea69fe30f7f1f9637d42e354a0a4fa96078be4f.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss48826", + "track_params": null + }, { + "season_id": 45789, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.1", + "left_subscript": "电视剧", + "title": "抓马侦探2", + "sub_title": "脑洞反转不停歇!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/4edf685280fba5d957ad57a3b15a5ebc3190fbbb.png", + "link": "https://www.bilibili.com/bangumi/play/ss45789", + "track_params": null + }, { + "season_id": 39866, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.3", + "left_subscript": "纪录片", + "title": "生活如沸2", + "sub_title": "煮沸背后的人生百味", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/5f48c27c848a01c1de468fa42bb46f1c011632c8.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss39866", + "track_params": null + }, { + "season_id": 48762, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.1", + "left_subscript": "电影", + "title": "勿言推理 电影版", + "sub_title": "菅田将晖硬核破案", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/32004bfaeabca5198803f9497e8b22313336d982.png", + "link": "https://www.bilibili.com/bangumi/play/ss48762?theme=movie", + "track_params": null + }, { + "season_id": 68491, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.5", + "left_subscript": "电影", + "title": "解除好友2:暗网", + "sub_title": "捡电脑惹出暗网危机", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/a6f88553ffa9c5f4701e74963e6a280609e49bc7.png", + "link": "https://www.bilibili.com/bangumi/play/ss68491?theme=movie", + "track_params": null + }, { + "season_id": 87199, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "电影", + "title": "不说话的爱", + "sub_title": "张艺兴演绎无声父爱", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/ff7d6574e4a48e16c2b9a69dc4af7485b57ee5a7.png", + "link": "https://www.bilibili.com/bangumi/play/ss87199?theme=movie", + "track_params": null + }, { + "season_id": 45936, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.8", + "left_subscript": "电视剧", + "title": "片场日记2", + "sub_title": "高分爆梗喜剧续作", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/4bda3d5e37b354946c0a866a20e7fffc495ce627.png", + "link": "https://www.bilibili.com/bangumi/play/ss45936", + "track_params": null + }, { + "season_id": 33623, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "电视剧", + "title": "西游记续集", + "sub_title": "师傅被妖怪抓走啦!", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/2a4782407b25733fbef641595214aa004247ae19.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33623", + "track_params": null + }, { + "season_id": 48014, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.2", + "left_subscript": "电影", + "title": "野孩子", + "sub_title": "流浪兄弟相互救赎!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/94be52c8ad4ed1a4ec74c118bf2260d0fc1df03f.png", + "link": "https://www.bilibili.com/bangumi/play/ss48014?theme=movie", + "track_params": null + }, { + "season_id": 47703, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.5", + "left_subscript": "电影", + "title": "宝贝老板", + "sub_title": "宝贝总裁来袭!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/5a79d2e49059f4e4f3f3977994ae334ea7fad32a.png", + "link": "https://www.bilibili.com/bangumi/play/ss47703?theme=movie", + "track_params": null + }, { + "season_id": 45582, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "电影", + "title": "蜡笔小新:新婚旅行飓风之遗失的野原广志", + "sub_title": "广志争夺战开启", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/ebebac20774747787cb01521e1165e97e47d4b6b.png", + "link": "https://www.bilibili.com/bangumi/play/ss45582?theme=movie", + "track_params": null + }, { + "season_id": 45579, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.6", + "left_subscript": "电影", + "title": "蜡笔小新:我的搬家物语 仙人掌大袭击", + "sub_title": "小新洒泪告别风间", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/d772a242b74b9ccc18b622a61a13e4f0005791e4.png", + "link": "https://www.bilibili.com/bangumi/play/ss45579?theme=movie", + "track_params": null + }, { + "season_id": 46350, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "纪录片", + "title": "何以中国", + "sub_title": "百年中国考古", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/81910f6dc47e499929d2e4c234531e1b46285cb0.png", + "link": "https://www.bilibili.com/bangumi/play/ss46350", + "track_params": null + }, { + "season_id": 39358, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "8.4", + "left_subscript": "电视剧", + "title": "六神无主", + "sub_title": "一个身体五个“魂”", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/d4e3e965ce95d3ea21d73fe8d84594f75770f46a.png", + "link": "https://www.bilibili.com/bangumi/play/ss39358", + "track_params": null + }, { + "season_id": 46178, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "纪录片", + "title": "地球脉动 第三季", + "sub_title": "口碑系列重磅回归", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/571c98fe1d9b532c98cb5aa23b9b875ced8dcae0.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss46178", + "track_params": null + }, { + "season_id": 37677, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.6", + "left_subscript": "电影", + "title": "笑破铁幕", + "sub_title": "无厘头搞笑电影先驱", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/6d8ea4a7cc81eb72e51fc68bef346f1f41904778.png", + "link": "https://www.bilibili.com/bangumi/play/ss37677?theme=movie", + "track_params": null + }, { + "season_id": 47750, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.2", + "left_subscript": "电影", + "title": "名侦探柯南:百万美元的五棱星", + "sub_title": "柯南与基德的对决", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/aa71c6cc991b3c60d8a7b5557e025de5ca422799.png", + "link": "https://www.bilibili.com/bangumi/play/ss47750?theme=movie", + "track_params": null + }, { + "season_id": 33628, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.5", + "left_subscript": "纪录片", + "title": "体内怪物 第八季", + "sub_title": "致命寄生虫", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/0afbcdf68628459419b095911e6cc2b5531063b2.png", + "link": "https://www.bilibili.com/bangumi/play/ss33628", + "track_params": null + }, { + "season_id": 47860, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "综艺", + "title": "此生要去的100个地方", + "sub_title": "共赴祖国大好河山!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/a9f0c60f74a21b9bd3f1451f61912d4df5dd403c.png", + "link": "https://www.bilibili.com/bangumi/play/ss47860", + "track_params": null + }, { + "season_id": 31800, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.7", + "left_subscript": "综艺", + "title": "非正式会谈 第6季", + "sub_title": "发现新世界的入口", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/83fd62f43b83f2e3916c5c112aa853e6133318f0.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss31800", + "track_params": null + }, { + "season_id": 45660, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.5", + "left_subscript": "电视剧", + "title": "西部世界 第一季", + "sub_title": "HBO科幻神剧", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/6befebb8e6390b1f496938c08ac43c0a7cf4fe3f.png", + "link": "https://www.bilibili.com/bangumi/play/ss45660", + "track_params": null + }, { + "season_id": 35874, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.5", + "left_subscript": "综艺", + "title": "2020最美的夜 bilibili晚会", + "sub_title": "来B站一起跨年", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/72cb12d4fd380865529ed16603c1001436977589.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss35874", + "track_params": null + }, { + "season_id": 41716, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "电视剧", + "title": "笑傲江湖", + "sub_title": "快意江湖,情仇难断", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/e82f485783df97ba0907576f29b9e6219769f1e4.png", + "link": "https://www.bilibili.com/bangumi/play/ss41716", + "track_params": null + }, { + "season_id": 90926, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "5.1", + "left_subscript": "电影", + "title": "老狗", + "sub_title": "戏骨上演全员恶人", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/9d7495085282c939491652ddd23d7fef3e116742.png", + "link": "https://www.bilibili.com/bangumi/play/ss90926?theme=movie", + "track_params": null + }, { + "season_id": 47684, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "4.4", + "left_subscript": "电影", + "title": "海关战线", + "sub_title": "激燃海战与人性深渊", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/1d2585639d6b4ff47e2f18706e075f7663752089.png", + "link": "https://www.bilibili.com/bangumi/play/ss47684?theme=movie", + "track_params": null + }, { + "season_id": 47918, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.8", + "left_subscript": "电影", + "title": "加菲猫家族", + "sub_title": "经典重启 笑中带泪", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/f4dbf78f0819771d12422bb61fd8874ec2b68b22.png", + "link": "https://www.bilibili.com/bangumi/play/ss47918?theme=movie", + "track_params": null + }, { + "season_id": 95645, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.5", + "left_subscript": "纪录片", + "title": "舌尖上的中国4", + "sub_title": "博大精深的中国饮食", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/d9be392c25addb449de9760cae48233ac1a1daf7.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss95645", + "track_params": null + }, { + "season_id": 48311, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.5", + "left_subscript": "电影", + "title": "重生", + "sub_title": "害必除 仇必报", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/ff7257c083d6684e4e5d178d8e553c89f6d79a42.png", + "link": "https://www.bilibili.com/bangumi/play/ss48311?theme=movie", + "track_params": null + }, { + "season_id": 48603, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "电影", + "title": "乔妍的心事", + "sub_title": "姐妹花双陷身份疑云", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/0d9dc6e9855b16260305cdc323fd7e7b94ad2484.png", + "link": "https://www.bilibili.com/bangumi/play/ss48603?theme=movie", + "track_params": null + }] + }, { + "tap_title": "电影", + "tap_type": 2, + "seasons": [{ + "season_id": 48415, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.9", + "left_subscript": "喜剧", + "title": "唐探1900", + "sub_title": "神探CP携手破悬案", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/07574ea9959f75d89b3c7f0f8e125188b7fadb0a.png", + "link": "https://www.bilibili.com/bangumi/play/ss48415?theme=movie", + "track_params": null + }, { + "season_id": 99873, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.3", + "left_subscript": "喜剧", + "title": "黄沙漫天", + "sub_title": "小沈阳伪装骗巨款", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/16f93b5398f4473fb1c202a60519d8bb91035af7.png", + "link": "https://www.bilibili.com/bangumi/play/ss99873?theme=movie", + "track_params": null + }, { + "season_id": 90102, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "剧情", + "title": "向阳·花", + "sub_title": "诠释女性绝境逆袭", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/c3d8516d035853f0cb3193e0b04c53e3ac0512ae.png", + "link": "https://www.bilibili.com/bangumi/play/ss90102?theme=movie", + "track_params": null + }, { + "season_id": 32419, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "动作", + "title": "碟中谍4", + "sub_title": "阿汤哥飞跃迪拜塔?", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/9f823b8936176c2de9f8a09094198013abe7eb0d.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss32419?theme=movie", + "track_params": null + }, { + "season_id": 48413, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "剧情", + "title": "误杀3", + "sub_title": "肖央再演绝望父亲", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/2d16c9853e9845b7aef9a9129e53a63fefb551ba.png", + "link": "https://www.bilibili.com/bangumi/play/ss48413?theme=movie", + "track_params": null + }, { + "season_id": 48548, + "right_superscript": "http://i0.hdslb.com/bfs/vip/a41bd3a088b9d869806406acb28ac7f2a1150a88.png", + "right_subscript": "", + "left_subscript": "动作", + "title": "毒液:最后一舞", + "sub_title": "“毒液”系列终章", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/22d871c521f6bfae45a990628c61fb4f87f4ee06.png", + "link": "https://www.bilibili.com/bangumi/play/ss48548?theme=movie", + "track_params": null + }, { + "season_id": 48894, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "冒险", + "title": "根本停不下来", + "sub_title": "刹车失灵全家狂飙!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/30e0887dba817b3b701a2bbe3f4f0cab1f0c6155.png", + "link": "https://www.bilibili.com/bangumi/play/ss48894?theme=movie", + "track_params": null + }, { + "season_id": 87199, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "剧情", + "title": "不说话的爱", + "sub_title": "张艺兴演绎无声父爱", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/ff7d6574e4a48e16c2b9a69dc4af7485b57ee5a7.png", + "link": "https://www.bilibili.com/bangumi/play/ss87199?theme=movie", + "track_params": null + }, { + "season_id": 68561, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.3", + "left_subscript": "剧情", + "title": "胜券在握", + "sub_title": "邓超智斗黑心公司", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/2d325939328078ade57877955602ba508807d70b.png", + "link": "https://www.bilibili.com/bangumi/play/ss68561?theme=movie", + "track_params": null + }, { + "season_id": 48234, + "right_superscript": "http://i0.hdslb.com/bfs/vip/a41bd3a088b9d869806406acb28ac7f2a1150a88.png", + "right_subscript": "", + "left_subscript": "科幻", + "title": "美国队长4", + "sub_title": "美队大战红浩克", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/1c4d71a0f5084098d124b9b6ea8c6e6bb80293cb.png", + "link": "https://www.bilibili.com/bangumi/play/ss48234?theme=movie", + "track_params": null + }, { + "season_id": 32436, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "动作", + "title": "环太平洋", + "sub_title": "巨型机甲对抗巨兽", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/5d2547fccd75717eab7426cb94b021ec71473b76.png", + "link": "https://www.bilibili.com/bangumi/play/ss32436?theme=movie", + "track_params": null + }, { + "season_id": 73302, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "剧情", + "title": "破·地狱", + "sub_title": "破心魔,解生死!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/d8309a399890ee5ab1245d45352ab19ebb9e996f.png", + "link": "https://www.bilibili.com/bangumi/play/ss73302?theme=movie", + "track_params": null + }, { + "season_id": 28281, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "剧情", + "title": "哈利·波特与魔法石", + "sub_title": "霍格沃茨开学了!", + "cover": "http://i0.hdslb.com/bfs/bangumi/dd9a113e724e3c60dc2da31562e45d57e3c2d707.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss28281?theme=movie", + "track_params": null + }, { + "season_id": 76849, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "科幻", + "title": "穿越时空的少女", + "sub_title": "细田守巅峰之作!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/dd195431e5d3985ff8391683b79ddeaf400c10de.png", + "link": "https://www.bilibili.com/bangumi/play/ss76849?theme=movie", + "track_params": null + }, { + "season_id": 47468, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "喜剧", + "title": "间谍过家家 代号:白", + "sub_title": "全家拯救阿尼亚", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/295c0441df86fb8ce6627db92edbc9e7bb453de0.png", + "link": "https://www.bilibili.com/bangumi/play/ss47468?theme=movie", + "track_params": null + }, { + "season_id": 33354, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.5", + "left_subscript": "喜剧", + "title": "夏洛特烦恼", + "sub_title": "马冬梅的排列组合", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/136d1616456e60732d3c84e40e0f925e5e119003.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33354?theme=movie", + "track_params": null + }, { + "season_id": 46054, + "right_superscript": "http://i0.hdslb.com/bfs/vip/a41bd3a088b9d869806406acb28ac7f2a1150a88.png", + "right_subscript": "9.9", + "left_subscript": "喜剧", + "title": "疯狂动物城", + "sub_title": "成人世界乌托邦", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/d250b5bde3c6556894684d4b0818841190df5743.png", + "link": "https://www.bilibili.com/bangumi/play/ss46054?theme=movie", + "track_params": null + }, { + "season_id": 32314, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.6", + "left_subscript": "动作", + "title": "碟中谍", + "sub_title": "阿汤哥经典特工系列", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/dbe606a3b5b6f7ff50fc8637e23ff71b6693eb1b.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss32314?theme=movie", + "track_params": null + }, { + "season_id": 48590, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "奇幻", + "title": "哈尔的移动城堡", + "sub_title": "魔法之下的爱与勇气", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/156dfe963b71dc62dbf4315f3985a070844d0593.png", + "link": "https://www.bilibili.com/bangumi/play/ss48590?theme=movie", + "track_params": null + }, { + "season_id": 96607, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "剧情", + "title": "瘦身大作战", + "sub_title": "失恋女孩的蜕变计划", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/b097f0584d5424892145e62518609af3215c09fd.png", + "link": "https://www.bilibili.com/bangumi/play/ss96607?theme=movie", + "track_params": null + }, { + "season_id": 44847, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "科幻", + "title": "流浪地球2", + "sub_title": "国产科幻巨制", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/4ee5e1cea45fb742c77d17c1b2d115f09508a878.png", + "link": "https://www.bilibili.com/bangumi/play/ss44847?theme=movie", + "track_params": null + }, { + "season_id": 42715, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.6", + "left_subscript": "喜剧", + "title": "坏蛋联盟", + "sub_title": "天生“坏蛋”想从良", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/7d63615d0749d8fe2ce161d155ac1c8198869591.png", + "link": "https://www.bilibili.com/bangumi/play/ss42715?theme=movie", + "track_params": null + }, { + "season_id": 41179, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.6", + "left_subscript": "喜剧", + "title": "史密斯夫妇", + "sub_title": "杀手夫妻甜蜜交锋", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/bc434e10efbfa7ab0efb3741b0be80fbf2bdef3b.png", + "link": "https://www.bilibili.com/bangumi/play/ss41179?theme=movie", + "track_params": null + }, { + "season_id": 42384, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "动作", + "title": "头号玩家", + "sub_title": "斯皮尔伯格科幻大片", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/d1baef273cc7862431978b2e63fc04ccdd353556.png", + "link": "https://www.bilibili.com/bangumi/play/ss42384?theme=movie", + "track_params": null + }, { + "season_id": 32523, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "喜剧", + "title": "功夫", + "sub_title": "周星驰经典动作喜剧", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/0c65b23a92ea20115147df841cdfaa3e4b1170df.png", + "link": "https://www.bilibili.com/bangumi/play/ss32523?theme=movie", + "track_params": null + }, { + "season_id": 45140, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.5", + "left_subscript": "剧情", + "title": "沙丘2", + "sub_title": "沙虫来袭 圣战将至", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/fb57cbefc33ed173e40299e243ef89fda151c9f4.png", + "link": "https://www.bilibili.com/bangumi/play/ss45140?theme=movie", + "track_params": null + }, { + "season_id": 47065, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "7.4", + "left_subscript": "剧情", + "title": "坚如磐石", + "sub_title": "权钱交叠破解杀机", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/e00f39c55ac9a89318144d1a5dd23d33b536cf9e.png", + "link": "https://www.bilibili.com/bangumi/play/ss47065?theme=movie", + "track_params": null + }, { + "season_id": 68514, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "动作", + "title": "因果报应", + "sub_title": "极限反转,恶有恶报", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/c570d4131545158f69623a0f0f6103e8994e1874.png", + "link": "https://www.bilibili.com/bangumi/play/ss68514?theme=movie", + "track_params": null + }, { + "season_id": 28585, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "科幻", + "title": "星际穿越", + "sub_title": "不要温和地走进良夜", + "cover": "http://i0.hdslb.com/bfs/bangumi/3b8acd7a69aa3462297b0b1f206e7093e4961914.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss28585?theme=movie", + "track_params": null + }, { + "season_id": 38552, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "动作", + "title": "正义联盟:扎克·施奈德版", + "sub_title": "最全DC宇宙阵容集结!", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/32a08ede8cdcf941b0a2d2ceddd6c1b991c64699.png", + "link": "https://www.bilibili.com/bangumi/play/ss38552?theme=movie", + "track_params": null + }, { + "season_id": 47750, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.2", + "left_subscript": "犯罪", + "title": "名侦探柯南:百万美元的五棱星", + "sub_title": "柯南与基德的对决", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/aa71c6cc991b3c60d8a7b5557e025de5ca422799.png", + "link": "https://www.bilibili.com/bangumi/play/ss47750?theme=movie", + "track_params": null + }, { + "season_id": 48013, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.2", + "left_subscript": "喜剧", + "title": "抓娃娃", + "sub_title": "沈马组合爆笑养娃", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/57f8d498302dddf7b197e3d3c9b0bccde9b80406.png", + "link": "https://www.bilibili.com/bangumi/play/ss48013?theme=movie", + "track_params": null + }, { + "season_id": 68640, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.6", + "left_subscript": "奇幻", + "title": "火影忍者剧场版:忍者之路", + "sub_title": "鸣人,欢迎回家!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/5fbf4380fd163b3817b9d6d595eb8875253b4e79.png", + "link": "https://www.bilibili.com/bangumi/play/ss68640?theme=movie", + "track_params": null + }, { + "season_id": 46319, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.6", + "left_subscript": "科幻", + "title": "后天", + "sub_title": "人类重回冰川时代", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/839683656c9eaabe48c5d84a06b47e52a613f422.png", + "link": "https://www.bilibili.com/bangumi/play/ss46319?theme=movie", + "track_params": null + }, { + "season_id": 33615, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "剧情", + "title": "绿皮书", + "sub_title": "黑白配公路片", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/99e62fe75cda9d99450df0460307a73d65cf1578.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33615?theme=movie", + "track_params": null + }, { + "season_id": 39558, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "动作", + "title": "碟中谍6:全面瓦解", + "sub_title": "阿汤哥招牌系列新作", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/4bfe927ed1254de55d32dabc6237630b10d8e1d9.png", + "link": "https://www.bilibili.com/bangumi/play/ss39558?theme=movie", + "track_params": null + }] + }, { + "tap_title": "电视剧", + "tap_type": 5, + "seasons": [{ + "season_id": 33626, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "战争", + "title": "三国演义", + "sub_title": "正所谓乱世出英雄", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/1d2eb8d863e3d0aa9d6557ae5b32764159953d8b.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33626", + "track_params": null + }, { + "season_id": 21020, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.2", + "left_subscript": "历史", + "title": "康熙王朝", + "sub_title": "康熙帝传奇的一生", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/4f6c371428b8679b3e84406437fde071aae3927b.png", + "link": "https://www.bilibili.com/bangumi/play/ss21020", + "track_params": null + }, { + "season_id": 44480, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "剧情", + "title": "雍正王朝", + "sub_title": "古装历史剧巅峰之作", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/308cb2bb99bbbbf507e2898460918689a514ac26.png", + "link": "https://www.bilibili.com/bangumi/play/ss44480", + "track_params": null + }, { + "season_id": 96905, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "喜剧", + "title": "我爱我家", + "sub_title": "中国首部情景喜剧", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/a600d1778e570fae87bbedb62ed724cfd04626d5.png", + "link": "https://www.bilibili.com/bangumi/play/ss96905", + "track_params": null + }, { + "season_id": 24053, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "悬疑", + "title": "非自然死亡", + "sub_title": "不自然死因研究所", + "cover": "http://i0.hdslb.com/bfs/bangumi/72f01ec71e4e8cb1a765f875b7a26fb78b9ae6eb.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss24053", + "track_params": null + }, { + "season_id": 33622, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "奇幻", + "title": "西游记", + "sub_title": "俺老孙来也!", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/f9b7d89acb62d2ed385381ebcf5ac80b65fadd73.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33622", + "track_params": null + }, { + "season_id": 68632, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.6", + "left_subscript": "剧情", + "title": "影后", + "sub_title": "娱乐圈抓马大戏", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/d8943e9a8682b7ec3a9c744e2d8aca40643cfba1.png", + "link": "https://www.bilibili.com/bangumi/play/ss68632", + "track_params": null + }, { + "season_id": 20117, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "搞笑", + "title": "家有儿女", + "sub_title": "童年回忆杀", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/ffd860e68feeccd2e14531ac5c7d12c683e0924c.png", + "link": "https://www.bilibili.com/bangumi/play/ss20117", + "track_params": null + }, { + "season_id": 96519, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.5", + "left_subscript": "剧情", + "title": "姜颂", + "sub_title": "高能反转虐恋爽剧", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/9871644abfdefeb026d2833312eedfdba3721d3b.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss96519", + "track_params": null + }, { + "season_id": 33625, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9", + "left_subscript": "古装", + "title": "水浒传", + "sub_title": "大郎,该吃药了", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/78241bf67ee92b92ca4e8d1b8006b939f602aad6.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33625", + "track_params": null + }, { + "season_id": 33624, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "古装", + "title": "红楼梦", + "sub_title": "这个妹妹我见过", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/7e52b373f9522884d4c878c042d46dd532774057.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33624", + "track_params": null + }, { + "season_id": 96622, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "搞笑", + "title": "粉红女郎", + "sub_title": "千禧回忆杀!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/47b9e608859127d57728911c0dd30517e74850da.png", + "link": "https://www.bilibili.com/bangumi/play/ss96622", + "track_params": null + }, { + "season_id": 38729, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "搞笑", + "title": "老友记 第一季", + "sub_title": "经典美剧六人行", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/28f4634e43c3b8def94218101622b9ae69a20d56.png", + "link": "https://www.bilibili.com/bangumi/play/ss38729", + "track_params": null + }, { + "season_id": 42962, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.3", + "left_subscript": "青春", + "title": "三悦有了新工作", + "sub_title": "废柴95后打工日记", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/a4070f48d2f2e2d482523a4df1f64ded626e010c.png", + "link": "https://www.bilibili.com/bangumi/play/ss42962", + "track_params": null + }, { + "season_id": 41349, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.8", + "left_subscript": "剧情", + "title": "机智的医生生活 第二季", + "sub_title": "还是医院五人组", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/367b0470947cab63c0ca8a5227c72688d6ea2be3.png", + "link": "https://www.bilibili.com/bangumi/play/ss41349", + "track_params": null + }, { + "season_id": 47841, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "8.8", + "left_subscript": "奇幻", + "title": "时光代理人", + "sub_title": "新的穿越委托请查收", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/f833064b3ba7777bd33c8ca75fe1499f38ae5b22.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss47841", + "track_params": null + }, { + "season_id": 43891, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.8", + "left_subscript": "剧情", + "title": "机智的医生生活", + "sub_title": "医院五人组", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/5cc912b5ecb8dbb82f8478e3db055c604675c8e3.png", + "link": "https://www.bilibili.com/bangumi/play/ss43891", + "track_params": null + }, { + "season_id": 85532, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "剧情", + "title": "神探夏洛克 第一季", + "sub_title": "天才侦探的推理奇旅", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/41c268584969ed952ac3c771855d705c912b0eee.png", + "link": "https://www.bilibili.com/bangumi/play/ss85532", + "track_params": null + }, { + "season_id": 33981, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "8.1", + "left_subscript": "青春", + "title": "风犬少年的天空", + "sub_title": "彭昱畅喊你来追剧", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/874d8d77ffd1e08c6706cd9a8358eaba27d5e36e.png", + "link": "https://www.bilibili.com/bangumi/play/ss33981", + "track_params": null + }, { + "season_id": 47710, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "剧情", + "title": "铠甲勇士刑天", + "sub_title": "难以超越的童年经典", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/ef0b9764b79c769f9ca698f40e1a7fffdb0d2a89.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss47710", + "track_params": null + }, { + "season_id": 73355, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "喜剧", + "title": "生活大爆炸 第一季", + "sub_title": "怪咖初聚开启新奇缘", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/c516741a0eda05c8fc33b7fad0c3ad8d473bb409.png", + "link": "https://www.bilibili.com/bangumi/play/ss73355", + "track_params": null + }, { + "season_id": 45658, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.6", + "left_subscript": "剧情", + "title": "白莲花", + "sub_title": "全员自私,人性博弈", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/42b40e255eeb2df7c562f15ce48d1840f03bf58e.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss45658", + "track_params": null + }, { + "season_id": 45419, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.9", + "left_subscript": "奇幻", + "title": "古相思曲", + "sub_title": "逆向时空,甜虐来袭", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/6dc69dcab662e0d35e36c8429ecba40ff8938507.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss45419", + "track_params": null + }, { + "season_id": 46356, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.9", + "left_subscript": "搞笑", + "title": "重启人生", + "sub_title": "高口碑治愈系日剧", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/905806209c3d1b1d69e83866b53ea0a16a344586.png", + "link": "https://www.bilibili.com/bangumi/play/ss46356", + "track_params": null + }, { + "season_id": 43551, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.3", + "left_subscript": "剧情", + "title": "学校2015", + "sub_title": "双胞胎姐妹命运错置", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/47ab568509ccee7794b257c53cb2bfe2bd56a189.png", + "link": "https://www.bilibili.com/bangumi/play/ss43551", + "track_params": null + }, { + "season_id": 46681, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "奇幻", + "title": "权力的游戏 第一季", + "sub_title": "经典史诗奇幻神剧", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/13e892349ff97970340eef24c20e163fe7aad842.png", + "link": "https://www.bilibili.com/bangumi/play/ss46681", + "track_params": null + }, { + "season_id": 41453, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "8.1", + "left_subscript": "古装", + "title": "珍馐记", + "sub_title": "暖胃珍馐,吃笑人间", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/736891730e6160b0d26897905b3b3f797df211cf.png", + "link": "https://www.bilibili.com/bangumi/play/ss41453", + "track_params": null + }, { + "season_id": 33623, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "奇幻", + "title": "西游记续集", + "sub_title": "师傅被妖怪抓走啦!", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/2a4782407b25733fbef641595214aa004247ae19.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33623", + "track_params": null + }, { + "season_id": 41716, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "武侠", + "title": "笑傲江湖", + "sub_title": "快意江湖,情仇难断", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/e82f485783df97ba0907576f29b9e6219769f1e4.png", + "link": "https://www.bilibili.com/bangumi/play/ss41716", + "track_params": null + }, { + "season_id": 46138, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.8", + "left_subscript": "搞笑", + "title": "法律至上1", + "sub_title": "日本幽默律政片", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/b68bd5e65a9f8e140ae912a8941871aff436a25a.png", + "link": "https://www.bilibili.com/bangumi/play/ss46138", + "track_params": null + }, { + "season_id": 43894, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.5", + "left_subscript": "剧情", + "title": "海岸村恰恰恰", + "sub_title": "社畜与咸鱼温情碰撞", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/46b2d1a1088214b314956b07a4ccd2aa05b0b154.png", + "link": "https://www.bilibili.com/bangumi/play/ss43894", + "track_params": null + }, { + "season_id": 38629, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "剧情", + "title": "白色巨塔", + "sub_title": "超经典医疗剧", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/b70a6a12cdab2bcb9f81677446ebf52f367ca7bd.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss38629", + "track_params": null + }, { + "season_id": 68652, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "", + "left_subscript": "搞笑", + "title": "孤独的美食家 番外篇", + "sub_title": "电子榨菜 下饭必看", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/5f4e236634b4d8caacc37b6cc3d161fe6020d1aa.png", + "link": "https://www.bilibili.com/bangumi/play/ss68652", + "track_params": null + }, { + "season_id": 20115, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "搞笑", + "title": "家有儿女2", + "sub_title": "经典大型情景喜剧", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/dabe19fe246c0422cb419237d78891fe4b2afe28.png", + "link": "https://www.bilibili.com/bangumi/play/ss20115", + "track_params": null + }, { + "season_id": 21288, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "家庭", + "title": "外来媳妇本地郎", + "sub_title": "最长寿的电视剧之一", + "cover": "http://i0.hdslb.com/bfs/bangumi/25cb9e09f1a7aa2f3575bacef6dadd1df3208881.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss21288", + "track_params": null + }, { + "season_id": 85911, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "剧情", + "title": "悠长假期", + "sub_title": "与木村拓哉的恋爱假", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/70577dcf598beb0663e028745835ae0f9b93ea0c.png", + "link": "https://www.bilibili.com/bangumi/play/ss85911", + "track_params": null + }] + }, { + "tap_title": "番剧", + "tap_type": 1, + "seasons": [{ + "season_id": 42176, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.2", + "left_subscript": "日常", + "title": "莉可丽丝", + "sub_title": "虚假的咖啡厅日常", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/53e6a08505b5a889feb14d2f5ee8a7f22b961847.png", + "link": "https://www.bilibili.com/bangumi/play/ss42176", + "track_params": null + }, { + "season_id": 91776, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.7", + "left_subscript": "漫画改", + "title": "摇滚乃是淑女的爱好", + "sub_title": "硬核摇滚,就好这个", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/9dc89cedb3d03174de8bfd24cceab74ce6b4ce0b.png", + "link": "https://www.bilibili.com/bangumi/play/ss91776", + "track_params": null + }, { + "season_id": 99688, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "漫画改", + "title": "丁丁历险记 第一季", + "sub_title": "一人一狗闯世界", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/1e0db7c64755ced8ad7a9615f692500cc2db9898.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss99688", + "track_params": null + }, { + "season_id": 91812, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.9", + "left_subscript": "漫画改", + "title": "赛马娘 芦毛灰姑娘", + "sub_title": "一马当先,万马无光", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/c72d6ee8b3cba39355af14bebd4857935b4fb083.png", + "link": "https://www.bilibili.com/bangumi/play/ss91812", + "track_params": null + }, { + "season_id": 90684, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.9", + "left_subscript": "原创", + "title": "末日后酒店", + "sub_title": "让AI顺应你的心", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/2f5946880c07914d1cccd112702884f232b647e0.png", + "link": "https://www.bilibili.com/bangumi/play/ss90684", + "track_params": null + }, { + "season_id": 96969, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "", + "left_subscript": "漫画改", + "title": "九龙大众浪漫", + "sub_title": "金鱼游弋的谎言盛宴", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/bf7a2acd78fffc9d0eb8887f92de681924afb73d.png", + "link": "https://www.bilibili.com/bangumi/play/ss96969", + "track_params": null + }, { + "season_id": 99644, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "", + "left_subscript": "漫画改", + "title": "小城日常", + "sub_title": "《日常》精神续作", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/6694a18852af6eeea804e604376bc014d1a84441.png", + "link": "https://www.bilibili.com/bangumi/play/ss99644", + "track_params": null + }, { + "season_id": 91513, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "8.2", + "left_subscript": "小说改", + "title": "天才治疗师退队作为无照治疗师快乐过活", + "sub_title": "真乃妙手回春啊大夫", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/75d7c9d18faa0b792d8bfab181f1608184c32b8b.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss91513", + "track_params": null + }, { + "season_id": 91498, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.5", + "left_subscript": "小说改", + "title": "打了300年的史莱姆,不知不觉就练到了满级 第二季", + "sub_title": "满级魔女只想养老", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/d9727717ee6b507225b26ae367a43a444a6157ca.png", + "link": "https://www.bilibili.com/bangumi/play/ss91498", + "track_params": null + }, { + "season_id": 33415, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "智斗", + "title": "名侦探柯南(中配)", + "sub_title": "小学生的侦探生活", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/38e2a273f528fd01c34f1fc4df0f69c64487efad.png", + "link": "https://www.bilibili.com/bangumi/play/ss33415", + "track_params": null + }, { + "season_id": 91894, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "5.8", + "left_subscript": "漫画改", + "title": "最强王者的第二人生", + "sub_title": "三岁开始当卷王", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/ff55e211d6ab08f063ef2df0bcb064854d08f221.png", + "link": "https://www.bilibili.com/bangumi/play/ss91894", + "track_params": null + }, { + "season_id": 99693, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "漫画改", + "title": "丁丁历险记 第三季", + "sub_title": "一人一狗闯世界", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/5a314ae3c28f1e7819c77672bbeeb99f7cda346b.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss99693", + "track_params": null + }, { + "season_id": 98687, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "原创", + "title": "少年骇客 第一季", + "sub_title": "少年获得神奇手表", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/7cf7938af567ceb370a72ac57433f346d6bd9b87.png", + "link": "https://www.bilibili.com/bangumi/play/ss98687", + "track_params": null + }, { + "season_id": 48810, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "热血", + "title": "假面骑士加布", + "sub_title": "令和第六作假面骑士", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/8f3bf6b461ad4dfae2a221e5b194415d5f7ca697.png", + "link": "https://www.bilibili.com/bangumi/play/ss48810", + "track_params": null + }, { + "season_id": 91577, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "8.9", + "left_subscript": "漫画改", + "title": "炎炎消防队 叁之章", + "sub_title": "焰人谜团步入最终章", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/efb0bfbcd060f1b2280bad5f40c8b75db64de442.png", + "link": "https://www.bilibili.com/bangumi/play/ss91577", + "track_params": null + }, { + "season_id": 91305, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.8", + "left_subscript": "漫画改", + "title": "测不准的阿波连同学 第二季", + "sub_title": "猜不中的来堂同学", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/f56b2c9938b981bc6e029ce4d7b2bb0bd29948c4.png", + "link": "https://www.bilibili.com/bangumi/play/ss91305", + "track_params": null + }, { + "season_id": 33378, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "智斗", + "title": "名侦探柯南", + "sub_title": "真相永远只有一个", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/38e2a273f528fd01c34f1fc4df0f69c64487efad.png", + "link": "https://www.bilibili.com/bangumi/play/ss33378", + "track_params": null + }, { + "season_id": 98696, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "原创", + "title": "少年骇客 第二季 中文配音", + "sub_title": "少年获得神奇手表", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/3ae6f63e48d295abb3f637ba71c007ca2c61f153.png", + "link": "https://www.bilibili.com/bangumi/play/ss98696", + "track_params": null + }, { + "season_id": 91466, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "漫画改", + "title": "魔女与使魔", + "sub_title": "魔女的魔法有副作用", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/d7337f0b197da961d57b4321011048db6b518bb8.png", + "link": "https://www.bilibili.com/bangumi/play/ss91466", + "track_params": null + }, { + "season_id": 100274, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "", + "left_subscript": "小说改", + "title": "肥宅勇者", + "sub_title": "牺牲颜值的强者之路", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/1e90562eb0a76cb48f1558ee73e2560104057ff0.png", + "link": "https://www.bilibili.com/bangumi/play/ss100274", + "track_params": null + }, { + "season_id": 92458, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.9", + "left_subscript": "原创", + "title": "宝可梦 地平线(中配)", + "sub_title": "与全新角色开始冒险", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/c82293ce7fadd7188be20c0487869a187f2ba0eb.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss92458", + "track_params": null + }, { + "season_id": 48297, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "日常", + "title": "小猪佩奇 第十季 中文配音", + "sub_title": "你好!我是佩奇!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/0ea83a1ab2ed156fd4b6ce2a29e0f209d0d43927.png", + "link": "https://www.bilibili.com/bangumi/play/ss48297", + "track_params": null + }, { + "season_id": 91755, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "漫画改", + "title": "哆啦A梦 第五季", + "sub_title": "哆啦A梦帮帮我!!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/5b05e7f5892b9cd1d8b41610114583e124f3c85a.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss91755", + "track_params": null + }, { + "season_id": 98694, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "原创", + "title": "少年骇客 第一季 中文配音", + "sub_title": "少年获得神奇手表", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/f72fac0118e212e577ee4a3a0b0b0e3021c90e58.png", + "link": "https://www.bilibili.com/bangumi/play/ss98694", + "track_params": null + }, { + "season_id": 48034, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "6.4", + "left_subscript": "小说改", + "title": "你与我最后的战场,亦或是世界起始的圣战 第二季", + "sub_title": "夫妻同心 其利断金", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/d4974b6f095f9362493f1ae79b36da8c96552f66.png", + "link": "https://www.bilibili.com/bangumi/play/ss48034", + "track_params": null + }, { + "season_id": 91777, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.4", + "left_subscript": "小说改", + "title": "直至魔女消逝", + "sub_title": "见习魔女生命倒计时", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/588cc1e9f43b57b1e8ec4fcb0f16bfc3f743de00.png", + "link": "https://www.bilibili.com/bangumi/play/ss91777", + "track_params": null + }, { + "season_id": 98698, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "原创", + "title": "少年骇客 第三季 中文配音", + "sub_title": "少年获得神奇手表", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/0ec2ec57520cff4ae813f51b633988fd37d7c00d.png", + "link": "https://www.bilibili.com/bangumi/play/ss98698", + "track_params": null + }, { + "season_id": 96971, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "4.3", + "left_subscript": "原创", + "title": "假面骑士利维斯", + "sub_title": "英雄与恶魔搭档最强", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/ffdea4aaa6324fc47f8cb89d540cebed25ee28fa.png", + "link": "https://www.bilibili.com/bangumi/play/ss96971", + "track_params": null + }, { + "season_id": 6262, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "搞笑", + "title": "蜡笔小新 第二季(中文)", + "sub_title": "不想来笑一下吗?", + "cover": "http://i0.hdslb.com/bfs/bangumi/6d8bd12e0e1ab2d4d5e8567bdba18240e75d7a1b.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss6262", + "track_params": null + }, { + "season_id": 5398, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.8", + "left_subscript": "热血", + "title": "JOJO的奇妙冒险 不灭钻石", + "sub_title": "不一样的热血动画", + "cover": "http://i0.hdslb.com/bfs/bangumi/6a04c87e990ab74cd8d555ef45a863de0993b161.png", + "link": "https://www.bilibili.com/bangumi/play/ss5398", + "track_params": null + }, { + "season_id": 91510, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "8", + "left_subscript": "小说改", + "title": "乡下大叔成为剑圣", + "sub_title": "剑术老师桃李满天下", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/0f5be9db05e5bc60c1ba6cee80debe02c5f73f56.png", + "link": "https://www.bilibili.com/bangumi/play/ss91510", + "track_params": null + }, { + "season_id": 48922, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.4", + "left_subscript": "原创", + "title": "假面骑士加布(中配)", + "sub_title": "令和第六作假面骑士", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/8f3bf6b461ad4dfae2a221e5b194415d5f7ca697.png", + "link": "https://www.bilibili.com/bangumi/play/ss48922", + "track_params": null + }, { + "season_id": 91814, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "", + "left_subscript": "漫画改", + "title": "战队大失格 第二季", + "sub_title": "五个人都是战队英雄", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/17cc74e662b3fb3c8fd35d0983c046f6b7e38700.png", + "link": "https://www.bilibili.com/bangumi/play/ss91814", + "track_params": null + }, { + "season_id": 25681, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.8", + "left_subscript": "热血", + "title": "JOJO的奇妙冒险 黄金之风", + "sub_title": "这就是黄金体验", + "cover": "http://i0.hdslb.com/bfs/bangumi/f34ff3975c39913af936c133ae60a5891babba08.png", + "link": "https://www.bilibili.com/bangumi/play/ss25681", + "track_params": null + }, { + "season_id": 38157, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "热血", + "title": "银魂", + "sub_title": "最无厘头的热血动画", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/087b862b772ee4e644478a36c757a26db476193d.png", + "link": "https://www.bilibili.com/bangumi/play/ss38157", + "track_params": null + }, { + "season_id": 92889, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "漫画改", + "title": "胆大党 中配版", + "sub_title": "妖怪vs外星人", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/0a681472cb6ccfcd7f18094433ea9b750d9c0849.png", + "link": "https://www.bilibili.com/bangumi/play/ss92889", + "track_params": null + }] + }, { + "tap_title": "纪录片", + "tap_type": 3, + "seasons": [{ + "season_id": 96695, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "", + "left_subscript": "人文", + "title": "人生海海", + "sub_title": "火锅江湖,人生沉浮", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/d3faa4f109b93f3c03a4ba6e8ec9cea02252fb31.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss96695", + "track_params": null + }, { + "season_id": 95645, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.5", + "left_subscript": "美食", + "title": "舌尖上的中国4", + "sub_title": "博大精深的中国饮食", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/d9be392c25addb449de9760cae48233ac1a1daf7.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss95645", + "track_params": null + }, { + "season_id": 88835, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.9", + "left_subscript": "社会", + "title": "你好,12315", + "sub_title": "12315打假实录", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/3c1a01ed28c60ac04b0f6edf2f0707ae1afa100f.png", + "link": "https://www.bilibili.com/bangumi/play/ss88835", + "track_params": null + }, { + "season_id": 48055, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.6", + "left_subscript": "社会", + "title": "守护解放西5", + "sub_title": "守护新篇章", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/dc62303faa9882065fe0d1ce3e7226f3eaad17fa.png", + "link": "https://www.bilibili.com/bangumi/play/ss48055", + "track_params": null + }, { + "season_id": 48056, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.9", + "left_subscript": "社会", + "title": "闪闪的儿科医生2", + "sub_title": "爆款医疗节目回归!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/8d5ffd3bacd836778474fa719a321f9f2a4948b6.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss48056", + "track_params": null + }, { + "season_id": 48826, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.9", + "left_subscript": "自然", + "title": "生命奇观", + "sub_title": "无穷小亮自然纪录片", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/0ea69fe30f7f1f9637d42e354a0a4fa96078be4f.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss48826", + "track_params": null + }, { + "season_id": 48800, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.9", + "left_subscript": "探险", + "title": "单挑荒野:水之章", + "sub_title": "德爷开启单挑新副本", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/29f33feb30d440aae93ac060377e29b61742c07f.png", + "link": "https://www.bilibili.com/bangumi/play/ss48800", + "track_params": null + }, { + "season_id": 44473, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.6", + "left_subscript": "社会", + "title": "守护解放西4", + "sub_title": "全新的守护者联盟", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/6dd3cd3b6105071f0c2e22032c3196175117333f.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss44473", + "track_params": null + }, { + "season_id": 33585, + "right_superscript": "https://i0.hdslb.com/bfs/bangumi/image/c264551547b8d7bf55d047002c27635bc180821e.png", + "right_subscript": "9.8", + "left_subscript": "人文", + "title": "中国通史", + "sub_title": "浩瀚历史图景", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/48fdbee6cd5ea2bef11863efcaa8b65498991fb7.png", + "link": "https://www.bilibili.com/bangumi/play/ss33585", + "track_params": null + }, { + "season_id": 45209, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.6", + "left_subscript": "探险", + "title": "野境求真", + "sub_title": "UP主野外调查", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/6a9fd3c1b0d79339dd4e6a552f59c4e862c27dee.png", + "link": "https://www.bilibili.com/bangumi/play/ss45209", + "track_params": null + }, { + "season_id": 26421, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "社会", + "title": "这就是中国", + "sub_title": "张维为解读中国", + "cover": "http://i0.hdslb.com/bfs/bangumi/c886d6dfa0fab0c80d2a005b50ae3fafa9394134.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss26421", + "track_params": null + }, { + "season_id": 39188, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.6", + "left_subscript": "人文", + "title": "人生一串 第三季", + "sub_title": "七荤八素的口腹之欲", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/6c05210675336fc19583ef76c38c893ac6ce46b0.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss39188", + "track_params": null + }, { + "season_id": 28277, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.8", + "left_subscript": "社会", + "title": "守护解放西", + "sub_title": "警动星城", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/13059001c9284c9392c7fb046cdb64dff159effa.png", + "link": "https://www.bilibili.com/bangumi/play/ss28277", + "track_params": null + }, { + "season_id": 44170, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.9", + "left_subscript": "社会", + "title": "闪闪的儿科医生", + "sub_title": "治愈系医疗纪实节目", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/c72a32feef5f3f0c9a9b6be4de529e483a639fa5.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss44170", + "track_params": null + }, { + "season_id": 95803, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "", + "left_subscript": "科技", + "title": "流言终结者 第一季", + "sub_title": "“梦开始的地方!”", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/b81cfdedefb20ba3d0334ae3aece16c690ef50cf.png", + "link": "https://www.bilibili.com/bangumi/play/ss95803", + "track_params": null + }, { + "season_id": 40509, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.8", + "left_subscript": "社会", + "title": "守护解放西3", + "sub_title": "星城守卫者", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/3a5e05f8277b4e6b0c5190d1e649a2ba856b2197.png", + "link": "https://www.bilibili.com/bangumi/play/ss40509", + "track_params": null + }, { + "season_id": 46178, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "自然", + "title": "地球脉动 第三季", + "sub_title": "口碑系列重磅回归", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/571c98fe1d9b532c98cb5aa23b9b875ced8dcae0.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss46178", + "track_params": null + }, { + "season_id": 43161, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.8", + "left_subscript": "人文", + "title": "不止考古·我与三星堆", + "sub_title": "真实考古三星堆", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/cea6473bb07655ac3d76954709d7e4da756daf41.png", + "link": "https://www.bilibili.com/bangumi/play/ss43161", + "track_params": null + }, { + "season_id": 34733, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.7", + "left_subscript": "社会", + "title": "守护解放西2", + "sub_title": "星城卫士", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/ba37f849fcc927fc532e9c790601608f1d8ae267.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss34733", + "track_params": null + }, { + "season_id": 45211, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.9", + "left_subscript": "人文", + "title": "国宝迷踪", + "sub_title": "千山万水,带你回家", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/42a88d17b68fa9f12724f6c88d5b49e3d50d3f52.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss45211", + "track_params": null + }, { + "season_id": 76317, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "动物", + "title": "宝贝星球", + "sub_title": "心有灵犀,爱无界限", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/c21d2b6c1258cca8ac81cc3f77f71b1c9881bdb4.png", + "link": "https://www.bilibili.com/bangumi/play/ss76317", + "track_params": null + }, { + "season_id": 39868, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9", + "left_subscript": "美食", + "title": "奇食记2", + "sub_title": "怪异的“美食”", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/4570f21b0d436879f458c96eeb83612cc1b810f4.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss39868", + "track_params": null + }, { + "season_id": 45196, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.8", + "left_subscript": "社会", + "title": "是坏情绪啊,没关系", + "sub_title": "正视自己的情绪", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/db2f2d89d7b65e46fb6c46e8a77920b6485858ad.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss45196", + "track_params": null + }, { + "season_id": 45187, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.6", + "left_subscript": "社会", + "title": "中国救护", + "sub_title": "聚焦院前急救现场", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/41a48436a32a4ff1783daf239d2538a0247de805.png", + "link": "https://www.bilibili.com/bangumi/play/ss45187", + "track_params": null + }, { + "season_id": 98337, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "历史", + "title": "庞贝古城:人民的故事", + "sub_title": "探索古罗马生活遗址", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/7294bad232ac788142a797e1344b5d143ff15507.png", + "link": "https://www.bilibili.com/bangumi/play/ss98337", + "track_params": null + }, { + "season_id": 41403, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.8", + "left_subscript": "社会", + "title": "案件聚焦", + "sub_title": "人民关心的案件", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/a4aabad2347d26350172b0411abd60775c3a8877.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss41403", + "track_params": null + }, { + "season_id": 39865, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.8", + "left_subscript": "人文", + "title": "众神之地", + "sub_title": "B站出品自然巨制", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/49249fd8bfcc1a461a1529debe14c7c4e176e4e9.png", + "link": "https://www.bilibili.com/bangumi/play/ss39865", + "track_params": null + }, { + "season_id": 44165, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "", + "left_subscript": "人文", + "title": "可以跟着去你家吗(精选版)", + "sub_title": "访问陌生人的家", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/b993eaae907e933f7e00d8c74dc4fdf0d61b5ba4.png", + "link": "https://www.bilibili.com/bangumi/play/ss44165", + "track_params": null + }, { + "season_id": 85805, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "美食", + "title": " 老广的味道 第十季", + "sub_title": "发掘地道广州味", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/c56a8210afd75e1b162f76276023fea90196d798.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss85805", + "track_params": null + }, { + "season_id": 94795, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "", + "left_subscript": "人文", + "title": "监狱纪实", + "sub_title": "引人深思的纪录片", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/ae5181a102705346ef84c51be79fe21806596f3f.png", + "link": "https://www.bilibili.com/bangumi/play/ss94795", + "track_params": null + }, { + "season_id": 89322, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "人文", + "title": "无穷之路4:一带一路", + "sub_title": "跨越七国记录时代", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/7dfaaedc9bfd7cbead819949de49e229b37a6061.png", + "link": "https://www.bilibili.com/bangumi/play/ss89322", + "track_params": null + }, { + "season_id": 27763, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "探险", + "title": "荒野求生 第四季", + "sub_title": "贝爷的生存大挑战", + "cover": "http://i0.hdslb.com/bfs/bangumi/086415cdafc5d5111fe847a1298f9cdcc8d9faf3.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss27763", + "track_params": null + }, { + "season_id": 99554, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "社会", + "title": "生命缘 第15季", + "sub_title": "执炬者行,向心燎原", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/f2635bff0d9762aa3667c0fa61f8a0c3b72eef71.png", + "link": "https://www.bilibili.com/bangumi/play/ss99554", + "track_params": null + }, { + "season_id": 24439, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.8", + "left_subscript": "美食", + "title": "人生一串", + "sub_title": "撸上烤串,快意江湖", + "cover": "http://i0.hdslb.com/bfs/bangumi/7a790c64ff70f12c11888be0532b6981a923afd5.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss24439", + "track_params": null + }, { + "season_id": 72882, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.8", + "left_subscript": "人文", + "title": "养猫的人", + "sub_title": "聚焦人与猫的故事", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/9fc65038b8b74f3f2895c2e8036b35ef0daf9e40.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss72882", + "track_params": null + }, { + "season_id": 34917, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.9", + "left_subscript": "自然", + "title": "绿色星球", + "sub_title": "4K沉浸式聚焦植物", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/eebafc7c374701cdf27e5068b5e885c6337d0567.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss34917", + "track_params": null + }] + }, { + "tap_title": "国创", + "tap_type": 4, + "seasons": [{ + "season_id": 28747, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.7", + "left_subscript": "小说改", + "title": "凡人修仙传", + "sub_title": "韩立驰骋外海震乾坤", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/ee347bb7ec67696bce514d51b4cef55a5e192284.png", + "link": "https://www.bilibili.com/bangumi/play/ss28747", + "track_params": null + }, { + "season_id": 46585, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.8", + "left_subscript": "原创", + "title": "灵笼 第二季", + "sub_title": "末世如何才能生存", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/08bf0c1e24e454de51b58d1e26c0a9aecbe9b0c1.png", + "link": "https://www.bilibili.com/bangumi/play/ss46585", + "track_params": null + }, { + "season_id": 22088, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.6", + "left_subscript": "", + "title": "灵笼", + "sub_title": "末世如何才能生存", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/cfab7e0fbdb4786ff4e885d050b7cf37f8829486.png", + "link": "https://www.bilibili.com/bangumi/play/ss22088", + "track_params": null + }, { + "season_id": 45969, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.6", + "left_subscript": "小说改", + "title": "牧神记", + "sub_title": "放牛少年,放牧诸神", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/53241feb96db2fbe353840b13a3c9412465a1cee.png", + "link": "https://www.bilibili.com/bangumi/play/ss45969", + "track_params": null + }, { + "season_id": 43554, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.6", + "left_subscript": "原创", + "title": "凸变英雄X", + "sub_title": "信赖造就英雄", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/82568ec2918b7e266ec82ce485ea14018a2d4e71.png", + "link": "https://www.bilibili.com/bangumi/play/ss43554", + "track_params": null + }, { + "season_id": 48518, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "", + "left_subscript": "小说改", + "title": "宗门里除了我都是卧底", + "sub_title": "宗门游戏,坐等开玩", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/7a72973b174b473850556d4b6402a13db679f7b8.png", + "link": "https://www.bilibili.com/bangumi/play/ss48518", + "track_params": null + }, { + "season_id": 48578, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.8", + "left_subscript": "漫画改", + "title": "镇魂街 第四季", + "sub_title": "天武风雷,群英大战", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/22c53b77405270b079b95ad6400d6d6bfd1de377.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss48578", + "track_params": null + }, { + "season_id": 2543, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.5", + "left_subscript": "", + "title": "狐妖小红娘", + "sub_title": "可爱狐妖为你牵红线", + "cover": "http://i0.hdslb.com/bfs/bangumi/cbb20ee03e97a9f3ad2e1506a10fd1271f1c584a.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss2543", + "track_params": null + }, { + "season_id": 45965, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.2", + "left_subscript": "漫画改", + "title": "鲲吞天下之掌门归来", + "sub_title": "修真全靠吞", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/fcd3e1f1c3eb2245a9c2b4da5c175fb2b8f628e7.png", + "link": "https://www.bilibili.com/bangumi/play/ss45965", + "track_params": null + }, { + "season_id": 28763, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "8.9", + "left_subscript": "", + "title": "天官赐福", + "sub_title": "天官赐福,百无禁忌", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/8d1be9e8c77696f34886b8f471d935f504a014d3.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss28763", + "track_params": null + }, { + "season_id": 46188, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "6.7", + "left_subscript": "小说改", + "title": "少年歌行 血染天启篇", + "sub_title": "踏破天启城", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/a50f4e5b7de83c2040a104c6d8bbaaa2bd9fac80.png", + "link": "https://www.bilibili.com/bangumi/play/ss46188", + "track_params": null + }, { + "season_id": 35220, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.7", + "left_subscript": "", + "title": "时光代理人", + "sub_title": "无论过去,不问将来", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/7dac8d193a93409777ce027dba35b068efaca718.png", + "link": "https://www.bilibili.com/bangumi/play/ss35220", + "track_params": null + }, { + "season_id": 46593, + "right_superscript": "https://i0.hdslb.com/bfs/bangumi/image/c264551547b8d7bf55d047002c27635bc180821e.png", + "right_subscript": "8.5", + "left_subscript": "", + "title": "月魁传 动态漫画", + "sub_title": "白月魁旧世界往事", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/1b836f93b1da90ab4d257d9bc58ab6f120e20683.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss46593", + "track_params": null + }, { + "season_id": 90883, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "", + "left_subscript": "小说改", + "title": "我的师兄太强了", + "sub_title": "正邪双魂逆袭爽!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/13713d7120e60cc5dedbef6118057357e9a9c6e5.png", + "link": "https://www.bilibili.com/bangumi/play/ss90883", + "track_params": null + }, { + "season_id": 24298, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "漫画改", + "title": "非人哉", + "sub_title": "神话人物的日常生活", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/8f96b3cb7a5018f7fb077549248568e2fcac3b2a.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss24298", + "track_params": null + }, { + "season_id": 39947, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.6", + "left_subscript": "", + "title": "有兽焉", + "sub_title": "神兽下凡,福瑞相伴", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/8aa38d11d0d174e554903aa6ca24f98fc1c5def7.png", + "link": "https://www.bilibili.com/bangumi/play/ss39947", + "track_params": null + }, { + "season_id": 48143, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.3", + "left_subscript": "原创", + "title": "时光代理人 英都篇", + "sub_title": "无论过去,不问将来", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/c3566191a329529014cbcc1cb141ba24b2baf366.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss48143", + "track_params": null + }, { + "season_id": 26257, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "8.2", + "left_subscript": "", + "title": "三体", + "sub_title": "面壁计划开启", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/9870f898b8a39bbb8048f34317f8d78a02cc1770.png", + "link": "https://www.bilibili.com/bangumi/play/ss26257", + "track_params": null + }, { + "season_id": 5626, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "7.4", + "left_subscript": "", + "title": "镇魂街 第二季", + "sub_title": "镇魂将之间的激斗", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/6a762ae614e567fc5c322c8cb240bcd4d1e06969.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss5626", + "track_params": null + }, { + "season_id": 33323, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.9", + "left_subscript": "", + "title": "雾山五行", + "sub_title": "热血水墨国风动画", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/c6d4a6d2f601aceb3b18124352e3cebd4c6e2e02.png", + "link": "https://www.bilibili.com/bangumi/play/ss33323", + "track_params": null + }, { + "season_id": 39666, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.1", + "left_subscript": "", + "title": "时光代理人 第二季", + "sub_title": "无论过去,不问将来", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/88f464de635f87a3a8cec8fbb2b106221efd84e6.png", + "link": "https://www.bilibili.com/bangumi/play/ss39666", + "track_params": null + }, { + "season_id": 48315, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "7.7", + "left_subscript": "", + "title": "伍六七之记忆碎片", + "sub_title": "伍六七身世之谜揭开", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/4ca65598089129eb1cb5916f97dbb2c5f032ae88.png", + "link": "https://www.bilibili.com/bangumi/play/ss48315", + "track_params": null + }, { + "season_id": 73961, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "", + "left_subscript": "小说改", + "title": "君有云 第二季", + "sub_title": "终其一生,唯君有云", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/a36c6e90cf612d58a99488c54af53c965b6cc863.png", + "link": "https://www.bilibili.com/bangumi/play/ss73961", + "track_params": null + }, { + "season_id": 35213, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "8.6", + "left_subscript": "", + "title": "永生", + "sub_title": "仙魔双修,唯我独尊", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/739a202bc7dd0efc2d9c7d2a130dd705324f928f.png", + "link": "https://www.bilibili.com/bangumi/play/ss35213", + "track_params": null + }, { + "season_id": 43547, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.7", + "left_subscript": "漫画改", + "title": "镇魂街 第三季", + "sub_title": "镇魂将至,故人归来", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/dbf13f1b3a55d9adc9fe5dcfc095b9835aabdef2.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss43547", + "track_params": null + }, { + "season_id": 47853, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "", + "title": "喜羊羊与灰太狼", + "sub_title": "别看我只是一只羊~", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/4cd5d75b3ac57d30a114bbe21a9dacd4f7c2fa81.png", + "link": "https://www.bilibili.com/bangumi/play/ss47853", + "track_params": null + }, { + "season_id": 1733, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.9", + "left_subscript": "", + "title": "罗小黑战记", + "sub_title": "有生之年", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/b00ab23001f8b73e7ed09fe55c1af14781f27d14.png", + "link": "https://www.bilibili.com/bangumi/play/ss1733", + "track_params": null + }, { + "season_id": 28758, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.8", + "left_subscript": "", + "title": "猫之茗", + "sub_title": "穿越成猫耳魔法师", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/5d8d1f7613f9b5ffc3cec898a7daf032e67ae052.png", + "link": "https://www.bilibili.com/bangumi/play/ss28758", + "track_params": null + }, { + "season_id": 47144, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.5", + "left_subscript": "", + "title": "熊出没", + "sub_title": "熊大熊二保护森林", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/fb6ca695ef8e93ea8cd8fde26ecddd2278cb1d8b.png", + "link": "https://www.bilibili.com/bangumi/play/ss47144", + "track_params": null + }, { + "season_id": 39659, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.6", + "left_subscript": "", + "title": "第一序列", + "sub_title": "人类文明依然屹立", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/5188f5b8a775b2d72581a6f83d1228db79d2d63e.png", + "link": "https://www.bilibili.com/bangumi/play/ss39659", + "track_params": null + }, { + "season_id": 97550, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "搞笑", + "title": "虫小绿历史为什么之穿越篇", + "sub_title": "与虫小绿一穿越古代", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/51092965ade79fd4c9277219e88637e8e46ace86.png", + "link": "https://www.bilibili.com/bangumi/play/ss97550", + "track_params": null + }, { + "season_id": 91575, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.3", + "left_subscript": "原创", + "title": "凸变英雄X 日语版", + "sub_title": "人人皆可为英雄", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/9580fba03884c8e0298683cb2302793324c787e1.png", + "link": "https://www.bilibili.com/bangumi/play/ss91575", + "track_params": null + }, { + "season_id": 26196, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9", + "left_subscript": "", + "title": "我家大师兄是个反派", + "sub_title": "你是正道?还是魔!", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/80b37298d6aa9c5d687c0446b5f63f6407a3ee2a.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss26196", + "track_params": null + }, { + "season_id": 26194, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "4.7", + "left_subscript": "", + "title": "仙王的日常生活", + "sub_title": "因为太强而苦恼", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/54bb0aa41490093244c33422883729dc36efe146.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss26194", + "track_params": null + }, { + "season_id": 44176, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.2", + "left_subscript": "", + "title": "伍六七之暗影宿命", + "sub_title": "打破暗影刺客的宿命", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/6e5c5848ff8943fd6366e943a4f75b98598c50c6.png", + "link": "https://www.bilibili.com/bangumi/play/ss44176", + "track_params": null + }, { + "season_id": 39696, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "7.8", + "left_subscript": "", + "title": "仙王的日常生活 第三季", + "sub_title": "地表最强男人又来了", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/96f86387f98d5dfcb864925f419431eb00577651.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss39696", + "track_params": null + }] + }, { + "tap_title": "综艺", + "tap_type": 6, + "seasons": [{ + "season_id": 95313, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.6", + "left_subscript": "音乐", + "title": "天赐的声音 第6季", + "sub_title": "推荐金曲争夺战", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/6dead58cd29bf75a46db57161f055ceaebefae48.png", + "link": "https://www.bilibili.com/bangumi/play/ss95313", + "track_params": null + }, { + "season_id": 39256, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.2", + "left_subscript": "音乐", + "title": "我的音乐你听吗", + "sub_title": "B站年度原创音综", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/58fc33109292a14e9861b50a24a189bd24e02807.png", + "link": "https://www.bilibili.com/bangumi/play/ss39256", + "track_params": null + }, { + "season_id": 33039, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "真人秀", + "title": "极限挑战 第1季", + "sub_title": "跟男人帮挑战极限", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/4818cb0854bab948631d0a23673fb0d973b80c54.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33039", + "track_params": null + }, { + "season_id": 33043, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "真人秀", + "title": "极限挑战 第3季", + "sub_title": "笑点和泪点出其不意", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/e96f282a38df98e3cef790b68ad355c6f9353291.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33043", + "track_params": null + }, { + "season_id": 33930, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.3", + "left_subscript": "真人秀", + "title": "奔跑吧兄弟 第1季", + "sub_title": "下饭综艺再来亿遍", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/a301c006f7326c5445ed2ff2998aa3e1dcf3dacf.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33930", + "track_params": null + }, { + "season_id": 33041, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "真人秀", + "title": "极限挑战 第2季", + "sub_title": "极挑男人帮这就是命", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/4745a2a3fc99ce4066e60f6ebcee4877143c5dde.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33041", + "track_params": null + }, { + "season_id": 48928, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.7", + "left_subscript": "音乐", + "title": "2024最美的夜 bilibili跨年晚会", + "sub_title": "跨年音乐现场狂欢", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/c086af8102fbec8a19c35ca0ae4f6ee7e01b2d96.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss48928", + "track_params": null + }, { + "season_id": 33795, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "7", + "left_subscript": "真人秀", + "title": "奔跑吧兄弟 第4季", + "sub_title": "经典IP再来亿遍", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/3ff61ef89717e8261333d74dc89da507079852e6.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33795", + "track_params": null + }, { + "season_id": 34053, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.6", + "left_subscript": "音乐", + "title": "说唱新世代", + "sub_title": "万物皆可说唱", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/e797e6d180d9f198a0cdc76fd75e50f29c745f25.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss34053", + "track_params": null + }, { + "season_id": 47737, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.7", + "left_subscript": "音乐", + "title": "天赐的声音 第5季", + "sub_title": "推荐金曲争夺战", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/bd3a1bc83a6cd2fd3da63568c0e3394c2484c61b.png", + "link": "https://www.bilibili.com/bangumi/play/ss47737", + "track_params": null + }, { + "season_id": 33999, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "7.1", + "left_subscript": "真人秀", + "title": "奔跑吧 第1季", + "sub_title": "奔跑吧伐木累", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/263dbd735331f4abd08199a4bbb223cf8e405125.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33999", + "track_params": null + }, { + "season_id": 92082, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "职场", + "title": "今晚不加班", + "sub_title": "职场人专属下班综艺", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/9793264f50220040c72eafee094e8728e150cc14.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss92082", + "track_params": null + }, { + "season_id": 33914, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "7.7", + "left_subscript": "真人秀", + "title": "奔跑吧兄弟 第3季", + "sub_title": "干大事!兄弟一起上", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/eb2bff1c40dbef4e62cebb292f366d0db7b4b7d8.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33914", + "track_params": null + }, { + "season_id": 33929, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "7.7", + "left_subscript": "真人秀", + "title": "奔跑吧兄弟 第2季", + "sub_title": "好兄弟就奔跑吧", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/55071eefcd545309de16db999aa405be9a5e0f01.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33929", + "track_params": null + }, { + "season_id": 45833, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "真人秀", + "title": "梦想改造家 第3季", + "sub_title": "挑战13套房屋改造", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/932fe3f3cfe6e87c903e0056cf3d12e677103de7.png", + "link": "https://www.bilibili.com/bangumi/play/ss45833", + "track_params": null + }, { + "season_id": 96546, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "", + "left_subscript": "音乐", + "title": "永远22!2025bilibili毕业歌会", + "sub_title": "万人心跳 同频共振", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/1a9780a1a5566127ae53e35bc76f2dd8cade5855.png", + "link": "https://www.bilibili.com/bangumi/play/ss96546", + "track_params": null + }, { + "season_id": 33044, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.7", + "left_subscript": "真人秀", + "title": "极限挑战 第4季", + "sub_title": "向美好生活出发", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/899d2909e71970e9cf153d872279705528329483.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33044", + "track_params": null + }, { + "season_id": 37761, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.6", + "left_subscript": "真人秀", + "title": "超级变变变", + "sub_title": "创意竞赛!干掉无聊", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/e3b53f0b1b3db8b6e524a2e5e791bf51643c9ffa.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss37761", + "track_params": null + }, { + "season_id": 34038, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "6.7", + "left_subscript": "真人秀", + "title": "奔跑吧 第2季", + "sub_title": "保持奔跑!", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/0e6b00fbcf0be62e82a1c6ecc306cb0db1e1d9fc.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss34038", + "track_params": null + }, { + "season_id": 38318, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "8.9", + "left_subscript": "脱口秀", + "title": "康熙来了 2014", + "sub_title": "下饭综艺回来啦", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/efb99941602b0fc22a241ec5dad0c406bcf8eea6.png", + "link": "https://www.bilibili.com/bangumi/play/ss38318", + "track_params": null + }, { + "season_id": 35905, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.2", + "left_subscript": "真人秀", + "title": "我是特优声", + "sub_title": "声音怪物集结", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/c2a9dce9e54e96964c5c31905a9208258152d5d9.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss35905", + "track_params": null + }, { + "season_id": 39117, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "8.6", + "left_subscript": "访谈", + "title": "康熙来了 2011", + "sub_title": "无脑欢乐多笑成猪叫", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/bf75625315da514581912aaaaba8ef3b9e72a902.png", + "link": "https://www.bilibili.com/bangumi/play/ss39117", + "track_params": null + }, { + "season_id": 33821, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.1", + "left_subscript": "访谈", + "title": "康熙来了 2015", + "sub_title": "爆笑神综", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/8845ba3b409fe31c70a2b1c2272f3b60e06885fa.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss33821", + "track_params": null + }, { + "season_id": 41442, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.5", + "left_subscript": "访谈", + "title": "非正式会谈 第7季", + "sub_title": "外国人用中文搞事情", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/3dcca94624af6a020a35b0d17a03179a34bc08c1.png", + "link": "https://www.bilibili.com/bangumi/play/ss41442", + "track_params": null + }, { + "season_id": 79829, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "", + "left_subscript": "晚会", + "title": "2025年中央广播电视总台春节联欢晚会", + "sub_title": "巳巳如意,生生不息", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/26c2b1106131d39241fd248d8f953cc8d9f23109.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss79829", + "track_params": null + }, { + "season_id": 45202, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.3", + "left_subscript": "真人秀", + "title": "奇迹焕新家", + "sub_title": "专注“妈见打”装修", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/109f74bd48a385475ce2138cd4c8f7af20245531.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss45202", + "track_params": null + }, { + "season_id": 46137, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.6", + "left_subscript": "访谈", + "title": "非正式会谈 第8季", + "sub_title": "分享各国趣闻", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/f784a15cea5d11a9f1ae29e5e6b57a1daf806f6e.png", + "link": "https://www.bilibili.com/bangumi/play/ss46137", + "track_params": null + }, { + "season_id": 34686, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9", + "left_subscript": "脱口秀", + "title": "康熙来了 2013", + "sub_title": "下饭综艺回来啦", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/35c7ae3df0225bcf5597384fb2249ef7bdeef6d1.png", + "link": "https://www.bilibili.com/bangumi/play/ss34686", + "track_params": null + }, { + "season_id": 47706, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png", + "right_subscript": "9.5", + "left_subscript": "音乐", + "title": "永远22!2024bilibili毕业歌会", + "sub_title": "万人心跳 同频共振", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/c61dea3cbc2ac597207b8541e955f13a53fd9016.png", + "link": "https://www.bilibili.com/bangumi/play/ss47706", + "track_params": null + }, { + "season_id": 39213, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.9", + "left_subscript": "真人秀", + "title": "你好生活 第3季", + "sub_title": "再度被节目治愈", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/e7d53a2309da297a76dc19d285239e7841745c6d.png", + "link": "https://www.bilibili.com/bangumi/play/ss39213", + "track_params": null + }, { + "season_id": 32363, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.5", + "left_subscript": "脱口秀", + "title": "今夜百乐门 ", + "sub_title": "青岛大姨名场面回顾", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/b62416c4704599a4d9a0cce9c3257cb9674d6c9d.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss32363", + "track_params": null + }, { + "season_id": 34109, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "8.7", + "left_subscript": "真人秀", + "title": "王牌对王牌 第5季", + "sub_title": "王牌家族等你来玩", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/ea449420304765fffc651c395ffa7a51b940fe59.jpg", + "link": "https://www.bilibili.com/bangumi/play/ss34109", + "track_params": null + }, { + "season_id": 39047, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.4", + "left_subscript": "访谈", + "title": "康熙来了 2010", + "sub_title": "下饭神综回来了", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/861c2990681d33f90ce914e670133e1273f27ea5.png", + "link": "https://www.bilibili.com/bangumi/play/ss39047", + "track_params": null + }, { + "season_id": 38316, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "9.4", + "left_subscript": "访谈", + "title": "康熙来了 2004", + "sub_title": "考古康熙好笑到炸裂", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/8cc29702578aa07348a6d769588c6f6805b20c3e.png", + "link": "https://www.bilibili.com/bangumi/play/ss38316", + "track_params": null + }, { + "season_id": 47860, + "right_superscript": "http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png", + "right_subscript": "9.8", + "left_subscript": "真人秀", + "title": "此生要去的100个地方", + "sub_title": "共赴祖国大好河山!", + "cover": "https://i0.hdslb.com/bfs/bangumi/image/a9f0c60f74a21b9bd3f1451f61912d4df5dd403c.png", + "link": "https://www.bilibili.com/bangumi/play/ss47860", + "track_params": null + }, { + "season_id": 38838, + "right_superscript": "http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png", + "right_subscript": "", + "left_subscript": "访谈", + "title": "康熙来了 2012", + "sub_title": "爆笑神综", + "cover": "http://i0.hdslb.com/bfs/bangumi/image/f9bcd6e6b8820b1717e675249cdc18e114b3a22c.png", + "link": "https://www.bilibili.com/bangumi/play/ss38838", + "track_params": null + }] + }], + "draw_cards_welfare": [], + "free_welfare": [], + "buoy": { + "id": 0, + "img": "", + "link": "", + "track_params": null + }, + "extra_params": { + "switch_on": false, + "birthday_sku_switch_on": false, + "is_buy_birthday_sku": false, + "is_birthday_off": false, + "phone_bind_state": 0, + "app_times": 0, + "na_ab": 0, + "na_ab_group_id": "", + "is_same_to_last_session": false, + "is_white_list": false, + "cloud_ab": 0, + "cloud_ab_group_id": "", + "banner_ab": 0, + "banner_ab_group_id": "", + "now_time": 0, + "free_welfare_ab": 0, + "device_limit_ab": 0, + "offline_ab": 0, + "welfare_module_height_ab": 0 + } + } + } + ``` +} diff --git a/bruno/api.bilibili.com/x/vip/web/vip_center/v2.bru b/bruno/api.bilibili.com/x/vip/web/vip_center/v2.bru new file mode 100644 index 0000000..7d4d32e --- /dev/null +++ b/bruno/api.bilibili.com/x/vip/web/vip_center/v2.bru @@ -0,0 +1,2024 @@ +meta { + name: v2 + type: http + seq: 1 +} + +get { + url: https://api.bilibili.com/x/vip/web/vip_center/v2?access_key={{access_key}}&act_id=872&appkey={{appKey}}&build=8451100&csrf={{csrf}}&device=phone&disable_rcmd=0&is_selected=false&mobi_app=android&platform=android&select_modules=VipExclusive,BigPoint&statistics={"appId":1,"platform":3,"version":"8.45.1","abtest":""}&ts=1748751549&web_location=666.146&sign=6e336f0cc44606c32560ee09b6ae23fa + body: none + auth: inherit +} + +params:query { + access_key: {{access_key}} + act_id: 872 + appkey: {{appKey}} + build: 8451100 + csrf: {{csrf}} + device: phone + disable_rcmd: 0 + is_selected: false + mobi_app: android + platform: android + select_modules: VipExclusive,BigPoint + statistics: {"appId":1,"platform":3,"version":"8.45.1","abtest":""} + ts: 1748751549 + web_location: 666.146 + sign: 6e336f0cc44606c32560ee09b6ae23fa +} + +headers { + Host: api.bilibili.com + Cookie: {{cookieStr}} + accept: application/json, text/plain, */* + bili-http-engine: ignet + buvid: {{buvid}} + native_api_from: h5 + referer: https://big.bilibili.com/mobile/index + user-agent: {{user-agent}} + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-metadata-legal-region: CN + x-bili-mid: {{mid}} + x-bili-net-bin: DQAAgL8gAQ + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDg3NzU1OTksImlhdCI6MTc0ODc0NjQ5OSwiYnV2aWQiOiJYVUE1NjUxQTlFREY3Mzg3MTUzQTk0NUNERTk2Q0FEQ0I2MDAwIn0.k0x2o3e2Q3W-6Wzc56IhbLgSjDKTaAuUV9om7K213fI + x-bili-trace-id: 14fbd3682d7fc79650e739ba8d683bd4:50e739ba8d683bd4:0:0 +} + +docs { + Response sample: + + ```json + { + "code": 0, + "message": "0", + "ttl": 1, + "data": { + "panel_info": { + "face": { + "container_size": { + "width": 1, + "height": 1 + }, + "fallback_layers": { + "layers": [{ + "visible": true, + "general_spec": { + "pos_spec": { + "coordinate_pos": 2, + "axis_x": 0.5, + "axis_y": 0.5 + }, + "size_spec": { + "width": 1.0555555555555556, + "height": 1.0555555555555556 + }, + "render_spec": { + "opacity": 1 + } + }, + "layer_config": { + "tags": { + "AVATAR_LAYER": {}, + "GENERAL_CFG": { + "config_type": 1, + "general_config": { + "web_css_style": { + "background-color": "rgb(255,255,255)", + "border": "1px solid rgba(255,255,255,1)", + "borderRadius": "50%", + "boxSizing": "border-box" + } + } + } + }, + "is_critical": true, + "allow_over_paint": true + }, + "resource": { + "res_type": 3, + "res_image": { + "image_src": { + "src_type": 1, + "placeholder": 6, + "remote": { + "url": "https://i2.hdslb.com/bfs/face/0fed8598ff2ae289bbb7efe62fba30914f906da9.jpg", + "bfs_style": "widget-layer-avatar" + } + } + } + } + }, { + "visible": true, + "general_spec": { + "pos_spec": { + "coordinate_pos": 1, + "axis_x": 0.638888888888889, + "axis_y": 0.638888888888889 + }, + "size_spec": { + "width": 0.38888888888888884, + "height": 0.38888888888888884 + }, + "render_spec": { + "opacity": 1 + } + }, + "layer_config": { + "tags": { + "GENERAL_CFG": { + "config_type": 1, + "general_config": { + "web_css_style": { + "background-color": "rgb(255,255,255)", + "border": "1px solid rgba(255,255,255,1)", + "borderRadius": "50%", + "boxSizing": "border-box" + } + } + }, + "ICON_LAYER": {} + }, + "allow_over_paint": true + }, + "resource": { + "res_type": 3, + "res_image": { + "image_src": { + "src_type": 1, + "placeholder": 1, + "remote": { + "url": "https://i0.hdslb.com/bfs/bangumi/kt/aba51485c0d02940c89aeefcf6680510d9858472.png", + "bfs_style": "widget-layer-avatar" + } + } + } + } + }], + "is_critical_group": true + }, + "mid": "341688380" + }, + "label": { + "text": "年度大会员", + "label_theme": "annual_vip", + "text_color": "#FFFFFF", + "bg_style": 1, + "bg_color": "#FB7299", + "border_color": "", + "use_img_label": true, + "img_label_uri_hans": "", + "img_label_uri_hant": "", + "img_label_uri_hans_static": "https://i0.hdslb.com/bfs/bangumi/kt/629e28d4426f1b44af1131ade99d27741cc61d4b.png", + "img_label_uri_hant_static": "https://i0.hdslb.com/bfs/activity-plat/static/20220614/e369244d0b14644f5e1a06431e22a4d5/VEW8fCC0hg.png" + }, + "panel_title": "TM说的", + "panel_sub_title": "大会员已连续陪伴17天", + "panel_sub_title2": "已连续陪伴17天", + "button": { + "text": "立即续费", + "action_type": "", + "link": "" + }, + "vip_overdue_explain": "大会员 2026年05月18日到期", + "tv_overdue_explain": "超级大会员 尚未开通", + "num_of_assets": 13, + "UserFace": "https://i2.hdslb.com/bfs/face/0fed8598ff2ae289bbb7efe62fba30914f906da9.jpg", + "offline_activity_link": "https://www.bilibili.com/blackboard/activity-vlCEesbdFB.html" + }, + "notice": { + "text": "", + "tv_text": "", + "type": 0, + "can_close": false, + "surplus_seconds": 30282051, + "tv_surplus_seconds": -1748751549, + "account_exception_text": "", + "link": "", + "unique_key": "" + }, + "vip_info": { + "mid": 341688380, + "vip_type": 2, + "vip_status": 1, + "vip_due_date": 1779033600, + "vip_pay_type": 0, + "vip_is_new_user": false, + "vip_is_annual": true, + "vip_is_month": false, + "is_auto_renew": false, + "vip_is_valid": true, + "vip_is_overdue": false, + "vip_keep_time": 1747398345, + "vip_expire_days": 351, + "vip_remain_days": 17, + "tv_vip_type": 0, + "tv_vip_pay_type": 0, + "tv_status": 0, + "tv_due_date": 0, + "tv_is_sign_surplus": false, + "tv_is_auto_renew": false, + "nickname_color": "#FB7299", + "label": { + "text": "年度大会员", + "label_theme": "annual_vip", + "text_color": "#FFFFFF", + "bg_style": 1, + "bg_color": "#FB7299", + "border_color": "", + "use_img_label": true, + "img_label_uri_hans": "", + "img_label_uri_hant": "", + "img_label_uri_hans_static": "https://i0.hdslb.com/bfs/bangumi/kt/629e28d4426f1b44af1131ade99d27741cc61d4b.png", + "img_label_uri_hant_static": "https://i0.hdslb.com/bfs/activity-plat/static/20220614/e369244d0b14644f5e1a06431e22a4d5/VEW8fCC0hg.png" + }, + "vip_role": 3 + }, + "privileges": { + "list": [{ + "id": 3, + "name": "大视听", + "child_privileges": [{ + "first_id": 3, + "report_id": "", + "name": "点映会", + "desc": "线下免费抢先", + "explain": "大会员可免费参与专属的线下点映活动,十年及以上大会员有机会带1位好友共同参与,最终根据参与人数随机抽取。福利活动不定期开展,具体城市及内容以实际通知为准。", + "icon_url": "http://i0.hdslb.com/bfs/vip/a38a61d90e871bec04f4bb39f425fb3890256275.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E7%82%B9%E6%98%A0%E4%BC%9A\u0026closable=1", + "image_url": "http://i0.hdslb.com/bfs/vip/7e30545c23658da326434449429192365ebe311c.jpg", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 89 + }, { + "first_id": 3, + "report_id": "freewatch", + "name": "免费看", + "desc": "会员用户免费看", + "explain": "需要付费才能观看的影视内容,大会员可以免费观看(播放页面提示“大会员半价”的除外,部分视频仅限在中国大陆观看)。", + "icon_url": "http://i0.hdslb.com/bfs/vip/55644358e506de5c55ae2447d51d1a59ad923eba.png", + "icon_gray_url": "", + "background_image_url": "http://i0.hdslb.com/bfs/vip/b8ea804c872fb2b096715f52b87deb0e6cdfd476.png", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E5%85%8D%E8%B4%B9%E7%9C%8B\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/22c3735f9db313b7be35d87c1b5dd6da81cea48e.jpg", + "type": 0, + "hot_type": 1, + "new_type": 0, + "id": 1 + }, { + "first_id": 3, + "report_id": "clearwatch", + "name": "超清看", + "desc": "会员用户超清晰观看", + "explain": "大会员可专享高帧率、高码率画质(最高可达超清4k),觉醒超凡视觉体验。", + "icon_url": "http://i0.hdslb.com/bfs/vip/f6714a06c34ac32b54cae4022a6f6b07b5c731aa.png", + "icon_gray_url": "", + "background_image_url": "http://i0.hdslb.com/bfs/vip/21d79540e10618ee9bbaf8874ae711442d10edf0.png", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E8%B6%85%E6%B8%85%E7%9C%8B\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/fbe4cf2288571d7b0a509c7014d5182789ffdd74.png", + "type": 0, + "hot_type": 0, + "new_type": 1, + "id": 3 + }, { + "first_id": 3, + "report_id": "firstwatch", + "name": "抢先看", + "desc": "会员用户可以快人一步抢先观看", + "explain": "连载内容中需要付费抢先看的内容,大会员可以直接观看,不限次数。(部分视频仅限在中国大陆观看)", + "icon_url": "http://i0.hdslb.com/bfs/vip/49762e82b09b6cfc572d8d8160f4dd72199c1403.png", + "icon_gray_url": "", + "background_image_url": "http://i0.hdslb.com/bfs/vip/20b40771e4bf180a606ddc021dfdfe6a7e56b713.png", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E6%8A%A2%E5%85%88%E7%9C%8B\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/21c0f30302944b694a12f12cbf4ee02733e1e580.png", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 2 + }, { + "first_id": 3, + "report_id": "halfprice", + "name": "半价点播", + "desc": "付费内容半价即享", + "explain": "部分付费点播内容,大会员可享受半价购买。购买成功后,48小时内不限次数观看该影片(部分内容仅限在中国大陆观看)。", + "icon_url": "http://i0.hdslb.com/bfs/vip/53f0df779fcf3b394a3f01bf9d837a061ce3de49.png", + "icon_gray_url": "", + "background_image_url": "http://i0.hdslb.com/bfs/vip/3a613880463e01c8f9496f3b571e198a111191e5.png", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E5%8D%8A%E4%BB%B7%E7%82%B9%E6%92%AD\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/07f8b89c6d044723ece8a42f558d1e84041ff991.png", + "type": 0, + "hot_type": 1, + "new_type": 0, + "id": 55 + }, { + "first_id": 3, + "report_id": "", + "name": "边下边播", + "desc": "追番看剧拒绝卡顿", + "explain": "大会员下载剧集时,已下载部分可以播放,不用等下载完成即可观看(仅限手机端使用)。", + "icon_url": "http://i0.hdslb.com/bfs/vip/6a37b055361b2f3a77e8a9ccfbd84c79d9efadcb.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E8%BE%B9%E4%B8%8B%E8%BE%B9%E6%92%AD\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/e3ae66f7c72056e95d252a33ebceac70d32a27cc.png", + "type": 0, + "hot_type": 1, + "new_type": 0, + "id": 53 + }, { + "first_id": 3, + "report_id": "", + "name": "专属缓存", + "desc": "随时随地想看就看", + "explain": "海量番剧、国创、电影大片,大会员独享专属缓存特权。(仅限手机端使用,部分内容受版权或地区限制无法缓存)。", + "icon_url": "http://i0.hdslb.com/bfs/vip/74f9924fdc32fa68043521f9df7699a5ae80f835.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E4%B8%93%E5%B1%9E%E7%BC%93%E5%AD%98\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/6cf08314a94aeb5579e956aa40a2f37ab68baa2d.png", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 51 + }, { + "first_id": 3, + "report_id": "dolby", + "name": "杜比音效", + "desc": "更优质的听觉盛宴", + "explain": "大会员专享杜比音效(立体声、环绕声)以及杜比全景声,采用全新的音效技术,为你带来身临其中的听觉盛宴。(该权益仅可在移动端上部分内容支持使用)", + "icon_url": "http://i0.hdslb.com/bfs/vip/2fe4fb620d7926df86d2819cb045b32596194560.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E6%9D%9C%E6%AF%94%E9%9F%B3%E6%95%88\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/08377c70a185359242cf5c83f1cd5ed5c8b3c057.png", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 59 + }, { + "first_id": 3, + "report_id": "hdr", + "name": "真彩HDR", + "desc": "更真实的视觉体验", + "explain": "哔哩哔哩提供基于HDR10技术的“真彩HDR”观影模式。HDR能够呈现更多的动态范围,细致优化画面中的明暗对比及色彩显示,更好的反映出真实环境中的视觉效果。使您可以享受到色彩细腻鲜艳,明暗层次丰富的高品质观影体验。\r\n注意事项: \r\n移动端请更新APP至6.9及以上版本;安卓机型需7及以上系统,iOS机型需13及以上系统,PC端仅部分浏览器支持。", + "icon_url": "http://i0.hdslb.com/bfs/vip/adea8d637e9dd4e2c9001357cd2cd4a36a743b68.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E7%9C%9F%E5%BD%A9HDR\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/240c332b35355cfbfd982aa7b3bc8e48b31672f0.png", + "type": 0, + "hot_type": 0, + "new_type": 1, + "id": 57 + }, { + "first_id": 3, + "report_id": "", + "name": "并行下载", + "desc": "3集一起下才够快", + "explain": "大会员下载视频时,至多可支持2-3个视频同时缓存(仅限手机端使用)。", + "icon_url": "http://i0.hdslb.com/bfs/vip/ababe7062afbb1e6240068662624f4b29c3a119a.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E5%B9%B6%E8%A1%8C%E4%B8%8B%E8%BD%BD\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/188e6a0905a3729905a3a053ffa7dad324705ca6.png", + "type": 0, + "hot_type": 1, + "new_type": 0, + "id": 54 + }, { + "first_id": 3, + "report_id": "", + "name": "预约缓存", + "desc": "后台运行即更即存", + "explain": "连载内容尚未播出的剧集可提前预约缓存,新剧集上线后,第一时间在wifi环境下自动缓存下载到本地,省时省力追番更轻松(仅限手机端使用)。\r\n使用说明:\r\n1.此权益需要将哔哩哔哩APP设置后台自动运行状态;\r\n2.具体以可预约下载剧集的播出安排为准。", + "icon_url": "http://i0.hdslb.com/bfs/vip/035ebd10109133c9d1e63bd72aeef571fe7584cf.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E9%A2%84%E7%BA%A6%E7%BC%93%E5%AD%98\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/8e01e169551909bcaae6ed8b40759c4e1bae95cf.png", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 52 + }] + }, { + "id": 4, + "name": "大排面", + "child_privileges": [{ + "first_id": 4, + "report_id": "", + "name": "漫展休息区", + "desc": "漫展享受免费休息区及系列专属活动", + "explain": "部分头部漫展设有大会员休息区,凭会员身份可入内休息充电,领取专属赠礼、与coser互动合影,实际体验以每场漫展为准。", + "icon_url": "http://i0.hdslb.com/bfs/vip/53dc3ad60da74161fcd03541c5faaba094239058.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=漫展休息区\u0026closable=1", + "image_url": "http://i0.hdslb.com/bfs/vip/175fe0a0d628887407abfb2bd4a8f61976cc32df.png", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 93 + }, { + "first_id": 4, + "report_id": "", + "name": "生日礼", + "desc": "专属生日福利", + "explain": "大会员用户可以在生日周期内领取生日权益,生日周期为用户生日当天及生日后7天;在一个自然年内,仅可领取一次生日权益。生日权益以当前活动页展示内容为准。", + "icon_url": "http://i0.hdslb.com/bfs/vip/efa3c31ac28374c2feb41515d1eb583d6d54bdd5.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=生日礼\u0026closable=1", + "image_url": "http://i0.hdslb.com/bfs/vip/4d992169ab45b5b58ee5c57dacbc859a330d4ce2.png", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 91 + }, { + "first_id": 4, + "report_id": "update", + "name": "身份升级", + "desc": "连续购买享受更高级权益", + "explain": "购买大会员连续累计时长超过366天,即可免费升级为年度大会员身份,升级后可立即享受粉色昵称、游戏礼包、B币券等年度大会员专享权益。\r\n注意:中断续费的话,年度大会员身份会收回哦~", + "icon_url": "http://i0.hdslb.com/bfs/vip/bd245efc500d86d1433402b225278362a0db6d54.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E8%BA%AB%E4%BB%BD%E5%8D%87%E7%BA%A7\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/75e8a25686e7556877be4074f002c426afe8d4a6.jpg", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 56 + }, { + "first_id": 4, + "report_id": "", + "name": "等级加速", + "desc": "加速升级Lv6", + "explain": "Lv1-Lv6的在期大会员每日观看视频1分钟,可在会员中心额外获得10经验值用于社区等级提升。", + "icon_url": "http://i0.hdslb.com/bfs/vip/afdf447d23112dbd313da104ae875f34a7f1d464.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E7%AD%89%E7%BA%A7%E5%8A%A0%E9%80%9F\u0026closable=1", + "image_url": "", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 88 + }, { + "first_id": 4, + "report_id": "boot_screen", + "name": "专属闪屏图片", + "desc": "解锁精美闪屏", + "explain": "大会员可选择会员专享的闪屏图片/视频,解锁精美开屏。具体路径为:我的-设置-开屏画面设置-自选模式-会员专享。注意事项:此功能仅在哔哩哔哩移动端7.32及以上版本可用。", + "icon_url": "http://i0.hdslb.com/bfs/vip/2cf33f1eda3df0250597e117ebd41404ef862f50.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E4%B8%93%E5%B1%9E%E9%97%AA%E5%B1%8F%E5%9B%BE%E7%89%87\u0026closable=1", + "image_url": "http://i0.hdslb.com/bfs/vip/4d099bb587392d6445d25b36dff0ec2b071d30bc.png", + "type": 0, + "hot_type": 0, + "new_type": 1, + "id": 87 + }, { + "first_id": 4, + "report_id": "barrage_colour", + "name": "彩色弹幕", + "desc": "炫酷粉蓝走起", + "explain": "大会员用户在发布弹幕时,可在弹幕框中点击A进入颜色选择,选中大会员专属的粉蓝渐变色, 发送独特炫酷弹幕。注意事项:此功能仅在哔哩哔哩移动端7.32及以上版本、web端可用。", + "icon_url": "http://i0.hdslb.com/bfs/vip/3982a5110b5615caa15c892d990ea374e61ec583.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E5%BD%A9%E8%89%B2%E5%BC%B9%E5%B9%95\u0026closable=1", + "image_url": "http://i0.hdslb.com/bfs/vip/4ca5f2d7b03c501f3c99174c0cb812a647ea9804.png", + "type": 0, + "hot_type": 0, + "new_type": 1, + "id": 86 + }, { + "first_id": 4, + "report_id": "gifcomment", + "name": "专属评论图片", + "desc": "评论发图不仅更大了、还能动了", + "explain": "大会员在视频播放页评论区发表的图片在预览时将被放大1.5倍。并且,大会员还能在视频播放页评论区发表动图。\r\n注意事项:\r\n1.此功能仅在哔哩哔哩手机端7.29.0及以上版本可用\r\n2.仅支持发表GIF格式、大小不超过14MB的动图", + "icon_url": "http://i0.hdslb.com/bfs/vip/8a9ee4af815b7368737ed02a40f116f9f5c3ac88.png", + "icon_gray_url": "http://i0.hdslb.com/bfs/vip/5df897b08912e60fc893cddaa0554d7564e34ae4.png", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E4%B8%93%E5%B1%9E%E8%AF%84%E8%AE%BA%E5%9B%BE%E7%89%87\u0026closable=1", + "image_url": "http://i0.hdslb.com/bfs/vip/bf8159c3c6de16ac23b4f6f613af74119fa6afe0.png", + "type": 0, + "hot_type": 0, + "new_type": 1, + "id": 85 + }, { + "first_id": 4, + "report_id": "nickname", + "name": "粉色昵称", + "desc": "尊享闪亮粉色昵称", + "explain": "大会员的昵称将以粉色高亮显示。", + "icon_url": "http://i0.hdslb.com/bfs/vip/2aca4c6dbc50caaae08949d4ff7380700fdb6c07.png", + "icon_gray_url": "http://i0.hdslb.com/bfs/vip/b67288e008a9fdfec0b13a27a527bb8db701c0d4.png", + "background_image_url": "http://i0.hdslb.com/bfs/vip/fa946f3c4011c28fe780d0cdd4da279fb996903f.png", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E7%B2%89%E8%89%B2%E6%98%B5%E7%A7%B0\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/b609d85d3e30450586653b245ac9772740ec184c.png", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 18 + }, { + "first_id": 4, + "report_id": "card", + "name": "动态卡片装扮", + "desc": "动态卡片装扮", + "explain": "大会员可以免费使用大会员专属动态卡片装扮,用于装扮自己的动态卡片,彰显不一样的自己!\r\n有效期内随意装扮,有效期结束后动态卡片装扮自动卸下~(当前仅限客户端)", + "icon_url": "http://i0.hdslb.com/bfs/vip/fd97209767a0d713c62b9a9d4c58a69777f75a53.png", + "icon_gray_url": "", + "background_image_url": "http://i0.hdslb.com/bfs/vip/8775308a302014e3bfbfa0dfc69faa2e8faeaa3a.png", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E5%8A%A8%E6%80%81%E5%8D%A1%E7%89%87%E8%A3%85%E6%89%AE\u0026closable=1", + "image_url": "http://i0.hdslb.com/bfs/vip/21880645864fbdace3d4d0b52eb895551ecd536d.png", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 46 + }, { + "first_id": 4, + "report_id": "pendant", + "name": "专属挂件", + "desc": "专属挂件免费换", + "explain": "大会员可免费领取专属挂件,用于装扮自己的头像,展示在评论区、个人空间等等位置。有钱也买不到哦!\r\n有效期内可以随便领,有效期结束后挂件自动卸下~", + "icon_url": "http://i0.hdslb.com/bfs/vip/dfdda1c6ee68d306061129aa63e8d889e80c8f8a.png", + "icon_gray_url": "", + "background_image_url": "http://i0.hdslb.com/bfs/vip/ae16ed0dcf8246a28e45403243bc65eea0e7b4c7.png", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E4%B8%93%E5%B1%9E%E6%8C%82%E4%BB%B6\u0026closable=1", + "image_url": "http://i0.hdslb.com/bfs/vip/1e6799197b0749c263dd8a28067c0e2b6327cab5.png", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 23 + }, { + "first_id": 4, + "report_id": "spacepicture", + "name": "空间自主头图", + "desc": "空间自主头图", + "explain": "大会员可上传个性化图片来装扮个人空间头图,让自己的空间独具魅力。\r\nweb端进入个人空间后,点击头图右上角更换头图时,可以上传自定义头图。\r\n手机客户端进入个人空间后,即可通过点击头图上的“小衣服”按钮更换头图。", + "icon_url": "http://i0.hdslb.com/bfs/vip/d406573095427320835eba3d20819a95c2a1de78.png", + "icon_gray_url": "http://i0.hdslb.com/bfs/vip/0ebb039d9fefd37e094f0f181d7cfac9efd019be.png", + "background_image_url": "http://i0.hdslb.com/bfs/vip/6c32fe89bb56096fc963ed35118092744cb463b6.png", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E7%A9%BA%E9%97%B4%E8%87%AA%E4%B8%BB%E5%A4%B4%E5%9B%BE\u0026closable=1", + "image_url": "http://i0.hdslb.com/bfs/vip/f0b521b39a941f0f7198fbe7884aa41af0817ffe.png", + "type": 0, + "hot_type": 1, + "new_type": 0, + "id": 24 + }, { + "first_id": 4, + "report_id": "emoticon", + "name": "评论表情", + "desc": "评论有表情", + "explain": "会员可在评论中发送图片表情,表情多多,表情包常常更新哦。", + "icon_url": "http://i0.hdslb.com/bfs/vip/0a2199caf544263440dbc3e60722881f1a4b6381.png", + "icon_gray_url": "", + "background_image_url": "http://i0.hdslb.com/bfs/vip/f882182fcbca520194d9047ca4903dc2c1e42372.png", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E8%AF%84%E8%AE%BA%E8%A1%A8%E6%83%85\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/bbb74fa3264ef9cc0ae1de15e4989b9473a0d6d3.png", + "type": 0, + "hot_type": 1, + "new_type": 0, + "id": 22 + }] + }, { + "id": 2, + "name": "大福利", + "child_privileges": [{ + "first_id": 2, + "report_id": "", + "name": "游戏代金券", + "desc": "多款游戏满10-3代金券", + "explain": "大会员在会员有效期内,每31天可领取1张适用于多款游戏的满10-3代金券。\r\n代金券适用于哔哩哔哩游戏中心下载的多款游戏,适用游戏名单每月随券更新;\r\n代金券有效期为每月1日至次月16日,具体适用游戏范围和有效期详见代金券说明;用户领取后可前往App“游戏中心--我的-右上角“代金券”查看游戏代金券,并在游戏提交内购订单时,选择代金券进行使用。", + "icon_url": "http://i0.hdslb.com/bfs/vip/1cd8ec97f28d030180671dd05026de692fc5fbac.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=游戏代金券\u0026closable=1", + "image_url": "", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 95 + }, { + "first_id": 2, + "report_id": "", + "name": "漫画商城券", + "desc": "每月可领", + "explain": "年度大会员在会员有效期内,在哔哩哔哩APP\"我的”-“我的资产”及哔哩哔哩漫画APP“我的”-“卡券包\"-\"大会员特权”,每31天可领取1张漫画商城福利券。福利券仅可在哩哩漫画App中使用。\r\n(可在\"漫画商城\"全品类中使用)", + "icon_url": "http://i0.hdslb.com/bfs/vip/317f64b8ff192bb37762499520aa985cb8d95492.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=漫画商城券\u0026closable=1", + "image_url": "", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 94 + }, { + "first_id": 2, + "report_id": "", + "name": "演出观影特权", + "desc": "提前抢演唱会、优惠看电影", + "explain": "开通时长大于等于31天的大会员,在会员有效期内,每30天可领取1次以下权益:30天淘麦VIP体验会员身份,同时赠送300淘麦会员积分。淘麦VIP会员的使用需遵循《淘麦VIP会员服务协议》。 30天淘麦VIP体验会员有效期为自用户领取本权益之日起算(含领取当日)30天。大会员用户可以基于淘麦身份及积分兑换,享受演出提前抢及电影优惠。", + "icon_url": "http://i0.hdslb.com/bfs/vip/ea23fe0fcbf309eeded2f64f3e510140e2e075c1.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=演出观影特权\u0026closable=1", + "image_url": "http://i0.hdslb.com/bfs/vip/395cb31f72505d3649d77e923283460b60602124.jpg", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 92 + }, { + "first_id": 2, + "report_id": "lesson", + "name": "课程折扣", + "desc": "课程5折优惠券", + "explain": "开通时长大于等于31天的大会员,在会员有效期内,每31天可领取1张课程5折优惠券,优惠券领取后15天内有效,具体适用范围及更多详情可前往:“哔哩哔哩App-我的-我的课程-券包”。", + "icon_url": "http://i0.hdslb.com/bfs/vip/0378963a07b8e866e8303892c1d21e358e3b4143.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E8%AF%BE%E7%A8%8B%E6%8A%98%E6%89%A3\u0026closable=1", + "image_url": "http://i0.hdslb.com/bfs/vip/06f0cc1095c402595648db8deb5e7ba67cdec1df.png", + "type": 0, + "hot_type": 1, + "new_type": 1, + "id": 84 + }, { + "first_id": 2, + "report_id": "discountcos", + "name": "装扮折扣", + "desc": "全场装扮8折起", + "explain": "大会员用户可在会员有效期内,可享受专属折扣购买个性装扮套装(除部分版权受限装扮以外),适用装扮和折扣详情可前往“哔哩哔哩App-我的-个性装扮”查看。", + "icon_url": "http://i0.hdslb.com/bfs/vip/320bf3cc05173e59c9af4d65e282ea23016b8431.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://www.bilibili.com/h5/mall/home?navhide=1\u0026f_source=vip", + "image_url": "http://i0.hdslb.com/bfs/vip/eddd3cf6508e4304e47a6e1c93a78e24aa1d1149.png", + "type": 0, + "hot_type": 1, + "new_type": 1, + "id": 83 + }, { + "first_id": 2, + "report_id": "freecos", + "name": "装扮体验", + "desc": "全场装扮免费体验", + "explain": "大会员用户可在会员有效期内,每31天可领取3张装扮体验卡,每张体验卡可体验穿戴任一装扮套装3天(除部分版权受限装扮以外)。体验卡领取后30天内有效,详情可前往“哔哩哔哩App-我的-个性装扮-我的装扮-体验卡”查看。", + "icon_url": "http://i0.hdslb.com/bfs/vip/569dc5289dc12b2cf8e8694c00862603437c6e30.png", + "icon_gray_url": "", + "background_image_url": "", + "link": "https://www.bilibili.com/h5/mall/home?navhide=1\u0026f_source=vip", + "image_url": "http://i0.hdslb.com/bfs/vip/801f7799d14786281d33adffd8cd998b3d7402f6.png", + "type": 0, + "hot_type": 1, + "new_type": 1, + "id": 82 + }, { + "first_id": 2, + "report_id": "bcoupon", + "name": "B币券/观影券", + "desc": "特色权益二选一", + "explain": "开通大会员时长大于等于31天的年度大会员,在会员有效期内,每31天可领取1张5B币券或1张观影券。当月开通或升级的年度大会员,也可以立即领取。该权益属于附赠权益。\r\n观影券:领取后30天内有效,可用于兑换“电影”分区内需单片付费的1部的电影(实际应支付金额在6元及以内)的观看资格,兑换后观看有效期为48小时,请在规定有效期内兑换并观看,到期未使用不予补偿。\r\nB币券:领取后30天内有效,可用于版权内容点播、哔哩哔哩漫画、装扮等场景中的支付抵扣。赠送的B币券在使用时不再赠送会员积分,到期未使用不予补偿。", + "icon_url": "http://i0.hdslb.com/bfs/vip/2f5e098267b070ef42ba9ee58520b858e5bc00b7.png", + "icon_gray_url": "http://i0.hdslb.com/bfs/vip/c3dcef2bfea737a1342dacea1027a3b299d3cf71.png", + "background_image_url": "http://i0.hdslb.com/bfs/vip/20de7f4e81775c4cec1a9653131e5b10c8c8f41d.png", + "link": "https://big.bilibili.com/mobile/explainDetails?name=B%E5%B8%81%E5%88%B8\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/11b77dd4ac7aabe42616031a7fe4b2a17f9e632d.png", + "type": 1, + "hot_type": 0, + "new_type": 0, + "id": 19 + }, { + "first_id": 2, + "report_id": "vipmall", + "name": "会员购", + "desc": "会员购优惠券", + "explain": "开通时长大于等于31天的大会员,在会员有效期内,每31天可领取1张会员购10元包邮券;开通时长大于等于31天的年度大会员,在会员有效期内,每31天可领取1张会员购10元包邮券、1张会员购满50-10元优惠券。当月开通或升级的年度大会员,也可以立即领取;\r\n优惠券及包邮券有效期至领取后15天,具体有效期及使用范围详见优惠券说明;\r\n年度大会员可前往App“分区--会员购--右上角“优惠券”查看优惠券及包邮券,并前往App“分区--会员购”,在提交订单时选择优惠券及包邮券进行使用;该大会员特权需将哔哩哔哩APP升级至6.65版本及以上领取和使用。", + "icon_url": "http://i0.hdslb.com/bfs/vip/7384adee91e7afa1e2d2a97f0c2aaaab71f0e415.png", + "icon_gray_url": "http://i0.hdslb.com/bfs/vip/972283284cfb7f3b3063b1b391aeeb4cbed3249d.png", + "background_image_url": "http://i0.hdslb.com/bfs/vip/21d79540e10618ee9bbaf8874ae711442d10edf0.png", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E4%BC%9A%E5%91%98%E8%B4%AD\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/9af168f768312da3c31df23c056f33b3dcaefe8a.jpg", + "type": 1, + "hot_type": 0, + "new_type": 1, + "id": 20 + }, { + "first_id": 2, + "report_id": "giftbag", + "name": "年度游戏礼包", + "desc": "不定期更新", + "explain": "年度大会员可以在游戏礼包中心领取不同游戏的多款超值礼包,礼包数量和内容常常更新。\r\n\r\n具体使用方法请参照各个礼包的使用详情。", + "icon_url": "http://i0.hdslb.com/bfs/vip/6af11bdd8bb30d187daa6215bc1f4098498caa9c.png", + "icon_gray_url": "http://i0.hdslb.com/bfs/vip/c1c810d0ad13b325da6f3dbde1adb5f351adc55c.png", + "background_image_url": "http://i0.hdslb.com/bfs/vip/013b5c7b3ba45c7a0f4b7a4967cf55aca3c92e40.png", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E6%B8%B8%E6%88%8F%E7%A4%BC%E5%8C%85\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/80a5e1a19192ae65f2c267f1a672c3aaeb582447.png", + "type": 1, + "hot_type": 0, + "new_type": 0, + "id": 21 + }, { + "first_id": 2, + "report_id": "comic", + "name": "漫读券", + "desc": "每月赠送漫画阅读券", + "explain": "开通时长大于等于31天的大会员,会员有效期内,在哔哩哔哩APP“我的”-“卡券包”,及哔哩哔哩漫画APP“我的”-“卡券包”-“大会员特权”,每31天可领取5张漫读券;开通时长大于等于31天的年度大会员,会员有效期内,每31天可领取10张漫读券(可在“哔哩哔哩漫画app”中用于观看付费漫画);\r\n该特权自开通起每31天可领取一次,当期内未领取则视为作废;\r\n漫读券使用有效期至领取后30天,具体有效期及适用范围详见券面说明;\r\n领取的漫读券可在哔哩哔哩APP“我的”-“卡券包”,及哔哩哔哩漫画APP“我的”-“卡券包”中查看;该大会员特权需将漫画APP升级至3.9版本及以上领取和使用;\r\n该特权有效期至2022年12月31日。", + "icon_url": "http://i0.hdslb.com/bfs/vip/3c542377eaa697620a5ff856ff994db6198cb8a6.png", + "icon_gray_url": "", + "background_image_url": "http://i0.hdslb.com/bfs/vip/ba0c9df7c41d6c23c3c2470b5dbbbd5cf4d3d9c2.png", + "link": "https://big.bilibili.com/mobile/explainDetails?name=%E6%BC%AB%E8%AF%BB%E5%88%B8\u0026closable=1\u0026navhide=1", + "image_url": "http://i0.hdslb.com/bfs/vip/8a0d392d0d509c4bdff76aa98ccc007cd22b65a9.png", + "type": 0, + "hot_type": 0, + "new_type": 0, + "id": 47 + }] + }], + "privileges_covers": [{ + "text": "免费看", + "image": "https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/wbpZMLYRH2.png", + "image_small": "", + "link": "" + }, { + "text": "超清看", + "image": "https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/N32Mx1ZAPE.png", + "image_small": "", + "link": "" + }, { + "text": "粉色昵称", + "image": "https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/7ODRSwp27r.png", + "image_small": "", + "link": "" + }, { + "text": "等级加速", + "image": "https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/0p5VT8WFDb.png", + "image_small": "", + "link": "" + }, { + "text": "彩色弹幕", + "image": "https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/25iQqgExtx.png", + "image_small": "", + "link": "" + }, { + "text": "抢先看", + "image": "https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/8Ul33HFTHD.png", + "image_small": "", + "link": "" + }, { + "text": "半价点播", + "image": "https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/E4ivS8ARC5.png", + "image_small": "", + "link": "" + }, { + "text": "杜比音效", + "image": "https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/saSBoy57sz.png", + "image_small": "", + "link": "" + }, { + "text": "装扮体验", + "image": "https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/uhqJANCq1u.png", + "image_small": "", + "link": "" + }, { + "text": "点映会", + "image": "https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/SPNOKtD7oT.png", + "image_small": "", + "link": "" + }], + "num": 33 + }, + "banner": [{ + "id": 115071, + "index": 1, + "image": "", + "bigger_image": "https://i1.hdslb.com/bfs/vip/44acd5ca4e98b646d5edc2fd268138ec36c76be0.png", + "title": "ads_title_1", + "link": "https://www.bilibili.com/blackboard/activity-XABRe5d2aJ.html?msource=centerbanner\u0026order_report_params=%7B%22exp_group_tag%22%3A%22def%22%2C%22exp_tag%22%3A%22def%22%2C%22material_type%22%3A%223%22%2C%22position_id%22%3A%2213%22%2C%22request_id%22%3A%2229d91790874e23e60e8e2224ac683bd4%22%2C%22tips_id%22%3A%22115071%22%2C%22tips_repeat_key%22%3A%22115071%3A13%3A1748751549%3A341688380%22%2C%22unit_id%22%3A%2231819%22%2C%22vip_status%22%3A%221%22%2C%22vip_type%22%3A%222%22%7D\u0026source_from=333.156.selfDef.slideBannerClick", + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "13", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "115071", + "tips_repeat_key": "115071:13:1748751549:341688380", + "unit_id": "31819", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 115637, + "index": 2, + "image": "", + "bigger_image": "https://i0.hdslb.com/bfs/vip/5ad9c5740658d9148a83b8af21d18bf4a2a8eaa1.png", + "title": "ads_title_2", + "link": "https://www.bilibili.com/blackboard/activity-loYs8BvZKi.html?order_report_params=%7B%22exp_group_tag%22%3A%22def%22%2C%22exp_tag%22%3A%22def%22%2C%22material_type%22%3A%223%22%2C%22position_id%22%3A%2213%22%2C%22request_id%22%3A%2229d91790874e23e60e8e2224ac683bd4%22%2C%22tips_id%22%3A%22115637%22%2C%22tips_repeat_key%22%3A%22115637%3A13%3A1748751549%3A341688380%22%2C%22unit_id%22%3A%2231970%22%2C%22vip_status%22%3A%221%22%2C%22vip_type%22%3A%222%22%7D\u0026source_from=333.156.selfDef.slideBannerClick", + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "13", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "115637", + "tips_repeat_key": "115637:13:1748751549:341688380", + "unit_id": "31970", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 114722, + "index": 3, + "image": "", + "bigger_image": "https://i0.hdslb.com/bfs/vip/0b0d1ce706df42537d05fbc1c783c3875aeec7e8.jpg", + "title": "ads_title_3", + "link": "https://xdxw.biligame.com/sfhd?order_report_params=%7B%22exp_group_tag%22%3A%22def%22%2C%22exp_tag%22%3A%22def%22%2C%22material_type%22%3A%223%22%2C%22position_id%22%3A%2213%22%2C%22request_id%22%3A%2229d91790874e23e60e8e2224ac683bd4%22%2C%22tips_id%22%3A%22114722%22%2C%22tips_repeat_key%22%3A%22114722%3A13%3A1748751549%3A341688380%22%2C%22unit_id%22%3A%2231640%22%2C%22vip_status%22%3A%221%22%2C%22vip_type%22%3A%222%22%7D\u0026source_from=333.156.selfDef.slideBannerClick", + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "13", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "114722", + "tips_repeat_key": "114722:13:1748751549:341688380", + "unit_id": "31640", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 115552, + "index": 4, + "image": "", + "bigger_image": "https://i0.hdslb.com/bfs/vip/40cc4380429c1fdd638978c3aefd71331bd2da09.jpg", + "title": "ads_title_4", + "link": "https://mall.bilibili.com/act/aicms/QKrdSanW4.html?order_report_params=%7B%22exp_group_tag%22%3A%22def%22%2C%22exp_tag%22%3A%22def%22%2C%22material_type%22%3A%223%22%2C%22position_id%22%3A%2213%22%2C%22request_id%22%3A%2229d91790874e23e60e8e2224ac683bd4%22%2C%22tips_id%22%3A%22115552%22%2C%22tips_repeat_key%22%3A%22115552%3A13%3A1748751549%3A341688380%22%2C%22unit_id%22%3A%2231949%22%2C%22vip_status%22%3A%221%22%2C%22vip_type%22%3A%222%22%7D\u0026source_from=333.156.selfDef.slideBannerClick", + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "13", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "115552", + "tips_repeat_key": "115552:13:1748751549:341688380", + "unit_id": "31949", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 115356, + "index": 5, + "image": "", + "bigger_image": "https://i0.hdslb.com/bfs/vip/705588e4db9342ea9c5dd0645d5ac13476a79008.jpg", + "title": "ads_title_5", + "link": "https://game.bilibili.com/nslg/dhyhdSE/?sourceFrom=1000680042\u0026order_report_params=%7B%22exp_group_tag%22%3A%22def%22%2C%22exp_tag%22%3A%22def%22%2C%22material_type%22%3A%223%22%2C%22position_id%22%3A%2213%22%2C%22request_id%22%3A%2229d91790874e23e60e8e2224ac683bd4%22%2C%22tips_id%22%3A%22115356%22%2C%22tips_repeat_key%22%3A%22115356%3A13%3A1748751549%3A341688380%22%2C%22unit_id%22%3A%2231893%22%2C%22vip_status%22%3A%221%22%2C%22vip_type%22%3A%222%22%7D\u0026source_from=333.156.selfDef.slideBannerClick", + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "13", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "115356", + "tips_repeat_key": "115356:13:1748751549:341688380", + "unit_id": "31893", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 112569, + "index": 7, + "image": "", + "bigger_image": "https://i0.hdslb.com/bfs/vip/d7f15a9feffa8f11ff39f152e96db3c58735e294.jpg", + "title": "ads_title_7", + "link": "https://www.bilibili.com/blackboard/activity-g97AHtCUsb.html?order_report_params=%7B%22exp_group_tag%22%3A%22def%22%2C%22exp_tag%22%3A%22def%22%2C%22material_type%22%3A%223%22%2C%22position_id%22%3A%2213%22%2C%22request_id%22%3A%2229d91790874e23e60e8e2224ac683bd4%22%2C%22tips_id%22%3A%22112569%22%2C%22tips_repeat_key%22%3A%22112569%3A13%3A1748751549%3A341688380%22%2C%22unit_id%22%3A%2230705%22%2C%22vip_status%22%3A%221%22%2C%22vip_type%22%3A%222%22%7D\u0026source_from=333.156.selfDef.slideBannerClick", + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "13", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "112569", + "tips_repeat_key": "112569:13:1748751549:341688380", + "unit_id": "30705", + "vip_status": "1", + "vip_type": "2" + } + }], + "union_vip": { + "union_vips": [], + "sort": 1, + "title": "联合会员", + "jump_link": "https://www.bilibili.com/blackboard/activity-yIBnONpv1d.html?msource=juheye\u0026app_id=125\u0026source_from=666.146.selfDef.coWorkBannerClick" + }, + "other_open_info": { + "open_infos": [{ + "title": "赠送好友", + "url": "https://m.bilibili.com/doria/vip-gift.html?navhide=1\u0026app_id=125\u0026source_from=666.146.moreAddons.Click", + "icon_url": "http://i0.hdslb.com/bfs/vip/8468cab8be6ad16ac42e55c2c62191b65ca251cc.png", + "desc": "与好友一起干杯", + "sort": 1, + "id": 15 + }, { + "title": "激活码开通", + "url": "https://big.bilibili.com/mobile/activation?closable=1\u0026navhide=1", + "icon_url": "http://i0.hdslb.com/bfs/archive/78b8e391b06a55f70b20362d8245682f491c0a77.png", + "desc": "免费开通大会员", + "sort": 2, + "id": 16 + }], + "sort": 2 + }, + "benefits": [{ + "id": 114391, + "index": 0, + "type": 5, + "sub_type": null, + "title": "大会员专属演出观影特权", + "sub_title": "300积分+8元电影优惠券", + "sub_title_type": 0, + "dress_id": 0, + "collection_id": 0, + "collection_activity_id": 0, + "color": "", + "img": { + "text": "", + "image": "https://i0.hdslb.com/bfs/vip/f214387b86ce6826c7edc7a3dacde9c215c46bf0.jpg", + "image_small": "https://i0.hdslb.com/bfs/vip/8003bf0608f943709a792efbeea89dcca5b58608.jpg", + "link": "" + }, + "garb_imgs": [], + "button": { + "text": "每月领", + "action_type": "", + "link": "https://m.bilibili.com/doria/v2/union-benefits.html?msource=getbenefits" + }, + "state": 0, + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "outer_type": "taomai", + "position_id": "62", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "taomai_period_end_unix": "0", + "taomai_timestamp": "1748751549", + "tips_id": "114391", + "tips_repeat_key": "114391:62:1748751549:341688380", + "unit_id": "31440", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 113041, + "index": 0, + "type": 1, + "sub_type": [6], + "title": "良辰美景·不问天", + "sub_title": "3张免费体验卡可领取", + "sub_title_type": 2, + "dress_id": 4019, + "collection_id": 0, + "collection_activity_id": 0, + "color": "#e7e1f1", + "img": { + "text": "主题", + "image": "https://i0.hdslb.com/bfs/garb/item/3dacdb35435c9474fe89bb69174af61b38befa8d.jpg", + "image_small": "", + "link": "" + }, + "garb_imgs": [{ + "text": "空间背景", + "image": "https://i0.hdslb.com/bfs/garb/item/2961ae11f3f77f9086fd9702e2cb5b30e6f4d39f.jpg", + "image_small": "", + "link": "" + }, { + "text": "表情包", + "image": "https://i0.hdslb.com/bfs/emote/1c455847f5f1521345b108bdbc1761715850a15e.png", + "image_small": "", + "link": "" + }, { + "text": "动态卡片", + "image": "https://i0.hdslb.com/bfs/garb/item/ef9c5ac04356c52907dbad8e54d630338045476e.png", + "image_small": "", + "link": "" + }, { + "text": "评论背景", + "image": "https://i0.hdslb.com/bfs/garb/item/8626432939ec2a06f482cd7d87dfe66aa44036b2.png", + "image_small": "", + "link": "" + }], + "button": { + "text": "立即领取", + "action_type": "", + "link": "" + }, + "state": 0, + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "62", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "113041", + "tips_repeat_key": "113041:62:1748751549:341688380", + "unit_id": "14545", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 115499, + "index": 0, + "type": 4, + "sub_type": null, + "title": "《三国:谋定天下》福利", + "sub_title": "领超萌小电视定制表情", + "sub_title_type": 0, + "dress_id": 0, + "collection_id": 0, + "collection_activity_id": 0, + "color": "", + "img": { + "text": "", + "image": "https://i0.hdslb.com/bfs/vip/b724cea88d0e4003a1fc4d62d59ca61f4b150df0.jpg", + "image_small": "https://i0.hdslb.com/bfs/vip/a532ee4b1061a8424e5e8c57378e5d269d03a32f.jpg", + "link": "" + }, + "garb_imgs": [], + "button": { + "text": "立刻领取", + "action_type": "", + "link": "https://game.bilibili.com/nslg/dhyhdSE/?sourceFrom=1000680022" + }, + "state": 0, + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "62", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "115499", + "tips_repeat_key": "115499:62:1748751549:341688380", + "unit_id": "31908", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 114536, + "index": 0, + "type": 4, + "sub_type": null, + "title": "大会员专享游戏权益", + "sub_title": "游戏折扣随心领", + "sub_title_type": 0, + "dress_id": 0, + "collection_id": 0, + "collection_activity_id": 0, + "color": "", + "img": { + "text": "", + "image": "https://i0.hdslb.com/bfs/vip/03643637fb6720e88063f5483eaca4f1b7875154.jpg", + "image_small": "https://i0.hdslb.com/bfs/vip/ea774ed25b98e5668c30753c816ff21c1775d1ed.png", + "link": "" + }, + "garb_imgs": [], + "button": { + "text": "去领取", + "action_type": "", + "link": "https://big.bilibili.com/mobile/cardBag?closable=1\u0026navhide=1\u0026tab=welfare\u0026skuType=118" + }, + "state": 0, + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "62", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "114536", + "tips_repeat_key": "114536:62:1748751549:341688380", + "unit_id": "31546", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 45028, + "index": 0, + "type": 3, + "sub_type": [2, 4], + "title": "专属会员购福利", + "sub_title": "精品周边超值购", + "sub_title_type": 0, + "dress_id": 0, + "collection_id": 0, + "collection_activity_id": 0, + "color": "", + "img": { + "text": "", + "image": "https://i0.hdslb.com/bfs/activity-plat/static/20241105/4cefecc6742f8995a6bd22402a6d0b8b/jqJzlNE45x.png", + "image_small": "https://i0.hdslb.com/bfs/vip/c019881f9e0c4a20a99a3cc3604ca2f3cdbaf858.jpg", + "link": "" + }, + "garb_imgs": [], + "button": { + "text": "去使用", + "action_type": "", + "link": "" + }, + "state": 0, + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "62", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "45028", + "tips_repeat_key": "45028:62:1748751549:341688380", + "unit_id": "14542", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 45032, + "index": 0, + "type": 3, + "sub_type": [3], + "title": "专享漫画礼包", + "sub_title": "海量漫画随心看", + "sub_title_type": 0, + "dress_id": 0, + "collection_id": 0, + "collection_activity_id": 0, + "color": "", + "img": { + "text": "", + "image": "https://i0.hdslb.com/bfs/activity-plat/static/20241105/4cefecc6742f8995a6bd22402a6d0b8b/06GTgEunNO.png", + "image_small": "https://i0.hdslb.com/bfs/vip/54adfdfeb811917a4d41ce618a82b9966f4e0e49.jpg", + "link": "" + }, + "garb_imgs": [], + "button": { + "text": "去使用", + "action_type": "", + "link": "" + }, + "state": 0, + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "62", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "45032", + "tips_repeat_key": "45032:62:1748751549:341688380", + "unit_id": "14546", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 45990, + "index": 0, + "type": 3, + "sub_type": [7], + "title": "会员特享课程折扣", + "sub_title": "品质课程5折享", + "sub_title_type": 0, + "dress_id": 0, + "collection_id": 0, + "collection_activity_id": 0, + "color": "", + "img": { + "text": "", + "image": "https://i0.hdslb.com/bfs/activity-plat/static/20241105/4cefecc6742f8995a6bd22402a6d0b8b/55JxkQCJpd.png", + "image_small": "https://i0.hdslb.com/bfs/vip/080f4fb4f7789f6bf7e3a71907690f0f1906d66c.jpg", + "link": "" + }, + "garb_imgs": [], + "button": { + "text": "去使用", + "action_type": "", + "link": "" + }, + "state": 0, + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "62", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "45990", + "tips_repeat_key": "45990:62:1748751549:341688380", + "unit_id": "14853", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 46005, + "index": 0, + "type": 4, + "sub_type": null, + "title": "王者荣耀每日礼包", + "sub_title": "概率得皮肤心意金", + "sub_title_type": 0, + "dress_id": 0, + "collection_id": 0, + "collection_activity_id": 0, + "color": "", + "img": { + "text": "", + "image": "https://i0.hdslb.com/bfs/bangumi/kt/ba11dab14bce2372c9e7f13ee617e4e9f8bc1701.jpg", + "image_small": "https://i0.hdslb.com/bfs/vip/2f6f16ca854b0a1e55aa726f3353cb7c5d11e068.jpg", + "link": "" + }, + "garb_imgs": [], + "button": { + "text": "点我领取", + "action_type": "", + "link": "https://igame.qq.com/tip/ingame-page/igame-regift-box/index.html?brandid=b1683628293\u0026multiCfgId=b16836282931693884106" + }, + "state": 0, + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "62", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "46005", + "tips_repeat_key": "46005:62:1748751549:341688380", + "unit_id": "14865", + "vip_status": "1", + "vip_type": "2" + } + }], + "big_point": { + "point_info": null, + "sign_info": null, + "sku_info": null, + "point_switch_off": false, + "tips": null, + "button_text": "", + "sku_price_hidden": false + }, + "welfare": { + "count": 2, + "list": [{ + "id": 122, + "name": "移动免费领2G流量", + "homepage_uri": "https://i1.hdslb.com/bfs/vip/14a31d6e1ee260db2114b80e26469432880375ae.png", + "backdrop_uri": "https://i1.hdslb.com/bfs/vip/9343d88045e915e76e1f6eb40887810a8da00997.png", + "tid": 0, + "rank": 1, + "receive_uri": "https://wx.10086.cn/qwhdhub/leadin/1025012314?A_C_CODE=tApqjxVygk\u0026channelId=P00000112259" + }, { + "id": 80, + "name": "联通首月1分钱", + "homepage_uri": "https://i0.hdslb.com/bfs/vip/30f34965287b07e2fd752c114a90aa275951940a.png", + "backdrop_uri": "https://i0.hdslb.com/bfs/vip/73897d68b84e2caa47fc5f84953ae5b94df28208.png", + "tid": 0, + "rank": 3, + "receive_uri": "https://operation.bol.wo.cn/a/#/e86e506acf4f" + }] + }, + "experience": { + "level": 6, + "cur_exp": 49792, + "next_exp": -1, + "is_senior_member": 0, + "is_get_exp": false, + "is_task_complete": true, + "state": 0 + }, + "vip_exclusive": [], + "draw_cards_welfare": [{ + "id": 105056, + "title": "少女乐队的呐喊", + "sub_title": "会员首抽福利,低至4.9元", + "img": {}, + "button": { + "text": "去抽取" + }, + "state": 0, + "garb_imgs": [{ + "img_url": "https://i0.hdslb.com/bfs/garb/open/d1ec5f815df64fe371d3a9150f19f466fee5ea2e.jpg", + "text": "13张卡片", + "color": "#4e3738" + }, { + "img_url": "https://i0.hdslb.com/bfs/garb/open/1b4501496bce918624c4d8054c072e3eacb7a5ae.png", + "text": "评论背景" + }, { + "img_url": "https://i0.hdslb.com/bfs/garb/open/1ad674dc624758e8e7d7875a00d2895316f78a77.png", + "text": "头像框" + }], + "collection_activity_id": 104978, + "exp_params": null, + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "65", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "104928", + "tips_repeat_key": "104928:65:1748751549:341688380", + "unit_id": "27796", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 105607, + "title": "蔚蓝档案", + "sub_title": "会员首抽福利,低至4.9元", + "img": {}, + "button": { + "text": "去抽取" + }, + "state": 0, + "garb_imgs": [{ + "img_url": "https://i0.hdslb.com/bfs/garb/open/8e5c8a257b8cb75ad1921cde40e1f3834b4b0649.png", + "text": "16张卡片", + "color": "#dfc0d9" + }, { + "img_url": "http://i0.hdslb.com/bfs/archive/3d9af842999732755c5181497a1bf6e17574a2b1.png", + "text": "评论背景" + }, { + "img_url": "https://i0.hdslb.com/bfs/garb/open/40c5554bec046c8c58c7f18f8d845a2c8f51e6a0.png", + "text": "头像框" + }], + "collection_activity_id": 105606, + "exp_params": null, + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "65", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "104928", + "tips_repeat_key": "104928:65:1748751549:341688380", + "unit_id": "27796", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 102858, + "title": "良辰共此曲", + "sub_title": "会员首抽福利,低至4.9元", + "img": {}, + "button": { + "text": "去抽取" + }, + "state": 0, + "garb_imgs": [{ + "img_url": "https://i0.hdslb.com/bfs/garb/open/05f5737c93982f6ac76fe43e101ed7aa015977f8.png", + "text": "25张卡片", + "color": "#3b312e" + }, { + "img_url": "https://i0.hdslb.com/bfs/garb/open/c97b571ae4e93ed7aff312e324992e0d08c16a14.png", + "text": "评论背景" + }, { + "img_url": "https://i0.hdslb.com/bfs/garb/open/0e8854c5f5572cd282e3dd1749ed8f312f25e663.png", + "text": "头像框" + }], + "collection_activity_id": 102857, + "exp_params": null, + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "65", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "104928", + "tips_repeat_key": "104928:65:1748751549:341688380", + "unit_id": "27796", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 101580, + "title": "EVA:序破", + "sub_title": "会员首抽福利,低至4.9元", + "img": {}, + "button": { + "text": "去抽取" + }, + "state": 0, + "garb_imgs": [{ + "img_url": "https://i0.hdslb.com/bfs/garb/open/4c043be753fcbf2f827553de9f01a5ba4231ddc6.png", + "text": "19张卡片", + "color": "#5f1a0d" + }, { + "img_url": "https://i0.hdslb.com/bfs/garb/open/80920496892f96ba14b2869676fc449969cdfda8.png", + "text": "评论背景" + }, { + "img_url": "https://i0.hdslb.com/bfs/garb/open/6fe67f4fd45ae75e31a706bb7b89c19d0d09c719.png", + "text": "头像框" + }], + "collection_activity_id": 101579, + "exp_params": null, + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "65", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "104928", + "tips_repeat_key": "104928:65:1748751549:341688380", + "unit_id": "27796", + "vip_status": "1", + "vip_type": "2" + } + }, { + "id": 106111, + "title": "魔女之夜", + "sub_title": "会员首抽福利,低至4.9元", + "img": {}, + "button": { + "text": "去抽取" + }, + "state": 0, + "garb_imgs": [{ + "img_url": "https://i0.hdslb.com/bfs/garb/open/23ef7df4711a1418e9d5d56f9b48937abd4b8564.png", + "text": "16张卡片", + "color": "#2b2e20" + }, { + "img_url": "https://i0.hdslb.com/bfs/garb/open/867e6db4dd50523ca22cfef98eea069cd8949fc6.png", + "text": "评论背景" + }, { + "img_url": "https://i0.hdslb.com/bfs/garb/open/b9a055a3e4340f3291e4c934999b886562ef64c2.png", + "text": "头像框" + }], + "collection_activity_id": 102161, + "exp_params": null, + "track_params": { + "exp_group_tag": "def", + "exp_tag": "def", + "material_type": "3", + "position_id": "65", + "request_id": "29d91790874e23e60e8e2224ac683bd4", + "tips_id": "104928", + "tips_repeat_key": "104928:65:1748751549:341688380", + "unit_id": "27796", + "vip_status": "1", + "vip_type": "2" + } + }], + "free_welfare": [{ + "id": 4650011, + "icon": "https://i0.hdslb.com/bfs/bangumi/image/d6b4efd6fc925dcb98aae009432101f892795f84.jpg", + "title": "电信·星级服务|专属礼遇", + "titleColor": "", + "subtitle": "大会员任意兑", + "subtitleColor": "#9499A0", + "subtitle2": "2500积分起", + "subtitle2Color": "#FF6699", + "task_state": 1, + "link": "https://www.bilibili.com/blackboard/activity-r4Qm9nZabK.html?taskId=4650011\u0026channel=outer_h5", + "channel": "outer_h5" + }, { + "id": 4200111, + "icon": "https://i0.hdslb.com/bfs/bangumi/image/d2b22160e79c43399c34f40ff1b8ccaadd0d95d5.png", + "title": "开通江苏电信X连续会员包", + "titleColor": "", + "subtitle": "首月1分钱", + "subtitleColor": "#9499A0", + "subtitle2": "每月享大会员", + "subtitle2Color": "#FF6699", + "task_state": 1, + "link": "https://www.bilibili.com/blackboard/activity-8mit5o2IL9.html?taskId=4200111\u0026channel=outer_h5", + "channel": "outer_h5" + }, { + "id": 4620003, + "icon": "https://i0.hdslb.com/bfs/bangumi/image/6fd085fca6f1ad5c35bcba5f64a8959801eba563.jpg", + "title": "农行信用卡客户专享", + "titleColor": "", + "subtitle": "积分兑大会员", + "subtitleColor": "#9499A0", + "subtitle2": "月卡0元得", + "subtitle2Color": "#FF6699", + "task_state": 1, + "link": "https://www.bilibili.com/blackboard/activity-EfBpx3pZAx.html?taskId=4620003\u0026channel=outer_h5", + "channel": "outer_h5" + }, { + "id": 4800017, + "icon": "https://i0.hdslb.com/bfs/bangumi/image/9a6849d7dceff553f88e7b49beb810ac82900bc2.png", + "title": "开通电信XB站会员流量包", + "titleColor": "", + "subtitle": "大会员月月发", + "subtitleColor": "#9499A0", + "subtitle2": "+2G流量", + "subtitle2Color": "#FF6699", + "task_state": 1, + "link": "https://b23.tv/p7JnLcH?taskId=4800017\u0026channel=outer_h5", + "channel": "outer_h5" + }, { + "id": 4230055, + "icon": "https://i0.hdslb.com/bfs/bangumi/image/68729e950e732218de8e5deb803a8822190f6607.png", + "title": "招行信用卡积分兑会员", + "titleColor": "", + "subtitle": "大会员任意兑", + "subtitleColor": "#9499A0", + "subtitle2": "899积分起", + "subtitle2Color": "#FF6699", + "task_state": 1, + "link": "https://www.bilibili.com/blackboard/activity-NWCapLCOgS.html?taskId=4230055\u0026channel=outer_h5", + "channel": "outer_h5" + }, { + "id": 4290081, + "icon": "https://i0.hdslb.com/bfs/bangumi/image/5a0027a21d5298fe62266968a0b65f1ab84efa72.png", + "title": "开通联通王卡", + "titleColor": "#000000", + "subtitle": "每年享960G流量", + "subtitleColor": "#9499A0", + "subtitle2": "领24个月大会员", + "subtitle2Color": "#FF6699", + "task_state": 1, + "link": "https://www.bilibili.com/blackboard/activity-amrPj9oRBu.html?taskId=4290081\u0026channel=cucc_king", + "channel": "cucc_king" + }, { + "id": 4260105, + "icon": "https://i0.hdslb.com/bfs/bangumi/image/19a8a2c8437e36560885adaf18ca382f024dffd8.png", + "title": "南航里程兑大会员", + "titleColor": "", + "subtitle": "南航用户专享", + "subtitleColor": "#9499A0", + "subtitle2": "最高领366天大会员", + "subtitle2Color": "#FF6699", + "task_state": 1, + "link": "https://www.bilibili.com/blackboard/activity-U0jNxo0ARy.html?taskId=4260105\u0026channel=outer_h5", + "channel": "outer_h5" + }], + "model_order": [{ + "is_hidden": false, + "name": "Property,", + "order": 0, + "fixed": false, + "is_show": false, + "style": 0 + }, { + "is_hidden": false, + "name": "Notice", + "order": 0, + "fixed": false, + "is_show": false, + "style": 0 + }, { + "is_hidden": false, + "name": "Privilige", + "order": 0, + "fixed": false, + "is_show": false, + "style": 0 + }, { + "is_hidden": false, + "name": "Banner", + "order": 0, + "fixed": false, + "is_show": false, + "style": 0 + }, { + "is_hidden": false, + "name": "UnionVip", + "order": 0, + "fixed": false, + "is_show": false, + "style": 0 + }, { + "is_hidden": false, + "name": "VipExclusive", + "order": 0, + "fixed": false, + "is_show": false, + "style": 0 + }, { + "is_hidden": false, + "name": "Experience", + "order": 0, + "fixed": false, + "is_show": false, + "style": 0 + }, { + "is_hidden": false, + "name": "Benefit", + "order": 0, + "fixed": false, + "is_show": false, + "style": 0 + }, { + "is_hidden": false, + "name": "GarbCard", + "order": 0, + "fixed": false, + "is_show": false, + "style": 0 + }, { + "is_hidden": false, + "name": "BigPoint", + "order": 0, + "fixed": false, + "is_show": false, + "style": 0 + }, { + "is_hidden": false, + "name": "FreeGet", + "order": 0, + "fixed": false, + "is_show": false, + "style": 0 + }, { + "is_hidden": false, + "name": "Welfare", + "order": 0, + "fixed": false, + "is_show": false, + "style": 0 + }, { + "is_hidden": false, + "name": "OtherOpen", + "order": 0, + "fixed": false, + "is_show": false, + "style": 0 + }], + "buoy": { + "id": 0, + "img": "", + "link": "", + "track_params": null + }, + "offline_activity": { + "link": "https://b23.tv/gjVVysQ", + "title": "大会员专享点映会", + "items": [{ + "id": 4511, + "index": 0, + "title": "免费看《攻壳机动队》", + "type": 3, + "city": "石家庄、青岛、厦门、宁波等10城", + "color": "#4e2b1e", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/45cadd4555494ce0403b310d38ea65e16f9f0092.png", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg", + "time_range": "2025-05-10 15:00:00", + "link": "https://www.bilibili.com/opus/1066777107187630083?spm_id_from=333.1387.0.0", + "start_time": 1745906400, + "end_time": 1746374399, + "movie_reservation_state": false, + "activity_code": "aswkxyiezx", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-JVSbo5S3PJ.html" + }, { + "id": 4499, + "index": 1, + "title": "《孤独摇滚》系列首映礼", + "type": 3, + "city": "上海", + "color": "#4c4039", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/f3f96dd77a5a75988b00b8ddc69515c991b2b17e.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/ONJBt3XdwS.jpg", + "time_range": "2025-05-01 14:00:00", + "link": "https://www.bilibili.com/opus/1062394468895817744?spm_id_from=333.1387.0.0", + "start_time": 1745557200, + "end_time": 1745726400, + "movie_reservation_state": false, + "activity_code": "dhtlvygfvo", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-bxcF1ZxuCO.html" + }, { + "id": 4466, + "index": 2, + "title": "《雷霆特攻队》", + "type": 3, + "city": "成都、武汉、苏州等全国10城", + "color": "#272116", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/9da3eacfa8b8a42e186dd5f438c2818244ce14e6.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg", + "time_range": "2025-04-30 19:00:00", + "link": "https://www.bilibili.com/opus/1064828910022164505?spm_id_from=333.1387.0.0", + "start_time": 1745208000, + "end_time": 1745640000, + "movie_reservation_state": false, + "activity_code": "rwoskyltbn", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-LWhbslNy4m.html" + }, { + "id": 4432, + "index": 3, + "title": "大会员免费看《鲲吞天下》", + "type": 3, + "city": "广州", + "color": "#6a707b", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/9cb6ab34f3e71bb20dcdd2c4331166deea21ddfa.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/ONJBt3XdwS.jpg", + "time_range": "2025-04-26 18:30:00", + "link": "https://www.bilibili.com/bangumi/play/ep1644561?spm_id_from=333.337.search-card.all.click", + "start_time": 1744603200, + "end_time": 1745164799, + "movie_reservation_state": false, + "activity_code": "tcpbpyvpxf", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-wGA7tabyt7.html" + }, { + "id": 4454, + "index": 4, + "title": "大会员免费看《孤独摇滚(上)》", + "type": 3, + "city": "青岛 上海 温州 西安 成都", + "color": "#3e2b23", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/d5ea0f60f075e1f5a7cdf3c7273a0df8e7ec0f39.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg", + "time_range": "2025-04-25 19:00:00", + "link": "https://www.bilibili.com/opus/1060789761724121112?spm_id_from=333.1387.0.0", + "start_time": 1744783200, + "end_time": 1745208000, + "movie_reservation_state": false, + "activity_code": "gatbfzmsan", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-oe5xpiydmA.html" + }, { + "id": 4355, + "index": 5, + "title": "大会员免费看《火之鸟 伊甸之花》", + "type": 3, + "city": "上海 北京 广州 武汉 天津 合肥 福州 昆明 无锡 青岛", + "color": "#413d4e", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/ac8a32709afaf984be1d7d689f89639f85f88907.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg", + "time_range": "2025-04-18 19:00:00", + "link": "https://www.bilibili.com/opus/1059637821452582949?spm_id_from=333.1387.0.0", + "start_time": 1744192800, + "end_time": 1744603200, + "movie_reservation_state": false, + "activity_code": "efgfnwffut", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-MzADOE0NrP.html" + }, { + "id": 4230, + "index": 6, + "title": "《我的世界大电影》点映会", + "type": 3, + "city": "上海、北京、郑州、苏州、西安、成都", + "color": "#3b5553", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/4f9e349909a9b518c6be170ecd7e56b7128eaf6b.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg", + "time_range": "2025-04-04 14:00:00", + "link": "https://www.bilibili.com/opus/1054102270026711044?spm_id_from=333.1387.0.0", + "start_time": 1742788800, + "end_time": 1743307200, + "movie_reservation_state": false, + "activity_code": "qbmniosrdn", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-Sgflj5mZCQ.html" + }, { + "id": 4232, + "index": 7, + "title": "《机动战士高达:跨时之战》", + "type": 3, + "city": "上海 北京 杭州 重庆 武汉 南京 长沙 宁波 济南 广州", + "color": "#344c65", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/bd9ef6967e89e3aec959d03e6a5113ee1efbd10b.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/ONJBt3XdwS.jpg", + "time_range": "2025-04-03 19:00:00", + "link": "https://www.bilibili.com/opus/1053819132876685329?spm_id_from=333.1387.0.0", + "start_time": 1743064200, + "end_time": 1743332400, + "movie_reservation_state": false, + "activity_code": "uanckmbinj", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-5hqPBpKE8c.html" + }, { + "id": 4212, + "index": 8, + "title": "《凸变英雄X》点映会(大陆地区)", + "type": 3, + "city": " 北京、上海、天津、重庆、兰州、西安、福州等34城", + "color": "#65515c", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/67873b32cbf4f2ccb220f0ede452aad82ba52335.png", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg", + "time_range": "2025-03-29 14:00:00", + "link": "https://t.bilibili.com/1051195363130605570?spm_id_from=333.1387.0.0", + "start_time": 1742004000, + "end_time": 1742745599, + "movie_reservation_state": false, + "activity_code": "vygcgwztne", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-0zTwwAW1P2.html" + }, { + "id": 4098, + "index": 9, + "title": "《猫猫的奇幻漂流》", + "type": 3, + "city": "上海、广州、深圳、重庆、武汉、天津、福州、昆明、济南、厦门", + "color": "#37292f", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/413453de3d830c3fd3fcb74b88cd783beddce160.png", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg", + "time_range": "2025-02-28 19:00:00", + "link": "https://www.bilibili.com/opus/1040061725606412297?spm_id_from=333.1365.0.0", + "start_time": 1739851200, + "end_time": 1740326399, + "movie_reservation_state": false, + "activity_code": "akinevpeff", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-5YZwSRTkr3.html" + }, { + "id": 4074, + "index": 10, + "title": "《你的颜色》", + "type": 3, + "city": "上海、北京、成都、深圳、杭州、武汉、南京、西安、长沙、合肥", + "color": "#4f3d44", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/c8c608e1ff80e662d9e8f07518349b20d92533be.png", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg", + "time_range": "2025-02-21 19:00:00", + "link": "https://www.bilibili.com/opus/1037454345412542464?spm_id_from=333.1365.0.0", + "start_time": 1739412000, + "end_time": 1739764799, + "movie_reservation_state": false, + "activity_code": "afamrmcijv", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-wrTmFmjqYg.html" + }, { + "id": 3946, + "index": 11, + "title": "《哪吒之魔童闹海》", + "type": 3, + "city": "上海、北京、广州、成都、西安、重庆、郑州、青岛、东莞、哈尔滨", + "color": "#322126", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/02cabab77c79e3f8da6c1b1e61d64881c273d144.png", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/sCLQXxhiFZ.jpg", + "time_range": "2025-02-02 14:00:00", + "link": "https://www.bilibili.com/opus/1030756202187849733?spm_id_from=333.1365.0.0", + "start_time": 1737432000, + "end_time": 1737993599, + "movie_reservation_state": false, + "activity_code": "dzwazoqncj", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-g97AHtCUsb.html" + }, { + "id": 3719, + "index": 12, + "title": "《坂本日常》", + "type": 3, + "city": "上海· SFC上影影城(八佰伴店)", + "color": "#534042", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/96698374759e02e963140f79572a49beac29b84a.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/ONJBt3XdwS.jpg", + "time_range": "2025-01-05 15:00:00", + "link": "https://www.bilibili.com/bangumi/play/ep1360452?spm_id_from=333.999.list.card_archive.click", + "start_time": 1734946200, + "end_time": 1735660799, + "movie_reservation_state": false, + "activity_code": "jctqvnokfb", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-wDvDvbiDXs.html" + }, { + "id": 3697, + "index": 13, + "title": "《名侦探柯南:迷宫的十字路口》", + "type": 3, + "city": "北京、成都、广州、南京、上海、深圳、沈阳、天津、武汉、西安", + "color": "#5c4748", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/dd3ca2860c87352086020cf9312ccb56c0f1e0ba.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg", + "time_range": "2024-12-27 19:30:00", + "link": "https://www.bilibili.com/video/BV1KD67YPEKL/?spm_id_from=333.999.list.card_archive.click\u0026vd_source=a8d547984b84c6842dee0d5de84e3413", + "start_time": 1734503400, + "end_time": 1734926400, + "movie_reservation_state": false, + "activity_code": "phkoulvyhq", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-i5WfTjIads.html" + }, { + "id": 3274, + "index": 14, + "title": "《蓦然回首》", + "type": 3, + "city": "北京、上海、杭州、天津、成都、武汉、厦门、福州、昆明、青岛", + "color": "#2f1c1f", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/d4105070e4cc5a99a0ae3fd06eb3f8935f3b7a9b.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg", + "time_range": "2024-10-25 19:10:00", + "link": "https://www.bilibili.com/video/BV1hYStYZEwj/?spm_id_from=333.999.0.0\u0026vd_source=a8d547984b84c6842dee0d5de84e3413", + "start_time": 1729245600, + "end_time": 1729526399, + "movie_reservation_state": false, + "activity_code": "zgftsxqegy", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-G9X0bwaTNd.html" + }, { + "id": 3308, + "index": 15, + "title": "《镇魂街》天武风雷篇", + "type": 3, + "city": "上海、广州、成都", + "color": "#382220", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/32dc66c169654154fd55a9b3f13d6dceb13262ba.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/B5hEufyuXo.jpg", + "time_range": "2024-11-16 16:00:00", + "link": "https://www.bilibili.com/bangumi/play/ep1114855?spm_id_from=333.999.0.0", + "start_time": 1730718000, + "end_time": 1731340799, + "movie_reservation_state": false, + "activity_code": "hinjqiumuj", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-48TAYrtJr0.html" + }, { + "id": 3286, + "index": 16, + "title": "《火影忍者:忍者之路 》", + "type": 3, + "city": "北京、上海、广州、成都、深圳、南京、重庆、长沙、苏州、郑州", + "color": "#422b27", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/9f29c05877092f6dc0010af578efe6e83cfd824a.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg", + "time_range": "2024-11-02 16:55:00", + "link": "https://www.bilibili.com/video/BV19BDbYTEb6/?spm_id_from=333.999.0.0\u0026vd_source=a8d547984b84c6842dee0d5de84e3413", + "start_time": 1729742400, + "end_time": 1730131199, + "movie_reservation_state": false, + "activity_code": "hahrideehl", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-BmThoecvQn.html" + }, { + "id": 3220, + "index": 17, + "title": "《毒液》", + "type": 3, + "city": "北京、上海、杭州、哈尔滨、深圳、广州、成都、西安、合肥、重庆", + "color": "#291f22", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/58723e95c544349bcc197bd410cd1b8857edcf57.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg", + "time_range": "2024-10-23 19:00:00", + "link": "https://www.bilibili.com/video/BV1y81LYqEJc/?spm_id_from=333.999.0.0\u0026vd_source=a8d547984b84c6842dee0d5de84e3413", + "start_time": 1728705600, + "end_time": 1729353599, + "movie_reservation_state": false, + "activity_code": "wcazjqsfzh", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-dJielz3qj4.html" + }, { + "id": 3098, + "index": 18, + "title": "《荒野机器人》", + "type": 3, + "city": "上海、北京、广州、杭州、深圳、武汉、长沙、重庆、成都、天津", + "color": "#23170b", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/ba65216b160c8237eaa323043160e99c97278f07.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/ONJBt3XdwS.jpg", + "time_range": "2024-09-07 19:00:00", + "link": "https://www.bilibili.com/video/BV18o48ehEsg/?spm_id_from=333.999.0.0\u0026vd_source=a8d547984b84c6842dee0d5de84e3413", + "start_time": 1725004800, + "end_time": 1725379199, + "movie_reservation_state": false, + "activity_code": "hulrbpulhm", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-Iy6p0W79lO.html" + }, { + "id": 3156, + "index": 19, + "title": "《变形金刚:起源》", + "type": 3, + "city": "上海、南京、西安、成都、广州、北京、武汉、沈阳、太原、青岛", + "color": "#4e3841", + "state": 4, + "image": "https://i0.hdslb.com/bfs/bangumi/kt/f6f4605aea0153e8a5f961c04a030bf35b3330bf.jpg", + "sub_image": "https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg", + "time_range": "2024-09-28 15:00:00", + "link": "https://www.bilibili.com/video/BV13d2cY7Exn/?spm_id_from=333.999.0.0\u0026vd_source=a8d547984b84c6842dee0d5de84e3413", + "start_time": 1726113600, + "end_time": 1727107199, + "movie_reservation_state": false, + "activity_code": "krefykcmys", + "register_info_share_link": "https://www.bilibili.com/blackboard/activity-U3JZmvoM3m.html" + }] + }, + "extra_params": { + "switch_on": false, + "birthday_sku_switch_on": false, + "is_buy_birthday_sku": false, + "is_birthday_off": false, + "phone_bind_state": 0, + "app_times": 1, + "na_ab": 2, + "na_ab_group_id": "59534,56618,56049,35348", + "is_same_to_last_session": true, + "is_white_list": false, + "cloud_ab": 2, + "cloud_ab_group_id": "59958,59534,56618,56049,35348", + "banner_ab": 2, + "banner_ab_group_id": "59534,56049", + "now_time": 0, + "free_welfare_ab": 3, + "device_limit_ab": 0, + "offline_ab": 2, + "welfare_module_height_ab": 2 + }, + "hit_ab": false, + "has_privilege_coupon": false, + "is_new_header": 0, + "header_bubble": null + } + } + ``` +} diff --git a/bruno/app.bilibili.com/folder.bru b/bruno/app.bilibili.com/folder.bru new file mode 100644 index 0000000..5117601 --- /dev/null +++ b/bruno/app.bilibili.com/folder.bru @@ -0,0 +1,48 @@ +meta { + name: app.bilibili.com +} + +script:pre-request { + const CryptoJS = require('crypto-js'); + + console.log("start"); + + const md5 = (str) => CryptoJS.MD5(str).toString(CryptoJS.enc.Hex); + + const replacePlaceholders = (body) => { + for (const key in body) { + if (typeof body[key] === 'string') { + // Check if value contains {{}} placeholders + const matches = body[key].match(/{{(.*?)}}/g); + if (matches) { + matches.forEach(match => { + const placeholder = match.slice(2, -2); // Remove the {{ and }} + const value = bru.getEnvVar(placeholder); + body[key] = body[key].replace(match, value); + }); + } + } + } + }; + + function appSign(params, appkey, appsec) { + params.appkey = appkey; + delete body.sign; + const sortedKeys = Object.keys(params).sort(); + const sortedParams = sortedKeys.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&'); + console.log(sortedParams); + return md5(sortedParams + appsec); + } + + const body = req.getBody(); + + if (body && body.hasOwnProperty('sign')) { + replacePlaceholders(body); + const sign = appSign(body, bru.getEnvVar("appKey"), bru.getEnvVar("appSec")); + console.log("calculate sign:" + sign); + + body.sign = sign; + } + + req.setBody(body); +} diff --git a/bruno/app.bilibili.com/pgc/activity/deliver/material/receive.bru b/bruno/app.bilibili.com/pgc/activity/deliver/material/receive.bru new file mode 100644 index 0000000..f961ed8 --- /dev/null +++ b/bruno/app.bilibili.com/pgc/activity/deliver/material/receive.bru @@ -0,0 +1,96 @@ +meta { + name: receive + type: http + seq: 1 +} + +post { + url: https://app.bilibili.com/pgc/activity/deliver/material/receive + body: formUrlEncoded + auth: none +} + +headers { + Host: api.bilibili.com + buvid: {{buvid}} + fp_local: {{device_id}} + fp_remote: {{device_id}} + session_id: e04d2e05 + env: prod + app-key: android64 + user-agent: {{user-agent}} + x-bili-trace-id: 0564afa825e0e1ec59164fe59367755a:59164fe59367755a:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NzI2NDcsImlhdCI6MTczNTc0MzU0NywiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.eafhpooLoe2q6cA45_Xrgq1VO-y490pxP5gwJ4qm_ik + bili-http-engine: cronet + Cookie: {{cookieStr}} +} + +body:form-urlencoded { + activity_code: + appkey: {{appKey}} + build: {{build}} + c_locale: zh_CN + channel: bili + disable_rcmd: 0 + ep_id: 328482 + from_spmid: activity.h5.0.0 + mobi_app: android + platform: android + s_locale: zh_CN + season_id: 12548 + spmid: united.player-video-detail.0.0 + statistics: {"appId":1,"platform":3,"version":"7.72.0","abtest":""} + ts: 1736179521 + sign: 132d2532467ef649a925aece247cdb4b + access_key: {{access_key}} +} + +docs { + 终端:APP + + 作用:开始大会员赚大积分任务-观看剧集内容 + + 入口: + - 我的->会员中心->赚大积分->查看8项任务,点击“观看剧集内容”,选择视频后触发 + + 传入剧集的id,会返回task_id和token,用于标识该次观看任务 + + 该sample的视频为《让子弹飞》 + + 完整的观看剧集内容任务调用接口如下: + + - 领取:app.bilibili.com/pgc/activity/score/task/receive/v2 + - 开始:app.bilibili.com/pgc/activity/deliver/material/receive + - 上报完成:app.bilibili.com/pgc/activity/deliver/task/complete + + Response Sample: + + ```json + { + "code": 0, + "data": { + "closeType": "close_win", + "container": [], + "showTime": "", + "watch_count_down_cfg": { + "action": "url", + "closeType": "close_win", + "complete_status_desc": "大积分已到账", + "complete_status_jump_url": "https://big.bilibili.com/mobile/bigPoint?navhide=1&closable=1", + "count_down_status_desc": "看${time}获大积分", + "login": true, + "milliseconds": 600000, + "pause_status_desc": "计时暂停", + "showTime": "ENTER", + "task_id": "4320003", + "token": "67ba5888e7" + } + }, + "message": "success" + } + ``` +} diff --git a/bruno/app.bilibili.com/pgc/activity/deliver/task/complete-ogv.bru b/bruno/app.bilibili.com/pgc/activity/deliver/task/complete-ogv.bru new file mode 100644 index 0000000..48db862 --- /dev/null +++ b/bruno/app.bilibili.com/pgc/activity/deliver/task/complete-ogv.bru @@ -0,0 +1,70 @@ +meta { + name: complete-ogv + type: http + seq: 2 +} + +post { + url: https://app.bilibili.com/pgc/activity/deliver/task/complete + body: formUrlEncoded + auth: none +} + +headers { + Host: api.bilibili.com + buvid: {{buvid}} + fp_local: {{device_id}} + fp_remote: {{device_id}} + session_id: e04d2e05 + env: prod + app-key: android64 + user-agent: {{user-agent}} + x-bili-trace-id: a301946d9621645a707b40973f67755c:707b40973f67755c:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NzI2NDcsImlhdCI6MTczNTc0MzU0NywiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.eafhpooLoe2q6cA45_Xrgq1VO-y490pxP5gwJ4qm_ik + bili-http-engine: cronet + Cookie: {{cookieStr}} +} + +body:form-urlencoded { + build: {{build}} + c_locale: zh_CN + channel: bili + disable_rcmd: 0 + mobi_app: android + platform: android + s_locale: zh_CN + statistics: {{statistics}} + access_key: {{access_key}} + ts: 1735744760 + sign: 2292d647d9b3f6dbd2f99b5a90cbddaf + appkey: {{appKey}} + task_id: 4320003 + task_sign: 95cbef871100151e526fa5580534a364 + timestamp: 1748884714621 + token: 67ba5888e7 +} + +docs { + 终端:APP + + 作用:上报完成大会员赚大积分任务-观看剧集内容 + + 入口: + - 我的->会员中心->赚大积分->查看8项任务,点击“观看剧集内容”,挑选视频,观看10分钟后,自动触发 + + 传入剧集的id,会返回task_id和token,用于标识该次观看任务 + + task_sign必传,与sign的生成方式相同。即,先排除掉task_sign和sign,生成签名后赋值给task_sign,然后在签名一次得到sign + + 且只能调用成功一次,第二次及之后会返回400 + + 完整的观看剧集内容任务调用接口如下: + + - 领取:app.bilibili.com/pgc/activity/score/task/receive/v2 + - 开始:app.bilibili.com/pgc/activity/deliver/material/receive + - 上报完成:app.bilibili.com/pgc/activity/deliver/task/complete +} diff --git a/bruno/app.bilibili.com/pgc/activity/deliver/task/complete.bru b/bruno/app.bilibili.com/pgc/activity/deliver/task/complete.bru new file mode 100644 index 0000000..10580a7 --- /dev/null +++ b/bruno/app.bilibili.com/pgc/activity/deliver/task/complete.bru @@ -0,0 +1,66 @@ +meta { + name: complete + type: http + seq: 1 +} + +post { + url: https://app.bilibili.com/pgc/activity/deliver/task/complete + body: formUrlEncoded + auth: none +} + +headers { + Host: api.bilibili.com + buvid: {{buvid}} + fp_local: {{device_id}} + fp_remote: {{device_id}} + session_id: e04d2e05 + env: prod + app-key: android64 + user-agent: {{user-agent}} + x-bili-trace-id: a301946d9621645a707b40973f67755c:707b40973f67755c:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NzI2NDcsImlhdCI6MTczNTc0MzU0NywiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.eafhpooLoe2q6cA45_Xrgq1VO-y490pxP5gwJ4qm_ik + bili-http-engine: cronet + Cookie: {{cookieStr}} +} + +body:form-urlencoded { + build: {{build}} + c_locale: zh_CN + channel: bili + disable_rcmd: 0 + mobi_app: android + platform: android + s_locale: zh_CN + statistics: {{statistics}} + access_key: {{access_key}} + ts: 1735744760 + sign: 2292d647d9b3f6dbd2f99b5a90cbddaf + appkey: {{appKey}} + position: tv_channel + win_id: bigscore-filmtab +} + +docs { + 终端:APP + + 作用:上报完成大会员赚大积分任务-浏览追番频道页、浏览影视频道页(观看剧集内容接口相同,但入参不同,在另一个接口) + + 入口: + - 我的->会员中心->赚大积分->查看8项任务,点击跳转后,自动触发 + + 完整的观看剧集内容任务调用接口如下: + + - 领取:app.bilibili.com/pgc/activity/score/task/receive/v2 + - 上报完成:app.bilibili.com/pgc/activity/deliver/task/complete + + 入参position: + + - animatetab: jp_channel + - filmtab: tv_channel +} diff --git a/bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/dressbuyamount.bru b/bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/dressbuyamount.bru new file mode 100644 index 0000000..322152d --- /dev/null +++ b/bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/dressbuyamount.bru @@ -0,0 +1,73 @@ +meta { + name: dressbuyamount + type: http + seq: 3 +} + +post { + url: https://api.bilibili.com/pgc/activity/score/task/receive/v2 + body: formUrlEncoded + auth: none +} + +headers { + Host: api.bilibili.com + Cookie: {{cookieStr}} + native_api_from: h5 + buvid: {{buvid}} + accept: application/json, text/plain, */* + referer: https://big.bilibili.com/mobile/bigPoint/task + user-agent: {{user-agent}} + x-bili-trace-id: 39dab959605906ee420167e8af677533:420167e8af677533:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NjI4NDksImlhdCI6MTczNTczMzc0OSwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.EIrjYHrmFeTJXZjxsWki_ZloVvL9IK_aDgpqslMASy0 + bili-http-engine: cronet +} + +body:form-urlencoded { + access_key: {{access_key}} + appKey: {{appKey}} + appkey: {{appKey}} + bili_local_id: {{device_id}} + build: 7720200 + buvid: {{buvid}} + channel: yingyongbao + containerName: AbstractWebActivity + csrf: {{csrf}} + device: phone + deviceId: f9abaee74692f9e9 + deviceName: samsungNexus + devicePlatform: Android10samsungNexus + device_id: {{device_id}} + device_name: samsungNexus + device_platform: Android10samsungNexus + disable_rcmd: 0 + fingerprint: {{device_id}} + isPad: false + localFingerprint: {{device_id}} + local_id: {{buvid}} + mobi_app: android + modelName: Nexus + networkState: 2 + networkstate: 2 + osVer: 10 + platform: android + sessionID: 92c5ad7a + statistics: {"appId":1,"platform":3,"version":"7.72.0","abtest":""} + statusBarHeight: 77 + taskCode: dressbuyamount + ts: 1735734245 + sign: 293cc4d525cf41cfb8adb69f42185ec0: +} + +docs { + 终端:APP + + 作用:领取大会员赚大积分任务-购买指定装扮商品 + + 入口: + - 我的->会员中心->赚大积分->查看8项任务->领取任务 +} diff --git a/bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/ogvwatchnew.bru b/bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/ogvwatchnew.bru new file mode 100644 index 0000000..0568185 --- /dev/null +++ b/bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/ogvwatchnew.bru @@ -0,0 +1,79 @@ +meta { + name: ogvwatchnew + type: http + seq: 4 +} + +post { + url: https://app.bilibili.com/pgc/activity/score/task/receive/v2 + body: formUrlEncoded + auth: none +} + +headers { + Host: api.bilibili.com + Cookie: {{cookieStr}} + native_api_from: h5 + buvid: {{buvid}} + accept: application/json, text/plain, */* + referer: https://big.bilibili.com/mobile/bigPoint/task + user-agent: {{user-agent}} + x-bili-trace-id: 9e3ed57f35a83d4edb8160805867752f:db8160805867752f:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NjE2NjQsImlhdCI6MTczNTczMjU2NCwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.YVtwH53dLJ1l6n6aFcvwNZ4MBkgnBPtxE8UfD7u9J4I + bili-http-engine: cronet +} + +body:form-urlencoded { + access_key: {{access_key}} + appKey: {{appKey}} + appkey: {{appKey}} + bili_local_id: {{device_id}} + build: 7720200 + buvid: {{buvid}} + channel: yingyongbao + containerName: AbstractWebActivity + csrf: {{csrf}} + device: phone + deviceId: f9abaee74692f9e9 + deviceName: samsungNexus + devicePlatform: Android10samsungNexus + device_id: {{device_id}} + device_name: samsungNexus + device_platform: Android10samsungNexus + disable_rcmd: 0 + fingerprint: {{device_id}} + isPad: false + localFingerprint: {{device_id}} + local_id: {{buvid}} + mobi_app: android + modelName: Nexus + networkState: 2 + networkstate: 2 + osVer: 10 + platform: android + sessionID: 120548f6 + statistics: {"appId":1,"platform":3,"version":"7.72.0","abtest":""} + statusBarHeight: 77 + taskCode: ogvwatchnew + ts: 1735733021 + sign: 5cc38f578700cfdb506f7e489abdf442: +} + +docs { + 终端:APP + + 作用:领取大会员赚大积分任务-观看剧集内容 + + 入口: + - 我的->会员中心->赚大积分->查看8项任务->领取任务 + + 完整的观看剧集内容任务调用接口如下: + + - 领取:app.bilibili.com/pgc/activity/score/task/receive/v2 + - 开始:app.bilibili.com/pgc/activity/deliver/material/receive + - 上报完成:app.bilibili.com/pgc/activity/deliver/task/complete +} diff --git a/bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/tvodbuy.bru b/bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/tvodbuy.bru new file mode 100644 index 0000000..889ab35 --- /dev/null +++ b/bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/tvodbuy.bru @@ -0,0 +1,73 @@ +meta { + name: tvodbuy + type: http + seq: 2 +} + +post { + url: https://api.bilibili.com/pgc/activity/score/task/receive/v2 + body: formUrlEncoded + auth: none +} + +headers { + Host: api.bilibili.com + Cookie: {{cookieStr}} + native_api_from: h5 + buvid: {{buvid}} + accept: application/json, text/plain, */* + referer: https://big.bilibili.com/mobile/bigPoint/task + user-agent: {{user-agent}} + x-bili-trace-id: 39dab959605906ee420167e8af677533:420167e8af677533:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NjI4NDksImlhdCI6MTczNTczMzc0OSwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.EIrjYHrmFeTJXZjxsWki_ZloVvL9IK_aDgpqslMASy0 + bili-http-engine: cronet +} + +body:form-urlencoded { + access_key: {{access_key}} + appKey: {{appKey}} + appkey: {{appKey}} + bili_local_id: {{device_id}} + build: 7720200 + buvid: {{buvid}} + channel: yingyongbao + containerName: AbstractWebActivity + csrf: {{csrf}} + device: phone + deviceId: f9abaee74692f9e9 + deviceName: samsungNexus + devicePlatform: Android10samsungNexus + device_id: {{device_id}} + device_name: samsungNexus + device_platform: Android10samsungNexus + disable_rcmd: 0 + fingerprint: {{device_id}} + isPad: false + localFingerprint: {{device_id}} + local_id: {{buvid}} + mobi_app: android + modelName: Nexus + networkState: 2 + networkstate: 2 + osVer: 10 + platform: android + sessionID: 92c5ad7a + statistics: {"appId":1,"platform":3,"version":"7.72.0","abtest":""} + statusBarHeight: 77 + taskCode: tvodbuy + ts: 1735734245 + sign: 293cc4d525cf41cfb8adb69f42185ec0: +} + +docs { + 终端:APP + + 作用:领取大会员赚大积分任务-购买单点付费影片 + + 入口: + - 我的->会员中心->赚大积分->查看8项任务->领取任务 +} diff --git a/bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/vipmallbuy.bru b/bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/vipmallbuy.bru new file mode 100644 index 0000000..1e554aa --- /dev/null +++ b/bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/vipmallbuy.bru @@ -0,0 +1,73 @@ +meta { + name: vipmallbuy + type: http + seq: 1 +} + +post { + url: https://app.bilibili.com/pgc/activity/score/task/receive/v2 + body: formUrlEncoded + auth: none +} + +headers { + Host: api.bilibili.com + Cookie: {{cookieStr}} + native_api_from: h5 + buvid: {{buvid}} + accept: application/json, text/plain, */* + referer: https://big.bilibili.com/mobile/bigPoint/task + user-agent: {{user-agent}} + x-bili-trace-id: 9e3ed57f35a83d4edb8160805867752f:db8160805867752f:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NjE2NjQsImlhdCI6MTczNTczMjU2NCwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.YVtwH53dLJ1l6n6aFcvwNZ4MBkgnBPtxE8UfD7u9J4I + bili-http-engine: cronet +} + +body:form-urlencoded { + access_key: {{access_key}} + appKey: {{appKey}} + appkey: {{appKey}} + bili_local_id: {{device_id}} + build: 7720200 + buvid: {{buvid}} + channel: yingyongbao + containerName: AbstractWebActivity + csrf: {{csrf}} + device: phone + deviceId: f9abaee74692f9e9 + deviceName: samsungNexus + devicePlatform: Android10samsungNexus + device_id: {{device_id}} + device_name: samsungNexus + device_platform: Android10samsungNexus + disable_rcmd: 0 + fingerprint: {{device_id}} + isPad: false + localFingerprint: {{device_id}} + local_id: {{buvid}} + mobi_app: android + modelName: Nexus + networkState: 2 + networkstate: 2 + osVer: 10 + platform: android + sessionID: 120548f6 + statistics: {"appId":1,"platform":3,"version":"7.72.0","abtest":""} + statusBarHeight: 77 + taskCode: vipmallbuy + ts: 1735733021 + sign: 5cc38f578700cfdb506f7e489abdf442: +} + +docs { + 终端:APP + + 作用:领取大会员赚大积分任务-购买指定会员购商品 + + 入口: + - 我的->会员中心->赚大积分->查看8项任务->领取任务 +} diff --git a/bruno/app.bilibili.com/x/report/heartbeat/mobile/end.bru b/bruno/app.bilibili.com/x/report/heartbeat/mobile/end.bru new file mode 100644 index 0000000..0096496 --- /dev/null +++ b/bruno/app.bilibili.com/x/report/heartbeat/mobile/end.bru @@ -0,0 +1,76 @@ +meta { + name: end + type: http + seq: 1 +} + +post { + url: https://app.bilibili.com/x/report/heartbeat/mobile + body: formUrlEncoded + auth: none +} + +headers { + Host: api.bilibili.com + buvid: {{buvid}} + fp_local: {{device_id}} + fp_remote: {{device_id}} + session_id: e04d2e05 + env: prod + app-key: android64 + user-agent: {{user-agent}} + x-bili-trace-id: 2c64470432d0c5346a475a449467755d:6a475a449467755d:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NzI2NDcsImlhdCI6MTczNTc0MzU0NywiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.eafhpooLoe2q6cA45_Xrgq1VO-y490pxP5gwJ4qm_ik + bili-http-engine: cronet +} + +body:form-urlencoded { + access_key: {{access_key}} + actual_played_time: 548 + aid: 726710400 + appkey: {{appKey}} + auto_play: 99 + build: 7720200 + c_locale: zh_CN + channel: yingyongbao + cid: 785731972 + disable_rcmd: 0 + epid: 511578 + epid_status: 13 + extra: {"from_outer_spmid":"activity.h5.0.0"} + from: 12 + from_spmid: united.player-video-detail.player.continue + last_play_progress_time: 638 + list_play_time: 0 + max_play_progress_time: 638 + mid: {{mid}} + miniplayer_play_time: 0 + mobi_app: android + network_type: 1 + paused_time: 0 + platform: android + play_mode: 1 + play_status: 1 + play_type: + played_time: 548 + quality: 64 + report_flow_data: + s_locale: zh_CN + session: 2edcb8dc8ff0b6f13dd685a23aff692b72bf2869 + sid: 41410 + spmid: united.player-video-detail.0.0 + start_ts: 1735744223 + statistics: {"appId":1,"platform":3,"version":"7.72.0","abtest":""} + sub_type: 1 + total_time: 548 + track_id: + ts: 1735744771 + type: 4 + user_status: 1 + video_duration: 1450 + sign: 1021d178fb342c0c48617d3692c97d46 +} diff --git a/bruno/app.bilibili.com/x/report/heartbeat/mobile/start.bru b/bruno/app.bilibili.com/x/report/heartbeat/mobile/start.bru new file mode 100644 index 0000000..e38b734 --- /dev/null +++ b/bruno/app.bilibili.com/x/report/heartbeat/mobile/start.bru @@ -0,0 +1,76 @@ +meta { + name: start + type: http + seq: 2 +} + +post { + url: https://app.bilibili.com/x/report/heartbeat/mobile + body: formUrlEncoded + auth: none +} + +headers { + Host: api.bilibili.com + buvid: {{buvid}} + fp_local: {{device_id}} + fp_remote: {{device_id}} + session_id: e04d2e05 + env: prod + app-key: android64 + user-agent: {{user-agent}} + x-bili-trace-id: 8fcda5ac30f6510905b2834bbb67755a:05b2834bbb67755a:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NzI2NDcsImlhdCI6MTczNTc0MzU0NywiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.eafhpooLoe2q6cA45_Xrgq1VO-y490pxP5gwJ4qm_ik + bili-http-engine: cronet +} + +body:form-urlencoded { + access_key: {{access_key}} + actual_played_time: 0 + aid: 726710400 + appkey: {{appKey}} + auto_play: 99 + build: 7720200 + c_locale: zh_CN + channel: yingyongbao + cid: 785731972 + disable_rcmd: 0 + epid: 511578 + epid_status: 13 + extra: {"from_outer_spmid":"activity.h5.0.0"} + from: 12 + from_spmid: united.player-video-detail.player.continue + last_play_progress_time: 0 + list_play_time: 0 + max_play_progress_time: 0 + mid: {{mid}} + miniplayer_play_time: 0 + mobi_app: android + network_type: 1 + paused_time: 0 + platform: android + play_mode: 1 + play_status: 1 + play_type: + played_time: 0 + quality: 64 + report_flow_data: + s_locale: zh_CN + session: 2edcb8dc8ff0b6f13dd685a23aff692b72bf2869 + sid: 41410 + spmid: united.player-video-detail.0.0 + start_ts: 0 + statistics: {"appId":1,"platform":3,"version":"7.72.0","abtest":""} + sub_type: 1 + total_time: 0 + track_id: + ts: 1735744223 + type: 4 + user_status: 1 + video_duration: 1450 + sign: 37b0acd3bab7b40a082ce510041a0a2b +} diff --git a/bruno/big.bilibili.com/pgc/activity/score/task/sign.bru b/bruno/big.bilibili.com/pgc/activity/score/task/sign.bru new file mode 100644 index 0000000..b8f810e --- /dev/null +++ b/bruno/big.bilibili.com/pgc/activity/score/task/sign.bru @@ -0,0 +1,40 @@ +meta { + name: sign + type: http + seq: 1 +} + +post { + url: https://api.bilibili.com/pgc/activity/score/task/sign + body: formUrlEncoded + auth: none +} + +headers { + Host: api.bilibili.com + Cookie: {{cookieStr}} + native_api_from: h5 + buvid: {{buvid}} + accept: application/json, text/plain, */* + referer: https://big.bilibili.com/mobile/index?exp_symbol=release_version&oflAb=1 + user-agent: {{user-agent}} + x-bili-trace-id: 9c642fae280ce80077653eef826774e3:77653eef826774e3:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzgxMTEsImlhdCI6MTczNTcwOTAxMSwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.J28IlxZ6SjllQEQkq_OvFUiRYAEL2VhQG_WWBcmNppE + bili-http-engine: cronet +} + +body:form-urlencoded { + access_key: {{access_key}} + appkey: {{appKey}} + csrf: {{csrf}} + disable_rcmd: 0 + mobi_app: android + platform: android + statistics: {"appId":1,"platform":3,"version":"7.72.0","abtest":""} + ts: 1735713546 + sign: aeaeff881a147dd5cd2c6e24df9dc21b +} diff --git a/bruno/big.bilibili.com/x/vip/experience/add.bru b/bruno/big.bilibili.com/x/vip/experience/add.bru new file mode 100644 index 0000000..7cab117 --- /dev/null +++ b/bruno/big.bilibili.com/x/vip/experience/add.bru @@ -0,0 +1,41 @@ +meta { + name: add + type: http + seq: 2 +} + +post { + url: https://big.bilibili.com/x/vip/experience/add + body: formUrlEncoded + auth: none +} + +headers { + Host: api.bilibili.com + Cookie: {{cookieStr}} + native_api_from: h5 + buvid: {{buvid}} + accept: application/json, text/plain, */* + referer: https://big.bilibili.com/mobile/index?exp_symbol=release_version&oflAb=1 + user-agent: {{user-agent}} + x-bili-trace-id: 46656ec97cb019beac1da03fd56774d6:ac1da03fd56774d6:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzgxMTEsImlhdCI6MTczNTcwOTAxMSwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.J28IlxZ6SjllQEQkq_OvFUiRYAEL2VhQG_WWBcmNppE + bili-http-engine: cronet +} + +body:form-urlencoded { + access_key: {{access_key}} + appkey: {{appKey}} + buvid: {{buvid}} + csrf: {{csrf}} + disable_rcmd: 0 + mobi_app: android + platform: android + statistics: {"appId":1,"platform":3,"version":"7.72.0","abtest":""} + ts: 1735710395 + sign: 694521e34b88ec0593a8cc3edbc4e117 +} diff --git a/bruno/bruno.json b/bruno/bruno.json new file mode 100644 index 0000000..673ba42 --- /dev/null +++ b/bruno/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "Bili", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/bruno/environments/default.bru b/bruno/environments/default.bru new file mode 100644 index 0000000..32c1462 --- /dev/null +++ b/bruno/environments/default.bru @@ -0,0 +1,16 @@ +vars { + phone: {{process.env.phone}} + pwd: {{process.env.pwd}} + mid: {{process.env.mid}} + buvid: {{process.env.buvid}} + csrf: {{process.env.csrf}} + appKey: 1d8b6e7d45233436 + access_key: {{process.env.access_key}} + cookieStr: {{process.env.cookieStr}} + user-agent: Mozilla/5.0 (Linux; Android 10; Nexus Build/KOT49H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.186 Mobile Safari/537.36 os/android model/Nexus build/7720200 osVer/10 sdkInt/29 network/2 BiliApp/7720200 mobi_app/android channel/yingyongbao Buvid/{{buvid}} sessionID/92c5ad7a innerVer/7720210 c_locale/zh_CN s_locale/zh_CN disable_rcmd/0 7.72.0 os/android model/Nexus mobi_app/android build/7720200 channel/yingyongbao innerVer/7720210 osVer/10 network/2 + device_id: {{process.env.device_id}} + user-agent-simple: Mozilla/5.0 (Linux; Android 12; SM-S9080 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Mobile Safari/537.36 os/android model/SM-S9080 build/7760700 osVer/12 sdkInt/32 network/2 BiliApp/7760700 mobi_app/android channel/bili innerVer/7760710 c_locale/zh_CN s_locale/zh_CN disable_rcmd/0 7.76.0 os/android model/SM-S9080 mobi_app/android build/7760700 channel/bili innerVer/7760710 osVer/12 network/2 + appSec: 560c52ccd288fed045859ed18bffd973 + build: 8451100 + statistics: {"appId":1,"platform":3,"version":"8.45.1","abtest":""} +} diff --git a/bruno/mall.bilibili.com/combine.bru b/bruno/mall.bilibili.com/combine.bru new file mode 100644 index 0000000..f17fa16 --- /dev/null +++ b/bruno/mall.bilibili.com/combine.bru @@ -0,0 +1,314 @@ +meta { + name: combine + type: http + seq: 2 +} + +get { + url: https://mall.bilibili.com/x/vip_point/task/combine?access_key={{access_key}}&appKey={{appKey}}&bili_local_id=910f57d9d7feaf6928e592860452d06b202505211207597aa9050a2e5c31f431&brand=Samsung&build={{build}}&buvid={{buvid}}&channel=bili&containerName=AbstractWebActivity&csrf={{csrf}}&device=phone&deviceId={{device_id}}&deviceName=SamsungSM-A5560&devicePlatform=Android12SamsungSM-A5560&device_id={{device_id}}&device_name=SamsungSM-A5560&device_platform=Android12SamsungSM-A5560&disable_rcmd=0&fingerprint=910f57d9d7feaf6928e592860452d06b202505211207597aa9050a2e5c31f431&isPad=false&localFingerprint=910f57d9d7feaf6928e592860452d06b202505211207597aa9050a2e5c31f431&local_id=XUA5651A9EDF7387153A945CDE96CADCB6000&mobi_app=android&modelName=SM-A5560&networkState=2&networkstate=2&osVer=12&platform=android&sessionID=01db4bc0&statistics={"appId":1,"platform":3,"version":"8.45.1","abtest":""}&statusBarHeight=72&ts=1748764069&sign=de7e2667157cccf4af3def524b6796e0 + body: none + auth: inherit +} + +params:query { + access_key: {{access_key}} + appKey: {{appKey}} + bili_local_id: 910f57d9d7feaf6928e592860452d06b202505211207597aa9050a2e5c31f431 + brand: Samsung + build: {{build}} + buvid: {{buvid}} + channel: bili + containerName: AbstractWebActivity + csrf: {{csrf}} + device: phone + deviceId: {{device_id}} + deviceName: SamsungSM-A5560 + devicePlatform: Android12SamsungSM-A5560 + device_id: {{device_id}} + device_name: SamsungSM-A5560 + device_platform: Android12SamsungSM-A5560 + disable_rcmd: 0 + fingerprint: 910f57d9d7feaf6928e592860452d06b202505211207597aa9050a2e5c31f431 + isPad: false + localFingerprint: 910f57d9d7feaf6928e592860452d06b202505211207597aa9050a2e5c31f431 + local_id: XUA5651A9EDF7387153A945CDE96CADCB6000 + mobi_app: android + modelName: SM-A5560 + networkState: 2 + networkstate: 2 + osVer: 12 + platform: android + sessionID: 01db4bc0 + statistics: {"appId":1,"platform":3,"version":"8.45.1","abtest":""} + statusBarHeight: 72 + ts: 1748764069 + sign: de7e2667157cccf4af3def524b6796e0 +} + +headers { + Host: api.bilibili.com + Cookie: {{cookieStr}} + accept: application/json, text/plain, */* + bili-http-engine: ignet + buvid: {{buvid}} + content-type: application/json + native_api_from: h5 + referer: https://big.bilibili.com/mobile/bigPoint/task + user-agent: {{user-agent}} + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-metadata-legal-region: CN + x-bili-mid: 341688380 + x-bili-net-bin: DQAAgL8gAQ + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDg3NzU1OTksImlhdCI6MTc0ODc0NjQ5OSwiYnV2aWQiOiJYVUE1NjUxQTlFREY3Mzg3MTUzQTk0NUNERTk2Q0FEQ0I2MDAwIn0.k0x2o3e2Q3W-6Wzc56IhbLgSjDKTaAuUV9om7K213fI + x-bili-trace-id: d605d9f1dc3818a42f3a22b5c4683c05:2f3a22b5c4683c05:0:0 +} + +docs { + 终端:APP + + 作用:获取大会员赚大积分的任务列表 + + 入口: + - 我的->会员中心->赚大积分->查看8项任务 + + Response sample: + + ```json + { + "code": 0, + "message": "0", + "ttl": 1, + "data": { + "vip_info": { + "type": 2, + "status": 1, + "due_date": 1779033600000, + "vip_pay_type": 0, + "label": { + "path": "http://i0.hdslb.com/bfs/vip/label_annual.png", + "text_color": "", + "bg_style": 0, + "bg_color": "", + "border_color": "", + "use_img_label": false, + "img_label_uri_hans": "", + "img_label_uri_hant": "", + "img_label_uri_hans_static": "", + "img_label_uri_hant_static": "" + }, + "start_time": 1657418210, + "paid_type": 0, + "mid": 341688380, + "role": 3, + "tv_vip_status": 0, + "tv_vip_pay_type": 0, + "tv_due_date": 0, + "vip_recent_time": 1747398345 + }, + "point_info": { + "point": 415, + "expire_point": 0, + "expire_time": 0, + "expire_days": 0 + }, + "task_info": { + "modules": [{ + "module_title": "福利任务", + "common_task_item": [{ + "task_code": "bonus", + "state": 3, + "title": "大会员福利大积分", + "icon": "https://i0.hdslb.com/bfs/activity-plat/static/20220607/b66bfe4ccfd6bed05bdb54008ff5c7aa/IbtMl6R3yt.png", + "subtitle": "大会员/年度大会员\u003cbr /\u003e\u003cspan class=\"active\"\u003e+100/200大积分", + "explain": "在期大会员可领取100大积分,在期年度大会员可领取200大积分,每人限领1次。如当身份为大会员时已领取过本任务大积分,则后续成为年度大会员也无法补领取差值大积分。", + "vip_limit": 1, + "complete_times": 1, + "max_times": 1, + "recall_num": 0 + }] + }, { + "module_title": "体验任务", + "common_task_item": [{ + "task_code": "privilege", + "state": 3, + "title": "浏览大会员权益页面", + "icon": "https://i0.hdslb.com/bfs/activity-plat/static/20220607/b66bfe4ccfd6bed05bdb54008ff5c7aa/IbtMl6R3yt.png", + "subtitle": "\u003cspan class=\"active\"\u003e+50大积分\u003c/span\u003e", + "explain": "从本任务入口跳转至大会员权益页,浏览后可得50大积分,每人限完成1次。", + "link": "https://big.bilibili.com/mobile/rights?closable=1\u0026navhide=1", + "vip_limit": 0, + "complete_times": 1, + "max_times": 1, + "recall_num": 0 + }] + }, { + "module_title": "日常任务", + "module_sub_title": "截止至06-01 23:59:59", + "common_task_item": [{ + "task_code": "dress-view", + "state": 1, + "title": "浏览装扮商城主页", + "icon": "https://i0.hdslb.com/bfs/activity-plat/static/20230316/b66bfe4ccfd6bed05bdb54008ff5c7aa/d8FFfwIwFC.png", + "subtitle": "\u003cspan class=\"active\"\u003e+10大积分\u003c/span\u003e", + "explain": "从本任务入口跳转至装扮商城主页即可完成任务,每人限完成1次。", + "link": "https://www.bilibili.com/h5/mall/home?f_source=vip\u0026navhide=1", + "vip_limit": 1, + "complete_times": 0, + "max_times": 1, + "recall_num": 0 + }, { + "task_code": "vipmallview", + "state": 1, + "title": "浏览会员购页面10秒", + "icon": "https://i0.hdslb.com/bfs/activity-plat/static/20220613/b66bfe4ccfd6bed05bdb54008ff5c7aa/RnlARrUdOY.png", + "subtitle": "\u003cspan class=\"active\"\u003e+10大积分\u003c/span\u003e", + "explain": "从本任务入口跳转至会员购页面,并连续浏览页面达10秒可得10大积分,每天可完成1次。如浏览过程中离开会员购页面则中断计时,任务判定失败,需重新从本任务入口再次跳转。", + "link": "bilibili://mall/home?msource=member_integral_browse\u0026action=browse_all\u0026eventId=hevent_oy4b7h3epeb\u0026eventTime=10\u0026showCountDown=2\u0026taskName1=%E6%B5%8F%E8%A7%8810%E7%A7%92\u0026taskName1Placeholder=%E6%B5%8F%E8%A7%88%25ss\u0026taskName2=%E5%BE%97%E7%A7%AF%E5%88%86\u0026taskEndText=%E4%BB%BB%E5%8A%A1%E5%AE%8C%E6%88%90", + "vip_limit": 1, + "complete_times": 0, + "max_times": 1, + "recall_num": 0 + }, { + "task_code": "vipmallbuy", + "state": 0, + "title": "购买指定会员购商品", + "icon": "https://i0.hdslb.com/bfs/activity-plat/static/20220613/b66bfe4ccfd6bed05bdb54008ff5c7aa/RnlARrUdOY.png", + "subtitle": "实付1元\u003cspan class=\"active\"\u003e+10大积分\u003c/span\u003e,当前0/2000", + "explain": "领取本任务后,当天内购买会员购商品的首笔订单(除魔力赏/一番赏/票务/先行预定订单外)可获得大积分,每实付1元+10大积分,每天上限2000大积分。本任务不支持先行购买后补领取大积分。如用户通过购买会员购商品获得了本任务大积分,后续将购买的商品退款,则同时会扣除当时获得的任务大积分。如购买商品获得大积分后的当天内产生退款,则大积分扣除后,当天无法再完成此任务。", + "link": "bilibili://mall/home?msource=member_integral_buy", + "vip_limit": 1, + "complete_times": 0, + "max_times": 20000, + "recall_num": 0 + }, { + "task_code": "animatetab", + "state": 1, + "title": "浏览追番频道页10秒", + "icon": "https://i0.hdslb.com/bfs/activity-plat/static/20220607/b66bfe4ccfd6bed05bdb54008ff5c7aa/uOwc1tuJwm.png", + "subtitle": "\u003cspan class=\"active\"\u003e+10大积分\u003c/span\u003e", + "explain": "从本任务入口跳转至追番频道页,并连续浏览页面达10秒可得10大积分,每天可完成1次。如浏览过程中离开追番频道页则中断计时,任务判定失败,需重新从本任务入口再次跳转。", + "link": "bilibili://home?bottom_tab_id=home\u0026tab_id=bangumi\u0026vip_task_countdown=10000\u0026win_id=bigscore-animatetab", + "vip_limit": 1, + "complete_times": 0, + "max_times": 1, + "recall_num": 0 + }, { + "task_code": "filmtab", + "state": 1, + "title": "浏览影视频道页10秒", + "icon": "https://i0.hdslb.com/bfs/activity-plat/static/20220607/b66bfe4ccfd6bed05bdb54008ff5c7aa/bWPJRBuMh3.png", + "subtitle": "\u003cspan class=\"active\"\u003e+10大积分\u003c/span\u003e", + "explain": "从本任务入口跳转至影视频道页,并连续浏览页面达10秒可得10大积分,每天可完成1次。如浏览过程中离开影视频道页则中断计时,任务判定失败,需重新从本任务入口再次跳转。", + "link": "bilibili://home?bottom_tab_id=home\u0026tab_id=bilibili://pgc/cinema-tab\u0026vip_task_countdown=10000\u0026win_id=bigscore-filmtab", + "vip_limit": 1, + "complete_times": 0, + "max_times": 1, + "recall_num": 0 + }, { + "task_code": "ogvwatchnew", + "state": 0, + "title": "观看剧集内容", + "icon": "https://i0.hdslb.com/bfs/activity-plat/static/20220607/b66bfe4ccfd6bed05bdb54008ff5c7aa/6prGo240Md.png", + "subtitle": "观看任一剧集达10分钟\u003cspan class=\"active\"\u003e+40大积分\u003c/span\u003e", + "explain": "领取本任务后,当天内单次观看番剧/国创/电影/电视剧/综艺/纪录片的任一视频的连续时长达到10分钟可得40大积分。单次观看行为需连续达10分钟,切换至其他视频内容会导致观看时长重新计数。仅大陆版手机端APP可领取、可完成任务。", + "link": "https://www.bilibili.com/blackboard/activity-NUbGzdNJpz.html?msource=jifen", + "vip_limit": 1, + "complete_times": 0, + "max_times": 1, + "recall_num": 0 + }, { + "task_code": "tvodbuy", + "state": 0, + "title": "购买单点付费影片", + "icon": "https://i0.hdslb.com/bfs/activity-plat/static/20220607/b66bfe4ccfd6bed05bdb54008ff5c7aa/6prGo240Md.png", + "subtitle": "会员特价/独家付费任意影片\u003cbr /\u003e\u003cspan class=\"active\"\u003e+70大积分\u003c/span\u003e", + "explain": "领取本任务后,当天内购买任意1部会员特价/独家付费的影片可得70大积分,每天上限70大积分。", + "link": "https://www.bilibili.com/blackboard/activity-F6sCMq4w92.html?msource=jifen", + "vip_limit": 1, + "complete_times": 0, + "max_times": 1, + "recall_num": 0 + }, { + "task_code": "dressbuyamount", + "state": 0, + "title": "购买指定装扮商品", + "icon": "https://i0.hdslb.com/bfs/activity-plat/static/20230316/b66bfe4ccfd6bed05bdb54008ff5c7aa/d8FFfwIwFC.png", + "subtitle": "实付1元\u003cspan class=\"active\"\u003e+10大积分\u003c/span\u003e,当前0/1000", + "explain": "领取本任务后,当天内购买装扮商城商品的订单(除装扮赏/祈愿/数字藏品订单外)可获得大积分,每实付1元+10大积分,每天上限1000大积分。本任务不支持先行购买后补领取大积分。", + "link": "https://www.bilibili.com/h5/mall/home?navhide=1\u0026f_source=vip\u0026from=vipzx.shop", + "vip_limit": 1, + "complete_times": 0, + "max_times": 10000, + "recall_num": 0 + }] + }], + "sing_task_item": { + "histories": [{ + "day": "2025-05-26", + "signed": false, + "score": 0 + }, { + "day": "2025-05-27", + "signed": false, + "score": 0 + }, { + "day": "2025-05-28", + "signed": false, + "score": 0 + }, { + "day": "2025-05-29", + "signed": false, + "score": 0 + }, { + "day": "2025-05-30", + "signed": false, + "score": 0 + }, { + "day": "2025-05-31", + "signed": false, + "score": 0 + }, { + "day": "2025-06-01", + "signed": false, + "score": 5, + "is_today": true + }, { + "day": "2025-06-02", + "signed": false, + "score": 5 + }, { + "day": "2025-06-03", + "signed": false, + "score": 5 + }, { + "day": "2025-06-04", + "signed": false, + "score": 5 + }, { + "day": "2025-06-05", + "signed": false, + "score": 5 + }, { + "day": "2025-06-06", + "signed": false, + "score": 5 + }, { + "day": "2025-06-07", + "signed": false, + "score": 10 + }], + "count": 0, + "base_score": 5 + }, + "score_month": 10, + "score_limit": 10000, + "exp_value": 3 + }, + "current_ts": 1748764070, + "integration_task": false + } + } + ``` + + `build`版本号不同,会返回不同的结果。 +} diff --git a/bruno/mall.bilibili.com/folder.bru b/bruno/mall.bilibili.com/folder.bru new file mode 100644 index 0000000..05d4895 --- /dev/null +++ b/bruno/mall.bilibili.com/folder.bru @@ -0,0 +1,3 @@ +meta { + name: mall.bilibili.com +} diff --git a/bruno/mall.bilibili.com/sign2.bru b/bruno/mall.bilibili.com/sign2.bru new file mode 100644 index 0000000..46d0050 --- /dev/null +++ b/bruno/mall.bilibili.com/sign2.bru @@ -0,0 +1,115 @@ +meta { + name: sign2 + type: http + seq: 1 +} + +post { + url: https://mall.bilibili.com/pgc/activity/score/task/sign2?mobi_app=android&csrf={{csrf}}&platform=android + body: json + auth: inherit +} + +params:query { + mobi_app: android + csrf: {{csrf}} + platform: android +} + +headers { + Host: api.bilibili.com + Cookie: {{cookieStr}} + accept: application/json, text/plain, */* + bili-http-engine: ignet + buvid: {{buvid}} + content-type: application/json; charset=utf-8 + guestid: 24675260415603 + native_api_from: h5 + referer: https://big.bilibili.com/mobile/index + user-agent: {{user-agent}} + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-metadata-legal-region: CN + x-bili-mid: {{mid}} + x-bili-net-bin: DQAAgL8gAQ + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDg0NzM4MTEsImlhdCI6MTc0ODQ0NDcxMSwiYnV2aWQiOiJYVUE1NjUxQTlFREY3Mzg3MTUzQTk0NUNERTk2Q0FEQ0I2MDAwIn0.IvpforrVemmRAvyQF7svr4fd-RnfP_lEt6g6pWt4AGk + x-bili-trace-id: 0d6c319f356dbc6191becbf764683728:91becbf764683728:0:0 +} + +body:json { + { + "t": 1748445354567, + "device": "phone", + "ts": 1748445354 + } +} + +docs { + Response sample: + + ```json + { + "code": 0, + "data": { + "count": 2, + "countdown": 0, + "day3WinImg": "https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_win_img.png", + "day3WinImgVip": "https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_win_img_vip.png", + "duration": 7, + "goods": [ + { + "picture": "https://i0.hdslb.com/bfs/activity-plat/d3df8012062c6996c26f989ed6a4f0752e5d5049.png", + "sale": 128888, + "title": "御坂美琴&食蜂操祈 大霸星祭Ver. 手办" + }, + { + "picture": "https://i0.hdslb.com/bfs/activity-plat/245337f3c305886e4937430335218f2d7bf92362.jpg", + "sale": 39, + "title": "暗黑不朽积分兑换礼包" + }, + { + "picture": "https://i0.hdslb.com/bfs/activity-plat/b782d7228e8a58d2562d26f33448a50519ce4ec5.png", + "sale": 33600, + "title": "BEMOE 初音未来 樱花未来 可爱体UWA系列 毛绒4wa" + } + ], + "hasCoupon": false, + "score": 5, + "seasons": [ + { + "badge": "独家", + "badgeType": 1, + "cover": "http://i0.hdslb.com/bfs/bangumi/6a04c87e990ab74cd8d555ef45a863de0993b161.png", + "ratingScore": 9.8, + "seasonId": 5398, + "seasonType": 1, + "subtitle": "不一样的热血动画", + "title": "JOJO的奇妙冒险 不灭钻石" + }, + { + "badge": "大会员", + "badgeType": 0, + "cover": "http://i0.hdslb.com/bfs/bangumi/6d8bd12e0e1ab2d4d5e8567bdba18240e75d7a1b.jpg", + "ratingScore": 9.8, + "seasonId": 6262, + "seasonType": 1, + "subtitle": "不想来笑一下吗?", + "title": "蜡笔小新 第二季(中文)" + }, + { + "badge": "出品", + "badgeType": 1, + "cover": "https://i0.hdslb.com/bfs/bangumi/image/08bf0c1e24e454de51b58d1e26c0a9aecbe9b0c1.png", + "ratingScore": 0.0, + "seasonId": 46585, + "seasonType": 4, + "subtitle": "末世如何才能生存", + "title": "灵笼 第二季" + } + ], + "vipScore": 5, + "vipStatus": 1 + }, + "message": "success" + } + ``` +} diff --git a/bruno/passport.bilibili.com/x/passport-login/oauth2/login.bru b/bruno/passport.bilibili.com/x/passport-login/oauth2/login.bru new file mode 100644 index 0000000..b841a80 --- /dev/null +++ b/bruno/passport.bilibili.com/x/passport-login/oauth2/login.bru @@ -0,0 +1,56 @@ +meta { + name: login + type: http + seq: 1 +} + +post { + url: https://passport.bilibili.com/x/passport-login/oauth2/login + body: formUrlEncoded + auth: none +} + +headers { + Host: passport.bilibili.com + buvid: {{buvid}} + env: prod + app-key: android64 + user-agent: Mozilla/5.0 BiliDroid/7.72.0 (bbcallen@gmail.com) os/android model/Nexus mobi_app/android build/7720200 channel/yingyongbao innerVer/7720210 osVer/10 network/2 + x-bili-trace-id: fe7876684669e19624e1290d506774bf:24e1290d506774bf:0:0 + x-bili-aurora-eid: + x-bili-mid: + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzI4OTIsImlhdCI6MTczNTcwMzc5MiwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.hEDtIHZoN5Wyoja2hNuPcfy-laa1r05ObiEhJ5gPSt4 + bili-http-engine: cronet + content-type: application/x-www-form-urlencoded; charset=utf-8 +} + +body:form-urlencoded { + appkey: 783bbb7264451d82 + bili_local_id: {{device_id}} + build: 7720200 + buvid: {{buvid}} + c_locale: zh_CN + channel: yingyongbao + device: phone + device_id: {{device_id}} + device_meta: EA0906375AA2116F395747958E296DABD2DB115A596F20FEF9C281BC443B52439A8897F1E1E39D2C396B1320F5B47A22120559402BE3F2F6FAB4089CB04B0BA42436E3E28CDBDB46920F42E9B3C6ABB0D26C1B8E3808C143962BD06CAE0B555F01917334431CEF6E5F0372B1B8043D01FEDE6E8406EAA6580005ECEA5771FA1F12252FCA1DAF83A135061B5950D1A0E98C94B5AB91C5916B5CF9A363977BA21F511D742A58ED1E414E1350CA6CDE755086D72BB9FAA7718B497093723E77CA976C0F6946445E8862923DB59F632C4952446ADABF9B07D6022FBF40C821797FCEE2231F5F7ABBB85C1769CEF82A43A5EF9A662A2FDC9C9E1F028FDC66E46D654838A7D3737092C6AC52C2895903DAC8DAB3DCF14E917A9AA246220C3CFCD33175C55E5A1C122FF73F7E99495DE0EED8FCD4037C4A053372EBACED4C5BCB99AFFB24002936B00E90A6B52B4BBCCF2A4BD9680F16B58864E065B94B9D3C4F7216BED851F830D8BF0FD56B16893914E38095DC928F8A9C00DDABE8A7533245F53509582939293D7307A0839A3FE6CA0974C9FE545D8D796955804E63A65D776FD4160A47FC4C9A45C5C5CA3B233AF162CE8458F20D8762C02815E4F1645276D1764AFC6D0785F94D8D0534E266A316BAC348924E9199A07CBDAEC49869CEC41B0263A5A725C86F154297E3F6F62AA118F0DB34B2B5ABF04E97AADED2820A1AA45E0660FFC34289AB519146F4D59EFFAADE07E83AEE64ED995239DA23F3A8FF73E4735BF913FC8E5742251815DF403AE367526F4D660857799852F39904C5AB0AAA7E5ABCEE2EB05DD50D119A9E0D99B7ADF4AB4EEC86FE3504D4E6F7E255642CC94E26BD14FA28D67CC18BFE055A460E19320E923B44F5EBBD29E0495AC23B81861FFCE52E30E61A27A65C3D0AE4D8A9AB5E4E4E92804553EBA0AB9A53B6E5FA0215ABF5E91AD6F1F53E654617ED23EE4B2A97EE5A05C1F491FA424C13431D1760E792E90E9E1886C2CE4E72F77669BB408D45005E306001D6F9ECC714A539224751E750A73E378F62E158293B3C3AD4354A3580F802F3DE34F9031204380F50C7F0513985B8CA09DC1BF7A78755AF12243BB3227EA00A176EC17005C681569DB6CA2FAA8ADB0454939567BD96608271536D459C42CCA16EA7A55F0CC975F526C278055D36AB45B3F2D72216FFA4B0107A7EC826ED08F8570B3D44BB518FA82A1A9A4E527FEA59A32E5354F5B53B3305AAC80C36488D736E40AE6A530DEBA491D77E44F68E2DE54B074D98D87B8FB2789105C19729E38BF0A01C5B86F9E14750DDE66D8E308D48045E567169F0122500CE1A7C30838F1B84C41C4B55B9AFFA3F3A7E2DF4D86505A93E9EC3C376C8C3BE935D783722C010065EB23DA93D7B338C8DA9C1424FB15866E87160DFB9AD9C2954C9B1D2D568296329FC5FFB8F0A7F1D7C4E3269E95679031791D18EB5447760A6A8BD824D7DFF68119D5B38C78E180410CE877B232BD65770751EEB84A1A76696752304614717403DCB9C57AB5C9F2AF82D3C30C9A15254EC7CB623F52693BC9F15A7416934DB23BD1671BA4A98FCA7AB5E3D037C3655FBF3AFD0D4C6079DE045AB1A00E00469300C0F3F27A482924926319709C20E7A06BEA10E69F975BDF155FD66600729DD0514A896E061ACC14EF9EBD74E5E33B266050F602666CEDB0ED5A35A567048EFAD88AF4CFD54783C23F40F35FC950CEF3A4E4459D745F6FEE67CB09CE8E60BC4A1603AE90680382A90D2A98E9B70BB8497002E8EBCB200AD83317CFAA6CD58663CA16FA3E6145DE9B0AD761033E43C6DC01B14331116EB683037BDAA4341809DCA05E43F80BBDA548EF6D6E1509026F9883F696A19F96BDD58239DE87B1A16F2AABF5A8FF56B25FA4897E5E7EBDECA944732EF157F7AE6475EDB9B17CAB70654119992EBB00C355DB400FCD2671EB7101D9E171B390320109975FE77850A080B1D786DD5AD05D2833B65CCA3626C6FA1884991637C2DFE2DEDD6B76624568AC4E09E884174D3072A11F1584E141D53CE0DEBD9D3109827B33DEC12672663A9093D010FA7E7D6B7F2C4524AB3433D7E2D9B2E40B604D995601FCC81E29997D1BBB39D9C74986BCDEAB84CB3D95529F4B676C12F3DEEE32E62C23DBB1388CFA8DAA5E4F02D53C5545DE65AA023071F6D362A2AB090A9AB7EE1333066835E91564396AAE679644D682515D022C116E6F2DBEAF1EC3A12327825B8132E0872D7BDAE6357D9C657CB68AFCBC1985F33B005347176717CE5317EB47CA0EE3E1F0EE4EC6C07FCE9A61D3B71DDDC9917662AF3BE68407FCCBD973CEA8AF37661781D2B332D0C9D69F2963642937DCAEC62E42678C11EF2DBB1F3EA1F30D5081645C6A43D5A3B539DA8FAF33654D7C41FD5F0AACDF0CEB0E6510A270F3A306ED3C57D13E9FDD530D49EADDAC6BDB93566F4AAF1328CFB20C1D1DE8896120CF3FA6B4710297FD0BE05939FC62E0136F8AF12169534DC83519E2F520541346F1AB93E61E6C1D3ECA062EE9BB6328C2185EA9D36384B542C872D50D2E8BD660E18AE26148ED203596EF04457BC40862A5FC7F23D64019ED7FB6483F72979F3882ADDF890F0E39088B2182C796C5C4120799EF9043C246B5AEC140938D6133EFD2DBF272651754157D3D463DF90E7FD7F692D2261AAEFD91B7FB37CFD646C3008F9E3DDA06C9CCCB294A468289FA1C35F040E3B65FC81635552229D58E1E59B1DF70DEC36083DAB04AED854F9B0915E5A3F038895E1F839730954C76EEB92C2B2B48AFD994C2F2A3EACBFF3B7AAC91240F24A21DD2DCDF772BABA21040977080139F202E84D22CBF15C8D0082AC647D52D3C931C4ECDFED19A4FDB832E551894DC329CB66AEC1C490C002EA2A87BE236789E1038944CF81584701051CD131C9FD4942A6E1AE168FDB9B8FB9FBF64FD0CDA77CC1071F5469FFDBC2AD94B67829814BD3367BE5AE6CE485B4EA943D8047482BDB28B2CFD4C78A1473F126466369711E8543C9D313F7F8E06E6ECDF4F49889D72BCDE39C75514D0FCD027F0B7EBC698BD89B4843440C031CF9C0F63D689B3C1419877BC623D56E0FC1876D9F6E294D6D8D9B7AEC70D28ECB9395F1813A1E197B95ABD2BF8151ADB0D54ED9D59246B485B9D51CA5A624617E71580C40AA966663092F3CE88A559133ADA2352C32AD1F4B6114AB670DEB4BB0BACBA0134E481B12C9B38BEF3DEE328ED49997CDEDB41A94B8F4875FFA108CA78F698953976161C90E8DB99CFFC7740AF12785311B1F554CA497D69EACF83D5BD64926186BC7DFC4C965BE60071F9BEF65ABE0282C67FF0B2C199657FB7D147658607E57D016184A96F8FF7CEDCE6BCD5593A8FF848F2BD1FA23CB6AF63AFAC95BF31557206417F45F3AAB9E193513B1CDB35F74A9D064218D09699143114CB98E8D0463C43C107EDE52EB64E7E032A6217A15BB91A6E117849BF2FCFB5A23CD4A5F111572C57C961271E4E1A6FC85D749686A1F9346CC8CF4A5AF4C031A6F11189AD04C1232BCC05CF7AECA80723E446007F3248780EDF99A21DB5A8CC0ABDFD480CFE0357F7035DD0527B103EA5C4303EB4AA061230187C4DF51D65654EE047E82B19D1B4BBC22027C8D3B597D5FAB7DF992F99DD81EE2DDA6CA6C1C82922ED4C7456A0A5078C20BE8AEC82A509D55CDE8217DEF4C9C504D7358F377C54BE5B2EA10192D7C32B24A14F49DC759812285559510223866DFEF358C00F5D5F3DABBD1228C0A54835F591B788BA543376E4A816596FA3148112FBB3778F32BF3B4C21D608F767CA99CFB7E51424826D20D9F3436545598E8720687172DC5D5F8A6BCBD27E441CE5D0AE672DDD24557A9E8671E63E809DF220EB02B0FF519BFF5F646F92049225A44285E503875719DE7205BE2D472A84B7613A3DBE0DA3F0F668F5545F77B86A5AFEAC127B09E78720A5EF3E127B97CE7370C1E2243298E884FA65CA91A21178A2F4DDC8497364B9E9A37D3901DACA2F3D05D6A71416BFD1A2A33B36964C0CED3C00CCF0AEE0D35054B7BDFDA79198F4EFE7517F2009125C46F4AEF4E044FF438B6583636772E2A0D0CEE5E2555D5BF2A4B0602F6D998319D922936F8D831A9335785DE5169B4CB7CCD00AC2052774E30EE4C33AA8603075A2BB4DFFD3907AB85032A72A23C95DF301ACE087FD36291735CC04A7290069BDA5F1EAEA8ACBD169D73FADE59FA71F530728098FDF42E87123BC87C4870E24105B90B47871C2B7AE9F4029EC26A6B72A6B96758C9504881A535D8B8D3CDE5C8CCFCFD6E9A4DCFA61611413AE1AB248ACF95F95586914FCCD2791B7BA7314F930D2C24CDF520527094C0B5430D3D4B042C2CBA7E0174A633BE17CB97D1AE0A4B9E2D0220F5DAEC396D4A4781901AC61B3A065C18EFEC22E1DD40B47AAF4E464D749227028C63E1B81B6DDA8821D860B414366744A81069C8B7AC71BE44A3C3A2E3054C684900B446F3B534AC5AA325690C38539FF37F26E49B151D352F819777B071C85716640AE82DE9CCFA04B3AA3ECDA1F9553070C577FC6855A14E0C6493B4BCED3680D3896761031871B74F9CE2A630A4185FBD105B90224F2E7AD989F76997048A683EFAE693D521703E5F2AD15A3A9C449DA07A5731F75034C63A84FFA4D6CA3914CF83041FCC33815F46E06C5821136EF6B2325ECC845D227C20D2A698F99C6A8BBA74A5DFC22381E1A087EF26E20583D4A57EB8690C55AC3377C0E5E3E9897B635C2F4506C4AF4F5F40456A462A794095CF4E6ED733C58E924E932D87E00F051391425AFD3E57970AE9830F9E6D7846BBC66F9363DF568D9E6DDE1B496C18D38DD43B08191A7C147927F127B05E2D64EE8839B15968EBBCB55332F561E9E63B85AB048B95B6A80C63B3D7998F8F514D77E9902C70364F657EB3669D208C9E0E008BB76F1723FEAF644CF82111245B649C3EC5C2A52421C8F6B30B03921897067350C20B911FA67512BC7B0EFA9FDDA3300D370489B901605A895FDAF8DD205268C4DB9A9CFD225502DF03A9930843D788C50324D0DDF393897D65E4BB1B9960F0237B58C218382AE361D82B558CEBEA9DA0217D1D55B969FC0A56F8776ED3B874832AC1B71B2908A957391C5CA97FCF13346E6A4F2F39C9924551156D1AEFA500FEEEE335AD0F549AB711BEFACDD7166E8D3AA436BFB8DF6C16AE712A43D221C1D40A7F4F38993213B6CCA83DA41EC3F781613954B3C52899ECBC4B38594C6D3F0D5823795C68B67D014119483092754B388882F03148903ABA902C89F353156256587F62057B53E4F5EB043E0346254AA713A822C6A6DE931AC5F43F8462FC86BAF475B338A63F33DC534D5E5CFC7D4368E1A769A25F5ADD8EAF8020781EB07B9FD30CAC6E7343ABC70250FF724E7C31958FB1876513A43B0ADB807264AF52136FFE68A2BCDBAA8A3F372DD416F689016E544C197B8C116CC94CBD6C4A2DD7A2EFE15FC35D709AD6512144755F8364452CF80AD09C90120DB0C78D322F6F535574CDF5F9F5C04B72E86D4B8602A0656BADEF45FE5F818D628DB4A72337C4FF8CABF129C6EF529A504B0BADC8B89030F0E4BBBC70BAFE3A472DC08847DDA2E5D71C6BA767D80F18B40DA4B8FB1A094E15CFFE77AE5CCB25238B10DD3F298511914F20BC5C11E38F2D8011DEF03A4284E7CF1E525B603BCB61FCFF3342B03B8E1EF6B74F5530DD30D25DB311B148071D56FCA5377F2A7BD41D6348E4DB340DD8E45E12158925A5EB644324125AE882D933AAA0F58DCB03F2A95EBE5501A787EDFF9E067AADB42EDEBBF9753144C84E24336626F1735E3C786B4F6A61CA6C4F36C313097CA9B64FF9A2BE05A678AD55FB2276BA2D3161539320D1389AE6FB0B2D161AD1E7921DECF79A7D814DA30A59F98A7D9640A80A4502E42B7AB0024357B2017353F8361ED497A8DEEFC2C054D1C4EA0BA95637F4153CDB67A408ADFEF0ABE8EA55399382374974D034087E638E0DBBC7274E5E42A9D6D777B3C61A82BA47ED9045FE3F6F063AC86BD5A22AF161036F0B96B19449885F079395271A4CCC9B5A904F00BBF1F432B94DC5BF34BED9AB3FE2FBBF8CB9E2E4DA44ABB262EEA72318409D1A6DE2AE36718FDC5B7E6E90177F245BB92F18684DD5390289E2984F86382FA724081CF6C2F179C94F234064A9B35F62F6ACA14C3EE82AA862215DE77320DE15D6F0E82F349ECD735CEBE6DCFC493A5C93627C81D37371F47371F3625C5ACFBDC721FFF6CAEBF7A46A25D90681BEC8868B9AD9434B34AC75C30F035A9F7AAD16437D80CFA45F0E5F26430A9FF904E3A62EF8E668883DBB5D2C3EC4C4AC56D230B9A93BEC4538CC10F4E20BFDAD8FB5523EEA944825746B78E7CE581324457958FA6E736BCC2FCAA269545458BB63D085F2C08DE4053CC9B4897A007707BB32572693261B86E04B26DE98048A5B37ABBB49886D9997AA34D4A0E0B0AC3CDA15A016EC3B38B02C40D6C9839F0B48F0D8795E6CD0B899A020EB4F8B869E7347061FD2DD263C1361A7AE664B117312AFCC63939D75451087B49C8E86B95A4EEA9A0F00CE43CA81C8BB0194332A57BDF62CB3F681C940B0BB6C06F8ADDCD0D7BBCCAEF87F5FF63073EBBA5508A7D2A6A8567D9F5B297577E83987341A1E5BF61934935DA606A2B1FCA0D12B4235BC188519FC5764F6464809E642B53FAA76DB9FDF8D880AADED5FF58324E0F22C7ECC16D4BE16A81F69103FD5F118AA02E4BC9450383A8B87BE2000064C9FE23D6567A9C8C99CC246B8791D28C60B4B5E0198A07A34A571CEE3B63DDF5F0C25F6761B40797D0EF2D23938AF7600F873F65A3BC42B7A71D039B9C29CC134ED162A684E9104618D7C97C8FFA687A7D48FD55F09853D0C061C158BE47D184084604CD41C4AEBC3C1AD387B63EF41B5C1C58503BFA05A99015B74C3062ACCA33676E1C4D5EC64E20A423946A08553428BAD7D6B01495666F9375E5092E252CFAFACB20340A9B55C9886F76991DFA31BAC377CD852983162E078F5028C47944B019513113B2CB2E4015CE2FC3D8A40AAAE7F0DCE5E4935C796E959700963BA3BCEDFFC464DD76473A3FC3381888FEF753D6D743654603F56A87B3C323AA6D725381A52F7C89E5AE05B96100FC3B077A0501AE8918D807881529C4991663C6233BDB62E3CCBE8A4A73751683BB5BE15661D0992D635E4B1ED6B12A80B7BE6D87FB4CFE546E2FDA3C71E79CC5BE1F4112E3B02E2FAC33661AC208606DCDD418603D396E3E277762EC5C63168EA48374326EE80749423B63F46326E4E9D0D36D68C9C735ECEBB2DD5247C6199CE2B3D5EDC5D919DF07B4ACC87CD35E57E090200F50B8514196BEDCEA0F9D19214AFA3EE875C315E911C1842BFB9DEF6ECE97C5FCC12A0151C29E37316C2B225B8B36BBCBC44B69BD3C712C38D820028DC50264D1E6F6D6162D9209D4FD89E4EF2E79A56BE34C8145A29E83E3D05EB81076B42BD7505183579E0E3A25AA164EFA33FEC0AA7AAF6AA953416ECC762914EB03BCA6592514B4BC9007285A479E422BC15ADEADF0C652CF2719B9A79F71E7EDD2AABF8933B1A06FAD6DFE948B99C5AAA3B07D92A05A9CB1D54A7D342708CF3ECDE444AAF2B9E434D5FB4AB41AABFBD104377C69C3055814B83553E25DA9B0B235BE8FB87A63D8FFD4B95D76B30245A23BF06A33D169A2FF19159FDDCF3382B28BFF68C6DD64E30D9EDCE62A08FE6552165D622A45C9A42A665F5C0F7A24DBB8C76B55494349C9F7F87DE83B37FBC58211F556C9EFC6DEBB1E37883DE50FFCE0CA41912B4CEDCAB0BBF90207CD88CFA485BBB33E458D916952ADCE46431B6DE783143C31A0E1CF0E21EF66542FC17FC8BE2D6FE34325AC37AA22BE0DE92BDBC92282A459A5D74E7161890CC0D4BF41CD437DF31DE31A8BFD7A628FBF870470566655D5E282E3CFD89FEB57FFBA315090952922BA2FAF2F3BD0D7AD8C8E1CBF2CB32E6EC254DCBEABFC0CF303741FABCE6360998F9CF147F4450C54F96AAAD028286E12DB2BCCFC7FB0B2163F28F668D2D9ACBD616847845E71547E121D188DAD69EA6A75909B65446B30C478F254AA3C305DE58DF000B2C11F6B9EE82ED3EB1562D2EEE945C599E5D9BF2C1B0618A919721CB3E2C1A2B8306BFD95D2DEF0B4AF06FDEA71CD8A4B5BCB1F38D7379D772F0DB83960B13A0E269184E9D927A149B1916F3CC1E3352E624FE5C58C1C45B8B06B6072D08F02C29346C0D7B252EAC17BCFD723515C0428F99CD5DA4F58B7E027ABAC931EADD17FB978348629EA6B2AA210E368D80A37C686E1F3E2FF818148A9B69FACEB1E571F0C3F52F794683924CE8C5B6F236F3C93F247C722EC89BB7637F08CD84412FD9A18C9767F74E6286B9938CA984592343B56E6350A85A0FB0819F9B49A6CD4F8C549FAA5399E8A07BEE0EBE54CD6137FC3F5A909B730C1C9FAE2BEFF9316B3457EB99B46B4DCF00EDE7BC23738E2F9108C3631B3C31573C636D7F8047386EBDC115C67097F5D282053E10FE726A9369F22F2D72734E559DCC8B36D3D4FA2E08CA6F4AF32A3FD8897CB88FFECF8CF91D6C0537F160CF56B5B9427E0BFBD968F0659979053476A228ED7047F07A480473C9C04C8EE6892FD7C703B9AFC9B936944EB6E957EAEAB75310F0ADE94B840BB06FD85373C8CD0A54A746E7EA1B42C9F0046C7879A35081F3D25CB877CFE70705188D23C6DD44F2D0D9C881D2991EB64D2923023D152EA2CC0A8804310D4B50142319A2E3C85AB50D75B02C525D69955A900ED33E5F49DB02BF449173651FE1D9F512E46B3117D0214C71B0719726F19B0A59EA691224BF5EBD82ED5815F2B80CA0EF26F7ED2ACECFFBCE6B1CB1372BA53225FD4628B064D565FCE9427A365A1E49AA2FFDF171D21208A5AFDCE7FEB60FDE9F2D1EC0648C6E7A9E8B5C7FE6A5514D238C929A6CBF8CD2464C8E006DFF01A099F6DD154EE7E01E1E79A564F5D27FEDA26E3D687D08F50ABF4F3C459206011ADDE63AC54CB718A60734F3466277158AB6E6FF59BDAF1A36D6C4ADFC6E40A338CAFC8B4F59ADCF1E86A16123B55344B93BDFB37CF17CF4512836D398043E8E4F0C89E1289BD8EAEABC4B9DA0FDA507A4140E95302E9C52FAB622D7644FBB16F7FFBC87C3CE74CFC3E919699E693888D12104D3863D5929CCE1387D408A85DAE5495316B34992CC95280F607EFDA67E73166A7FC6111D94CE6A402C37A9EC67234ECC16FB9E7F3D17ABFCB672C530E56073B68FBD025487DC88C01B6ADDD7DFCC4000EB233CD11D444F068E4E1DF0E9E6E4E06B85CD80FEDDEC6FF5A4AD6F03ED29BC40B999219405AD4466E389F6CE82B14318FA32B1FA0F7CC53159BBA39B6398E03A0617A369C2CB8D991C1A131954C26229B6C133C68C5C311361F5433683314A91F75DF81344D0D7834CD8E6BFABBF6A0A2B0F08347AAE64B96392AD9D00FEE690F388715704B480DEC6AA025728049217799149D191305B7FFCD64EC57A4D671A1FD95A91706C04224C35B3F1D92CB95AB102A92E3CD7DAE6041ED24CB1588F88ED82D9DE4885B4F074EFACDF70CB700176BEEB62312045C950F37795D8666943C4D97554D0850F5F8AD2D7315201E3F214D4C005AFDBED5AD4FB9E7395B9EC7432F20A23F6EB944D7D9CD26BA6AB7E0BFA9E6050DAB043CC6E2772D4F7B6DECD8C7AFE35D6D7BBE512EE47C473456A0F2C97B7890F14D6344658B34A6067C94B0AF7BF4235C2A78F8CF7B59F677F3B5EFC3E891DB6D5DC3C1466A546FE9EF712FB1BC40C3685E014541625574A68891C06B8E11FE501E2DDA57B32EF1DDFD4102793F1CA6808B8E3B0E2058C811C945C2C0823BBA25F0A38684B5E965BB1BCE48E41C90BA910DF0B1C0367CF19A8616F48D77A65A3B39F8833CC54CE708D9CF9B6FC299788335DFE3165B0E4D070EFF602B1588E9204721379440A26C496AACBC1409AE4805FAB0E9FAB25341B92565B09C7591205AF8F2597CB8405094CAC8CF2AE0562866284EA1C35B5B759A877214E5C3056BADCEC3CA4A4747CFB3CAB2EA8F02FEC668722A78CDC6488C04D5C54EC5DD921B7755FE692954309A7AA43298682DB89AAA0AF82BBD4CE9229B4EFB90DF78D49AC1F590556827DDE9071B666FB4E1FB03D19219A132C5FC5ABDFE6B6F504F62268C1479EA87B778F726A48F563DBD7D6A1C9DB0FAE4A872A1F2B873B1E6ED1B8CF02C7C0D58A99EE8B287A50FB2AA04D9258D6FBA6BB53EA50203BBE49D4E7C7E1924E2BA18497DA1B77947B5A5BDCCBE93F7AF9A24E9BF74C47A625637CB9821585DC93BFBF28D01B909DB9190CC82EFF00018388071FE305B04D66C1F613AE7E167EB76F09DA46FC3B14E6DD666621EE24649B203C6C5286A56DA0EA36DC166E5F4D4B88E3003CAEF412E2480FC8ECB333361660275249F4AA339BFF0C56890E9BF1BB5AB7526AE4ED62CE01A2D1968ADDD5A7BC38E5C1C289BE8C185D6C8B537D801A7C5AD939923B0D1D92762753E719D1AFB7D7AA3377B6906E9CEAC848CEFB9BC725D30129542AFEED2BABE1B025839C1108EAAA6A10A0B9B30CF05CE6157BE309F09968A4B4CC291F0221C2C9783B65A3F66E64A21841F4AE7B46A897246E8440F315136FC07499917DB63B8B6FC980B8BD7BA804023A350BD1009AE049EF609C287D6D2EB77CF4FECE55F59B9E4FCC91D2BC599FC46F07F430209A52DFC12734057C59B578BB5959C0D305EECFC679990A50D739954922E57111D0D65C5D5805EF36FE0EFE7A6E8548507D514508E70ABDF13DF85FD9EB9582A22E8EE4A1AAE000627FE1A13C8D4AB02613E3E70E593D1C204712FC81D256F82760C02ED4D85681B9F0DB7120BA0F8D6203767E7EE111B859759F7F9935DE6A9CED4F61AEFF0443D865B36FAA4B7DE70BE9A2C70C6EF1AF0C18D9F1077D5540266D55F67930F7EE5D91A3DFFA2774F7A88B540E3EE3D6C1EFC7505E7FCC8EDCBB4A31308A471E9A8F7018B8D3B8000F304FB8878DD89CB5CAF98EF312DBD2D15C4E9153B0F47B51921C429CBD7B3873FC74C139352FCD83F42367D6C72E23DBF5D640C6B2FA43827673146F44FF46D5B5143AE4137D92E44C87D196CE375B7670E61C4D94C9A1ECCBD873C8703C2A5A85BC847486BB6375F10B85A343F95A8B01E028E22F1064EBFA92808D5A78153F025D9EF1D4FF57CEBFF07DECC463293ED0ADEBA54D21F9AFBD9F506B58FBB5F4A7857EB2AEE6C412F1003DCD4A780EDBC0A7F1B1F669DCEB5DA50C7CA45112AA8FBB8E48215ACAD0C379494BB6C05219BA77BD4166885B25E08685A06CBFA84B4709F5499AEECD9503DDF7C5BA88D379F35500CD35D9C9A74B2EE86FC962DE5A1D6488AFEC573CAF55EAD16413E2531AEA8C7B1F79059EDA8F179C25F6DC80FCF842BDD453C15CFE20A7DB3BF2BB8067880D74F909DC64936583DEA5143EE7D479D53ED60698626A050A962B073E1E9F1F5A1CBE5A0D0ECD4C385271B95806D7190BAA23688AADD2D461C14781424B7B6295F6F56D49F6790569E1B8765B098B476A054161465D0CDD378AD6A24C1EB84A6CC0571AE5E015987246AD2633BD123B62667A536EB330209ACE3BA68EC30F9FD0DB97B3C06CE74953B7EAE7D1E1844F7E4502A58141B1B569E650CA64EC7CC613B07384C557A2B1742A578069E2DE57ED3A0446A55F241F38BBC6AA2FA045F0CA5F01F2E1115A031F4C05A01E1CFFE344107532A0B5549DDBF4683F1C41F66D971A5DBFDE6B077FA01C068718C860E0DCD22AF7840206747CEF5983BB1EF3F198924C3B0E15013938E19DF84257A6ED54CF66FA7F567B7BD2B35DE92DA58CDBFD2F294774DF731920B3A64791E8FE65C1F263F959 + device_name: samsungNexus + device_platform: Android10samsungNexus + device_tourist_id: 22824046377449 + disable_rcmd: 0 + dt: VvUCfbGyEyKA%2FoHRmvCgKyJ%2FzeDQ17WLhT6pA%2BqzF2DGxKKaHBadIZFgk%2BG6ZW%2FP72Ft1nuhWki1%0Ay551QsLAenLneTaD3Rhf7BSXtY5MykDkH9MTWDWyxuWVDwjhLduCzDywgq%2BtxjVgXV1sBQWvyISh%0AkIlGGuAp40TzbQi%2Br0Y%3D%0A + from_pv: main.homepage.avatar-nologin.all.click + from_url: bilibili%3A%2F%2Fpegasus%2Fpromo + local_id: {{buvid}} + login_session_id: f152581bf6b3a1b4bad532f836a4e878 + mobi_app: android + password: {{pwd}} + platform: android + s_locale: zh_CN + statistics: %7B%22appId%22%3A1%2C%22platform%22%3A3%2C%22version%22%3A%227.72.0%22%2C%22abtest%22%3A%22%22%7D + ts: 1735704456 + username: {{phone}} + sign: 8e9d9f9da8d5a9355ef8ba620de75175 +} diff --git a/bruno/passport.bilibili.com/x/passport-login/web/key.bru b/bruno/passport.bilibili.com/x/passport-login/web/key.bru new file mode 100644 index 0000000..9bd4875 --- /dev/null +++ b/bruno/passport.bilibili.com/x/passport-login/web/key.bru @@ -0,0 +1,26 @@ +meta { + name: key + type: http + seq: 1 +} + +get { + url: https://passport.bilibili.com/x/passport-login/web/key + body: none + auth: none +} + +headers { + Host: passport.bilibili.com + buvid: {{buvid}} + env: prod + app-key: android64 + user-agent: Mozilla/5.0 BiliDroid/7.72.0 (bbcallen@gmail.com) os/android model/Nexus mobi_app/android build/7720200 channel/yingyongbao innerVer/7720210 osVer/10 network/2 + x-bili-trace-id: 43e1a54f60cfbf0bb9788d4d9e6774bf:b9788d4d9e6774bf:0:0 + x-bili-aurora-eid: + x-bili-mid: + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzI4OTIsImlhdCI6MTczNTcwMzc5MiwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.hEDtIHZoN5Wyoja2hNuPcfy-laa1r05ObiEhJ5gPSt4 + bili-http-engine: cronet +} diff --git a/bruno/passport.bilibili.com/x/relation/followings/simple.bru b/bruno/passport.bilibili.com/x/relation/followings/simple.bru new file mode 100644 index 0000000..759c538 --- /dev/null +++ b/bruno/passport.bilibili.com/x/relation/followings/simple.bru @@ -0,0 +1,49 @@ +meta { + name: simple + type: http + seq: 1 +} + +get { + url: https://passport.bilibili.com/x/relation/followings/simple?_device=android&_hwid=e0N3QnRBJEV0QiEXaxdrXG8LPgw1UDECYFU0AWJTZQ&access_key={{access_key}}&appkey={{appKey}}&build=7720200&c_locale=zh_CN&channel=yingyongbao&disable_rcmd=0&mobi_app=android&platform=android&s_locale=zh_CN&src=yingyongbao&statistics={"appId":1,"platform":3,"version":"7.72.0","abtest":""}&trace_id=20250101120700037&ts=1735704457&version=7.72.0.7720200&sign=3a7ddaa98dbf792b654bdaabdf3dcd1d + body: none + auth: none +} + +params:query { + _device: android + _hwid: e0N3QnRBJEV0QiEXaxdrXG8LPgw1UDECYFU0AWJTZQ + access_key: {{access_key}} + appkey: {{appKey}} + build: 7720200 + c_locale: zh_CN + channel: yingyongbao + disable_rcmd: 0 + mobi_app: android + platform: android + s_locale: zh_CN + src: yingyongbao + statistics: {"appId":1,"platform":3,"version":"7.72.0","abtest":""} + trace_id: 20250101120700037 + ts: 1735704457 + version: 7.72.0.7720200 + sign: 3a7ddaa98dbf792b654bdaabdf3dcd1d +} + +headers { + Host: api.bilibili.com + buvid: {{buvid}} + fp_local: {{device_id}} + fp_remote: {{device_id}} + session_id: 3160e892 + env: prod + app-key: android64 + user-agent: Mozilla/5.0 BiliDroid/7.72.0 (bbcallen@gmail.com) os/android model/Nexus mobi_app/android build/7720200 channel/yingyongbao innerVer/7720210 osVer/10 network/2 + x-bili-trace-id: 97831b4bddcb6011ee84ae59806774bf:ee84ae59806774bf:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzI4OTIsImlhdCI6MTczNTcwMzc5MiwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.hEDtIHZoN5Wyoja2hNuPcfy-laa1r05ObiEhJ5gPSt4 + bili-http-engine: cronet +} diff --git a/bruno/passport.bilibili.com/x/relation/tag/special.bru b/bruno/passport.bilibili.com/x/relation/tag/special.bru new file mode 100644 index 0000000..f10c5fb --- /dev/null +++ b/bruno/passport.bilibili.com/x/relation/tag/special.bru @@ -0,0 +1,49 @@ +meta { + name: special + type: http + seq: 1 +} + +get { + url: https://passport.bilibili.com/x/relation/tag/special?_device=android&_hwid=e0N3QnRBJEV0QiEXaxdrXG8LPgw1UDECYFU0AWJTZQ&access_key={{access_key}}&appkey={{appKey}}&build=7720200&c_locale=zh_CN&channel=yingyongbao&disable_rcmd=0&mobi_app=android&platform=android&s_locale=zh_CN&src=yingyongbao&statistics={"appId":1,"platform":3,"version":"7.72.0","abtest":""}&trace_id=20250101120700047&ts=1735704467&version=7.72.0.7720200&sign=c49a2ca7bf93e8e65e77fe285fb71b1e + body: none + auth: none +} + +params:query { + _device: android + _hwid: e0N3QnRBJEV0QiEXaxdrXG8LPgw1UDECYFU0AWJTZQ + access_key: {{access_key}} + appkey: {{appKey}} + build: 7720200 + c_locale: zh_CN + channel: yingyongbao + disable_rcmd: 0 + mobi_app: android + platform: android + s_locale: zh_CN + src: yingyongbao + statistics: {"appId":1,"platform":3,"version":"7.72.0","abtest":""} + trace_id: 20250101120700047 + ts: 1735704467 + version: 7.72.0.7720200 + sign: c49a2ca7bf93e8e65e77fe285fb71b1e +} + +headers { + Host: api.bilibili.com + buvid: {{buvid}} + fp_local: {{device_id}} + fp_remote: {{device_id}} + session_id: 3160e892 + env: prod + app-key: android64 + user-agent: Mozilla/5.0 BiliDroid/7.72.0 (bbcallen@gmail.com) os/android model/Nexus mobi_app/android build/7720200 channel/yingyongbao innerVer/7720210 osVer/10 network/2 + x-bili-trace-id: f8209f943aca4379b5b407142a6774bf:b5b407142a6774bf:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzI4OTIsImlhdCI6MTczNTcwMzc5MiwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.hEDtIHZoN5Wyoja2hNuPcfy-laa1r05ObiEhJ5gPSt4 + bili-http-engine: cronet +} diff --git a/bruno/passport.bilibili.com/x/v2/account/myinfo.bru b/bruno/passport.bilibili.com/x/v2/account/myinfo.bru new file mode 100644 index 0000000..97ce44d --- /dev/null +++ b/bruno/passport.bilibili.com/x/v2/account/myinfo.bru @@ -0,0 +1,43 @@ +meta { + name: myinfo + type: http + seq: 1 +} + +get { + url: https://passport.bilibili.com/x/v2/account/myinfo?access_key={{access_key}}&appkey=783bbb7264451d82&build=7720200&buvid={{buvid}}&c_locale=zh_CN&channel=yingyongbao&disable_rcmd=0&local_id={{buvid}}&mobi_app=android&platform=android&s_locale=zh_CN&statistics={"appId":1,"platform":3,"version":"7.72.0","abtest":""}&ts=1735704457&sign=b33e7a0aa1aef3d1d4284c759fa12857 + body: none + auth: none +} + +params:query { + access_key: {{access_key}} + appkey: 783bbb7264451d82 + build: 7720200 + buvid: {{buvid}} + c_locale: zh_CN + channel: yingyongbao + disable_rcmd: 0 + local_id: {{buvid}} + mobi_app: android + platform: android + s_locale: zh_CN + statistics: {"appId":1,"platform":3,"version":"7.72.0","abtest":""} + ts: 1735704457 + sign: b33e7a0aa1aef3d1d4284c759fa12857 +} + +headers { + Host: app.bilibili.com + buvid: {{buvid}} + env: prod + app-key: android64 + user-agent: Mozilla/5.0 BiliDroid/7.72.0 (bbcallen@gmail.com) os/android model/Nexus mobi_app/android build/7720200 channel/yingyongbao innerVer/7720210 osVer/10 network/2 + x-bili-trace-id: 46e70b4538aaf4001079cf97f86774bf:1079cf97f86774bf:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzI4OTIsImlhdCI6MTczNTcwMzc5MiwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.hEDtIHZoN5Wyoja2hNuPcfy-laa1r05ObiEhJ5gPSt4 + bili-http-engine: cronet +} diff --git a/bruno/passport.bilibili.com/x/v2/feed/index.bru b/bruno/passport.bilibili.com/x/v2/feed/index.bru new file mode 100644 index 0000000..8e58e02 --- /dev/null +++ b/bruno/passport.bilibili.com/x/v2/feed/index.bru @@ -0,0 +1,29 @@ +meta { + name: index + type: http + seq: 1 +} + +get { + url: https://passport.bilibili.com/x/v2/feed/index + body: none + auth: none +} + +headers { + Host: app.bilibili.com + buvid: {{buvid}} + fp_local: {{device_id}} + fp_remote: {{device_id}} + session_id: 3160e892 + env: prod + app-key: android64 + user-agent: Mozilla/5.0 BiliDroid/7.72.0 (bbcallen@gmail.com) os/android model/Nexus mobi_app/android build/7720200 channel/yingyongbao innerVer/7720210 osVer/10 network/2 + x-bili-trace-id: ce74f22f44dec31cc331a61b2a6774bf:c331a61b2a6774bf:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzI4OTIsImlhdCI6MTczNTcwMzc5MiwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.hEDtIHZoN5Wyoja2hNuPcfy-laa1r05ObiEhJ5gPSt4 + bili-http-engine: cronet +} diff --git a/bruno/passport.bilibili.com/x/vip/web/vip_center/v2.bru b/bruno/passport.bilibili.com/x/vip/web/vip_center/v2.bru new file mode 100644 index 0000000..0c4236b --- /dev/null +++ b/bruno/passport.bilibili.com/x/vip/web/vip_center/v2.bru @@ -0,0 +1,29 @@ +meta { + name: v2 + type: http + seq: 1 +} + +get { + url: https://api.bilibili.com/x/vip/web/vip_center/v2 + body: none + auth: none +} + +headers { + Host: api.bilibili.com + Cookie: {{cookieStr}} + native_api_from: h5 + buvid: {{buvid}} + accept: application/json, text/plain, */* + referer: https://big.bilibili.com/mobile/index?exp_symbol=release_version&oflAb=1 + content-type: application/json + user-agent: {{user-agent}} + x-bili-trace-id: 0b9853d9388b01892edb6939c16774d4:2edb6939c16774d4:0:0 + x-bili-aurora-eid: UlAAQFkMBVkH + x-bili-mid: {{mid}} + x-bili-aurora-zone: + x-bili-gaia-vtoken: + x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzgxMTEsImlhdCI6MTczNTcwOTAxMSwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.J28IlxZ6SjllQEQkq_OvFUiRYAEL2VhQG_WWBcmNppE + bili-http-engine: cronet +} diff --git a/common.props b/common.props new file mode 100644 index 0000000..18efac7 --- /dev/null +++ b/common.props @@ -0,0 +1,7 @@ + + + Ray + 3.8.2 + $(NoWarn);CS1591;CS0436 + + diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..1f24688 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,171 @@ +# Docker 使用说明 + + +- [1. 前期工作](#1-前期工作) +- [2. 方式一:一键脚本(推荐)](#2-方式一一键脚本推荐) +- [3. 方式二:手动 Docker Compose](#3-方式二手动-docker-compose) + - [3.1. 启动](#31-启动) + - [3.2. 其他命令参考](#32-其他命令参考) +- [4. 方式三:手动Docker指令](#4-方式三手动docker指令) + - [4.1. Docker启动](#41-docker启动) + - [4.2. 其他指令参考](#42-其他指令参考) + - [4.3. 使用Watchtower更新容器](#43-使用watchtower更新容器) +- [5. 登录](#5-登录) +- [6. 添加 Bili 账号](#6-添加-bili-账号) +- [7. 自己构建镜像(非必须)](#7-自己构建镜像非必须) +- [8. 其他](#8-其他) + + + +## 1. 前期工作 + +``` +apt-get update +apt-get install curl +``` + +## 2. 方式一:一键脚本(推荐) + +``` +bash <(curl -sSL https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/main/docker/install.sh) +``` + +## 3. 方式二:手动 Docker Compose + +### 3.1. 启动 + +``` +# 创建目录 +mkdir bili_tool_web && cd bili_tool_web + +# 下载 +wget https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/main/docker/sample/docker-compose.yml +mkdir -p config +cd ./config +wget https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/main/docker/sample/config/cookies.json +cd .. + +# 启动 +docker compose up -d + +# 查看启动日志 +docker logs -f bili_tool_web +``` + +最终文件结构如下: + +``` +bili_tool_web +├── Logs +├── config +├──── cookies.json +└── docker-compose.yml +``` + +### 3.2. 其他命令参考 + +``` +# 启动 docker-compose +docker compose up -d + +# 停止 docker-compose +docker compose stop + +# 查看实时日志 +docker logs -f bili_tool_web + +# 进入容器 +docker exec -it bili_tool_web /bin/bash + +# 手动更新容器 +docker compose pull && docker compose up -d +``` + +## 4. 方式三:手动Docker指令 + +### 4.1. Docker启动 + +``` +# 创建目录 +mkdir bili_tool_web && cd bili_tool_web + +# 生成并运行容器 +docker pull ghcr.io/raywangqvq/bili_tool_web +docker run -d --name="bili_tool_web" \ + -p 22330:8080 \ + -e TZ=Asia/Shanghai \ + -v ./Logs:/app/Logs \ + -v ./config:/app/config \ + ghcr.io/raywangqvq/bili_tool_web + +# 查看实时日志 +docker logs -f bili_tool_web +``` + +其中,`cookie`需要替换为自己真实的cookie字符串 + +### 4.2. 其他指令参考 + +``` +# 启动容器 +docker start bili_tool_web + +# 停止容器 +docker stop bili_tool_web + +# 重启容器 +docker restart bili_tool_web + +# 删除容器 +docker rm bili_tool_web + +# 进入容器 +docker exec -it bili_tool_web /bin/bash +``` + +### 4.3. 使用Watchtower更新容器 +``` +docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + containrrr/watchtower \ + --run-once --cleanup \ + bili_tool_web +``` + +## 5. 登录 + +- 默认用户:`admin` +- 默认密码:`BiliTool@2233` + +首次登陆后,请到`Admin`页面修改密码。 + +## 6. 添加 Bili 账号 + +扫码进行账号添加。 + +![trigger](../docs/imgs/web-trigger-login.png) + +![login](../docs/imgs/docker-login.png) + +## 7. 自己构建镜像(非必须) + +目前我提供和维护的镜像: + +- DockerHub: `[zai7lou/bili_tool_web](https://hub.docker.com/repository/docker/zai7lou/bili_tool_web)` +- GitHub: `[bili_tool_web](https://github.com/RayWangQvQ/BiliBiliToolPro/pkgs/container/bili_tool_web)` + +如果有需要(大部分都不需要),可以使用源码自己构建镜像,如下: + +在有项目的Dockerfile的目录运行 + +`docker build -t TARGET_NAME .` + +`TARGET_NAME`为镜像名称和版本,可以自己起个名字 + +## 8. 其他 + +代码编译和发布环境: mcr.microsoft.com/dotnet/sdk:8.0 + +代码运行环境: mcr.microsoft.com/dotnet/aspnet:8.0 + +如果下载`github`资源有问题,可以尝试添加加速器。 diff --git a/docker/build/buildAndPushImage_multiArch.ps1 b/docker/build/buildAndPushImage_multiArch.ps1 new file mode 100644 index 0000000..3014f7c --- /dev/null +++ b/docker/build/buildAndPushImage_multiArch.ps1 @@ -0,0 +1,8 @@ +echo "start to build" +# https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/ +# https://segmentfault.com/a/1190000021166703 +# linux/arm/v6,linux/riscv64,linux/s390x,linux/ppc64le,linux/386,,linux/arm/v7 偶发异常,待进一步测试 +echo "Start to build docker image with multi-arch" +# $image="zai7lou/bilibili_tool_pro" +# $version="0.0.5" +docker buildx build --tag "zai7lou/bilibili_tool_pro:0.0.5" --tag "zai7lou/bilibili_tool_pro:latest" --output "type=image,push=true" --platform linux/amd64,linux/arm64 ../.. diff --git a/docker/build/buildImage.cmd b/docker/build/buildImage.cmd new file mode 100644 index 0000000..6a84584 --- /dev/null +++ b/docker/build/buildImage.cmd @@ -0,0 +1,8 @@ +@echo off + +REM start to build +echo Start to build docker image +@echo on +docker build --tag zai7lou/bilibili_tool_pro:latest ../.. +@echo off +pause diff --git a/docker/build/buildImage_amd64.cmd b/docker/build/buildImage_amd64.cmd new file mode 100644 index 0000000..58255a9 --- /dev/null +++ b/docker/build/buildImage_amd64.cmd @@ -0,0 +1,10 @@ +@echo off + +REM start to build +REM https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/ +REM https://segmentfault.com/a/1190000021166703 +echo Start to build docker image with amd64-arch +@echo on +docker buildx build --tag zai7lou/bilibili_tool_pro:latest --output "type=image,push=false" --platform linux/amd64 ../.. +@echo off +pause diff --git a/docker/build/buildImage_arm64.cmd b/docker/build/buildImage_arm64.cmd new file mode 100644 index 0000000..749c9aa --- /dev/null +++ b/docker/build/buildImage_arm64.cmd @@ -0,0 +1,10 @@ +@echo off + +REM start to build +REM https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/ +REM https://segmentfault.com/a/1190000021166703 +echo Start to build docker image with arm64-arch +@echo on +docker buildx build --platform linux/arm64 -o type=docker -t zai7lou/bilibili_tool ../.. +@echo off +pause diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..bc727c5 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e +set -o pipefail + +echo "Starting BiliTool container..." +mkdir -p /app/config + +echo "Running maintenance scripts..." + +# 3.3.0 need migrate db file location to /app/config +if [ -f "/app/BiliBiliTool.db" ]; then + echo "[3.3.0] Migrate db file location to /app/config" + mv /app/BiliBiliTool.db /app/config/BiliBiliTool.db +fi + +echo "Starting application..." +exec dotnet Ray.BiliBiliTool.Web.dll "$@" \ No newline at end of file diff --git a/docker/install.sh b/docker/install.sh new file mode 100644 index 0000000..6f92138 --- /dev/null +++ b/docker/install.sh @@ -0,0 +1,337 @@ +#!/usr/bin/env bash +### +# @Author: Ray zai7lou@outlook.com +# @Date: 2023-02-11 23:13:19 + # @LastEditors: Ray zai7lou@outlook.com + # @LastEditTime: 2023-02-12 20:51:19 +# @FilePath: \BiliBiliToolPro\docker\install.sh +# @Description: +### +set -e +set -u +set -o pipefail + +echo ' ____ _ _____ _ ' +echo ' | __ ) _| |_|_ _|__ ___ | | ' +echo ' | _ \(_) (_) | |/ _ \ / _ \| | ' +echo ' | |_) | | | | | | (_) | (_) | | ' +echo ' |____/|_|_|_| |_|\___/ \___/|_| ' + +current_dir=$(pwd) +base_dir="${current_dir}/bili_tool_web" +github_proxy="" +github_branch="main" +remote_compose_url="${github_proxy}https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/refs/heads/${github_branch}/docker/sample/docker-compose.yml" +remote_ckJson_url="${github_proxy}https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/refs/heads/${github_branch}/docker/sample/config/cookies.json" +docker_img_name="ghcr.io/raywangqvq/bili_tool_web" +container_name="bili_tool_web" + +### infra +verbose=false + +invocation='echo "" && say_verbose "Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}"' + +if [ -t 1 ] && command -v tput >/dev/null; then + ncolors=$(tput colors || echo 0) + if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then + bold="$(tput bold || echo)" + normal="$(tput sgr0 || echo)" + black="$(tput setaf 0 || echo)" + red="$(tput setaf 1 || echo)" + green="$(tput setaf 2 || echo)" + yellow="$(tput setaf 3 || echo)" + blue="$(tput setaf 4 || echo)" + magenta="$(tput setaf 5 || echo)" + cyan="$(tput setaf 6 || echo)" + white="$(tput setaf 7 || echo)" + fi +fi + +say_verbose() { + if [ "$verbose" = true ]; then + # using stream 3 (defined in the beginning) to not interfere with stdout of functions + # which may be used as return value + printf "%b\n" "${cyan:-}$(date "+%Y-%m-%d %H:%M:%S")[VER]:${normal:-} $1" >&3 + fi +} + +say_info() { + printf "%b\n" "${green:-}$(date "+%Y-%m-%d %H:%M:%S")[INF]:$1${normal:-}" >&2 +} + +say_warning() { + printf "%b\n" "${yellow:-}$(date "+%Y-%m-%d %H:%M:%S")[WAR]:$1${normal:-}" >&3 +} + +say_err() { + printf "%b\n" "${red:-}$(date "+%Y-%m-%d %H:%M:%S")[ERR]:$1${normal:-}" >&2 +} + +machine_has() { + eval $invocation + + command -v "$1" >/dev/null 2>&1 + return $? +} + +# args: +# remote_path - $1 +get_http_header_curl() { + eval $invocation + + local remote_path="$1" + + curl_options="-I -sSL --retry 5 --retry-delay 2 --connect-timeout 15 " + curl $curl_options "$remote_path" 2>&1 || return 1 + return 0 +} + +# args: +# remote_path - $1 +get_http_header_wget() { + eval $invocation + + local remote_path="$1" + local wget_options="-q -S --spider --tries 5 " + # Store options that aren't supported on all wget implementations separately. + local wget_options_extra="--waitretry 2 --connect-timeout 15 " + local wget_result='' + + wget $wget_options $wget_options_extra "$remote_path" 2>&1 + wget_result=$? + + if [[ $wget_result == 2 ]]; then + # Parsing of the command has failed. Exclude potentially unrecognized options and retry. + wget $wget_options "$remote_path" 2>&1 + return $? + fi + + return $wget_result +} + +# Updates global variables $http_code and $download_error_msg +downloadcurl() { + eval $invocation + + unset http_code + unset download_error_msg + local remote_path="$1" + local out_path="${2:-}" + local remote_path_with_credential="${remote_path}" + local curl_options="--retry 20 --retry-delay 2 --connect-timeout 15 -sSL -f --create-dirs " + local failed=false + if [ -z "$out_path" ]; then + curl $curl_options "$remote_path_with_credential" 2>&1 || failed=true + else + curl $curl_options -o "$out_path" "$remote_path_with_credential" 2>&1 || failed=true + fi + if [ "$failed" = true ]; then + local response=$(get_http_header_curl $remote_path) + http_code=$(echo "$response" | awk '/^HTTP/{print $2}' | tail -1) + download_error_msg="Unable to download $remote_path." + if [[ $http_code != 2* ]]; then + download_error_msg+=" Returned HTTP status code: $http_code." + fi + say_verbose "$download_error_msg" + return 1 + fi + return 0 +} + +# Updates global variables $http_code and $download_error_msg +downloadwget() { + eval $invocation + + unset http_code + unset download_error_msg + local remote_path="$1" + local out_path="${2:-}" + local remote_path_with_credential="${remote_path}" + local wget_options="--tries 20 " + # Store options that aren't supported on all wget implementations separately. + local wget_options_extra="--waitretry 2 --connect-timeout 15 " + local wget_result='' + + if [ -z "$out_path" ]; then + wget -q $wget_options $wget_options_extra -O - "$remote_path_with_credential" 2>&1 + wget_result=$? + else + wget $wget_options $wget_options_extra -O "$out_path" "$remote_path_with_credential" 2>&1 + wget_result=$? + fi + + if [[ $wget_result == 2 ]]; then + # Parsing of the command has failed. Exclude potentially unrecognized options and retry. + if [ -z "$out_path" ]; then + wget -q $wget_options -O - "$remote_path_with_credential" 2>&1 + wget_result=$? + else + wget $wget_options -O "$out_path" "$remote_path_with_credential" 2>&1 + wget_result=$? + fi + fi + + if [[ $wget_result != 0 ]]; then + local disable_feed_credential=false + local response=$(get_http_header_wget $remote_path $disable_feed_credential) + http_code=$(echo "$response" | awk '/^ HTTP/{print $2}' | tail -1) + download_error_msg="Unable to download $remote_path." + if [[ $http_code != 2* ]]; then + download_error_msg+=" Returned HTTP status code: $http_code." + fi + say_verbose "$download_error_msg" + return 1 + fi + + return 0 +} + +# args: +# remote_path - $1 +# [out_path] - $2 - stdout if not provided +download() { + eval $invocation + + local remote_path="$1" + local out_path="${2:-}" + + if [[ "$remote_path" != "http"* ]]; then + cp "$remote_path" "$out_path" + return $? + fi + + local failed=false + local attempts=0 + while [ $attempts -lt 3 ]; do + attempts=$((attempts + 1)) + failed=false + if machine_has "curl"; then + downloadcurl "$remote_path" "$out_path" || failed=true + elif machine_has "wget"; then + downloadwget "$remote_path" "$out_path" || failed=true + else + say_err "Missing dependency: neither curl nor wget was found." + exit 1 + fi + + if [ "$failed" = false ] || [ $attempts -ge 3 ] || { [ ! -z $http_code ] && [ $http_code = "404" ]; }; then + break + fi + + say_info "Download attempt #$attempts has failed: $http_code $download_error_msg" + say_info "Attempt #$((attempts + 1)) will start in $((attempts * 10)) seconds." + sleep $((attempts * 10)) + done + + if [ "$failed" = true ]; then + say_verbose "Download failed: $remote_path" + return 1 + fi + return 0 +} + +createBaseDir() { + eval $invocation + mkdir -p $base_dir + cd $base_dir +} + +installDocker() { + eval $invocation + if machine_has "docker"; then + say_info "已安装docker" + docker --version + return 0 + else + say_warning "未安装docker,尝试安装" + download "https://get.docker.com" ./get-docker.sh + chmod +x ./get-docker.sh + get-docker.sh + + if machine_has "docker"; then + say_info "已安装docker" + docker --version + return 0 + else + say_err "docker 安装失败,请手动安装成功后再执行该脚本" + exit 1 + fi + fi +} + +downloadResources() { + eval $invocation + say_info "开始下载资源" + + # docker compose + [ -f "docker-compose.yml" ] || download $remote_compose_url ./docker-compose.yml + + # ckJson + mkdir -p config + cd ./config + [ -f "cookies.json" ] || download $remote_ckJson_url ./cookies.json + chmod +x ./cookies.json + cd .. + + ls -l +} + +runContainer() { + eval $invocation + + say_info "开始拉取镜像" + docker pull $docker_img_name + + say_info "开始运行容器" + { + docker compose version && docker compose up -d + } || { + docker-compose version && docker-compose up -d + } || { + docker run -d --name="${container_name}" \ + -p 22330:8080 \ + -e TZ=Asia/Shanghai \ + -v $base_dir/Logs:/app/Logs \ + -v $base_dir/config:/app/config \ + $docker_img_name + } || { + say_err "创建容器失败,请检查" + exit 1 + } +} + +checkResult() { + eval $invocation + say_info "检测容器运行情况" + + docker ps --filter "name=${container_name}" + + containerId=$(docker ps -q --filter "name=^${container_name}$") + if [ -n "$containerId" ]; then + docker logs ${container_name} + echo "" + echo "===============================================" + echo "Congratulations! 恭喜!" + echo "创建并运行${container_name}容器成功。" + echo "访问地址:http:{ip}:22330" + echo "云服务器防火墙请自行开放22330端口" + echo "首次运行后,请执行扫码登录任务添加账号" + echo "Enjoy it~" + echo "===============================================" + else + echo "" + echo "请查看运行日志,确认容器是否正常运行,点击 Ctrl+c 退出日志追踪" + echo "" + docker logs -f ${container_name} + fi +} + +main() { + installDocker + createBaseDir + downloadResources + runContainer + checkResult +} + +main diff --git a/docker/sample/config/cookies.json b/docker/sample/config/cookies.json new file mode 100644 index 0000000..f123dab --- /dev/null +++ b/docker/sample/config/cookies.json @@ -0,0 +1,4 @@ +{ + "BiliBiliCookies":[ + ] +} diff --git a/docker/sample/docker-compose.yml b/docker/sample/docker-compose.yml new file mode 100644 index 0000000..b200912 --- /dev/null +++ b/docker/sample/docker-compose.yml @@ -0,0 +1,14 @@ +services: + bili_tool_web: + image: ghcr.io/raywangqvq/bili_tool_web + container_name: bili_tool_web + restart: unless-stopped + tty: true + volumes: + - ./Logs:/app/Logs + - ./config:/app/config + ports: + - "22330:8080" + environment: + TZ: "Asia/Shanghai" + DailyTaskConfig__Cron: "0 0 15 * * ?" diff --git a/docs/claw-cloud.md b/docs/claw-cloud.md new file mode 100644 index 0000000..f185776 --- /dev/null +++ b/docs/claw-cloud.md @@ -0,0 +1,54 @@ +# Claw免费容器部署 + +## 教程 + +点击 [https://console.run.claw.cloud/signin](https://console.run.claw.cloud/signin?link=FNTTMHS056E5) 注册账号,选择使用 GitHub 账号注册并登录。 + +成功后,每个月会赠送 $5 额度,跑 BiliTool 绰绰有余。 + +左上角可以选择一个区域,然后点击 **App Store**。 + +![claw-app-store.png](/docs/imgs/claw-app-store.png) + +搜索`BiliTool`并点击如下搜索结果。 + +![claw-search.png](/docs/imgs/claw-search.png) + +点击`Deploy App`按钮一键部署。 + +![claw-deploy.png](/docs/imgs/claw-deploy.png) + +等待半分钟左右,进入容器 Detail 页面,点击如下链接即可访问站点: + +![claw-addr.png](/docs/imgs/claw-addr.png) + +## 消息推送 + +建议使用环境变量配置: + +![claw-notification.png](/docs/imgs/claw-notification.png) + +配置值见:[confifuration](/docs/configuration.md) + +## 费用 + +官方模板默认配置为 `0.25C 512M`,每天 $0.06,一个月 `30 * 0.06 = $1.8`,每月赠送是 $5,还很富裕。 + +## 其他 + +### 账号 + +- 默认用户名:admin +- 默认密码:BiliTool@2233 + +首次登陆后,请立即修改账号和密码! + +### 速度 + +不同区域速度可能有差异,可自己切换尝试。 + +如果速度慢可能会导致页面短时无响应,可稍作等待,或手动刷新。 + +### 更新 + +右上角`Update`进行版本更新,如果更新后启动异常,请尝试`Pause`然后再`Restart`。 diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..ad30ea0 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,814 @@ +# 配置说明 + +**[目录]** + + + +- [1. 配置方式](#1-配置方式) + - [1.1. 方式一:修改配置文件](#11-方式一修改配置文件) + - [1.2. 方式二:命令启动时通过命令行参数配置](#12-方式二命令启动时通过命令行参数配置) + - [1.3. 方式三:添加环境变量(推荐)](#13-方式三添加环境变量推荐) + - [1.4. 方式四:托管在青龙面板上,使用面板的环境变量页或配置文件页进行配置](#14-方式四托管在青龙面板上使用面板的环境变量页或配置文件页进行配置) +- [2. 优先级](#2-优先级) +- [3. 详细配置说明](#3-详细配置说明) + - [3.1. Cookie字符串](#31-cookie字符串) + - [3.2. 安全相关的配置](#32-安全相关的配置) + - [3.2.1. 是否跳过执行任务](#321-是否跳过执行任务) + - [3.2.2. 随机睡眠的最大时长](#322-随机睡眠的最大时长) + - [3.2.3. 两次调用B站Api之间的间隔秒数](#323-两次调用b站api之间的间隔秒数) + - [3.2.4. 间隔秒数所针对的HttpMethod](#324-间隔秒数所针对的httpmethod) + - [3.2.5. 请求B站接口时头部传递的User-Agent](#325-请求b站接口时头部传递的user-agent) + - [3.2.6. App请求B站接口时头部传递的User-Agent](#326-app请求b站接口时头部传递的user-agent) + - [3.2.7. WebProxy(代理)](#327-webproxy代理) + - [3.3. 每日任务相关](#33-每日任务相关) + - [3.3.1. 是否开启观看视频任务](#331-是否开启观看视频任务) + - [3.3.2. 是否开启分享视频任务](#332-是否开启分享视频任务) + - [3.3.3. 每日投币数量](#333-每日投币数量) + - [3.3.4. 投币时是否同时点赞](#334-投币时是否同时点赞) + - [3.3.5. 优先选择支持的up主Id集合](#335-优先选择支持的up主id集合) + - [3.3.6. 每月几号自动领取会员权益](#336-每月几号自动领取会员权益) + - [3.3.7. 每月几号进行直播中心银瓜子兑换硬币](#337-每月几号进行直播中心银瓜子兑换硬币) + - [3.3.8. Lv6后开启硬币白嫖模式](#338-lv6后开启硬币白嫖模式) + - [3.3.9. 是否开启专栏投币](#339-是否开启专栏投币) + - [3.4. 天选时刻抽奖相关](#34-天选时刻抽奖相关) + - [3.4.1. 根据关键字排除奖品](#341-根据关键字排除奖品) + - [3.4.2. 根据关键字指定奖品](#342-根据关键字指定奖品) + - [3.4.3. 天选抽奖后是否自动分组关注的主播](#343-天选抽奖后是否自动分组关注的主播) + - [3.4.4. 天选筹抽奖主播Uid黑名单](#344-天选筹抽奖主播uid黑名单) + - [3.5. 批量取关相关](#35-批量取关相关) + - [3.5.1. 想要批量取关的分组名称](#351-想要批量取关的分组名称) + - [3.5.2. 批量取关的人数](#352-批量取关的人数) + - [3.5.3. 取关白名单](#353-取关白名单) + - [3.6. 大积分相关](#36-大积分相关) + - [3.6.1. 自定义观看番剧](#361-自定义观看番剧) + - [3.7. 免费B币券充电](#37-免费b币券充电) + - [3.7.1. 充电对象](#371-充电对象) + - [3.8. 推送相关](#38-推送相关) + - [3.8.1. 是否开启每个账号单独推送消息](#381-是否开启每个账号单独推送消息) + - [3.8.2. Telegram机器人](#382-telegram机器人) + - [3.8.2.1. botToken](#3821-bottoken) + - [3.8.2.2. chatId](#3822-chatid) + - [3.8.2.3. proxy](#3823-proxy) + - [3.8.3. 企业微信机器人](#383-企业微信机器人) + - [3.8.3.1. webHookUrl](#3831-webhookurl) + - [3.8.4. 钉钉机器人](#384-钉钉机器人) + - [3.8.4.1. webHookUrl](#3841-webhookurl) + - [3.8.5. Server酱](#385-server酱) + - [3.8.5.1. TurboScKey(Server酱SCKEY)](#3851-turbosckeyserver酱sckey) + - [3.8.6. 酷推](#386-酷推) + - [3.8.6.1. sKey](#3861-skey) + - [3.8.7. 推送到自定义Api](#387-推送到自定义api) + - [3.8.7.1. api](#3871-api) + - [3.8.7.2. placeholder](#3872-placeholder) + - [3.8.7.3. bodyJsonTemplate](#3873-bodyjsontemplate) + - [3.8.8. PushPlus[推荐]](#388-pushplus推荐) + - [3.8.8.1. PushPlus的Token](#3881-pushplus的token) + - [3.8.8.2. PushPlus的Topic](#3882-pushplus的topic) + - [3.8.8.3. PushPlus的Channel](#3883-pushplus的channel) + - [3.8.8.4. PushPlus的Webhook](#3884-pushplus的webhook) + - [3.8.9. Microsoft Teams](#389-microsoft-teams) + - [3.8.9.1. Microsoft Teams的Webhook](#3891-microsoft-teams的webhook) + - [3.8.10. 企业微信应用推送](#3810-企业微信应用推送) + - [3.8.10.1. 企业微信应用推送的corpId](#38101-企业微信应用推送的corpid) + - [3.8.10.2. 企业微信应用推送的agentId](#38102-企业微信应用推送的agentid) + - [3.8.10.3. 企业微信应用推送的secret](#38103-企业微信应用推送的secret) + - [3.9. 日志相关](#39-日志相关) + - [3.9.1. 日志输出等级](#391-日志输出等级) + - [3.9.2. 日志输出样式](#392-日志输出样式) + - [3.9.3. 定时任务相关](#393-定时任务相关) + - [3.9.4. 定时任务](#394-定时任务) + + + + +## 1. 配置方式 + + +### 1.1. 方式一:修改配置文件 + +推荐使用Release包在本地运行的朋友使用,直接打开文件,将对应的配置值填入,保存即可生效。 + +默认有3个配置文件:`appsettings.json`、`appsettings.Development.json`、`appsettings.Production.json`,分别对应默认、开发与生产环境。 + +对于不是开发人员的大部分人来说,只需要关注`appsettings.Production.json`即可。 + + +### 1.2. 方式二:命令启动时通过命令行参数配置 + +在使用命令行启动时,可使用`-key=value`的形式附加配置,所有可用的命令行参数均在 [命令行参数映射表](../src/Ray.BiliBiliTool.Config/Constants.cs#L76-L105) 中。 + +* 使用跨平台的依赖包 + +各个系统只要安装了net5环境,均可使用dotnet命令启动,命令样例: + +``` +dotnet Ray.BiliBiliTool.Console.dll -cookieStr=abc -numberOfCoins=5 +``` + +* Windows系统 + +使用自包含包(win-x86-x64.zip),命令样例: + +``` +Ray.BiliBiliTool.Console.exe -cookieStr=abc -numberOfCoins=5 +``` + +* Linux系统 + +使用自包含包(linux.zip),命令样例: + +``` +Ray.BiliBiliTool.Console -cookieStr=abc -numberOfCoins=5 +``` + +如映射文件所展示,支持使用命令行配置的配置项并不多,也不建议大量地使用该种方式进行配置。使用包运行的朋友,除了改配置文件和命令行参数配置外,还可以使用环境变量进行配置,这也是推荐的做法,如下。 + + +### 1.3. 方式三:添加环境变量(推荐) + +所有的配置项均可以通过添加环境变量来进行配置。如: + +Linux下运行Web: + +```bash +# 添加环境变量作为配置: +export RunTasks="Daily" +export BiliBiliCookies__1="abc" +export BiliBiliCookies__2="efg" +export DailyTaskConfig__NumberOfCoins="3" + +# 开始运行程序: +dotnet BiliBiliTool.Web.dll +``` + +Linux下运行Console: + +```bash +# 添加环境变量作为配置: +export Ray_RunTasks="Daily" +export Ray_BiliBiliCookies__1="abc" +export Ray_BiliBiliCookies__2="efg" +export Ray_DailyTaskConfig__NumberOfCoins="3" + +# 开始运行程序: +dotnet Ray.BiliBiliTool.Console.dll +``` + +注意Console需要添加`Ray_`前缀,win系统使用`set`关键字代替`export`。 + + +### 1.4. 方式四:托管在青龙面板上,使用面板的环境变量页或配置文件页进行配置 + +青龙面板配置,其本质还是通过环境变量进行配置,有如下两种方式。 + +- 环境变量页[推荐] + +例如: + +名称:`Ray_BiliBiliCookies__1` + +值:`abcde` + +qinglong-env + +- 配置文件页 + +例如,配置Cookie和推送: + +``` +export Ray_BiliBiliCookies__1="_uuid=abc..." +export Ray_Serilog__WriteTo__9__Args__token="abcde" +``` + +qinglong-config + +配置文件页添加、修改配置,需要重启青龙容器使之生效,环境变量页则可以立即生效,所以推荐使用环境变量页配置。 + + +## 2. 优先级 + +以上 4 种配置源,其优先级由低到高依次是:json文件 < 环境变量 < 命令行。 + +高优先级的配置会覆盖低优先级的配置。 + + +## 3. 详细配置说明 + +Console项目(青龙)的环境变量需要添加`Ray_`前缀,其他不用。 +比如,原始配置Key为`BiliBiliCookies__1`,Console则为`Ray_BiliBiliCookies__1`。 + + +### 3.1. Cookie字符串 + +必填,数组,可以多个。 + +| TITLE | CONTENT | 示例 | +| ----- | ------------------- | -------------------------------------------- | +| 配置Key | `BiliBiliCookies__1` | | +| 值域 | 字符串,英文分号分隔,来自浏览器抓取 | `export BiliBiliCookies__1=abc=123;def=456;` | +| 默认值 | 空 | | + +| TITLE | CONTENT | 示例 | +| ----- | ------------------- | -------------------------------------------- | +| 配置Key | `BiliBiliCookies__2` | | +| 值域 | 字符串,英文分号分隔,来自浏览器抓取 | `export BiliBiliCookies__1=abc=123;def=456;` | +| 默认值 | 空 | | + + +### 3.2. 安全相关的配置 + +#### 3.2.1. 是否跳过执行任务 + +用于特殊情况下,通过配置灵活的开启和关闭整个应用。 + +配置为关闭后,程序会跳过所有任务,不会调用B站任何接口。 + +| TITLE | CONTENT | 示例 | +| ----- | ------------------------------- | ------------------------------------ | +| 配置Key | `Security__IsSkipDailyTask` | | +| 值域 | [true,false] | `export Security__IsSkipDailyTask=true` | +| 默认值 | false | | + + +#### 3.2.2. 随机睡眠的最大时长 + +用于设置程序启动后,随机睡眠时间的最大上限值,单位为分钟。 + +这样可以避免程序每天准点地在同一时间运行太像机器。 + +配置为0则不进行睡眠。 + +| TITLE | CONTENT | +| ----- | --------------------------------- | +| 配置Key | `Security__RandomSleepMaxMin` | +| 值域 | 数字 | +| 默认值 | 20 | + + +#### 3.2.3. 两次调用B站Api之间的间隔秒数 + +用于设置两次Api请求之间的最短时间间隔,避免程序在1到2秒内连续调用B站的Api过快。 + +| TITLE | CONTENT | +| ----- | ------------------------------------------------ | +| 配置Key | `Security__IntervalSecondsBetweenRequestApi` | +| 值域 | [0,+] | +| 默认值 | 20 | + + +#### 3.2.4. 间隔秒数所针对的HttpMethod + +间隔秒数所针对的HttpMethod类型,服务于上一个配置。服务器一般对GET请求不是很敏感,建议只针对POST请求做间隔就可以了。 + +| TITLE | CONTENT | +| ----- | ----------------------------------- | +| 配置Key | `Security__IntervalMethodTypes` | +| 值域 | [GET,POST],多个以英文逗号分隔 | +| 默认值 | POST | + + +#### 3.2.5. 请求B站接口时头部传递的User-Agent + +| TITLE | CONTENT | +| ----- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| 配置Key | `Security__UserAgent` | +| 值域 | 字符串,可以F12从自己的浏览器获取 | +| 默认值 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36 Edg/87.0.664.41 | + +获取浏览器中自己的UA的方法见下图: + +get-user-agent + + +#### 3.2.6. App请求B站接口时头部传递的User-Agent + +| TITLE | CONTENT | +| ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 配置Key | `Security__UserAgentApp` | +| 值域 | 字符串,可以F12从自己的浏览器获取 | +| 默认值 | Mozilla/5.0 (Linux; Android 12; SM-S9080 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Mobile Safari/537.36 os/android model/SM-S9080 build/7760700 osVer/12 sdkInt/32 network/2 BiliApp/7760700 mobi_app/android channel/bili innerVer/7760710 c_locale/zh_CN s_locale/zh_CN disable_rcmd/0 7.76.0 os/android model/SM-S9080 mobi_app/android build/7760700 channel/bili innerVer/7760710 osVer/12 network/2 | + +获取浏览器中自己的UA的方法见下图: + +get-user-agent + + +#### 3.2.7. WebProxy(代理) + +支持需要账户密码的代理。 + +| TITLE | CONTENT | +| -------------- | ------------------------------ | +| 配置Key | `Security__WebProxy` | +| 值域 | 字符串,形如:user:password@host:port | +| 默认值 | 无 | + + +### 3.3. 每日任务相关 + + +#### 3.3.1. 是否开启观看视频任务 + +当该配置被设置为`false`时会导致大积分任务中的签到领额外10点经验的任务不能自动完成。 + +| TITLE | CONTENT | +| ----- | ------------------------------- | +| 配置Key | `DailyTaskConfig__IsWatchVideo` | +| 值域 | [true,false] | +| 默认值 | true | + + +#### 3.3.2. 是否开启分享视频任务 + +| TITLE | CONTENT | +| ----- | ------------------------------- | +| 配置Key | `DailyTaskConfig__IsShareVideo` | +| 值域 | [true,false] | +| 默认值 | true | + + +#### 3.3.3. 每日投币数量 + +每天投币的总目标数量,因为投币获取经验只与次数有关,所以程序每次投币只会投1个,也就是说该配置也表示每日投币次数。 + +| TITLE | CONTENT | +| ----- | -------------------------------- | +| 配置Key | `DailyTaskConfig__NumberOfCoins` | +| 值域 | [0,5],为安全考虑,程序内部还会做验证,最大不能超过5 | +| 默认值 | 5 | + + +#### 3.3.4. 投币时是否同时点赞 + +| TITLE | CONTENT | +| ----- | ----------------------------- | +| 配置Key | `DailyTaskConfig__SelectLike` | +| 值域 | [true,false] | +| 默认值 | false | + + +#### 3.3.5. 优先选择支持的up主Id集合 + +通过填入自己选择的up主ID,以后观看、分享和投币,都会优先从配置的up主下面挑选视频,如果没有找到,则会去你的**特别关注**列表中随机再获取,再然后会去**普通关注**列表中随机获取,最后会去排行榜中随机获取。 + +**注意:该配置的默认值是作者的upId,如需换掉的话,直接更改即可。** + +| TITLE | CONTENT | +| ----- | --------------------------------------------------------------- | +| 配置Key | `DailyTaskConfig__SupportUpIds` | +| 值域 | up主ID,多个用英文逗号分隔,默认是作者本人的UpId,如需删除可以配置为空格字符串或"-1",也可以配置为其他人的UpId | +| 默认值 | 作者的upId | + +获取UP主的Id方法:打开bilibili,进入欲要选择的UP主主页,在url中和简介中,都可获得该UP主的Id,如下图所示: + +get-up-id + + +#### 3.3.6. 每月几号自动领取会员权益 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `DailyTaskConfig__DayOfReceiveVipPrivilege` | +| 值域 | [-1,31],-1表示不指定,默认每月1号;0表示不领取 | +| 默认值 | 1 | + + +#### 3.3.7. 每月几号进行直播中心银瓜子兑换硬币 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `DailyTaskConfig__DayOfExchangeSilver2Coin` | +| 值域 | [-1,31],-1表示不指定,默认每月最后一天;-2表示每天;0表示不进行兑换 | +| 默认值 | -1 | + + +#### 3.3.8. Lv6后开启硬币白嫖模式 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `DailyTaskConfig__SaveCoinsWhenLv6` | +| 值域 | [true,false],true表示开启,Lv6的账号不会投币 | +| 默认值 | false | + + +#### 3.3.9. 是否开启专栏投币 + +| TITLE | CONTENT | | +| ----- | ----------------------------------------- | --- | +| 配置Key | `DailyTaskConfig__IsDonateCoinForArticle` | | +| 值域 | [true,false] | | +| 默认值 | false | | + + +### 3.4. 天选时刻抽奖相关 + + +#### 3.4.1. 根据关键字排除奖品 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `LiveLotteryTaskConfig__ExcludeAwardNames` | +| 值域 | 一串字符串,多个关键字使用`\|`符号隔开 | +| 默认值 | `舰\|船\|航海\|代金券\|自拍\|照\|写真\|图` | + + +#### 3.4.2. 根据关键字指定奖品 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `LiveLotteryTaskConfig__IncludeAwardNames` | +| 值域 | 一串字符串,多个关键字使用`\|`符号隔开 | +| 默认值 | 空 | + + +#### 3.4.3. 天选抽奖后是否自动分组关注的主播 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `LiveLotteryTaskConfig__AutoGroupFollowings` | +| 值域 | [true,false] | +| 默认值 | true | + + +#### 3.4.4. 天选筹抽奖主播Uid黑名单 + +不想参与抽奖的主播Upid集合,多个用英文逗号分隔,配置后不会参加黑名单中的主播的抽奖活动。默认值是目前已知的中奖后拒绝发奖的Up,后期还会继续补充,也反映反馈。 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `LiveLotteryTaskConfig__DenyUids` | +| 值域 | 字符串,如"65566781,1277481241" | +| 默认值 | "65566781,1277481241,1643654862,603676925" | + + +### 3.5. 批量取关相关 + + +#### 3.5.1. 想要批量取关的分组名称 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `UnfollowBatchedTaskConfig__GroupName` | +| 值域 | 字符串 | +| 默认值 | 天选时刻 | + + +#### 3.5.2. 批量取关的人数 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `UnfollowBatchedTaskConfig__Count` | +| 值域 | 数字,[-1,+],-1表示全部 | +| 默认值 | 5 | + + +#### 3.5.3. 取关白名单 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `UnfollowBatchedTaskConfig__RetainUids` | +| 值域 | 字符串,多个使用英文逗号分隔 | +| 默认值 | 108569350 | + + +### 3.6. 大积分相关 + + +#### 3.6.1. 自定义观看番剧 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `VipBigPointConfig__ViewBangumis` | +| 值域 | 番剧的ssid(season_id) | +| 默认值 | `33378`(名侦探柯南) | + + +### 3.7. 免费B币券充电 + + +#### 3.7.1. 充电对象 + +充电对象的upId,-1表示不指定,~~默认为自己充电~~;其他Id则会尝试为配置的UpId充电。 + +注意:之前可以不配置,默认为自己充电,但后来阿B改了规则,不再允许自己冲自己。。。 + +建议配置为自己小号(小号需要认证为作者并开启充电),或者也可以配置为 -1,以支持作者~ + +| TITLE | CONTENT | +| ----- | --------------------------------- | +| 配置Key | `ChargeTaskConfig__AutoChargeUpId` | +| 值域 | up的Id字符串 | +| 默认值 | 无 | + + +### 3.8. 推送相关 + +v1.0.x仅支持推送到Server酱,v1.1.x之后重新定义了推送地概念,将推送仅看作不同地日志输出端,与Console、File没有本质区别。 + +配置多个,多个端均会收到日志消息。推荐Telegram、企业微信、Server酱。 + + +#### 3.8.1. 是否开启每个账号单独推送消息 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Notification__IsSingleAccountSingleNotify` | +| 意义 | 开启后,每个账号会单独推送消息。否则多账号合并只推送一条消息 | +| 值域 | [true,false] | +| 默认值 | true | + + +#### 3.8.2. Telegram机器人 + +push-tg + + +##### 3.8.2.1. botToken + +点击 https://core.telegram.org/api#bot-api 查看如何创建机器人并获取到机器人的botToken。 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__3__Args__botToken` | +| 意义 | 用于将日志输出到Telegram机器人 | +| 值域 | 一串字符串 | +| 默认值 | 空 | + + +##### 3.8.2.2. chatId +点击 https://api.telegram.org/bot{TOKEN}/getUpdates 获取到与机器人的chatId(需要用上面获取到的Token替换进链接里的{TOKEN}后访问) + +P.S.访问链接需要能访问"外网",有vpn的挂vpn。 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__3__Args__chatId` | +| 值域 | 一串字符串 | +| 默认值 | 空 | +| 命令行示范 | 无 | + + +##### 3.8.2.3. proxy + +使用代理 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__3__Args__proxy` | +| 值域 | 一串字符串,格式为user:password@host:port | +| 默认值 | 空 | +| 命令行示范 | 无 | + + +#### 3.8.3. 企业微信机器人 + +在群内添加机器人,获取到机器人的WebHook地址,添加到配置中。 + +push-workweixin + + +##### 3.8.3.1. webHookUrl + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__4__Args__webHookUrl` | +| 值域 | 一串字符串 | +| 默认值 | 空 | +| 命令行示范 | 无 | + + +#### 3.8.4. 钉钉机器人 + +在群内添加机器人,获取到机器人的WebHook地址,添加到配置中。 + +机器人的安全策略,当前不支持加签,请使用关键字策略,推荐关键字:`Ray` 或 `BiliBili` + +push-ding + + +##### 3.8.4.1. webHookUrl + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__5__Args__webHookUrl` | +| 值域 | 一串字符串 | +| 默认值 | 空 | + + +#### 3.8.5. Server酱 +官网: http://sc.ftqq.com/9.version + +wechat-push + + +##### 3.8.5.1. TurboScKey(Server酱SCKEY) +获取方式请参考官网。 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__6__Args__turboScKey` | +| 值域 | 一串字符串 | +| 默认值 | 空 | + + +#### 3.8.6. 酷推 +https://cp.xuthus.cc/ + +##### 3.8.6.1. sKey +该平台可能还在完善当中,对接时我发现其接口定义不规范,且机器人容易被封,所以不推荐使用,且不接受提酷推推送相关bug。 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__7__Args__sKey` | +| 值域 | 一串字符串 | +| 默认值 | 空 | + + +#### 3.8.7. 推送到自定义Api +这是我简单封装了一个通用的推送接口,可以推送到任意的api地址,如果有自己的机器人或自己的用于接受日志的api,可以根据需要自定义配置。 + +##### 3.8.7.1. api + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__8__Args__api` | +| 值域 | 一串字符串 | +| 默认值 | 空 | + + +##### 3.8.7.2. placeholder + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__8__Args__placeholder` | +| 值域 | 一串字符串 | +| 默认值 | 空 | + + +##### 3.8.7.3. bodyJsonTemplate + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__8__Args__bodyJsonTemplate` | +| 值域 | 一串字符串 | +| 默认值 | 空 | + + +#### 3.8.8. PushPlus[推荐] + +官网: http://www.pushplus.plus/doc/ + + +##### 3.8.8.1. PushPlus的Token + +获取方式请参考官网。 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__9__Args__token` | +| 值域 | 一串字符串 | +| 默认值 | 空 | + + +##### 3.8.8.2. PushPlus的Topic + +获取方式请参考官网。 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__9__Args__topic` | +| 值域 | 一串字符串 | +| 默认值 | 空 | + + +##### 3.8.8.3. PushPlus的Channel + +获取方式请参考官网。 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__9__Args__channel` | +| 值域 | 一串字符串,[wechat,webhook,cp,sms,mail] | +| 默认值 | 空 | + + +##### 3.8.8.4. PushPlus的Webhook + +获取方式请参考官网。 + +webhook编码(不是地址),在官网平台设定,仅在channel使用webhook渠道和CP渠道时需要填写 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__9__Args__webhook` | +| 值域 | 一串字符串 | +| 默认值 | 空 | +| 命令行示范 | | + + +#### 3.8.9. Microsoft Teams + +官网: https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook + + +##### 3.8.9.1. Microsoft Teams的Webhook + +webhook的完整地址,在Teams的Channel中获取,详细获取方式请参考官网。 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__10__Args__webhook` | +| 值域 | 一串字符串 | +| 默认值 | 空 | +| 命令行示范 | | + + +#### 3.8.10. 企业微信应用推送 + +官网: https://developer.work.weixin.qq.com/tutorial/application-message + +当`corpId`、`agentId`、`secret`均不为空时,自动开启推送,否则关闭。 + +`toUser`、`toParty`、`toTag`3个配置非必填,但不可同时为空,默认`toUser`为`@all`,向所有用户推送。 + + +##### 3.8.10.1. 企业微信应用推送的corpId + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__11__Args__corpId` | +| 值域 | 一串字符串 | +| 默认值 | 空 | +| 命令行示范 | | + + +##### 3.8.10.2. 企业微信应用推送的agentId + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__11__Args__agentId` | +| 值域 | 一串字符串 | +| 默认值 | 空 | +| 命令行示范 | | + + +##### 3.8.10.3. 企业微信应用推送的secret + +| TITLE | CONTENT | +| ----- | -------------------------------- | +| 配置Key | `Serilog__WriteTo__11__Args__secret` | +| 值域 | 一串字符串 | +| 默认值 | 空 | +| 命令行示范 | | + + +### 3.9. 日志相关 + + +#### 3.9.1. 日志输出等级 + +为了美观, BiliBiliTool 默认只输出最低等级为 Information 的日志,保证只展示最精简的信息。 + +通过更改等级,可以指定日志输出的详细程度。 + +BiliBiliTool 使用 Serilog 作为日志组件,所以其值域与 Serilog 的日志等级选项相同,这里只建议在需要调试时改为`Debug`,应用会输出详细的调试日志信息,包括每次调用B站Api的请求参数与返回数据。 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__0__Args__restrictedToMinimumLevel` | +| 值域 | [Information,Debug] | +| 默认值 | 1 | + + +#### 3.9.2. 日志输出样式 + +这里的日志样式指的是 Console 的等级,即 GitHub Actions 里和微信推送里看到的日志。 + +通过更改模板样式,可以指定日志输出的样式,比如不输出时间和等级,做到最精简的样式。 + +BiliBiliTool 使用 Serilog 作为日志组件,所以可以参考 Serilog 的日志样式模板。 + +| TITLE | CONTENT | +| ---------- | -------------- | +| 配置Key | `Serilog__WriteTo__0__Args__outputTemplate` | +| 值域 | 字符串 | +| 默认值 | `[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}` | + + +#### 3.9.3. 定时任务相关 + +适用于 [方式四:docker容器化运行(推荐)](../docker/README.md),用于配置定时任务。 + + +#### 3.9.4. 定时任务 + +以下环境变量的值应为有效的 [cron 表达式](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm)。 + +当被设置时,对应定时任务将开启。 + +| 环境变量 | 定时任务 | +| --------------------------------- | ------ | +| `DailyTaskConfig__Cron` | 每日任务 | +| `LiveLotteryTaskConfig__Cron` | 天选时刻抽奖 | +| `UnfollowBatchedTaskConfig__Cron` | 批量取关 | +| `VipBigPointConfig__Cron` | 大会员大积分 | diff --git a/docs/donate-list.md b/docs/donate-list.md new file mode 100644 index 0000000..97e58b2 --- /dev/null +++ b/docs/donate-list.md @@ -0,0 +1,87 @@ +# 赞赏 + +| 赞赏人 | 时间 | 金额 | 方式 | 留言 | 回复 +| ---------- | -------------- | -------------- | -------------- | -------------- | -------------- | +| Jonty | 2020-11-04 | ¥5 | 微信 | 不知可否加个联系方式,讨论一下b站小公举 | 你没有留微信号啊大兄弟 | +| 春歌 | 2020-11-06 | ¥5 | 微信 | 赞赏一下~ | | +| 雅南 | 2020-11-08 | ¥1 | 微信 | 赞赏一下~ | | +| RainMeter | 2020-11-09 | ¥1 | 微信 | 大佬牛逼 | | +| 1998 | 2020-11-10 | ¥1 | 微信 | 谢谢大佬,但是投币总是失败 | 有问题可以加群讨论 | +| Andy | 2020-11-10 | ¥1 | 微信 | 赞赏一下~ | | +| 不若艳阳 | 2020-11-11 | ¥10 | 微信 | 赞赏一下~ | | +| 努力努力再努力 | 2020-11-11 | ¥1 | 微信 | 赞赏一下~ | | +| Wandering Ghost | 2020-11-12 | ¥1 | 微信 | 好活,当赏 | | +| sadhu | 2020-11-12 | ¥1 | 支付宝 | 加油 | | +| 浮蘭·鳥ドス | 2020-11-12 | ¥1 | 微信 | 加油!在一个微信公众号看到这个 | | +| Ⅶ | 2020-11-12 | ¥1 | 微信 | 太棒啦!支持您! | | +| 舞飞扬 | 2020-11-12 | ¥1 | 支付宝 | 多谢bilibilitool | | +| 王雨桐 | 2020-11-13 | ¥1 | 微信 | 感谢ps借用作者项目完成我开源选修作业——分析一款开源软件 | | +| 郁宁 | 2020-11-13 | ¥5 | 微信 | Godd Job | | +| 半岛 | 2020-11-13 | ¥1 | 支付宝 | 赞赏一下~ | | +| Wenson | 2020-11-19 | ¥10 | 微信 | 搞的不错👍 | | +| xingxing | 2020-11-20 | ¥1 | 支付宝 | 这东西太好用了,点个 | | +| | 2020-11-21 | ¥10 | 微信 | 单纯问下,有做成云函数的可行性吗 | 目前没了解过云函数相关,有懂的朋友欢迎PR~ | +| Gaogao | 2020-11-21 | ¥1 | 支付宝 | 老哥加油加油^0^ | | +| 老狗 | 2020-11-23 | ¥10 | 微信 | 好活当赏吗 | | +| | 2020-11-23 | ¥10 | 微信 | | | +| 还输给回忆不成 | 2020-11-23 | ¥1 | 微信 | | | +| Winfor | 2020-11-23 | ¥5 | 微信 | 感谢分享 | | +| 那个冰 | 2020-11-23 | ¥1 | 微信 | 如果一开始努力的方向就是错误的,那么只会越来越忙。感谢 | | +| Luv(sic) part 2 | 2020-11-24 | ¥1 | 微信 | 牛逼嗷,上班划水新技能get | | +| 刘小明 | 2020-11-24 | ¥3 | 微信 | 大佬喝冰阔落 | | +| 青翘 | 2020-11-25 | ¥1 | 支付宝 | 赞赏一下~ | | +| CT | 2020-11-25 | ¥1 | 微信 | 感谢 | | +| Panda | 2020-11-25 | ¥1 | 微信 | 很好用的工具 | | +| Che | 2020-11-25 | ¥1 | 微信 | 微信推送的配置方法能不能说的再详细一点,没用过有点懵 | | +| 张浩 | 2020-11-25 | ¥1 | 微信 | 集资给你买霸王 | 真棒 | +| 骷髅刀皇 | 2020-11-26 | ¥1 | 支付宝 | 外行人第一个再GitHub跑成功的源码 | | +| | 2020-11-26 | ¥1 | 微信 | 项目有意思 | | +| ohh | 2020-11-27 | ¥1 | 微信 | 集资买霸王 duang😏 | 😏 | +| 夏风 | 2020-11-28 | ¥1 | 支付宝 | 赞赏一下~ | | +| 征服神的眼睛 | 2020-11-27 | ¥1 | 微信 | 头发+1 | | +| 长空X | 2020-11-30 | ¥1 | 支付宝 | 加油!我是hjkl950217 | 贡献的代码很棒,欢迎加入 | +| 旧城空梦 | 2020-11-30 | ¥5 | 微信 | 拉我进下微信群,谢谢,我微信*** | 已拉 | +| Mr.华 | 2020-12-01 | ¥1 | 微信 | 白嫖党 今天给你投币来了 | | +| 暮雨 | 2020-12-01 | ¥1 | 微信 | 不多说,好用412还没有解决 | 大于等于1.0.14版本解决啦 | +| 闪电 | 2020-12-01 | ¥1 | 微信 | 欧拉拉 | | +| 山水之间 | 2020-12-01 | ¥1 | 微信 | 加油💪 | 💪 | +| 八八九九 | 2020-12-02 | ¥1 | 微信 | cool | | +| 。 | 2020-12-02 | ¥1 | 微信 | 支持 | | +| Carnina | 2020-12-06 | ¥5 | 微信 | 谢谢大大的bili工具 | | +| 大疼 | 2020-12-07 | ¥1 | 支付宝 | 教程很细致,谢谢 | | +| | 2020-12-07 | ¥1 | 微信 | 谢谢大佬B站的项目 | | +| Nirvana | 2020-12-08 | ¥1 | 微信 | 求进群我的微信*** | 已拉 | +| | 2020-12-17 | ¥1 | 微信 | 微信昵称时空白的 | 是的 | +| aiyΑ | 2020-12-23 | ¥1 | 微信 | | | +| 就这样被作业征服 | 2021-01-02 | ¥1 | 微信 | 太棒了,十分感谢 | | +| けっこ | 2021-01-03 | ¥1 | 微信 | bilibilitools加油! | 加油~ | +| Ruo | 2021-01-05 | ¥1 | 微信 | 加油加油 | 加油~ | +| | 2021-01-06 | ¥1 | 微信 | | | +| 199863nothing | 2021-01-06 | ¥1 | 支付宝 | 等俺五级给你买霸王洗发水 | 棒 | +| 小伊 | 2021-01-06 | ¥1 | 支付宝 | 赞赏一下~ | | +| 多喝热水吧你 | 2021-01-07 | ¥10 | 微信 | 感谢作者,支持一下,太辛苦了 | 感谢 | +| YNight-FZQ | 2021-01-08 | ¥5 | 微信 | 2021,要加油哦!感谢作者分享 | 一起加油~ | +| 外比巴卜 | 2021-01-09 | ¥3 | 微信 | 加油^o^~,做的很棒 | 支持开源的你们更棒~ | +| 199863nothing | 2021-01-09 | ¥1 | 微信 | 求拉进群(ง •_•)ง | 你没有留微信号啊大兄弟 | +| 黑影 | 2021-01-10 | ¥5 | 微信 | Mreblack7感谢7楼大大可以加微信群嘛qwq | 你没有留微信号啊大兄弟 | +| 199863nothing | 2021-01-11 | ¥1 | 微信 | 微信号:\*\*\*,大佬捞一捞我 | 已拉~ | +| . | 2021-01-12 | ¥5 | 微信 | 进群进群,冲冲冲 | 你没有留微信号啊大兄弟 | + + +个人维护开源不易 + +如果你觉得我写的东西对你确实有帮助 + +或者,你就是单纯的想集资给我买瓶霸王增发液 + +那么下面的赞赏码可以扫一扫啦 + +(赞赏时记得留下【昵称】和【留言】,上面这么多留言要想要进群或者加好友的,一定一定要记得留微信号哈,微信赞赏页面是看不到微信号的~) + +* 微信扫码自动赞赏1元 + +![微信赞赏码](https://www.cnblogs.com/images/cnblogs_com/RayWang/1490646/o_%e5%be%ae%e4%bf%a1%e8%b5%9e%e8%b5%8f%e7%a0%81.jpg) + +* 支付宝扫码自动赞赏1元 + +![支付宝赞赏吗](https://img2018.cnblogs.com/blog/1327955/201907/1327955-20190722174147547-1575068076.jpg) diff --git a/docs/imgs/2233.png b/docs/imgs/2233.png new file mode 100644 index 0000000..59d6606 Binary files /dev/null and b/docs/imgs/2233.png differ diff --git a/docs/imgs/Tencent-log-bill-1.png b/docs/imgs/Tencent-log-bill-1.png new file mode 100644 index 0000000..c7571aa Binary files /dev/null and b/docs/imgs/Tencent-log-bill-1.png differ diff --git a/docs/imgs/Tencent-log-docs-1.png b/docs/imgs/Tencent-log-docs-1.png new file mode 100644 index 0000000..d7d26e5 Binary files /dev/null and b/docs/imgs/Tencent-log-docs-1.png differ diff --git a/docs/imgs/Tencent-logpage-1.png b/docs/imgs/Tencent-logpage-1.png new file mode 100644 index 0000000..f305689 Binary files /dev/null and b/docs/imgs/Tencent-logpage-1.png differ diff --git a/docs/imgs/appsettings-cookie.png b/docs/imgs/appsettings-cookie.png new file mode 100644 index 0000000..296b1f2 Binary files /dev/null and b/docs/imgs/appsettings-cookie.png differ diff --git a/docs/imgs/claw-addr.png b/docs/imgs/claw-addr.png new file mode 100644 index 0000000..1d87461 Binary files /dev/null and b/docs/imgs/claw-addr.png differ diff --git a/docs/imgs/claw-app-store.png b/docs/imgs/claw-app-store.png new file mode 100644 index 0000000..afe6b83 Binary files /dev/null and b/docs/imgs/claw-app-store.png differ diff --git a/docs/imgs/claw-deploy.png b/docs/imgs/claw-deploy.png new file mode 100644 index 0000000..4299815 Binary files /dev/null and b/docs/imgs/claw-deploy.png differ diff --git a/docs/imgs/claw-notification.png b/docs/imgs/claw-notification.png new file mode 100644 index 0000000..4357ef1 Binary files /dev/null and b/docs/imgs/claw-notification.png differ diff --git a/docs/imgs/claw-search.png b/docs/imgs/claw-search.png new file mode 100644 index 0000000..1620c97 Binary files /dev/null and b/docs/imgs/claw-search.png differ diff --git a/docs/imgs/docker-login.png b/docs/imgs/docker-login.png new file mode 100644 index 0000000..07d4d1f Binary files /dev/null and b/docs/imgs/docker-login.png differ diff --git a/docs/imgs/donate.jpg b/docs/imgs/donate.jpg new file mode 100644 index 0000000..daf07e9 Binary files /dev/null and b/docs/imgs/donate.jpg differ diff --git a/docs/imgs/dotnet-login.png b/docs/imgs/dotnet-login.png new file mode 100644 index 0000000..125631c Binary files /dev/null and b/docs/imgs/dotnet-login.png differ diff --git a/docs/imgs/get-bilibili-web-cookie.jpg b/docs/imgs/get-bilibili-web-cookie.jpg new file mode 100644 index 0000000..c3a2b1d Binary files /dev/null and b/docs/imgs/get-bilibili-web-cookie.jpg differ diff --git a/docs/imgs/get-up-id.png b/docs/imgs/get-up-id.png new file mode 100644 index 0000000..8703c96 Binary files /dev/null and b/docs/imgs/get-up-id.png differ diff --git a/docs/imgs/get-user-agent.png b/docs/imgs/get-user-agent.png new file mode 100644 index 0000000..383a16a Binary files /dev/null and b/docs/imgs/get-user-agent.png differ diff --git a/docs/imgs/git-secrets-add-cookie.png b/docs/imgs/git-secrets-add-cookie.png new file mode 100644 index 0000000..085cdb6 Binary files /dev/null and b/docs/imgs/git-secrets-add-cookie.png differ diff --git a/docs/imgs/git-secrets.png b/docs/imgs/git-secrets.png new file mode 100644 index 0000000..8a50b8b Binary files /dev/null and b/docs/imgs/git-secrets.png differ diff --git a/docs/imgs/github-actions-close.png b/docs/imgs/github-actions-close.png new file mode 100644 index 0000000..fd73af1 Binary files /dev/null and b/docs/imgs/github-actions-close.png differ diff --git a/docs/imgs/github-actions-log-1.png b/docs/imgs/github-actions-log-1.png new file mode 100644 index 0000000..12e8e53 Binary files /dev/null and b/docs/imgs/github-actions-log-1.png differ diff --git a/docs/imgs/github-actions-log-2.png b/docs/imgs/github-actions-log-2.png new file mode 100644 index 0000000..06c831c Binary files /dev/null and b/docs/imgs/github-actions-log-2.png differ diff --git a/docs/imgs/github-env-count-down.png b/docs/imgs/github-env-count-down.png new file mode 100644 index 0000000..e2dbcaf Binary files /dev/null and b/docs/imgs/github-env-count-down.png differ diff --git a/docs/imgs/github-env-list.png b/docs/imgs/github-env-list.png new file mode 100644 index 0000000..9fd72fe Binary files /dev/null and b/docs/imgs/github-env-list.png differ diff --git a/docs/imgs/github-env-wait-timer.png b/docs/imgs/github-env-wait-timer.png new file mode 100644 index 0000000..7bb7192 Binary files /dev/null and b/docs/imgs/github-env-wait-timer.png differ diff --git a/docs/imgs/github-secrets-other-configs.png b/docs/imgs/github-secrets-other-configs.png new file mode 100644 index 0000000..9bb68a2 Binary files /dev/null and b/docs/imgs/github-secrets-other-configs.png differ diff --git a/docs/imgs/node-support.png b/docs/imgs/node-support.png new file mode 100644 index 0000000..d702aff Binary files /dev/null and b/docs/imgs/node-support.png differ diff --git a/docs/imgs/push-ding.png b/docs/imgs/push-ding.png new file mode 100644 index 0000000..262bd2d Binary files /dev/null and b/docs/imgs/push-ding.png differ diff --git a/docs/imgs/push-tg.png b/docs/imgs/push-tg.png new file mode 100644 index 0000000..14f39dc Binary files /dev/null and b/docs/imgs/push-tg.png differ diff --git a/docs/imgs/push-workweixin.png b/docs/imgs/push-workweixin.png new file mode 100644 index 0000000..9ebd018 Binary files /dev/null and b/docs/imgs/push-workweixin.png differ diff --git a/docs/imgs/qinglong-application-key.png b/docs/imgs/qinglong-application-key.png new file mode 100644 index 0000000..22fac37 Binary files /dev/null and b/docs/imgs/qinglong-application-key.png differ diff --git a/docs/imgs/qinglong-application.png b/docs/imgs/qinglong-application.png new file mode 100644 index 0000000..95a5642 Binary files /dev/null and b/docs/imgs/qinglong-application.png differ diff --git a/docs/imgs/qinglong-config.png b/docs/imgs/qinglong-config.png new file mode 100644 index 0000000..af61fe6 Binary files /dev/null and b/docs/imgs/qinglong-config.png differ diff --git a/docs/imgs/qinglong-env.png b/docs/imgs/qinglong-env.png new file mode 100644 index 0000000..2877287 Binary files /dev/null and b/docs/imgs/qinglong-env.png differ diff --git a/docs/imgs/qinglong-extra.png b/docs/imgs/qinglong-extra.png new file mode 100644 index 0000000..13a7d85 Binary files /dev/null and b/docs/imgs/qinglong-extra.png differ diff --git a/docs/imgs/qinglong-login.png b/docs/imgs/qinglong-login.png new file mode 100644 index 0000000..c20d253 Binary files /dev/null and b/docs/imgs/qinglong-login.png differ diff --git a/docs/imgs/qinglong-run-as-bilitool.png b/docs/imgs/qinglong-run-as-bilitool.png new file mode 100644 index 0000000..6b7508a Binary files /dev/null and b/docs/imgs/qinglong-run-as-bilitool.png differ diff --git a/docs/imgs/qinglong-tasks.png b/docs/imgs/qinglong-tasks.png new file mode 100644 index 0000000..cc0ef69 Binary files /dev/null and b/docs/imgs/qinglong-tasks.png differ diff --git a/docs/imgs/run-exe.png b/docs/imgs/run-exe.png new file mode 100644 index 0000000..c35c70b Binary files /dev/null and b/docs/imgs/run-exe.png differ diff --git a/docs/imgs/run-workflow.png b/docs/imgs/run-workflow.png new file mode 100644 index 0000000..068d74a Binary files /dev/null and b/docs/imgs/run-workflow.png differ diff --git a/docs/imgs/tencent-scf-actions.png b/docs/imgs/tencent-scf-actions.png new file mode 100644 index 0000000..7e39ccf Binary files /dev/null and b/docs/imgs/tencent-scf-actions.png differ diff --git a/docs/imgs/tencent-scf-create-async.png b/docs/imgs/tencent-scf-create-async.png new file mode 100644 index 0000000..3f762c9 Binary files /dev/null and b/docs/imgs/tencent-scf-create-async.png differ diff --git a/docs/imgs/tencent-scf-create-basic.png b/docs/imgs/tencent-scf-create-basic.png new file mode 100644 index 0000000..fabb64a Binary files /dev/null and b/docs/imgs/tencent-scf-create-basic.png differ diff --git a/docs/imgs/tencent-scf-create-env.png b/docs/imgs/tencent-scf-create-env.png new file mode 100644 index 0000000..c8e6dc3 Binary files /dev/null and b/docs/imgs/tencent-scf-create-env.png differ diff --git a/docs/imgs/tencent-scf-create.png b/docs/imgs/tencent-scf-create.png new file mode 100644 index 0000000..93c1bdd Binary files /dev/null and b/docs/imgs/tencent-scf-create.png differ diff --git a/docs/imgs/tencent-scf-secret.png b/docs/imgs/tencent-scf-secret.png new file mode 100644 index 0000000..4200adb Binary files /dev/null and b/docs/imgs/tencent-scf-secret.png differ diff --git a/docs/imgs/tencent-scf-secret_yml.png b/docs/imgs/tencent-scf-secret_yml.png new file mode 100644 index 0000000..5ce0cda Binary files /dev/null and b/docs/imgs/tencent-scf-secret_yml.png differ diff --git a/docs/imgs/tencent-scf-test-1.png b/docs/imgs/tencent-scf-test-1.png new file mode 100644 index 0000000..043fc1b Binary files /dev/null and b/docs/imgs/tencent-scf-test-1.png differ diff --git a/docs/imgs/tencent-scf-test-2.png b/docs/imgs/tencent-scf-test-2.png new file mode 100644 index 0000000..6adf88d Binary files /dev/null and b/docs/imgs/tencent-scf-test-2.png differ diff --git a/docs/imgs/tencent-scf-trigger-add.png b/docs/imgs/tencent-scf-trigger-add.png new file mode 100644 index 0000000..2d741a5 Binary files /dev/null and b/docs/imgs/tencent-scf-trigger-add.png differ diff --git a/docs/imgs/tencent-scf-trigger-create.png b/docs/imgs/tencent-scf-trigger-create.png new file mode 100644 index 0000000..1d5c05d Binary files /dev/null and b/docs/imgs/tencent-scf-trigger-create.png differ diff --git a/docs/imgs/web-configs.png b/docs/imgs/web-configs.png new file mode 100644 index 0000000..2678e29 Binary files /dev/null and b/docs/imgs/web-configs.png differ diff --git a/docs/imgs/web-schedules-log.png b/docs/imgs/web-schedules-log.png new file mode 100644 index 0000000..666a061 Binary files /dev/null and b/docs/imgs/web-schedules-log.png differ diff --git a/docs/imgs/web-schedules.png b/docs/imgs/web-schedules.png new file mode 100644 index 0000000..0d5cc73 Binary files /dev/null and b/docs/imgs/web-schedules.png differ diff --git a/docs/imgs/web-trigger-login.png b/docs/imgs/web-trigger-login.png new file mode 100644 index 0000000..3248ccf Binary files /dev/null and b/docs/imgs/web-trigger-login.png differ diff --git a/docs/imgs/wechat-push.png b/docs/imgs/wechat-push.png new file mode 100644 index 0000000..c8a7994 Binary files /dev/null and b/docs/imgs/wechat-push.png differ diff --git a/docs/questions.md b/docs/questions.md new file mode 100644 index 0000000..b1d609d --- /dev/null +++ b/docs/questions.md @@ -0,0 +1,220 @@ +# 常见问题 + +**[目录]** + + +- [1. 运行出现异常怎么办?](#1-运行出现异常怎么办) +- [2. 如何提交issue(如何提交Bug或建议)](#2-如何提交issue如何提交bug或建议) +- [3. Actions定时任务没有每天自动运行](#3-actions定时任务没有每天自动运行) +- [4. Actions修改定时任务的执行时间](#4-actions修改定时任务的执行时间) + - [4.1. 方法一:修改yaml文件中的cron表达式](#41-方法一修改yaml文件中的cron表达式) + - [4.2. 方法二:添加 GitHub Environments 并设置延时](#42-方法二添加-github-environments-并设置延时) +- [5. 我 Fork 之后怎么同步原作者的更新内容?](#5-我-fork-之后怎么同步原作者的更新内容) + - [5.1. 方法一:删掉自己的仓库再重新Fork](#51-方法一删掉自己的仓库再重新fork) + - [5.2. 方法二:使用提供的 Repo Sync 工作流脚本同步](#52-方法二使用提供的-repo-sync-工作流脚本同步) + - [5.3. 方法三:手动PR同步](#53-方法三手动pr同步) + - [5.4. 方法四:使用插件 Pull App 同步](#54-方法四使用插件-pull-app-同步) + - [5.4.1. Pull App 方式一: 源作者内容直接覆盖自己内容](#541-pull-app-方式一-源作者内容直接覆盖自己内容) + - [5.4.2. Pull App 方式二: 保留自己内容](#542-pull-app-方式二-保留自己内容) +- [6. 本地或服务器如何安装.net环境](#6-本地或服务器如何安装net环境) +- [7. 如何关停Actions运行](#7-如何关停actions运行) + - [7.1. 方法一:使用配置关停每日任务](#71-方法一使用配置关停每日任务) + - [7.2. 方法二:关停Actions](#72-方法二关停actions) + + + +## 1. 运行出现异常怎么办? +第一步:根据异常信息,请先仔细阅读文档(特别是 [常见问题文档](https://github.com/RayWangQvQ/BiliBiliTool.Docs/blob/main/questions.md) 和 [配置说明文档](https://github.com/RayWangQvQ/BiliBiliTool.Docs/blob/main/configuration.md) ),查找相关信息 + +第二步:如果文档没有找到,请到 [issues](https://github.com/RayWangQvQ/BiliBiliTool/issues) 下面查找相关问题,看是否有人其他人也遇到类似问题,并确认issue下是否已经有解决方案 + +第三步:如果仍没有解决,请将日志输出等级配置为Debug,该等级会输出详细的日志信息,修改后请再次运行,并查看详细的日志信息。如何配置请详见 [配置说明文档](https://github.com/RayWangQvQ/BiliBiliTool.Docs/blob/main/configuration.md) + +第四步:拿到详细日志后,如果自己无法根据日志信息确定问题,请将日志信息贴到讨论群里,群里会有大佬及时帮忙解答 + +第五步:如果根据详细日志信息可以确认是 Bug(缺陷),可以到 [issues](https://github.com/RayWangQvQ/BiliBiliTool/issues) 下新建一条 issue 。如何新建issue请见下面的常见问题中的**如何提交issue**,如果是不符合要求的issue,会被关闭,严重的会被删除。 + +## 2. 如何提交issue(如何提交Bug或建议) +issues 被 GitHub 译为**议题**,用来为开源项目反馈 Bug、提出建议、讨论设计与需求等。 + +首先先提前感谢所有提交议题的朋友们,你们的反馈和建议会让开源程序优化的越来越好。 + +但为了使 issues 下面的议题便于维护,便于其他人搜索查找历史议题,避免淹没在一堆无用或重复的 issues 里,请大家自觉遵守下面的提交规范: + +Ⅰ. 提交前请先确认自己的议题是否是新的议题(是否在文档中已有说明、是否已经有其他人提过类似的议题),重复议题会被标记为重复并关闭,严重的会被删除 + +Ⅱ. issue标题请填写完整,语义需清晰,以便在不点击进入详情时,仅根据标题就可以定位到该 issue 所反应的问题 + +Ⅲ. 如果是提交 bug ,请描述清楚问题,**标明版本号、环境,并贴上详细日志信息(Debug等级的日志信息)**。如果获取Debug等级的日志信息请参见配置说明文档,如果没有日志信息,或日志信息不是Debug等级的日志信息,或在没有日志的情况下描述也不清晰,导致无法复现或无法定位问题,该 issue 会被标记为不清晰的议题,且会被忽略或关闭,严重的会被删除。 + +## 3. Actions定时任务没有每天自动运行 +Fork的仓库,actions默认是关闭的,需要对仓库进行1次操作才会触发webhook。 + +可以通过在页面上点击创建wiki来触发,也可以通过任意一次提交推送代码来触发。 + +## 4. Actions修改定时任务的执行时间 +每日任务执行的时间,由`.github/workflows/bilibili-daily-task.yml` 中的cron表达式指定,默认为每日的0点整: + +```yml + schedule: + - cron: '0 16 * * *' + # cron表达式,时区是UTC时间,比我们早8小时,如上所表示的是每天0点0分(24点整) +``` + +若要修改为自己指定的时间执行,有如下两种方式: + +### 4.1. 方法一:修改yaml文件中的cron表达式 +我们可以直接修改上述该文件中的cron表达式,然后提交。 + +个人不建议这么做,因为以后更新要注意冲突与覆盖问题,建议使用下面的方法二。 + +### 4.2. 方法二:添加 GitHub Environments 并设置延时 +v1.1.3及之后版本,支持通过添加GitHub Environments来设置延时运行,即在每日0点整触发 Actions 后,会再多执行一个延时操作,延时时长可由我们自己设置。 + +比如想设置为每天23点执行,只需要将这个延时时常设置为1380分钟(23个小时)即可。方法如下: + +* Ⅰ.找到 Production Environments + +运行完 bilibili-daily-task.yml 之后,在 `Settings` ——> `Environments` 中会自动多出一个名为 `Production` 的环境,如下图所示: + +![环境列表](imgs/github-env-list.png) + +如果没有,也可以自己手动点击添加。 + +* Ⅱ.设置延时时长 + +勾选 Wait timer,并填写延时时长,单位为分钟,如下图所示: + +![环境列表](imgs/github-env-wait-timer.png) + +下面给出一些常用的分钟数换算供参考: + + | 时间 | 从0点开始计算的分钟数 | + | -------------- | --------------------- | + | 6点整 | 360 | + | 8点整 | 480 | + | 9点整 | 540 | + | 12点整 | 720 | + | 14点整 | 840 | + | 18点整 | 1080 | + | 22点整 | 1320 | + | 23点整 | 1380 | + +注意,Actions 目前本身是有20分钟左右的延时的,是 GitHub 暂未解决的缺陷,属于正常现象。 + +设置成功后,再次运行 Actions 会发现触发后会自动进入倒计时状态,等倒计时结束后才会真正运行之后的内容,如下图所示: + +![环境列表](imgs/github-env-count-down.png) + +## 5. 我 Fork 之后怎么同步原作者的更新内容? +Fork 被 GitHub 译为复刻,相当于拷贝了一份源作者的代码到自己的 Repository (仓库)里,Fork 后,源作者更新自己的代码内容(比如发新的版本),一般情况下 Fork 的项目并不会自动更新源作者的修改。 + +BiliBiliTool内置了自动同步的 actions(即下面的方法二),默认情况下,Fork的仓库会在每周一会自动拉取更新源仓库内容,如想要更新请参考方法二。 + +这里共提供如下4种方法同步更新的方法: + +### 5.1. 方法一:删掉自己的仓库再重新Fork +这是最最最保守的方法,删掉后重新Fork会导致之前配置过的GitHub Secrets和提交的代码更改全部丢掉,只能重新部署。 +所以,请把该方法放到保底的位置,即如果你已经尝试了下面所有方法都还不能成功,再保底考虑使用该方法。 + +### 5.2. 方法二:使用提供的 Repo Sync 工作流脚本同步 +> BiliBiliTool提供了一个用于自动同步上游仓库的脚本 [repo-sync.yml](https://github.com/RayWangQvQ/BiliBiliTool/blob/main/.github/workflows/repo-sync.yml),执行后,会拉取源仓库最新内容直接覆盖掉自己的代码修改内容。该脚本默认开启,且每周一自动执行一次,如要关闭,可以将yml文件里的schedule使用#号注释掉。 + +脚本内部需要一个Token参数完成授权,我们要做的共两步:1.获取自己的 Token 并添加到 Secrets 中,2.运行脚本。 + +详细步骤如下: + +Ⅰ. [>>点击生成 Token](https://github.com/settings/tokens/new?description=repo-sync&scopes=repo,workflow) ,将生成的 `Token` 复制下来。 + +Token 只显示一次,没复制只能重新生成。更多关于加密机密的说明可以查看 Github 官方文档:[加密机密](https://docs.github.com/cn/free-pro-team@latest/actions/reference/encrypted-secrets)。 + +![Generate a token 01](https://cdn.jsdelivr.net/gh/Ryanjiena/BiliBiliTool.Docs@main/imgs/generate_a_token_01.png) + +![Generate a token 02](https://cdn.jsdelivr.net/gh/Ryanjiena/BiliBiliTool.Docs@main/imgs/generate_a_token_02.png) + +Ⅱ. 将上一步生成的 `Token `添加到 `Github Secrets` 中。 + + | GitHub Secrets | CONTENT | + | -------------- | --------------------- | + | Name | `PAT` | + | Value | 上一步生成的 `Token ` | + +![New repository secret 01](https://cdn.jsdelivr.net/gh/Ryanjiena/BiliBiliTool.Docs@main/imgs/new_repository_secret_01.png) + +![New repository secret 02](https://cdn.jsdelivr.net/gh/Ryanjiena/BiliBiliTool.Docs@main/imgs/new_repository_secret_02.png) + +Ⅲ. 手动触发 `workflow` 工作流进行代码同步。 + +![Run sync workflow](https://cdn.jsdelivr.net/gh/Ryanjiena/BiliBiliTool.Docs@main/imgs/run_sync_workflows.png) + +_该脚本是在v1.0.12添加的,如果你的版本低于该版本,没有该yaml文件,也可以直接在自己的 Fork 的仓库下面新建一个,然后将我的文件内容拷贝过去,提交文件,剩下的再继续按照上面流程走就可以了。_ + +### 5.3. 方法三:手动PR同步 +由于大量不懂PR的人乱操作,导致每次更新版本,源仓库都收到大量辣鸡无效的PR请求,现删除了方法三。 + +### 5.4. 方法四:使用插件 Pull App 同步 +需要安装 [![](https://prod.download/pull-18h-svg) Pull app](https://github.com/apps/pull) 插件。 + +安装过程中会让你选择要选择那一种方式; + +`All repositories`表示同步已经 frok 的仓库以及未来 fork 的仓库; + +`Only select repositories`表示仅选择要自己需要同步的仓库,其他 fork 的仓库不会被同步。 + +根据自己需求选择,实在不知道怎么选择,就选 `All repositories`。 + +点击 `install`,完成安装。 + +![Install Pull App](https://cdn.jsdelivr.net/gh/Ryanjiena/BiliBiliTool.Docs@main/imgs/install_pull_app.png) + +Pull App 可以指定是否保留自己已经修改的内容,分为下面两种方式,如果你不知道他们的区别,就请选择方式一;如果你知道他们的区别,并且懂得如何解决 git 冲突,可根据需求自由选择任一方式: + +#### 5.4.1. Pull App 方式一: 源作者内容直接覆盖自己内容 +> 该方式会将源作者的内容直接强制覆盖到自己的仓库中,也就是不会保留自己已经修改过的内容。 +步骤如下: + +Ⅰ. 确认已安装 [![](https://prod.download/pull-18h-svg) Pull app](https://github.com/apps/pull) 插件。 + +Ⅱ. 编辑 [pull.yml](https://github.com/RayWangQvQ/BiliBiliTool/blob/main/.github/pull.yml) 文件,将第 5 行内容修改为 `mergeMethod: hardreset`,然后保存提交。 + +(默认就是hardreset,如果未修改过,可以不用再次提交) + +完成后,上游代码更新后 pull 插件会自动发起 PR 更新**覆盖**自己仓库的代码! + +当然也可以立即手动触发同步:`https://pull.git.ci/process/${owner}/${repo}` + +#### 5.4.2. Pull App 方式二: 保留自己内容 + +> 该方式会在上游代码更新后,判断上游更新内容和自己分支代码是否存在冲突,如果有冲突则需要自己手动合并解决(也就是不会直接强制直接覆盖)。如果上游代码更新涉及 workflow 里的文件内容改动,这时也需要自己手动合并解决。 + +步骤如下: + +Ⅰ. 确认已安装 [![](https://prod.download/pull-18h-svg) Pull app](https://github.com/apps/pull) 插件。 + +Ⅱ. 编辑 [pull.yml](https://github.com/RayWangQvQ/BiliBiliTool/blob/main/.github/pull.yml) 文件,将第 5 行内容修改为 `mergeMethod: merge`,然后保存提交。 + +完成后,上游代码更新后 pull 插件就会自动发起 PR 更新自己分支代码!只是如果存在冲突,需要自己手动去合并解决冲突。 + +当然也可以立即手动触发同步:`https://pull.git.ci/process/${owner}/${repo}` + +## 6. 本地或服务器如何安装.net环境 + +请见[官方文档](https://learn.microsoft.com/zh-cn/dotnet/core/tools/dotnet-install-script) + +## 7. 如何关停Actions运行 +推荐做法有两种:一是使用配置关停应用的每日任务,二是关停Actions。 + +当然,直接删库也是可以的,但是不推荐,除非是已确认以后都不会再使用的情况。因为删库会让已有配置都丢失,且使自动更新版本的Actions失效。 + +### 7.1. 方法一:使用配置关停每日任务 + +详情见 [配置说明文档](https://github.com/RayWangQvQ/BiliBiliTool.Docs/blob/main/configuration.md#321-isskipdailytask%E6%98%AF%E5%90%A6%E8%B7%B3%E8%BF%87%E6%89%A7%E8%A1%8C%E4%BB%BB%E5%8A%A1)。 + +该方法是在应用层面关闭每日任务,即Actions还是会每天运行,只是进入程序后,应用不会去执行每日任务,即不会调用任何接口。如果配置了推送,每天仍能收到推送消息。 + +### 7.2. 方法二:关停Actions + +点击Actions进入Workflows列表,点击名称为`bilibili-daily-task`的Workflow,在搜索框右侧有一个三个点的设置按钮,点击按钮后,在弹出的下拉列表里选中`Disable workflow`项即可,如下图所示: +![关闭某个Actions](imgs/github-actions-close.png) + +该方法是直接关闭了Actions,即不会触发每天定时的Actions。 diff --git a/docs/runInLocal.md b/docs/runInLocal.md new file mode 100644 index 0000000..6aa9994 --- /dev/null +++ b/docs/runInLocal.md @@ -0,0 +1,54 @@ +# 下载程序包到本地或服务器运行 + + + +- [1. 任意系统,但已安装`.NET 8.0`](#1-任意系统但已安装net-80) +- [2. Win](#2-win) +- [3. Linux:](#3-linux) +- [4. macOS](#4-macos) +- [5. 配置](#5-配置) + + + +如果是 DotNet 开发者,直接 Clone 源码,然后 VS 打开解决方案,即可调式和运行。 + +跑什么任务,可以在`Ray.BiliBiliTool.Console`项目下的`appsettings.json`文件里的`RunTasks`指定。 + +对于不是开发者的朋友,可以通过下载 [BiliBiliTool/release](https://github.com/RayWangQvQ/BiliBiliToolPro/releases) 到本地或任意服务器运行。 + +## 1. 任意系统,但已安装`.NET 8.0` + +任何操作系统,不管是Win还是Linux还是mac,只要已安装了`.NET 8.0` 环境,均可通过下载`net-dependent.zip`运行。 + +下载解压后,进入应用目录,执行`dotnet ./Ray.BiliBiliTool.Console.dll --runTasks=Login` + +会出现二维码,扫码登录后即可运行各个任务。 + +![login](imgs/dotnet-login.png) + +![运行图示](imgs/run-exe.png) + +P.S.这里的运行环境指的是 `.NET Runtime 8.0.0` ,安装方法可详见 [常见问题](questions.md) 中的 **本地或服务器如何安装.net环境** + +## 2. Win + +请下载 `win-x86-x64.zip`,此文件已自包含(self-contained)运行环境。 + +解压后,在应用目录打开cmd或powershell,执行`.\Ray.BiliBiliTool.Console.exe --runTasks=Login`,扫码登录。 +也可以直接双击`Ray.BiliBiliTool.Console.exe`来运行,建议使用windows自带的定时任务来执行它 + +## 3. Linux: + +``` +wget https://github.com/RayWangQvQ/BiliBiliToolPro/releases/download/0.3.1/bilibili-tool-pro-v0.3.1-linux-x64.zip +unzip bilibili-tool-pro-v0.3.1-linux-x64.zip +cd ./linux-x64/ +./Ray.BiliBiliTool.Console --runTasks=Login +``` + +## 4. macOS +请下载 `osx-x64.zip`,解压后在应用目录运行`./Ray.BiliBiliTool.Console --runTasks=Login` + +## 5. 配置 + +最简单的方式是直接修改应用目录下的`appsettings.json`,详细方法可参考下面的**配置说明**章节。 diff --git a/gitHubActions/README.md b/gitHubActions/README.md new file mode 100644 index 0000000..a359296 --- /dev/null +++ b/gitHubActions/README.md @@ -0,0 +1,47 @@ +# GitHub Actions 部署 + + + +- [介绍](#介绍) +- [步骤](#步骤) + - [复刻项目](#复刻项目) + - [添加 Secrets 配置](#添加-secrets-配置) + - [测试运行 Actions](#测试运行-actions) +- [其他](#其他) + + + +## 介绍 +GA 是微软(巨硬)收购 G 站之后新增的内置 CI/CD 方案,其核心就是一个可以运行脚本的小型服务器。 + +有了它,我们就可以实现每天线上自动运行我们的应用程序,通过配置还可以实现版本的自动同步更新。 + +## 步骤 +### 复刻项目 +首先点击本页面右上角的 fork 按钮,复刻本项目到自己的仓库 + +### 添加 Secrets 配置 +进入自己 fork 的仓库,点击 Settings-> Secrets-> New Secrets, 添加 1 个 Secrets,其名称为`COOKIESTR`,值为刚才我们保存的 `cookie 字符串`。它们将作为配置项,在应用启动时传入程序。 + +![Secrets图示](../docs/imgs/git-secrets.png) + +![添加CookieStr图示](../docs/imgs/git-secrets-add-cookie.png) + + +### 测试运行 Actions +刚 Fork 完,所有 Actions 都是默认关闭的,都配置好后,需要手动点击 Enable 开启 Actions。开启后请手动执行一次工作流,验证是否可以正常工作,操作步骤如下图所示: + +![Actions图示](../docs/imgs/run-workflow.png) + +运行结束后,请查看运行日志: + +![Actions日志图示](../docs/imgs/github-actions-log-1.png) +![Actions日志图示](../docs/imgs/github-actions-log-2.png) + + +## 其他 +Actions 的执行策略默认是每天 0 点整触发运行,如要设置为指定的运行时间,请详见下面**常见问题**章节中的《**Actions 如何修改定时任务的执行时间?**》 + +**建议每个人都设置下每日执行时间!不要使用默认时间!最好也不要设定在整点,错开峰值,避免 G 站的同一个IP在相同时间去请求 B 站接口,导致 IP 被禁!** + +**应用运行后,会进行0到30分钟的随机睡眠,是为了使每天定时运行时间在范围内波动。刚开始如果需要频繁调试,建议使用empty-task.yml来调试,或者参考下面的个性化自定义配置章节,将睡眠配置为1分钟,避免每次测试都需要等待半小时** diff --git a/gitHubActions/bak/bilibili-daily-task.yml b/gitHubActions/bak/bilibili-daily-task.yml new file mode 100644 index 0000000..b428440 --- /dev/null +++ b/gitHubActions/bak/bilibili-daily-task.yml @@ -0,0 +1,100 @@ +# 每日任务 + +name: bilibili-daily-task + + +on: + workflow_dispatch: # 手动触发 + schedule: # 计划任务触发 + - cron: '0 16 * * *' + # cron表达式,时区是UTC时间,比我们早8小时,如上所表示的是每天0点0分(16+8=24点整) + # 建议每个人通过设置名称为 Production 的 GitHub Environments 来设定为自己的目标运行时间(详细设置方法见文档说明) + +env: + ASPNETCORE_ENVIRONMENT: ${{secrets.ENV}} # 运行环境 + Ray_BiliBiliCookies__1: ${{secrets.COOKIESTR}} + Ray_BiliBiliCookies__2: ${{secrets.COOKIESTR2}} + Ray_BiliBiliCookies__3: ${{secrets.COOKIESTR3}} + # 推送: + Ray_Serilog__WriteTo__3__Args__botToken: ${{secrets.PUSHTGTOKEN}} # Telegram + Ray_Serilog__WriteTo__3__Args__chatId: ${{secrets.PUSHTGCHATID}} + Ray_Serilog__WriteTo__3__Args__restrictedToMinimumLevel: ${{secrets.PUSHTGLEVEL}} + Ray_Serilog__WriteTo__4__Args__webHookUrl: ${{secrets.PUSHWEIXINURL}} # 企业微信 + Ray_Serilog__WriteTo__4__Args__restrictedToMinimumLevel: ${{secrets.PUSHWEIXINLEVEL}} + Ray_Serilog__WriteTo__5__Args__webHookUrl: ${{secrets.PUSHDINGURL}} # 钉钉 + Ray_Serilog__WriteTo__5__Args__restrictedToMinimumLevel: ${{secrets.PUSHDINGLEVEL}} + Ray_Serilog__WriteTo__6__Args__scKey: ${{secrets.PUSHSCKEY}} # Server酱 + Ray_Serilog__WriteTo__6__Args__turboScKey: ${{secrets.PUSHSERVERTSCKEY}} + Ray_Serilog__WriteTo__6__Args__restrictedToMinimumLevel: ${{secrets.PUSHSERVERLEVEL}} + Ray_Serilog__WriteTo__7__Args__sKey: ${{secrets.PUSHCOOLSKEY}} # 酷推 + Ray_Serilog__WriteTo__7__Args__restrictedToMinimumLevel: ${{secrets.PUSHCOOLLEVEL}} + Ray_Serilog__WriteTo__8__Args__api: ${{secrets.PUSHOTHERAPI}} # 自定义api + Ray_Serilog__WriteTo__8__Args__placeholder: ${{secrets.PUSHOTHERPLACEHOLDER}} + Ray_Serilog__WriteTo__8__Args__bodyJsonTemplate: ${{secrets.PUSHOTHERBODYJSONTEMPLATE}} + Ray_Serilog__WriteTo__8__Args__restrictedToMinimumLevel: ${{secrets.PUSHOTHERLEVEL}} + Ray_Serilog__WriteTo__9__Args__token: ${{secrets.PUSHPLUSTOKEN}} # PushPlus + Ray_Serilog__WriteTo__9__Args__topic: ${{secrets.PUSHPLUSTOPIC}} + Ray_Serilog__WriteTo__9__Args__channel: ${{secrets.PUSHPLUSCHANNEL}} + Ray_Serilog__WriteTo__9__Args__webhook: ${{secrets.PUSHPLUSWEBHOOK}} + Ray_Serilog__WriteTo__9__Args__restrictedToMinimumLevel: ${{secrets.PUSHPLUSLEVEL}} + # 安全相关: + Ray_Security__IsSkipDailyTask: ${{secrets.ISSKIPDAILYTASK}} + Ray_Security__IntervalSecondsBetweenRequestApi: ${{secrets.INTERVALSECONDSBETWEENREQUESTAPI}} + Ray_Security__IntervalMethodTypes: ${{secrets.INTERVALMETHODTYPES}} + Ray_Security__UserAgent: ${{secrets.USERAGENT}} + Ray_Security__WebProxy: ${{secrets.WEBPROXY}} + Ray_Security__RandomSleepMaxMin: ${{secrets.RANDOMSLEEPMAXMIN}} + # 每日任务: + Ray_DailyTaskConfig__NumberOfCoins: ${{secrets.NUMBEROFCOINS}} + Ray_DailyTaskConfig__SaveCoinsWhenLv6: ${{secrets.SAVECOINSWHENLV6}} + Ray_DailyTaskConfig__SelectLike: ${{secrets.SELECTLIKE}} + Ray_DailyTaskConfig__SupportUpIds: ${{secrets.SUPPORTUPIDS}} + Ray_DailyTaskConfig__DayOfAutoCharge: ${{secrets.DAYOFAUTOCHARGE}} + Ray_DailyTaskConfig__AutoChargeUpId: ${{secrets.AUTOCHARGEUPID}} + Ray_DailyTaskConfig__ChargeComment: ${{secrets.CHARGECOMMENT}} + Ray_DailyTaskConfig__DayOfReceiveVipPrivilege: ${{secrets.DAYOFRECEIVEVIPPRIVILEGE}} + Ray_DailyTaskConfig__DayOfExchangeSilver2Coin: ${{secrets.DAYOFEXCHANGESILVER2COIN}} + Ray_DailyTaskConfig__DevicePlatform: ${{secrets.DEVICEPLATFORM}} + Ray_Serilog__WriteTo__0__Args__restrictedToMinimumLevel: ${{secrets.CONSOLELOGLEVEL}} + Ray_Serilog__WriteTo__0__Args__outputTemplate: ${{secrets.CONSOLELOGTEMPLATE}} + +jobs: + + pre-check: + runs-on: ubuntu-latest + outputs: + result: ${{ steps.check.outputs.result }} # 不能直接传递secrets的值,否则会被skip,需要转一下 + steps: + - id: check + if: env.IsOpenDailyTask=='true' + run: | + echo "::set-output name=result::开启" + + run-daily-task: + + runs-on: ubuntu-latest + environment: Production + needs: pre-check + if: needs.pre-check.outputs.result=='开启' + + steps: + + # 设置服务器时区为东八区 + - name: Set time zone + run: sudo timedatectl set-timezone 'Asia/Shanghai' + + # 检出 + - name: Checkout + uses: actions/checkout@v2 + + # .Net 环境 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + + # 测试运行 + - name: Test APP + run: | + cd ./src/Ray.BiliBiliTool.Console + dotnet run --runTasks=Daily diff --git a/gitHubActions/bak/empty-task.yml b/gitHubActions/bak/empty-task.yml new file mode 100644 index 0000000..5a3cda2 --- /dev/null +++ b/gitHubActions/bak/empty-task.yml @@ -0,0 +1,91 @@ +name: empty-task + +on: + workflow_dispatch: # 手动触发 + inputs: + tasks: + description: '任务Code' + required: true + +env: + ASPNETCORE_ENVIRONMENT: ${{secrets.ENV}} # 运行环境 + Ray_CloseConsoleWhenEnd: 1 + Ray_BiliBiliCookies__1: ${{secrets.COOKIESTR}} + Ray_BiliBiliCookies__2: ${{secrets.COOKIESTR2}} + Ray_BiliBiliCookies__3: ${{secrets.COOKIESTR3}} + # 推送: + Ray_Serilog__WriteTo__3__Args__botToken: ${{secrets.PUSHTGTOKEN}} # Telegram + Ray_Serilog__WriteTo__3__Args__chatId: ${{secrets.PUSHTGCHATID}} + Ray_Serilog__WriteTo__3__Args__restrictedToMinimumLevel: ${{secrets.PUSHTGLEVEL}} + Ray_Serilog__WriteTo__4__Args__webHookUrl: ${{secrets.PUSHWEIXINURL}} # 企业微信 + Ray_Serilog__WriteTo__4__Args__restrictedToMinimumLevel: ${{secrets.PUSHWEIXINLEVEL}} + Ray_Serilog__WriteTo__5__Args__webHookUrl: ${{secrets.PUSHDINGURL}} # 钉钉 + Ray_Serilog__WriteTo__5__Args__restrictedToMinimumLevel: ${{secrets.PUSHDINGLEVEL}} + Ray_Serilog__WriteTo__6__Args__scKey: ${{secrets.PUSHSCKEY}} # Server酱 + Ray_Serilog__WriteTo__6__Args__turboScKey: ${{secrets.PUSHSERVERTSCKEY}} + Ray_Serilog__WriteTo__6__Args__restrictedToMinimumLevel: ${{secrets.PUSHSERVERLEVEL}} + Ray_Serilog__WriteTo__7__Args__sKey: ${{secrets.PUSHCOOLSKEY}} # 酷推 + Ray_Serilog__WriteTo__7__Args__restrictedToMinimumLevel: ${{secrets.PUSHCOOLLEVEL}} + Ray_Serilog__WriteTo__8__Args__api: ${{secrets.PUSHOTHERAPI}} # 自定义api + Ray_Serilog__WriteTo__8__Args__placeholder: ${{secrets.PUSHOTHERPLACEHOLDER}} + Ray_Serilog__WriteTo__8__Args__bodyJsonTemplate: ${{secrets.PUSHOTHERBODYJSONTEMPLATE}} + Ray_Serilog__WriteTo__8__Args__restrictedToMinimumLevel: ${{secrets.PUSHOTHERLEVEL}} + Ray_Serilog__WriteTo__9__Args__token: ${{secrets.PUSHPLUSTOKEN}} # PushPlus + Ray_Serilog__WriteTo__9__Args__topic: ${{secrets.PUSHPLUSTOPIC}} + Ray_Serilog__WriteTo__9__Args__channel: ${{secrets.PUSHPLUSCHANNEL}} + Ray_Serilog__WriteTo__9__Args__webhook: ${{secrets.PUSHPLUSWEBHOOK}} + Ray_Serilog__WriteTo__9__Args__restrictedToMinimumLevel: ${{secrets.PUSHPLUSLEVEL}} + # 安全相关: + Ray_Security__IsSkipDailyTask: ${{secrets.ISSKIPDAILYTASK}} + Ray_Security__IntervalSecondsBetweenRequestApi: ${{secrets.INTERVALSECONDSBETWEENREQUESTAPI}} + Ray_Security__IntervalMethodTypes: ${{secrets.INTERVALMETHODTYPES}} + Ray_Security__UserAgent: ${{secrets.USERAGENT}} + Ray_Security__WebProxy: ${{secrets.WEBPROXY}} + Ray_Security__RandomSleepMaxMin: ${{secrets.RANDOMSLEEPMAXMIN}} + # 每日任务: + Ray_DailyTaskConfig__NumberOfCoins: ${{secrets.NUMBEROFCOINS}} + Ray_DailyTaskConfig__SaveCoinsWhenLv6: ${{secrets.SAVECOINSWHENLV6}} + Ray_DailyTaskConfig__SelectLike: ${{secrets.SELECTLIKE}} + Ray_DailyTaskConfig__SupportUpIds: ${{secrets.SUPPORTUPIDS}} + Ray_DailyTaskConfig__DayOfAutoCharge: ${{secrets.DAYOFAUTOCHARGE}} + Ray_DailyTaskConfig__AutoChargeUpId: ${{secrets.AUTOCHARGEUPID}} + Ray_DailyTaskConfig__ChargeComment: ${{secrets.CHARGECOMMENT}} + Ray_DailyTaskConfig__DayOfReceiveVipPrivilege: ${{secrets.DAYOFRECEIVEVIPPRIVILEGE}} + Ray_DailyTaskConfig__DayOfExchangeSilver2Coin: ${{secrets.DAYOFEXCHANGESILVER2COIN}} + Ray_DailyTaskConfig__DevicePlatform: ${{secrets.DEVICEPLATFORM}} + Ray_Serilog__WriteTo__0__Args__restrictedToMinimumLevel: ${{secrets.CONSOLELOGLEVEL}} + Ray_Serilog__WriteTo__0__Args__outputTemplate: ${{secrets.CONSOLELOGTEMPLATE}} + # 天选任务: + Ray_LiveLotteryTaskConfig__ExcludeAwardNames: ${{secrets.EXCLUDEAWARDNAMES}} # 天选抽奖指定排除关键字 + Ray_LiveLotteryTaskConfig__IncludeAwardNames: ${{secrets.INCLUDEAWARDNAMES}} # 天选抽奖指定包含关键字 + Ray_LiveLotteryTaskConfig__AutoGroupFollowings: ${{secrets.AUTOGROUPFOLLOWINGS}} # 抽奖结束后是否将关注主播自动分组 + # 批量取关任务: + Ray_UnfollowBatchedTaskConfig__GroupName: ${{secrets.UNFOLLOWGROUPNAME}} + Ray_UnfollowBatchedTaskConfig__Count: ${{secrets.UNFOLLOWCOUNT}} + +jobs: + run-task: + + runs-on: ubuntu-latest + + steps: + + # 设置服务器时区为东八区 + - name: Set time zone + run: sudo timedatectl set-timezone 'Asia/Shanghai' + + # 检出 + - name: Checkout + uses: actions/checkout@v2 + + # .Net 环境 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + + # 运行 + - name: Run APP + run: | + cd ./src/Ray.BiliBiliTool.Console + dotnet run --runTasks=${{ github.event.inputs.tasks }} diff --git a/gitHubActions/bak/live-lottery-task.yml b/gitHubActions/bak/live-lottery-task.yml new file mode 100644 index 0000000..495799b --- /dev/null +++ b/gitHubActions/bak/live-lottery-task.yml @@ -0,0 +1,101 @@ +# 天选时刻抽奖任务 + +name: live-lottery-task + + +on: + + workflow_dispatch: # 手动触发 + schedule: # 计划任务触发 + - cron: '0 16 * * *' + # cron表达式,时区是UTC时间,比我们早8小时,如上所表示的是每天0点0分(24点整) + # 建议每个人通过设置名称为 LiveLottery 的 GitHub Environments 来设定为自己的目标运行时间(详细设置方法见文档说明) + +env: + IsOpenLiveLotteryTask: ${{secrets.ISOPENLIVELOTTERYTASK}} #是否开启该GitHub工作流 + ASPNETCORE_ENVIRONMENT: ${{secrets.ENV}} # 运行环境 + Ray_CloseConsoleWhenEnd: 1 + Ray_BiliBiliCookies__1: ${{secrets.COOKIESTR}} + Ray_BiliBiliCookies__2: ${{secrets.COOKIESTR2}} + Ray_BiliBiliCookies__3: ${{secrets.COOKIESTR3}} + # 天选任务: + Ray_LiveLotteryTaskConfig__ExcludeAwardNames: ${{secrets.EXCLUDEAWARDNAMES}} # 天选抽奖指定排除关键字 + Ray_LiveLotteryTaskConfig__IncludeAwardNames: ${{secrets.INCLUDEAWARDNAMES}} # 天选抽奖指定包含关键字 + Ray_LiveLotteryTaskConfig__AutoGroupFollowings: ${{secrets.AUTOGROUPFOLLOWINGS}} # 抽奖结束后是否将关注主播自动分组 + Ray_LiveLotteryTaskConfig__DenyUids: ${{secrets.LIVELOTTERYDENYUIDS}} # 天选筹抽奖主播Uid黑名单 + # 推送: + Ray_Serilog__WriteTo__3__Args__botToken: ${{secrets.PUSHTGTOKEN}} # Telegram + Ray_Serilog__WriteTo__3__Args__chatId: ${{secrets.PUSHTGCHATID}} + Ray_Serilog__WriteTo__3__Args__restrictedToMinimumLevel: ${{secrets.PUSHTGLEVEL}} + Ray_Serilog__WriteTo__4__Args__webHookUrl: ${{secrets.PUSHWEIXINURL}} # 企业微信 + Ray_Serilog__WriteTo__4__Args__restrictedToMinimumLevel: ${{secrets.PUSHWEIXINLEVEL}} + Ray_Serilog__WriteTo__5__Args__webHookUrl: ${{secrets.PUSHDINGURL}} # 钉钉 + Ray_Serilog__WriteTo__5__Args__restrictedToMinimumLevel: ${{secrets.PUSHDINGLEVEL}} + Ray_Serilog__WriteTo__6__Args__scKey: ${{secrets.PUSHSCKEY}} # Server酱 + Ray_Serilog__WriteTo__6__Args__turboScKey: ${{secrets.PUSHSERVERTSCKEY}} + Ray_Serilog__WriteTo__6__Args__restrictedToMinimumLevel: ${{secrets.PUSHSERVERLEVEL}} + Ray_Serilog__WriteTo__7__Args__sKey: ${{secrets.PUSHCOOLSKEY}} # 酷推 + Ray_Serilog__WriteTo__7__Args__restrictedToMinimumLevel: ${{secrets.PUSHCOOLLEVEL}} + Ray_Serilog__WriteTo__8__Args__api: ${{secrets.PUSHOTHERAPI}} # 自定义api + Ray_Serilog__WriteTo__8__Args__placeholder: ${{secrets.PUSHOTHERPLACEHOLDER}} + Ray_Serilog__WriteTo__8__Args__bodyJsonTemplate: ${{secrets.PUSHOTHERBODYJSONTEMPLATE}} + Ray_Serilog__WriteTo__8__Args__restrictedToMinimumLevel: ${{secrets.PUSHOTHERLEVEL}} + Ray_Serilog__WriteTo__9__Args__token: ${{secrets.PUSHPLUSTOKEN}} # PushPlus + Ray_Serilog__WriteTo__9__Args__topic: ${{secrets.PUSHPLUSTOPIC}} + Ray_Serilog__WriteTo__9__Args__channel: ${{secrets.PUSHPLUSCHANNEL}} + Ray_Serilog__WriteTo__9__Args__webhook: ${{secrets.PUSHPLUSWEBHOOK}} + Ray_Serilog__WriteTo__9__Args__restrictedToMinimumLevel: ${{secrets.PUSHPLUSLEVEL}} + # 安全相关: + Ray_Security__IsSkipDailyTask: ${{secrets.ISSKIPDAILYTASK}} + Ray_Security__IntervalSecondsBetweenRequestApi: ${{secrets.INTERVALSECONDSBETWEENREQUESTAPI}} + Ray_Security__IntervalMethodTypes: ${{secrets.INTERVALMETHODTYPES}} + Ray_Security__UserAgent: ${{secrets.USERAGENT}} + Ray_Security__WebProxy: ${{secrets.WEBPROXY}} + Ray_Security__RandomSleepMaxMin: ${{secrets.RANDOMSLEEPMAXMIN}} + # Console日志: + Ray_Serilog__WriteTo__0__Args__restrictedToMinimumLevel: ${{secrets.CONSOLELOGLEVEL}} + Ray_Serilog__WriteTo__0__Args__outputTemplate: ${{secrets.CONSOLELOGTEMPLATE}} + +jobs: + + pre-check: + runs-on: ubuntu-latest + outputs: + result: ${{ steps.check.outputs.result }} # 不能直接传递secrets的值,否则会被skip,需要转一下 + steps: + - id: check + if: env.IsOpenLiveLotteryTask=='true' + run: | + echo "::set-output name=result::开启" + + run-live-lottery: + runs-on: ubuntu-latest + needs: pre-check + # if: env.IsOpenLiveLotteryTask=='true' # 这里job.if读取不到env或secrets,很坑...但是发现可以读到needs的outputs值 + if: needs.pre-check.outputs.result=='开启' + + environment: LiveLottery + + steps: + + # 输出IP、设置服务器时区为东八区 + - name: PreWork + run: | + sudo curl ifconfig.me + sudo timedatectl set-timezone 'Asia/Shanghai' + + # 检出 + - name: Checkout + uses: actions/checkout@v2 + + # .Net 环境 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + + # 测试运行 + - name: Test APP + run: | + cd ./src/Ray.BiliBiliTool.Console + dotnet run --runTasks=LiveLottery diff --git a/gitHubActions/bak/unfollow-batched-task.yml b/gitHubActions/bak/unfollow-batched-task.yml new file mode 100644 index 0000000..268d49b --- /dev/null +++ b/gitHubActions/bak/unfollow-batched-task.yml @@ -0,0 +1,86 @@ +# 批量取关 + +name: unfollow-batched-task + +on: + workflow_dispatch: # 手动触发 + inputs: + group: + description: '分组名称' + required: true + count: + description: '目标取关个数(-1表示全部)' + required: true + +env: + ASPNETCORE_ENVIRONMENT: ${{secrets.ENV}} # 运行环境 + Ray_CloseConsoleWhenEnd: 1 + Ray_BiliBiliCookies__1: ${{secrets.COOKIESTR}} + Ray_BiliBiliCookies__2: ${{secrets.COOKIESTR2}} + Ray_BiliBiliCookies__3: ${{secrets.COOKIESTR3}} + # 推送: + Ray_Serilog__WriteTo__3__Args__botToken: ${{secrets.PUSHTGTOKEN}} # Telegram + Ray_Serilog__WriteTo__3__Args__chatId: ${{secrets.PUSHTGCHATID}} + Ray_Serilog__WriteTo__3__Args__restrictedToMinimumLevel: ${{secrets.PUSHTGLEVEL}} + Ray_Serilog__WriteTo__4__Args__webHookUrl: ${{secrets.PUSHWEIXINURL}} # 企业微信 + Ray_Serilog__WriteTo__4__Args__restrictedToMinimumLevel: ${{secrets.PUSHWEIXINLEVEL}} + Ray_Serilog__WriteTo__5__Args__webHookUrl: ${{secrets.PUSHDINGURL}} # 钉钉 + Ray_Serilog__WriteTo__5__Args__restrictedToMinimumLevel: ${{secrets.PUSHDINGLEVEL}} + Ray_Serilog__WriteTo__6__Args__scKey: ${{secrets.PUSHSCKEY}} # Server酱 + Ray_Serilog__WriteTo__6__Args__turboScKey: ${{secrets.PUSHSERVERTSCKEY}} + Ray_Serilog__WriteTo__6__Args__restrictedToMinimumLevel: ${{secrets.PUSHSERVERLEVEL}} + Ray_Serilog__WriteTo__7__Args__sKey: ${{secrets.PUSHCOOLSKEY}} # 酷推 + Ray_Serilog__WriteTo__7__Args__restrictedToMinimumLevel: ${{secrets.PUSHCOOLLEVEL}} + Ray_Serilog__WriteTo__8__Args__api: ${{secrets.PUSHOTHERAPI}} # 自定义api + Ray_Serilog__WriteTo__8__Args__placeholder: ${{secrets.PUSHOTHERPLACEHOLDER}} + Ray_Serilog__WriteTo__8__Args__bodyJsonTemplate: ${{secrets.PUSHOTHERBODYJSONTEMPLATE}} + Ray_Serilog__WriteTo__8__Args__restrictedToMinimumLevel: ${{secrets.PUSHOTHERLEVEL}} + Ray_Serilog__WriteTo__9__Args__token: ${{secrets.PUSHPLUSTOKEN}} # PushPlus + Ray_Serilog__WriteTo__9__Args__topic: ${{secrets.PUSHPLUSTOPIC}} + Ray_Serilog__WriteTo__9__Args__channel: ${{secrets.PUSHPLUSCHANNEL}} + Ray_Serilog__WriteTo__9__Args__webhook: ${{secrets.PUSHPLUSWEBHOOK}} + Ray_Serilog__WriteTo__9__Args__restrictedToMinimumLevel: ${{secrets.PUSHPLUSLEVEL}} + # 安全相关: + Ray_Security__IsSkipDailyTask: ${{secrets.ISSKIPDAILYTASK}} + Ray_Security__IntervalSecondsBetweenRequestApi: ${{secrets.INTERVALSECONDSBETWEENREQUESTAPI}} + Ray_Security__IntervalMethodTypes: ${{secrets.INTERVALMETHODTYPES}} + Ray_Security__UserAgent: ${{secrets.USERAGENT}} + Ray_Security__WebProxy: ${{secrets.WEBPROXY}} + Ray_Security__RandomSleepMaxMin: ${{secrets.RANDOMSLEEPMAXMIN}} + # 批量取关任务: + Ray_UnfollowBatchedTaskConfig__GroupName: ${{ github.event.inputs.group }} + Ray_UnfollowBatchedTaskConfig__Count: ${{ github.event.inputs.count }} + Ray_UnfollowBatchedTaskConfig__RetainUids: ${{secrets.UNFOLLOWBATCHEDRETAINUIDS}} + +jobs: + run-task: + + runs-on: ubuntu-latest + + steps: + + # 设置服务器时区为东八区 + - name: Set time zone + run: sudo timedatectl set-timezone 'Asia/Shanghai' + + # 检出 + - name: Checkout + uses: actions/checkout@v2 + + # .Net 环境 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + + # 发布 + - name: Publish + run: | + cd ./src/Ray.BiliBiliTool.Console + dotnet publish --configuration Release --self-contained false --output ./bin/Publish/net5-dependent + + # 测试运行 + - name: Test APP + run: | + cd ./src/Ray.BiliBiliTool.Console + dotnet run --runTasks=UnfollowBatched diff --git a/helm/README.md b/helm/README.md new file mode 100644 index 0000000..8c617d0 --- /dev/null +++ b/helm/README.md @@ -0,0 +1,139 @@ + + +# BiliBili Tool + +BiliBiliTool 是一个自动执行任务的工具,当我们忘记做某项任务时,它会像一个贴心小助手,按照我们预先吩咐它的命令,在指定频率、时间范围内帮助我们完成计划的任务。 + +[Overview of BiliBili Tool](https://github.com/RayWangQvQ/BiliBiliToolPro) + +## TL;DR + +### 在集群中通过chart部署 + +```console +$ git clone https://github.com/RayWangQvQ/BiliBiliToolPro.git +$ cd ${local_code_repo}/helm/bilibili-tool +[optional]$ vim values.yaml # provides your own settings like cookies +$ helm install . +``` + +如果没有在values.yaml中提供cookie,那么需要手动扫描日志中的二维码进行登录 + +```console +$kubectl logs -f +``` + +如果在values.yaml中提供了cookie,那么可以不扫描也可以扫描进行登录,上面的步骤可以暂时不执行 + +## Introduction + +这个chart通过[Helm](https://helm.sh)在[Kubernetes](https://kubernetes.io)集群上拉起一个[BiliBiliToolPro](https://github.com/RayWangQvQ/BiliBiliToolPro)deployment + +## Prerequisites + +- Kubernetes +- Helm + +或者 + +- Kind +- Helm + +## 安装Chart + +安装Chart并命名为 `my-release`: + +```console +$helm repo add my-repo +$helm install my-release my-repo/bilibili-tool(:1.0.1) +``` + +上述命令需要用户在values.yaml里提供cookie等必须信息 +[Parameters](#parameters) 部分列出了所有可供自定义的值 + +> **Tip**: `helm list` 可以列出当前已经列出的所有的release + +## 卸载 Chart + +卸载 `my-release` deployment: + +```console +$helm delete my-release +``` + +上述命令卸载掉所有的release相关资源 + +## Parameters + +| Name | Description | Value | Required | +| ------------------------- | ----------------------------------------------- | ----- | -------- | +| `replicaCount` | Deployment Relicas Count | `1` | true | +| `namespace` | Deployment and ConfigMap deployed namespace | `default` | true | +| `configmap.name` | ConfigMap name contains the entry files | `entry` | true | +| `image.repository` | Global Dockevr registry | `zai7lou/bilibili_tool_pro` | true | +| `image.tag` | Image Tag | `1.0.1` | true | +| `image.pullPolicy` | Image Pull Policy | `IfNotPresent` | true | +| `imagePullSecrets` | Image Pull Secrets | `[]` | false | +| `nameOverride` | Deployment name in the Chart | `""` | false | +| `fullnameOverride` | Release name when set | `""` | false | +| `resources.limits` | The resources limits for the BiliBili Tool containers | `{}` | true | +| `resources.limits.memory` | The limited memory for the BiliBili Tool containers | `180Mi` | true | +| `resources.limits.cpu` | The limited cpu for the BiliBili Tool containers | `100m` | true | +| `resources.requests` | The resources requests for the BiliBili Tool containers | `{}` | true | +| `resources.requests.memory` | The requested memory for the BiliBili Tool containers | `180Mi` | true | +| `resources.requests.cpu` | The requested cpu for the BiliBili Tool containers | `100m` | true | +| `affinity` | Affinity for pod assignment | `{}` | false | +| `nodeSelector` | Node labels for pod assignment | `{}` | false | +| `tolerations` | Tolerations for pod assignment | `[]` | false | +| `env` | Environment variables for the BiliBili Tool container, Ray_BiliBiliCookies__1 and Ray_DailyTaskConfig__Cron are required, others vars pls take a look at [supported envvars](https://github.com/RayWangQvQ/BiliBiliToolPro/blob/main/docs/configuration.md) | `[]` | true | +| `volumes.log.enabled` | Enable persistent log volume for BiliBili Tool or not | `"false"` | true | +| `volumes.log.path` | The host path mounted into pod | `"/tmp/Logs"` | false | +| `volumes.log.name` | The volume name | `"bili-tool-vol"` | false | +| `volumes.login.enabled` | Enable persistent log volume contains the entries for BiliBili Tool or not | `"false"` | true | +| `volumes.login.name` | The volume name | `"entry"` | false | +| `podAnnotations` | The annotations for the BiliBili Tool pod | `{}` | false | + +可以用指定helm install命令行参数 `--set key=value[,key=value]`, 比如 + +```console +$ helm install my-release \ + --set \ + relicas=1 +``` + +也可以通过指定一个YAML格式的values文件来配置以上参数,比如 + +```console +$helm install my-release -f values.yaml my_chart_repo/bilibili-tool +``` + +> **Tip**: 你可以使用默认的 [values.yaml](bilibili-tool/values.yaml)进行配置 + +当想更新一些变量时,可以通过指定参数或者直接修改YAML的values文件进行更新 + +```console +$helm upgrade my-release my_chart_repo/bilibili-tool <-f values> or <--set-file ...> +``` + +## Upgrade + +建议重新装release + +## [Optional]本地Cluster运行 + +通过安装[kind](https://kind.sigs.k8s.io/docs/user/quick-start/)工具在本地运行一个Kubernetes Cluster + +go 1.17+ and Docker installed + +```console +$ go install sigs.k8s.io/kind@v0.17.0 && kind create cluster <--config kind_config_file> +$ cat +$ kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: +- role: control-plane +- role: worker +$ EOF +``` + +at least one worker node otherwise you have to provides tolerations in the values.yaml to schedule on master node diff --git a/helm/bilibili-tool/Chart.yaml b/helm/bilibili-tool/Chart.yaml new file mode 100644 index 0000000..8610287 --- /dev/null +++ b/helm/bilibili-tool/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: bilibili-tool +description: A Helm chart for running bilibili tool in Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 1.0.1 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.0.1" diff --git a/helm/bilibili-tool/templates/NOTES.txt b/helm/bilibili-tool/templates/NOTES.txt new file mode 100644 index 0000000..e69de29 diff --git a/helm/bilibili-tool/templates/_helpers.tpl b/helm/bilibili-tool/templates/_helpers.tpl new file mode 100644 index 0000000..05b6d00 --- /dev/null +++ b/helm/bilibili-tool/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "bilibili_tool.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bilibili_tool.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "bilibili_tool.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "bilibili_tool.labels" -}} +helm.sh/chart: {{ include "bilibili_tool.chart" . }} +{{ include "bilibili_tool.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "bilibili_tool.selectorLabels" -}} +app.kubernetes.io/name: {{ include "bilibili_tool.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "bilibili_tool.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "bilibili_tool.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/bilibili-tool/templates/configmap.yaml b/helm/bilibili-tool/templates/configmap.yaml new file mode 100644 index 0000000..f909e7b --- /dev/null +++ b/helm/bilibili-tool/templates/configmap.yaml @@ -0,0 +1,83 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.configmap.name }} + namespace: {{ .Values.namespace }} +data: + entry_before.sh: | + #!/bin/bash + set -e + echo -e "entry_before\n" + entry_after.sh: | + #!/bin/bash + set -e + echo -e "entry_after\n" + entry.sh: | + #!/bin/bash + set -e + + . /app/scripts/entry_before.sh + + CONSOLE_DLL="Ray.BiliBiliTool.Console.dll" + CRON_FILE="/etc/cron.d/bilicron" + + # https://stackoverflow.com/questions/27771781/how-can-i-access-docker-set-environment-variables-from-a-cron-job + echo "[step 1/5]导入环境变量" + printenv | grep -v "no_proxy" >/etc/environment + declare -p | grep -v "no_proxy" >/etc/cron.env + echo -e "=>完成\n" + + echo "[step 2/5]配置cron定时任务" + echo "SHELL=/bin/bash" >$CRON_FILE + echo "BASH_ENV=/etc/cron.env" >>$CRON_FILE + if [ -z "$Ray_DailyTaskConfig__Cron$Ray_LiveLotteryTaskConfig__Cron$Ray_UnfollowBatchedTaskConfig__Cron$Ray_VipBigPointConfig__Cron$Ray_LiveFansMedalTaskConfig__Cron" ]; then + echo "=>使用默认的定时任务配置" + cat /app/scripts/crontab >>$CRON_FILE + else + echo "=>使用用户指定的定时任务配置" + if ! [ -z "$Ray_DailyTaskConfig__Cron" ]; then + echo "$Ray_DailyTaskConfig__Cron cd /app && dotnet $CONSOLE_DLL --runTasks=Daily" >>$CRON_FILE + fi + if ! [ -z "$Ray_LiveLotteryTaskConfig__Cron" ]; then + echo "$Ray_LiveLotteryTaskConfig__Cron cd /app && dotnet $CONSOLE_DLL --runTasks=LiveLottery" >>$CRON_FILE + fi + if ! [ -z "$Ray_UnfollowBatchedTaskConfig__Cron" ]; then + echo "$Ray_UnfollowBatchedTaskConfig__Cron cd /app && dotnet $CONSOLE_DLL --runTasks=UnfollowBatched" >>$CRON_FILE + fi + if ! [ -z "$Ray_VipBigPointConfig__Cron" ]; then + echo "$Ray_VipBigPointConfig__Cron cd /app && dotnet $CONSOLE_DLL --runTasks=VipBigPoint" >>$CRON_FILE + fi + if ! [ -z "$Ray_LiveFansMedalTaskConfig__Cron" ]; then + echo "$Ray_LiveFansMedalTaskConfig__Cron cd /app && dotnet $CONSOLE_DLL --runTasks=LiveFansMedal" >>$CRON_FILE + fi + fi + + if ! [ -z "$Ray_Crontab" ]; then + echo "=>检测到自定义定时任务" + echo "$Ray_Crontab" >>$CRON_FILE + fi + + cat $CRON_FILE + chmod 0644 $CRON_FILE + crontab $CRON_FILE # 指定定时列表文件 + + echo -e "=>完成\n" + + echo "[step 3/5]启动定时任务,开启每日定时运行" + cron + echo -e "=>完成\n" + + echo "[step 4/5]初始运行,进行Login" + cd /app && dotnet Ray.BiliBiliTool.Console.dll --runTasks=Login + echo -e "=>完成Login\n" + + echo "[step 5/5]初始运行,尝试测试Cookie" + dotnet Ray.BiliBiliTool.Console.dll --runTasks=Test + echo -e "=>完成\n" + + echo -e "[step 全部已完成]\n" + + . /app/scripts/entry_after.sh + + touch /var/log/cron.log #todo:debian似乎并没有记录cron的日志。。。 + tail -f /var/log/cron.log # 追踪cron日志,避免当前脚本终止导致容器终止 diff --git a/helm/bilibili-tool/templates/deployment.yaml b/helm/bilibili-tool/templates/deployment.yaml new file mode 100644 index 0000000..19e9997 --- /dev/null +++ b/helm/bilibili-tool/templates/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "bilibili_tool.fullname" . }} + namespace: {{ .Values.namespace }} + labels: + {{- include "bilibili_tool.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "bilibili_tool.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "bilibili_tool.selectorLabels" . | nindent 8 }} + spec: + {{- if or (eq .Values.volumes.log.enabled true) (eq .Values.volumes.login.enabled true) }} + volumes: + {{- if .Values.volumes.log.enabled }} + - name: {{ .Values.volumes.log.name }} + hostPath: + path: {{ .Values.volumes.log.path }} + {{- end }} + {{- if .Values.volumes.login.enabled }} + - name: {{ .Values.volumes.login.name }} + configMap: + name: {{ .Values.configmap.name }} + items: + - key: "entry.sh" + path: "entry.sh" + mode: 0755 + - key: "entry_before.sh" + path: "entry_before.sh" + mode: 0755 + - key: "entry_after.sh" + path: "entry_after.sh" + mode: 0755 + {{- end }} + {{- end }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + {{- toYaml .Values.env | nindent 12 }} + {{- if or (eq .Values.volumes.log.enabled true) (eq .Values.volumes.login.enabled true) }} + volumeMounts: + {{- if .Values.volumes.log.enabled }} + - mountPath: "/bilibili_tool/Logs" + name: {{ .Values.volumes.log.name }} + {{- end }} + {{- if .Values.volumes.login.enabled }} + - mountPath: "/app/scripts" + name: {{ .Values.volumes.login.name }} + {{- end }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/bilibili-tool/values.yaml b/helm/bilibili-tool/values.yaml new file mode 100644 index 0000000..79db9f2 --- /dev/null +++ b/helm/bilibili-tool/values.yaml @@ -0,0 +1,68 @@ +# Default values for bilibili_tool. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + + +namespace: default +replicaCount: 1 + +configmap: + name: entry + +image: + repository: zai7lou/bilibili_tool_pro + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "1.0.1" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +# For more envs pls take a view at https://github.com/RayWangQvQ/BiliBiliToolPro/blob/main/docs/configuration.md +env: + # cookie - required + - name: Ray_BiliBiliCookies__1 + # past your cookie value + value: "" + # DailyTrigger - required + - name: Ray_DailyTaskConfig__Cron + # This means BiliBili Toll triggers at every day's 08:10 AM + value: "10 8 * * *" + + # Add your custom env vars like + # - name: Ray_Security__IntervalSecondsBetweenRequestApi + # value: "20" + # - name: Ray_Security__RandomSleepMaxMin + # value: "20" + # - name: Ray_LiveLotteryTaskConfig__Cron + # value: "" + +volumes: + # if `enabled=true`, then path and name is required + log: + enabled: true + path: "/tmp/logs" + name: "bili-tool-vol" + login: + enabled: true + name: "entry" + + + +podAnnotations: {} + +resources: + # Recommended to set this resources field + limits: + cpu: 100m + memory: 120Mi + requests: + cpu: 100m + memory: 120Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/krew/.gitignore b/krew/.gitignore new file mode 100644 index 0000000..45f4d81 --- /dev/null +++ b/krew/.gitignore @@ -0,0 +1,2 @@ +bin +bilipro \ No newline at end of file diff --git a/krew/Makefile b/krew/Makefile new file mode 100644 index 0000000..23f9d3c --- /dev/null +++ b/krew/Makefile @@ -0,0 +1,55 @@ +.DEFAULT_GOAL:=help +SHELL := /usr/bin/env bash + +ROOT_DIR := $(shell git rev-parse --show-toplevel) +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) +BIN_NAME ?= kubectl-bilipro +KUBECTL_DIR ?= $(shell which kubectl | awk -F 'kubectl' '{printf "%s\n", $$1 }') + + + +GITCOMMIT ?= `git rev-parse HEAD` + +help: #### display help + @awk 'BEGIN {FS = ":.*## "; printf "\nTargets:\n"} /^[a-zA-Z_-]+:.*?#### / { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + @awk 'BEGIN {FS = ":.* ## "; printf "\n \033[1;32mBuild targets\033[36m\033[0m\n \033[0;37mTargets for building and/or installing CLI plugins on the system.\n Append \"ENVS=\" to the end of these targets to limit the binaries built.\n e.g.: make build-all-tanzu-cli-plugins ENVS=linux-amd64 \n List available at https://github.com/golang/go/blob/master/src/go/build/syslist.go\033[36m\033[0m\n\n"} /^[a-zA-Z_-]+:.*? ## / { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) +##### GLOBAL + +.PHONY: deploy +deploy: build install #### build + install + + +.PHONY: fmt +fmt: #### run go fmt against code + @go fmt ./... + + +.PHONY: vet +vet: #### run go vet against code + @go vet ./... + +.PHONY: update-modules +update-modules: tidy #### update go modules + +.PHONY: tidy +tidy: #### run go mod tidy + @go mod tidy + +.PHONY: build +build: #### build the plugin + @echo "build on ${GOOS}/${GOARCH}" && \ + GOOS=${GOOS} GOARCH=${GOARCH} CGO_ENABLED=0 go build -mod readonly -ldflags "-X github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/cmd.version=${GITCOMMIT}" -o bin/$(BIN_NAME) cmd/kubectl-bilipro.go + +.PHONY: install +install: #### install the plugin + @cd ${ROOT_DIR}/krew/bin && \ + sudo install ./$(BIN_NAME) ${KUBECTL_DIR}/$(BIN_NAME) + +.PHONY: test +test: #### run tests + @cd ${ROOT_DIR}/krew/pkg/utils && \ + rm -rf fixtures && \ + mkdir fixtures && \ + go test ./... -cover && \ + rm -rf fixtures \ No newline at end of file diff --git a/krew/README.md b/krew/README.md new file mode 100644 index 0000000..36697df --- /dev/null +++ b/krew/README.md @@ -0,0 +1,71 @@ +# BiliBiliPro Kubectl Plugin + +## Prerequisites + +- Kubernetes >= v1.23.0. +- go >= v1.18 +- kubectl installed on your local machine, configured to an existing healthy Kubernetes cluster. +- [krew](https://krew.sigs.k8s.io/docs/user-guide/setup/install/) plugin installed + +## Install Plugin + +Command: `cd ./krew && make deploy` +The binary will be generated in cmd/ install it alonside the kubectl binary. + +For example: the kubectl is installed under `/usr/bin`, then put the bilibilipro plugin under `/usr/bin` too. + +## Plugin Commands + +### Deployment && Update + +Prerequsites: please make sure you have the right permission to at least manage namespaces/deployments + +Command: `kubectl bilipro init --config config.yaml` + +Creates Deployment with the needed environments. + +Optional Options: + +- `--image=zai7lou/bilibili_tool_pro:2.0.1` +- `--namespace=bilipro` +- `--image-pull-secret=` +- `--login` to scan QR code to login + +Required Options: + +- `--config=` + +The content of is a yaml array, please refer to the example config yaml under the krew directory. + +For example + +````yaml +- name: Ray_BiliBiliCookies__2 + value: "cookie" + # DailyTrigger - required +- name: Ray_DailyTaskConfig__Cron + value: "11 11 * * *" +```` + +Suggestions: Deploy this workload in namespace other than default or kube-* namespace, because the delete logic should be improved + +### Deletion + +Command: `kubectl bilipro delete [options]` + +Deletes Deployment. +v +Optional Options: + +- `--namespace=` +- `--name=` + +### Version + +Command: `kubectl bilipro version` + +Output the plugin version. + +## Package + +Pls refer to [installation](https://krew.sigs.k8s.io/docs), you can package your own krew plugin diff --git a/krew/cmd/kubectl-bilipro.go b/krew/cmd/kubectl-bilipro.go new file mode 100644 index 0000000..5525636 --- /dev/null +++ b/krew/cmd/kubectl-bilipro.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/pflag" + + "github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/cmd" + helper "github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/utils" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +func main() { + flags := pflag.NewFlagSet("kubectl-bilipro", pflag.ExitOnError) + pflag.CommandLine = flags + + cmd := cmd.NewExecutor(genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) + if err := cmd.Execute(); err != nil { + fmt.Println(helper.GenErrorMsg(helper.SERVER_ERROR, err.Error()).Error()) + os.Exit(1) + } +} diff --git a/krew/config.yaml b/krew/config.yaml new file mode 100644 index 0000000..acb2dec --- /dev/null +++ b/krew/config.yaml @@ -0,0 +1,5 @@ +- name: Ray_BiliBiliCookies__1 + value: "cookie" + # DailyTrigger - required +- name: Ray_DailyTaskConfig__Cron + value: "11 11 * * *" \ No newline at end of file diff --git a/krew/go.mod b/krew/go.mod new file mode 100644 index 0000000..9f7fd48 --- /dev/null +++ b/krew/go.mod @@ -0,0 +1,67 @@ +module github.com/RayWangQvQ/BiliBiliToolPro/krew + +go 1.20 + +require ( + github.com/spf13/cobra v1.4.0 + github.com/spf13/pflag v1.0.5 + k8s.io/api v0.25.4 + k8s.io/apimachinery v0.25.4 + k8s.io/cli-runtime v0.25.4 + k8s.io/client-go v0.25.4 + sigs.k8s.io/kustomize/api v0.12.1 + sigs.k8s.io/kustomize/kyaml v0.13.9 + sigs.k8s.io/yaml v1.3.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-errors/errors v1.0.1 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/swag v0.19.14 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/xlab/treeprint v1.1.0 // indirect + go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/term v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.80.1 // indirect + k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect + k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect +) + +// replace github.com/RayWangQvQ/BiliBiliToolPro/krew => ../krew diff --git a/krew/go.sum b/krew/go.sum new file mode 100644 index 0000000..e381ead --- /dev/null +++ b/krew/go.sum @@ -0,0 +1,517 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= +github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.25.4 h1:3YO8J4RtmG7elEgaWMb4HgmpS2CfY1QlaOz9nwB+ZSs= +k8s.io/api v0.25.4/go.mod h1:IG2+RzyPQLllQxnhzD8KQNEu4c4YvyDTpSMztf4A0OQ= +k8s.io/apimachinery v0.25.4 h1:CtXsuaitMESSu339tfhVXhQrPET+EiWnIY1rcurKnAc= +k8s.io/apimachinery v0.25.4/go.mod h1:jaF9C/iPNM1FuLl7Zuy5b9v+n35HGSh6AQ4HYRkCqwo= +k8s.io/cli-runtime v0.25.4 h1:GTSBN7aKBrc2LqpdO30CmHQqJtRmotxV7XsMSP+QZIk= +k8s.io/cli-runtime v0.25.4/go.mod h1:JGOw1CR8v4Mcz6cEKA7bFQe0bPrNn1l5sGAX1/Ke4Eg= +k8s.io/client-go v0.25.4 h1:3RNRDffAkNU56M/a7gUfXaEzdhZlYhoW8dgViGy5fn8= +k8s.io/client-go v0.25.4/go.mod h1:8trHCAC83XKY0wsBIpbirZU4NTUpbuhc2JnI7OruGZw= +k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= +k8s.io/utils v0.0.0-20221107191617-1a15be271d1d h1:0Smp/HP1OH4Rvhe+4B8nWGERtlqAGSftbSbbmm45oFs= +k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM= +sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s= +sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk= +sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/krew/pkg/cmd/cmd.go b/krew/pkg/cmd/cmd.go new file mode 100644 index 0000000..5066f7a --- /dev/null +++ b/krew/pkg/cmd/cmd.go @@ -0,0 +1,39 @@ +package cmd + +import ( + "log" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +const ( + biliproDesc = `Manage and deploy bilibili pro tools on k8s` + kubeconfig = "kubeconfig" +) + +var ( + confPath string + rootCmd = &cobra.Command{ + Use: "bilipro", + Long: biliproDesc, + SilenceUsage: true, + } +) + +func init() { + rootCmd.PersistentFlags().StringVar(&confPath, kubeconfig, "", "Custom kubeconfig path") + + log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) +} + +// New creates a new root command for kubectl-bilipro +func NewExecutor(streams genericclioptions.IOStreams) *cobra.Command { + cobra.EnableCommandSorting = false + rootCmd.AddCommand(newInitCmd(rootCmd.OutOrStdout(), rootCmd.ErrOrStderr())) + // If you want to update, just init again + rootCmd.AddCommand(newGetCmd(rootCmd.OutOrStdout(), rootCmd.ErrOrStderr())) + rootCmd.AddCommand(newDeleteCmd(rootCmd.OutOrStdout(), rootCmd.ErrOrStderr())) + rootCmd.AddCommand(newVersionCmd(rootCmd.OutOrStdout(), rootCmd.ErrOrStderr())) + return rootCmd +} diff --git a/krew/pkg/cmd/delete.go b/krew/pkg/cmd/delete.go new file mode 100644 index 0000000..a291d4e --- /dev/null +++ b/krew/pkg/cmd/delete.go @@ -0,0 +1,130 @@ +package cmd + +import ( + "fmt" + "io" + "os/exec" + "strings" + + "sigs.k8s.io/kustomize/api/krusty" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/yaml" + + "github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/options" + helper "github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/utils" + "github.com/spf13/cobra" +) + +const ( + deleteDesc = ` +'delete' command delete bilibilipro tool.` + deleteExample = ` kubectl bilipro delete <--name deployment_name>` +) + +type deleteCmd struct { + out io.Writer + errOut io.Writer + output bool + deployOpts options.DeployOptions +} + +func newDeleteCmd(out io.Writer, errOut io.Writer) *cobra.Command { + o := &deleteCmd{out: out, errOut: errOut} + + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete bilibilipro", + Long: deleteDesc, + Example: deleteExample, + Args: cobra.MaximumNArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + + err := o.run(out) + if err != nil { + fmt.Println(err) + return err + } + fmt.Println("bilibili tool is removed from your cluster") + return nil + }, + } + + f := cmd.Flags() + f.StringVarP(&o.deployOpts.Namespace, "namespace", "n", "bilipro", "namespace scope for this request") + f.StringVarP(&o.deployOpts.Name, "name", "", "bilibilipro", "name of deployment to delete") + return cmd +} + +func (o *deleteCmd) run(writer io.Writer) error { + inDiskSys, err := helper.GetResourceFileSys() + if err != nil { + return err + } + + // remove namespace files lastly + resources := []string{"base/bilibiliPro/deployment.yaml"} + + // write the kustomization file + + kustomizationYaml := types.Kustomization{ + TypeMeta: types.TypeMeta{ + Kind: "Kustomization", + APIVersion: "kustomize.config.k8s.io/v1beta1", + }, + Resources: resources, + } + + if o.deployOpts.Namespace != "" { + kustomizationYaml.Namespace = o.deployOpts.Namespace + } + + // Compile the kustomization to a file and create on the in memory filesystem + kustYaml, err := yaml.Marshal(kustomizationYaml) + if err != nil { + return helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error()) + } + + kustFile, err := inDiskSys.Create("./bilipro/kustomization.yaml") + if err != nil { + return helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error()) + } + + _, err = kustFile.Write(kustYaml) + if err != nil { + return helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error()) + } + + // kustomize build the target location + k := krusty.MakeKustomizer( + krusty.MakeDefaultOptions(), + ) + + m, err := k.Run(inDiskSys, "./bilipro") + if err != nil { + return helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error()) + } + + yml, err := m.AsYaml() + if err != nil { + return helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error()) + } + + if o.output { + _, err = writer.Write(yml) + return helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error()) + } + + // do kubectl delete + cmd := exec.Command("kubectl", "delete", "-f", "-") + + if err := helper.Run(cmd, strings.NewReader(string(yml))); err != nil { + return err + } + + // delete the namespace + cmd = exec.Command("kubectl", "delete", "ns", o.deployOpts.Namespace) + if err := helper.Run(cmd, nil); err != nil { + return err + } + return nil +} diff --git a/krew/pkg/cmd/get.go b/krew/pkg/cmd/get.go new file mode 100644 index 0000000..15744cb --- /dev/null +++ b/krew/pkg/cmd/get.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "fmt" + "io" + "os/exec" + + "github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/options" + helper "github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/utils" + "github.com/spf13/cobra" +) + +const ( + getDesc = ` +'get' command get bilibilipro tool deployment.` + getExample = ` kubectl bilipro get <--name deployment_name --namespace namespace_name>` +) + +type getCmd struct { + out io.Writer + errOut io.Writer + + deployOpts options.DeployOptions +} + +func newGetCmd(out io.Writer, errOut io.Writer) *cobra.Command { + o := &getCmd{out: out, errOut: errOut} + + cmd := &cobra.Command{ + Use: "get", + Short: "Get bilibilipro", + Long: getDesc, + Example: getExample, + Args: cobra.MaximumNArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + + err := o.run(out) + if err != nil { + fmt.Println(err) + return err + } + return nil + }, + } + + f := cmd.Flags() + f.StringVarP(&o.deployOpts.Namespace, "namespace", "n", "bilipro", "namespace scope for this request") + f.StringVarP(&o.deployOpts.Name, "name", "", "bilibilipro", "name of deployment to get") + return cmd +} + +func (o *getCmd) run(writer io.Writer) error { + // do kubectl get + cmd := exec.Command("kubectl", "get", "deploy", o.deployOpts.Name, "-n", o.deployOpts.Namespace) + + if err := helper.Run(cmd, nil); err != nil { + return err + } + + return nil +} diff --git a/krew/pkg/cmd/init.go b/krew/pkg/cmd/init.go new file mode 100644 index 0000000..3a1f5cc --- /dev/null +++ b/krew/pkg/cmd/init.go @@ -0,0 +1,286 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "strings" + + corev1 "k8s.io/api/core/v1" + + "sigs.k8s.io/kustomize/kyaml/resid" + + "sigs.k8s.io/yaml" + + "sigs.k8s.io/kustomize/api/types" + + "github.com/spf13/cobra" + + "github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/options" + helper "github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/utils" + "sigs.k8s.io/kustomize/api/krusty" +) + +const ( + initDesc = ` + 'init' command creates BilibiliPro deployment along with all the dependencies.` + initExample = ` kubectl bilipro init --config ` +) + +type initCmd struct { + out io.Writer + errOut io.Writer + output bool + login bool + deployOpts options.DeployOptions +} + +func newInitCmd(out io.Writer, errOut io.Writer) *cobra.Command { + o := &initCmd{out: out, errOut: errOut} + + cmd := &cobra.Command{ + Use: "init", + Short: "Initialize bilipro", + Long: initDesc, + Example: initExample, + Args: cobra.MaximumNArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + err := o.run(out) + if err != nil { + fmt.Println(err) + return err + } + return nil + }, + } + + f := cmd.Flags() + f.StringVarP(&o.deployOpts.Image, "image", "i", "zai7lou/bilibili_tool_pro:2.0.1", "bilibilipro image") + f.StringVarP(&o.deployOpts.Namespace, "namespace", "n", "bilipro", "namespace scope for this request") + f.StringVar(&o.deployOpts.ImagePullSecret, "image-pull-secret", "", "image pull secret to be used for pulling bilibilipro image") + f.StringVarP(&o.deployOpts.ConfigFilePath, "config", "c", "", "the config file contanis the environment variables") + f.BoolVarP(&o.output, "output", "o", false, "dry run this command and generate requisite yaml") + f.BoolVarP(&o.login, "login", "l", false, "scan QR login code") + return cmd +} + +type opStr struct { + Op string `json:"op"` + Path string `json:"path"` + Value string `json:"value"` +} + +type opInterface struct { + Op string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value"` +} + +type normalEnvVars struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// run initializes local config and installs BiliBiliPro tool to Kubernetes Cluster. +func (o *initCmd) run(writer io.Writer) error { + inDiskSys, err := helper.GetResourceFileSys() + if err != nil { + return err + } + + // TODO: All about paths are a little bit tricky should give it more thoughts + + fmt.Println("Creating the kustomization file") + // if the bilibili tool is deployed under system/pre-defined namespace, ignore the namespace file + var resources []string // nolint: go-staticcheck + if o.deployOpts.Namespace == "default" || o.deployOpts.Namespace == "kube-system" || o.deployOpts.Namespace == "kube-public" { + resources = []string{"base/bilibiliPro/deployment.yaml"} + } else { + resources = []string{"base/ns/namespace.yaml", "base/bilibiliPro/deployment.yaml"} + } + + // write the kustomization file + kustomizationYaml := types.Kustomization{ + TypeMeta: types.TypeMeta{ + Kind: "Kustomization", + APIVersion: "kustomize.config.k8s.io/v1beta1", + }, + Resources: resources, + PatchesJson6902: []types.Patch{}, + } + + var deployDepPatches []interface{} + // create patches for the supplied arguments + if o.deployOpts.Image != "" { + deployDepPatches = append(deployDepPatches, opStr{ + Op: "replace", + Path: "/spec/template/spec/containers/0/image", + Value: o.deployOpts.Image, + }) + } + // create patches for the env + content, err := os.ReadFile(o.deployOpts.ConfigFilePath) + if err != nil { + return helper.GenErrorMsg(helper.FILE_ERROR, err.Error()) + } + + envs := []normalEnvVars{} + err = yaml.Unmarshal(content, &envs) + if err != nil { + return helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error()) + } + + deployDepPatches = append(deployDepPatches, opInterface{ + Op: "add", + Path: "/spec/template/spec/containers/0/env", + Value: envs, + }) + + if o.deployOpts.ImagePullSecret != "" { + deployDepPatches = append(deployDepPatches, opInterface{ + Op: "add", + Path: "/spec/template/spec/imagePullSecrets", + Value: []corev1.LocalObjectReference{{Name: o.deployOpts.ImagePullSecret}}, + }) + } + + // attach the patches to the kustomization file + if len(deployDepPatches) > 0 { + kustomizationYaml.PatchesJson6902 = append(kustomizationYaml.PatchesJson6902, types.Patch{ + Patch: o.serializeJSONPatchOps(deployDepPatches), + Target: &types.Selector{ + ResId: resid.ResId{ + Gvk: resid.Gvk{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Name: "bilibilipro", + Namespace: o.deployOpts.Namespace, + }, + }, + }) + } + + // Not deploying in kube-* namespace + if o.deployOpts.Namespace == "kube-system" || o.deployOpts.Namespace == "kube-public" { + fmt.Println("better not deployed under system namesapce") + } + + if o.deployOpts.Namespace != "" { + kustomizationYaml.Namespace = o.deployOpts.Namespace + } + // Compile the kustomization to a file and create on the in memory filesystem + kustYaml, err := yaml.Marshal(kustomizationYaml) + if err != nil { + return err + } + + kustFile, err := inDiskSys.Create("./bilipro/kustomization.yaml") + if err != nil { + return helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error()) + } + + _, err = kustFile.Write(kustYaml) + if err != nil { + return helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error()) + } + + // kustomize build the target location + k := krusty.MakeKustomizer( + krusty.MakeDefaultOptions(), + ) + + m, err := k.Run(inDiskSys, "./bilipro") + if err != nil { + return helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error()) + } + + yml, err := m.AsYaml() + if err != nil { + return helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error()) + } + + if o.output { + _, err = writer.Write(yml) + if err != nil { + return helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error()) + } + } + + fmt.Println("Applying the kustomization file") + // do kubectl apply + // make sure kubectl is under your PATH + cmd := exec.Command("kubectl", "apply", "-f", "-") + + if err := helper.Run(cmd, strings.NewReader(string(yml))); err != nil { + return err + } + + // if there is login required, exectue the login command as the last step + if o.login { + fmt.Println("please login...") + client, _, err := helper.GetK8sClient() + if err != nil { + return err + } + + // get the pod name + podName, err := helper.GetBiliName(client, o.deployOpts.Namespace, "bilibilipro") + if err != nil { + return err + } + + fmt.Println("wait for the deployment to be ready") + // Wait for the deployment ready + checkCmdArgs := []string{ + "rollout", + "status", + "deployment/bilibilipro", + "-n", + o.deployOpts.Namespace, + } + checkCmd := exec.Command("kubectl", checkCmdArgs...) + + for { + if err := checkCmd.Start(); err != nil { + fmt.Printf("deployment is not ready yet, current status: %v\n", err) + continue + } + + err := checkCmd.Wait() + if err == nil { + fmt.Printf("deployment is ready\n") + break + } + fmt.Printf("deployment is not ready yet, current status: %v\n", err) + } + + fmt.Println("please scan the QR code") + // Exec the login command + args := []string{ + "exec", + podName, + "-n", + o.deployOpts.Namespace, + "--", + "dotnet", + "Ray.BiliBiliTool.Console.dll", + "--runTasks=Login", + } + cmd := exec.Command("kubectl", args...) + + if err := helper.Run(cmd, nil); err != nil { + return err + } + } + + return nil +} + +func (o *initCmd) serializeJSONPatchOps(jp []interface{}) string { + jpJSON, _ := json.Marshal(jp) + return string(jpJSON) +} diff --git a/krew/pkg/cmd/version.go b/krew/pkg/cmd/version.go new file mode 100644 index 0000000..d0c85d8 --- /dev/null +++ b/krew/pkg/cmd/version.go @@ -0,0 +1,50 @@ +package cmd + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" +) + +// version provides the version of this plugin +var version = "NO.VERSION" + +const ( + versionDesc = ` +'version' command displays the kubectl plugin version.` + versionExample = ` kubectl bilipro version` +) + +type versionCmd struct { + out io.Writer + errOut io.Writer +} + +func newVersionCmd(out io.Writer, errOut io.Writer) *cobra.Command { + o := &versionCmd{out: out, errOut: errOut} + + cmd := &cobra.Command{ + Use: "version", + Short: "Display plugin version", + Long: versionDesc, + Example: versionExample, + Args: cobra.MaximumNArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + err := o.run() + if err != nil { + fmt.Println(err) + return err + } + return nil + }, + } + + return cmd +} + +// run initializes local config and installs BilibiliPro Plugin to Kubernetes cluster. +func (o *versionCmd) run() error { + fmt.Println(version) + return nil +} diff --git a/krew/pkg/options/deployment.go b/krew/pkg/options/deployment.go new file mode 100644 index 0000000..4382e85 --- /dev/null +++ b/krew/pkg/options/deployment.go @@ -0,0 +1,10 @@ +package options + +// DeployOptions encapsulates the CLI options for a BiliBiliPro +type DeployOptions struct { + Name string + Image string + Namespace string + ImagePullSecret string + ConfigFilePath string +} diff --git a/krew/pkg/resources/asset.go b/krew/pkg/resources/asset.go new file mode 100644 index 0000000..20cea31 --- /dev/null +++ b/krew/pkg/resources/asset.go @@ -0,0 +1,13 @@ +package resources + +import ( + "embed" +) + +//go:embed * +var fs embed.FS + +// GetStaticResources returns the fs with the embedded assets +func GetStaticResources() embed.FS { + return fs +} diff --git a/krew/pkg/resources/base/bilibiliPro/deployment.yaml b/krew/pkg/resources/base/bilibiliPro/deployment.yaml new file mode 100644 index 0000000..97b8d89 --- /dev/null +++ b/krew/pkg/resources/base/bilibiliPro/deployment.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bilibilipro +spec: + selector: + matchLabels: + app: bilibilipro + template: + metadata: + labels: + app: bilibilipro + spec: + containers: + - name: bilibilipro + image: zai7lou/bilibili_tool_pro:2.0.1 + imagePullPolicy: IfNotPresent + resources: + requests: + cpu: 80m + memory: 100M + limits: + cpu: 100m + memory: 120M + + \ No newline at end of file diff --git a/krew/pkg/resources/base/ns/namespace.yaml b/krew/pkg/resources/base/ns/namespace.yaml new file mode 100644 index 0000000..6057bc0 --- /dev/null +++ b/krew/pkg/resources/base/ns/namespace.yaml @@ -0,0 +1,5 @@ + +apiVersion: v1 +kind: Namespace +metadata: + name: bilipro \ No newline at end of file diff --git a/krew/pkg/utils/client.go b/krew/pkg/utils/client.go new file mode 100644 index 0000000..81f0587 --- /dev/null +++ b/krew/pkg/utils/client.go @@ -0,0 +1,73 @@ +package utils + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +func GetK8sClient() (*kubernetes.Clientset, *rest.Config, error) { + var kubeconfig string + + envKubeConfig := os.Getenv("KUBECONFIG") + if envKubeConfig == "" { + home, err := os.UserHomeDir() + if err != nil { + return nil, nil, GenErrorMsg(SERVER_ERROR, err.Error()) + } + kubeconfig = filepath.Join(home, ".kube", "config") + } else { + kubeconfig = envKubeConfig + } + + // use the current context in kubeconfig + config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return nil, nil, GenErrorMsg(SERVER_ERROR, err.Error()) + } + config.QPS = float32(10.0) + config.Burst = 20 + + // create the clientset + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, config, GenErrorMsg(SERVER_ERROR, err.Error()) + } + return clientset, config, nil +} + +func GetBiliName(client *kubernetes.Clientset, namespace, deploymentName string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + defer cancel() + deployment, err := client.AppsV1().Deployments(namespace).Get(ctx, deploymentName, metav1.GetOptions{}) + if err != nil { + return "", GenErrorMsg(SERVER_ERROR, err.Error()) + } + + // we dont do much checks here + selector := deployment.Spec.Selector.MatchLabels + if len(selector) == 0 { + return "", GenErrorMsg(SERVER_ERROR, "deployment doesn't have any selectors, please check the deploy template") + } + listOptions := metav1.ListOptions{ + LabelSelector: labels.Set(selector).String(), + } + pods, err := client.CoreV1().Pods(namespace).List(ctx, listOptions) + if err != nil { + return "", GenErrorMsg(SERVER_ERROR, "cannot list the pods with deploy selectors") + } + if len(pods.Items) != 1 { + return "", GenErrorMsg(SERVER_ERROR, fmt.Sprintf("pod number is expected to be 1, currently %d", len(pods.Items))) + } + + // only one pod is supposed to be existing, soft constraint + return pods.Items[0].ObjectMeta.GetName(), nil +} diff --git a/krew/pkg/utils/client_test.go b/krew/pkg/utils/client_test.go new file mode 100644 index 0000000..0b7dd5f --- /dev/null +++ b/krew/pkg/utils/client_test.go @@ -0,0 +1,19 @@ +package utils + +import ( + "testing" +) + +func TestGetK8sClient(t *testing.T) { + client, _, err := GetK8sClient() + if err != nil { + t.Logf("test failed due to %s", err.Error()) + t.FailNow() + } + + if client == nil { + t.Logf("test failed for returned client is empty, this should not happen") + t.FailNow() + } + +} diff --git a/krew/pkg/utils/cmd.go b/krew/pkg/utils/cmd.go new file mode 100644 index 0000000..5c1dee5 --- /dev/null +++ b/krew/pkg/utils/cmd.go @@ -0,0 +1,38 @@ +package utils + +import ( + "bufio" + "fmt" + "io" + "os/exec" +) + +func Run(cmd *exec.Cmd, in io.Reader) error { + cmd.Stdin = in + + stdoutReader, _ := cmd.StdoutPipe() + stdoutScanner := bufio.NewScanner(stdoutReader) + go func() { + for stdoutScanner.Scan() { + fmt.Println(stdoutScanner.Text()) + } + }() + stderrReader, _ := cmd.StderrPipe() + stderrScanner := bufio.NewScanner(stderrReader) + go func() { + for stderrScanner.Scan() { + fmt.Println(stderrScanner.Text()) + } + }() + err := cmd.Start() + if err != nil { + return GenErrorMsg(EXEC_ERROR, err.Error()) + } + + // Stuck here until there are out and err + err = cmd.Wait() + if err != nil { + return GenErrorMsg(EXEC_ERROR, err.Error()) + } + return nil +} diff --git a/krew/pkg/utils/cmd_test.go b/krew/pkg/utils/cmd_test.go new file mode 100644 index 0000000..2329a55 --- /dev/null +++ b/krew/pkg/utils/cmd_test.go @@ -0,0 +1,17 @@ +package utils + +import ( + "os/exec" + "testing" +) + +func TestRun(t *testing.T) { + + // Just make sure there is no error... + testCmd := exec.Command("ls") + err := Run(testCmd, nil) + if err != nil { + t.Logf("test failed due to %s", err.Error()) + t.FailNow() + } +} diff --git a/krew/pkg/utils/error.go b/krew/pkg/utils/error.go new file mode 100644 index 0000000..813d3a2 --- /dev/null +++ b/krew/pkg/utils/error.go @@ -0,0 +1,22 @@ +package utils + +import ( + "errors" + "fmt" +) + +const ( + // For errors about kustomize + TEMPLATE_ERROR = "template error" + // For errors about file system + FILE_ERROR = "file system error" + // For errors about create/delete/... resources in cluster + SERVER_ERROR = "cluster operation error" + // For exec errors + EXEC_ERROR = "exec error" +) + +func GenErrorMsg(errType, customMsg string) error { + errorMsg := fmt.Sprintf("[ERROR] %s: %s", errType, customMsg) + return errors.New(errorMsg) +} diff --git a/krew/pkg/utils/error_test.go b/krew/pkg/utils/error_test.go new file mode 100644 index 0000000..8625191 --- /dev/null +++ b/krew/pkg/utils/error_test.go @@ -0,0 +1,38 @@ +package utils + +import ( + "strings" + "testing" +) + +func TestServerGenErrorMsg(t *testing.T) { + expectedErr := GenErrorMsg(SERVER_ERROR, "test for server") + if !strings.Contains(expectedErr.Error(), SERVER_ERROR) { + t.Logf("server error generate failed") + t.FailNow() + } +} + +func TestTemplateGenErrorMsg(t *testing.T) { + expectedErr := GenErrorMsg(TEMPLATE_ERROR, "test for template") + if !strings.Contains(expectedErr.Error(), TEMPLATE_ERROR) { + t.Logf("template error generate failed") + t.FailNow() + } +} + +func TestExecGenErrorMsg(t *testing.T) { + expectedErr := GenErrorMsg(EXEC_ERROR, "test for exec") + if !strings.Contains(expectedErr.Error(), EXEC_ERROR) { + t.Logf("error error generate failed") + t.FailNow() + } +} + +func TestFileGenErrorMsg(t *testing.T) { + expectedErr := GenErrorMsg(FILE_ERROR, "test for file") + if !strings.Contains(expectedErr.Error(), FILE_ERROR) { + t.Logf("file error generate failed") + t.FailNow() + } +} diff --git a/krew/pkg/utils/fileSys.go b/krew/pkg/utils/fileSys.go new file mode 100644 index 0000000..30b20ab --- /dev/null +++ b/krew/pkg/utils/fileSys.go @@ -0,0 +1,83 @@ +package utils + +import ( + "io" + "io/fs" + "path" + "strings" + + "github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/resources" + "sigs.k8s.io/kustomize/kyaml/filesys" +) + +var assetFS = resources.GetStaticResources() + +// GetResourceFileSys file +func GetResourceFileSys() (filesys.FileSystem, error) { + inDiskSys := filesys.MakeFsOnDisk() + // copy from the resources into the target folder on the in memory FS + if err := copyDirtoDiskFS(".", "bilipro", inDiskSys); err != nil { + return nil, GenErrorMsg(FILE_ERROR, err.Error()) + } + return inDiskSys, nil +} + +func copyFileToDiskFS(src, dst string, diskFS filesys.FileSystem) error { + // skip all .go files + if strings.HasSuffix(src, ".go") { + return nil + } + var err error + var srcFileDesc fs.File + var dstFileDesc filesys.File + + if srcFileDesc, err = assetFS.Open(src); err != nil { + return GenErrorMsg(FILE_ERROR, err.Error()) + } + defer srcFileDesc.Close() + + if dstFileDesc, err = diskFS.Create(dst); err != nil { + return GenErrorMsg(FILE_ERROR, err.Error()) + } + defer dstFileDesc.Close() + + // Note: I had to read the whole string, for some reason io.Copy was not copying the whole content + input, err := io.ReadAll(srcFileDesc) + if err != nil { + return GenErrorMsg(FILE_ERROR, err.Error()) + } + + _, err = dstFileDesc.Write(input) + if err != nil { + return GenErrorMsg(FILE_ERROR, err.Error()) + } + return nil +} + +func copyDirtoDiskFS(src string, dst string, diskFS filesys.FileSystem) error { + var err error + var fds []fs.DirEntry + + if err = diskFS.MkdirAll(dst); err != nil { + return GenErrorMsg(FILE_ERROR, err.Error()) + } + + if fds, err = assetFS.ReadDir(src); err != nil { + return GenErrorMsg(FILE_ERROR, err.Error()) + } + for _, fd := range fds { + srcfp := path.Join(src, fd.Name()) + dstfp := path.Join(dst, fd.Name()) + + if fd.IsDir() { + if err = copyDirtoDiskFS(srcfp, dstfp, diskFS); err != nil { + return GenErrorMsg(FILE_ERROR, err.Error()) + } + } else { + if err = copyFileToDiskFS(srcfp, dstfp, diskFS); err != nil { + return GenErrorMsg(FILE_ERROR, err.Error()) + } + } + } + return nil +} diff --git a/krew/pkg/utils/fileSys_test.go b/krew/pkg/utils/fileSys_test.go new file mode 100644 index 0000000..6e9c5dd --- /dev/null +++ b/krew/pkg/utils/fileSys_test.go @@ -0,0 +1,88 @@ +package utils + +import ( + "io" + "os" + "path/filepath" + "testing" + + "sigs.k8s.io/kustomize/kyaml/filesys" +) + +func TestCopyFiletoDiskFS(t *testing.T) { + expectedFile, err := assetFS.Open("base/ns/namespace.yaml") + if err != nil { + t.Logf("test failed due to %s", err.Error()) + t.FailNow() + } + + expectedOutput, err := io.ReadAll(expectedFile) + if err != nil { + t.Logf("test failed due to %s", err.Error()) + t.FailNow() + } + + dir, err := os.Getwd() + if err != nil { + t.Logf("test failed due to %s", err.Error()) + t.FailNow() + } + + inDiskSys := filesys.MakeFsOnDisk() + + err = copyFileToDiskFS("base/ns/namespace.yaml", filepath.Join(dir, "fixtures/testNamespace.yaml"), inDiskSys) + if err != nil { + t.Logf("test failed due to %s", err.Error()) + t.FailNow() + } + + actualOutput, err := os.ReadFile(filepath.Join(dir, "fixtures/testNamespace.yaml")) + if err != nil { + t.Logf("test failed due to %s", err.Error()) + t.FailNow() + } + + if string(expectedOutput) != string(actualOutput) { + t.Logf("test failed due to copy file failed") + t.FailNow() + } +} + +func TestCopyDirtoDiskFS(t *testing.T) { + expectedFile, err := assetFS.Open("base/bilibiliPro/deployment.yaml") + if err != nil { + t.Logf("test failed due to %s", err.Error()) + t.FailNow() + } + + expectedOutput, err := io.ReadAll(expectedFile) + if err != nil { + t.Logf("test failed due to %s", err.Error()) + t.FailNow() + } + + dir, err := os.Getwd() + if err != nil { + t.Logf("test failed due to %s", err.Error()) + t.FailNow() + } + + inDiskSys := filesys.MakeFsOnDisk() + + err = copyDirtoDiskFS("base/bilibiliPro", filepath.Join(dir, "fixtures"), inDiskSys) + if err != nil { + t.Logf("test failed due to %s", err.Error()) + t.FailNow() + } + + actualOutput, err := os.ReadFile(filepath.Join(dir, "fixtures/deployment.yaml")) + if err != nil { + t.Logf("test failed due to %s", err.Error()) + t.FailNow() + } + + if string(expectedOutput) != string(actualOutput) { + t.Logf("test failed due to copy file failed") + t.FailNow() + } +} diff --git a/podman/README.md b/podman/README.md new file mode 100644 index 0000000..33100f9 --- /dev/null +++ b/podman/README.md @@ -0,0 +1,130 @@ +# Podman 使用说明 + + +- [1. 前期工作](#1-前期工作) + - [1.1. Podman环境](#11-podman环境) + - [1.2. 从Docker迁移](#12-从docker迁移) +- [2. 运行容器](#2-运行容器) + - [2.1. 极简版](#21-极简版) + - [2.2. 综合版](#22-综合版) +- [3. 登录](#3-登录) +- [4. 添加 Bili 账号](#4-添加-bili-账号) +- [5. 自己构建镜像(非必须)](#5-自己构建镜像非必须) +- [6. 其他](#6-其他) + + + +## 1. 前期工作 + +### 1.1. Podman环境 + +请确认已安装了Podman所需环境([Podman](https://podman.io/) + +安装完成后,请执行`podman -v`检查是否安装成功,请执行`podman info`检查虚拟机环境是否正常。 + +常用命令参考: + +``` +# 查看版本 +podman -v + +# 初始化虚拟机 +podman machine init + +# 启动虚拟机 +podman machine start + +# 查看信息 +podman info +``` + +### 1.2. 从Docker迁移 + +Podman可以和Docker共存,命令也基本可以通用。 + +但挂载逻辑有点区别,podman挂载时,如果宿主机下没有指定的文件夹,podman不会像docker一样去自动创建文件夹,而是会报异常。 + +所以在挂载文件夹时,需要先手动在宿主机上mkdir创建文件夹。 + +## 2. 运行容器 + +以下提供极简版和综合版两个版本,一个简单一个复杂,供参考 + +### 2.1. 极简版 + +``` +# 生成并运行容器 +podman run -itd --name="bili_tool_web" docker.io/zai7lou/bili_tool_web + +# 查看实时日志 +podman logs -f bili_tool_web +``` + +### 2.2. 综合版 + +``` +# 创建文件和文件夹 +mkdir -p /bili_tool_web && cd /bili_tool_web +mkdir -p Logs + +# 下载appsettings.json +mkdir -p config +cd ./config +wget https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/main/docker/sample/config/cookies.json +cd .. + +# 运行 +podman run -itd --name="bili_tool_web" \ + -v ./Logs:/app/Logs \ + -v ./config:/app/config \ + -e DailyTaskConfig__Cron="0 0 15 * * ?" \ + -e LiveLotteryTaskConfig__Cron="0 0 22 * * ?" \ + -e UnfollowBatchedTaskConfig__Cron="0 0 6 1 * ?" \ + -e VipBigPointConfig__Cron="0 7 1 * * *" \ + -e DailyTaskConfig__NumberOfCoins="5" + docker.io/zai7lou/bili_tool_web + +# 查看实时日志 +podman logs -f bili +``` + +其他指令参考: + +``` +# 查看容器运行状态 +podman ps -a + +# 进入容器 +podman exec -it bili bash +``` + +## 3. 登录 + +- 默认用户:`admin` +- 默认密码:`BiliTool@2233` + +首次登陆后,请到`Admin`页面修改密码。 + +## 4. 添加 Bili 账号 + +扫码进行登录。 + +![trigger](../docs/imgs/web-trigger-login.png) + +![login](../docs/imgs/docker-login.png) + +## 5. 自己构建镜像(非必须) + +目前我提供和维护的镜像:`[zai7lou/bilibili_tool_web](https://hub.docker.com/repository/docker/zai7lou/bilibili_tool_web)`; + +如果有需要(大部分都不需要),可以使用源码自己构建镜像,如下: + +在有项目的Dockerfile的目录运行 + +`podman build -t TARGET_NAME .` + + `TARGET_NAME`为镜像名称和版本,可以自己起个名字 + +## 6. 其他 + +镜像使用的是docker仓库的镜像。 diff --git a/podman/build/buildImage.cmd b/podman/build/buildImage.cmd new file mode 100644 index 0000000..6804631 --- /dev/null +++ b/podman/build/buildImage.cmd @@ -0,0 +1,8 @@ +@echo off + +REM start to build +echo Start to build image +@echo on +podman build -t docker.io/zai7lou/bilibili_tool_pro:latest ../.. +@echo off +pause diff --git a/qinglong/DefaultTasks/bili_task_base.sh b/qinglong/DefaultTasks/bili_task_base.sh new file mode 100644 index 0000000..c3ca993 --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_base.sh @@ -0,0 +1,472 @@ +#!/usr/bin/env bash +# cron:0 0 1 1 * +# new Env("bili_base") + +# Stop script on NZEC +set -e +# Stop script if unbound variable found (use ${var:-} if intentional) +set -u +# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success +# This is causing it to fail +set -o pipefail + +verbose=false # 开启debug日志 +bili_repo="raywangqvq/bilibilitoolpro" # 仓库地址 +bili_branch="" # 分支名,空或_develop +prefer_mode=${BILI_MODE:-"dotnet"} # dotnet或bilitool,需要通过环境变量配置 +github_proxy=${BILI_GITHUB_PROXY:-""} # 下载github release包时使用的代理,会拼在地址前面,需要通过环境变量配置 +export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 # 解决抽风问题 + +# Use in the the functions: eval $invocation +invocation='say_verbose "Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}"' + +# standard output may be used as a return value in the functions +# we need a way to write text on the screen in the functions so that +# it won't interfere with the return value. +# Exposing stream 3 as a pipe to standard output of the script itself +exec 3>&1 + +# Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors. +# See if stdout is a terminal +if [ -t 1 ] && command -v tput >/dev/null; then + # see if it supports colors + ncolors=$(tput colors || echo 0) + if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then + bold="$(tput bold || echo)" + normal="$(tput sgr0 || echo)" + black="$(tput setaf 0 || echo)" + red="$(tput setaf 1 || echo)" + green="$(tput setaf 2 || echo)" + yellow="$(tput setaf 3 || echo)" + blue="$(tput setaf 4 || echo)" + magenta="$(tput setaf 5 || echo)" + cyan="$(tput setaf 6 || echo)" + white="$(tput setaf 7 || echo)" + fi +fi + +say_warning() { + printf "%b\n" "${yellow:-}bilitool: Warning: $1${normal:-}" >&3 +} + +say_err() { + printf "%b\n" "${red:-}bilitool: Error: $1${normal:-}" >&2 +} + +say() { + # using stream 3 (defined in the beginning) to not interfere with stdout of functions + # which may be used as return value + printf "%b\n" "${cyan:-}bilitool:${normal:-} $1" >&3 +} + +say_verbose() { + if [ "$verbose" = true ]; then + say "$1" + fi +} + +QL_DIR=${QL_DIR:-"/ql"} +QL_BRANCH=${QL_BRANCH:-"develop"} +DefaultCronRule=${DefaultCronRule:-""} +CpuWarn=${CpuWarn:-""} +MemoryWarn=${MemoryWarn:-""} +DiskWarn=${DiskWarn:-""} + +dir_repo=${dir_repo:-"$QL_DIR/data/repo"} +# 需要兼容老版本青龙,https://github.com/RayWangQvQ/BiliBiliToolPro/issues/728 +if [ ! -d "$dir_repo" ] && [ -d "$QL_DIR/repo" ]; then + dir_repo="$QL_DIR/repo" +fi +dir_shell=$QL_DIR/shell +touch $dir_shell/env.sh && . $dir_shell/env.sh +touch /root/.bashrc && . /root/.bashrc + +# 目录 +say "青龙repo目录: $dir_repo" +qinglong_bili_repo="$(echo "$bili_repo" | sed 's/\//_/g')${bili_branch}" +qinglong_bili_repo_dir="$(find $dir_repo -type d \( -iname $qinglong_bili_repo -o -iname ${qinglong_bili_repo}_main \) | head -1)" +say "bili仓库目录: $qinglong_bili_repo_dir" + +current_linux_os="debian" # 或alpine +current_os="linux" # 或linux-musl +machine_architecture="x64" # 或arm、arm64 + +bilitool_installed_version=0 + +# 以下操作仅在bilitool仓库的根bin文件下执行 +cd $qinglong_bili_repo_dir +mkdir -p bin && cd $qinglong_bili_repo_dir/bin + +# 判断是否存在某指令 +machine_has() { + eval $invocation + + command -v "$1" >/dev/null 2>&1 + return $? +} + +# 判断系统架构 +# 输出:arm、arm64、x64 +get_machine_architecture() { + eval $invocation + + if command -v uname >/dev/null; then + CPUName=$(uname -m) + case $CPUName in + armv*l) + echo "arm" + return 0 + ;; + aarch64 | arm64) + echo "arm64" + return 0 + ;; + esac + fi + + # Always default to 'x64' + echo "x64" + return 0 +} + +# 获取linux系统名称 +# 输出:debian.10、debian.11、debian.12、ubuntu.20.04、ubuntu.22.04、alpine.3.4.3... +get_linux_platform_name() { + eval $invocation + + if [ -e /etc/os-release ]; then + . /etc/os-release + echo "$ID${VERSION_ID:+.${VERSION_ID}}" + return 0 + elif [ -e /etc/redhat-release ]; then + local redhatRelease=$(&1 || true) | grep -q musl +} + +# 获取当前系统名称 +# 输出:linux、linux-musl、osx、freebsd +get_current_os_name() { + eval $invocation + + local uname=$(uname) + if [ "$uname" = "Darwin" ]; then + say_warning "当前系统:osx" + echo "osx" + return 1 + elif [ "$uname" = "FreeBSD" ]; then + say_warning "当前系统:freebsd" + echo "freebsd" + return 1 + elif [ "$uname" = "Linux" ]; then + local linux_platform_name="" + linux_platform_name="$(get_linux_platform_name)" || true + say "当前系统发行版本:$linux_platform_name" + + if [ "$linux_platform_name" = "rhel.6" ]; then + echo $linux_platform_name + return 1 + elif is_musl_based_distro; then + echo "linux-musl" + return 0 + elif [ "$linux_platform_name" = "linux-musl" ]; then + echo "linux-musl" + return 0 + else + echo "linux" + return 0 + fi + fi + + say_err "OS name could not be detected: UName = $uname" + return 1 +} + +# 检查操作系统 +check_os() { + eval $invocation + + current_os="$(get_current_os_name)" + say "当前系统:$current_os" + + machine_architecture="$(get_machine_architecture)" + say "当前架构:$machine_architecture" + + if [ "$current_os" = "linux" ]; then + current_linux_os="debian" # 当前青龙只有debian和aplpine两种 + if ! machine_has curl; then + say "curl未安装,开始安装依赖..." + apt-get update + apt-get install -y curl + fi + else + current_linux_os="alpine" + if ! machine_has curl; then + say "curl未安装,开始安装依赖..." + apk update + apk add -y curl + fi + fi + + say "当前选择的运行方式:$prefer_mode" +} + +# 检查安装jq +check_jq() { + if [ "$current_linux_os" = "debian" ]; then + if ! machine_has jq; then + say "jq未安装,开始安装依赖..." + apt-get update + apt-get install -y jq + fi + else + if ! machine_has jq; then + say "jq未安装,开始安装依赖..." + apk update + apk add -y jq + fi + fi +} + +# 检查安装unzip +check_unzip() { + if [ "$current_linux_os" = "debian" ]; then + if ! machine_has unzip; then + say "unzip未安装,开始安装依赖..." + apt-get update + apt-get install -y unzip + fi + else + if ! machine_has unzip; then + say "jq未安装,开始安装依赖..." + apk update + apk add -y unzip + fi + fi +} + +# 检查dotnet +check_dotnet() { + eval $invocation + + dotnetVersion=$(dotnet --version) + say "当前dotnet版本:$dotnetVersion" + if [[ $(echo "$dotnetVersion" | grep -oE '^[0-9]+') -ge 8 ]]; then + say "已安装,且版本满足" + say "which dotnet: $(which dotnet)" + return 0 + else + say "未安装" + return 1 + fi +} + +# 检查bilitool +check_bilitool() { + eval $invocation + + TAG_FILE="./tag.txt" + touch $TAG_FILE + local STORED_TAG=$(cat $TAG_FILE 2>/dev/null) + + #如果STORED_TAG为空,则返回1 + if [[ -z $STORED_TAG ]]; then + say "tag.txt为空,未安装过" + return 1 + fi + + say "tag.txt记录的版本:$STORED_TAG" + + # 查找当前目录下是否有叫Ray.BiliBiliTool.Console的文件 + if [ -f "./Ray.BiliBiliTool.Console" ]; then + say "bilitool已安装" + bilitool_installed_version=$STORED_TAG + return 0 + else + say "bilitool未安装" + return 1 + fi +} + +# 检查环境 +check_installed() { + eval $invocation + + if [ "$prefer_mode" == "dotnet" ]; then + check_dotnet + return $? + fi + + if [ "$prefer_mode" == "bilitool" ]; then + check_bilitool + return $? + fi + + return 1 +} + +# 使用官方脚本安装dotnet +install_dotnet_by_script() { + eval $invocation + + say "再尝试使用官方脚本安装" + curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 8.0 --verbose + + say "添加到PATH" + local exportFile="/root/.bashrc" + touch $exportFile + echo '' >>$exportFile + echo 'export DOTNET_ROOT=$HOME/.dotnet' >>$exportFile + echo 'export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools' >>$exportFile + . $exportFile +} + +# 安装dotnet环境 +install_dotnet() { + eval $invocation + + say "开始安装dotnet" + say "当前系统:$current_linux_os" + if [[ $current_linux_os == "debian" ]]; then + say "使用apt安装" + + if ! (curl -s -m 5 www.google.com >/dev/nul); then + say "机器位于墙内,切换为包源为国内镜像源" + cp /etc/apt/sources.list /etc/apt/sources.list.bak + sed -i 's/https:\/\/deb.debian.org/https:\/\/mirrors.ustc.edu.cn/g' /etc/apt/sources.list + sed -i 's/http:\/\/deb.debian.org/https:\/\/mirrors.ustc.edu.cn/g' /etc/apt/sources.list + apt-get update + fi + { + . /etc/os-release + curl -o packages-microsoft-prod.deb https://packages.microsoft.com/config/debian/$VERSION_ID/packages-microsoft-prod.deb + dpkg -i packages-microsoft-prod.deb + rm packages-microsoft-prod.deb + apt-get update && apt-get install -y dotnet-sdk-8.0 + } || { + install_dotnet_by_script + } + else + say "使用apk安装" + if ! (curl -s -m 5 www.google.com >/dev/nul); then + say "机器位于墙内,切换为包源为国内镜像源" + cp /etc/apk/repositories /etc/apk/repositories.bak + sed -i 's/https:\/\/dl-cdn.alpinelinux.org/https:\/\/mirrors.ustc.edu.cn/g' /etc/apk/repositories + sed -i 's/http:\/\/dl-cdn.alpinelinux.org/https:\/\/mirrors.ustc.edu.cn/g' /etc/apk/repositories + apk update + fi + { + apk add dotnet8-sdk # https://pkgs.alpinelinux.org/packages + } || { + install_dotnet_by_script + } + fi + dotnet --version && say "which dotnet: $(which dotnet)" && say "安装成功" + return $? +} + +# 从github获取bilitool下载地址 +get_download_url() { + eval $invocation + + tag=$1 + url="${github_proxy}https://github.com/RayWangQvQ/BiliBiliToolPro/releases/download/$tag/bilibili-tool-pro-v$tag-$current_os-$machine_architecture.zip" + say "下载地址:$url" + echo $url + return 0 +} + +# 安装bilitool +install_bilitool() { + eval $invocation + + say "开始安装bilitool" + # 获取最新的release信息 + LATEST_RELEASE=$(curl -s https://api.github.com/repos/$bili_repo/releases/latest) + + # 解析最新的tag名称 + check_jq + LATEST_TAG=$(echo $LATEST_RELEASE | jq -r '.tag_name') + say "最新版本:$LATEST_TAG" + + # 读取之前存储的tag并比较 + if [ "$LATEST_TAG" != "$bilitool_installed_version" ]; then + # 如果不一样,则需要更新安装 + ASSET_URL=$(get_download_url $LATEST_TAG) + + # 使用curl下载文件到当前目录下的test.zip文件 + local zip_file_name="bilitool-$LATEST_TAG.zip" + curl -L -o "$zip_file_name" $ASSET_URL + + # 解压 + check_unzip + unzip -jo "$zip_file_name" -d ./ && + rm "$zip_file_name" && + rm -f appsettings.* + + # 更新tag.txt文件 + echo $LATEST_TAG >./tag.txt + else + say "已经是最新版本,无需下载。" + fi +} + +## 安装dotnet(如果未安装过) +install() { + eval $invocation + + if check_installed; then + say "环境正常,本次无需安装" + else + say "开始安装环境" + if [ "$prefer_mode" == "dotnet" ]; then + install_dotnet || { + say_err "安装失败" + say_err "请根据文档自行在青龙容器中安装dotnet:https://learn.microsoft.com/zh-cn/dotnet/core/install/linux-$current_linux_os" + say_err "或者尝试切换运行模式为bilitool,它不需要安装dotnet:https://github.com/RayWangQvQ/BiliBiliToolPro/blob/develop/qinglong/README.md" + } + fi + + if [ "$prefer_mode" == "bilitool" ]; then + install_bilitool || { + say_err "安装失败,请检查日志并重试" + say_err "或者尝试切换运行模式为dotnet:https://github.com/RayWangQvQ/BiliBiliToolPro/blob/develop/qinglong/README.md" + } + fi + fi +} + +# 运行bilitool任务 +run_task() { + eval $invocation + + local target_code=$1 + + export Ray_PlatformType=QingLong + export Ray_RunTasks=$target_code + + cd $qinglong_bili_repo_dir/src/Ray.BiliBiliTool.Console + + if [ "$prefer_mode" == "dotnet" ]; then + dotnet run --ENVIRONMENT=Production + else + cp -f $qinglong_bili_repo_dir/bin/Ray.BiliBiliTool.Console . + chmod +x ./Ray.BiliBiliTool.Console && ./Ray.BiliBiliTool.Console --ENVIRONMENT=Production + fi +} + +check_os +install diff --git a/qinglong/DefaultTasks/bili_task_charge.sh b/qinglong/DefaultTasks/bili_task_charge.sh new file mode 100644 index 0000000..10a7c9a --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_charge.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 12 * * * +# new Env("bili免费B币券充电任务") + +. bili_task_base.sh + +target_task_code="Charge" +run_task "${target_task_code}" diff --git a/qinglong/DefaultTasks/bili_task_daily.sh b/qinglong/DefaultTasks/bili_task_daily.sh new file mode 100644 index 0000000..2c8e075 --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_daily.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 9 * * * +# new Env("bili每日任务") + +. bili_task_base.sh + +target_task_code="Daily" +run_task "${target_task_code}" \ No newline at end of file diff --git a/qinglong/DefaultTasks/bili_task_liveFansMedal.sh b/qinglong/DefaultTasks/bili_task_liveFansMedal.sh new file mode 100644 index 0000000..c8141a2 --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_liveFansMedal.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:5 0 * * * +# new Env("bili直播粉丝牌") + +. bili_task_base.sh + +target_task_code="LiveFansMedal" +run_task "${target_task_code}" \ No newline at end of file diff --git a/qinglong/DefaultTasks/bili_task_liveLottery.sh b/qinglong/DefaultTasks/bili_task_liveLottery.sh new file mode 100644 index 0000000..15eed16 --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_liveLottery.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 13 * * * +# new Env("bili天选时刻") + +. bili_task_base.sh + +target_task_code="LiveLottery" +run_task "${target_task_code}" \ No newline at end of file diff --git a/qinglong/DefaultTasks/bili_task_login.sh b/qinglong/DefaultTasks/bili_task_login.sh new file mode 100644 index 0000000..a345e3a --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_login.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 0 1 1 * +# new Env("bili扫码登录") + +. bili_task_base.sh + +target_task_code="Login" +run_task "${target_task_code}" \ No newline at end of file diff --git a/qinglong/DefaultTasks/bili_task_manga.sh b/qinglong/DefaultTasks/bili_task_manga.sh new file mode 100644 index 0000000..c1b0be3 --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_manga.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 14 * * * +# new Env("bili漫画任务") + +. bili_task_base.sh + +target_task_code="Manga" +run_task "${target_task_code}" diff --git a/qinglong/DefaultTasks/bili_task_manga_privilege.sh b/qinglong/DefaultTasks/bili_task_manga_privilege.sh new file mode 100644 index 0000000..cf8874a --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_manga_privilege.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 15 * * * +# new Env("bili领取大会员漫画权益任务") + +. bili_task_base.sh + +target_task_code="MangaPrivilege" +run_task "${target_task_code}" diff --git a/qinglong/DefaultTasks/bili_task_silver2coin.sh b/qinglong/DefaultTasks/bili_task_silver2coin.sh new file mode 100644 index 0000000..8984f7e --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_silver2coin.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 8 * * * +# new Env("bili银瓜子兑换硬币任务") + +. bili_task_base.sh + +target_task_code="Silver2Coin" +run_task "${target_task_code}" diff --git a/qinglong/DefaultTasks/bili_task_test.sh b/qinglong/DefaultTasks/bili_task_test.sh new file mode 100644 index 0000000..56e51b1 --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_test.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 8 * * * +# new Env("bili测试ck") + +. bili_task_base.sh + +target_task_code="Test" +run_task "${target_task_code}" \ No newline at end of file diff --git a/qinglong/DefaultTasks/bili_task_tryFix.sh b/qinglong/DefaultTasks/bili_task_tryFix.sh new file mode 100644 index 0000000..122f139 --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_tryFix.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# cron:0 0 1 1 * +# new Env("bili尝试修复异常") + +dir_shell=$QL_DIR/shell +. $dir_shell/share.sh +. /root/.bashrc + +bili_repo="raywangqvq_bilibilitoolpro" +bili_branch="" + +echo "青龙repo目录: $dir_repo" +qinglong_bili_repo="$(echo "$bili_repo" | sed 's/\//_/g')${bili_branch}" +qinglong_bili_repo_dir="$(find $dir_repo -type d \( -iname $qinglong_bili_repo -o -iname ${qinglong_bili_repo}_main \) | head -1)" +echo "bili仓库目录: $qinglong_bili_repo_dir" + +echo -e "清理缓存...\n" +cd $qinglong_bili_repo_dir +find . -type d -name "bin" -exec rm -rf {} + +find . -type d -name "obj" -exec rm -rf {} + +echo -e "清理完成\n" + +echo "检测dotnet..." +dotnetVersion=$(dotnet --version) +echo "当前dotnet版本:$dotnetVersion" +if [[ $(echo "$dotnetVersion" | grep -oE '^[0-9]+') -ge 8 ]]; then + echo "已安装,且版本满足" +else + echo "which dotnet: $(which dotnet)" + echo "Path: $PATH" + rm -f /usr/local/bin/dotnet +fi +echo "检测dotnet结束" \ No newline at end of file diff --git a/qinglong/DefaultTasks/bili_task_unfollowBatched.sh b/qinglong/DefaultTasks/bili_task_unfollowBatched.sh new file mode 100644 index 0000000..523f2ce --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_unfollowBatched.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 12 1 * * +# new Env("bili批量取关主播") + +. bili_task_base.sh + +target_task_code="UnfollowBatched" +run_task "${target_task_code}" \ No newline at end of file diff --git a/qinglong/DefaultTasks/bili_task_vipBigPoint.sh b/qinglong/DefaultTasks/bili_task_vipBigPoint.sh new file mode 100644 index 0000000..d88f655 --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_vipBigPoint.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:7 1 * * * +# new Env("bili大会员大积分") + +. bili_task_base.sh + +target_task_code="VipBigPoint" +run_task "${target_task_code}" \ No newline at end of file diff --git a/qinglong/DefaultTasks/bili_task_vip_privilege.sh b/qinglong/DefaultTasks/bili_task_vip_privilege.sh new file mode 100644 index 0000000..a2df739 --- /dev/null +++ b/qinglong/DefaultTasks/bili_task_vip_privilege.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 1 * * * +# new Env("bili领取大会员福利任务") + +. bili_task_base.sh + +target_task_code="VipPrivilege" +run_task "${target_task_code}" diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_base.sh b/qinglong/DefaultTasks/dev/bili_dev_task_base.sh new file mode 100644 index 0000000..bf4c9ed --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_base.sh @@ -0,0 +1,472 @@ +#!/usr/bin/env bash +# cron:0 0 1 1 * +# new Env("bili_dev_task_base"); + +# Stop script on NZEC +set -e +# Stop script if unbound variable found (use ${var:-} if intentional) +set -u +# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success +# This is causing it to fail +set -o pipefail + +verbose=false # 开启debug日志 +bili_repo="raywangqvq/bilibilitoolpro" # 仓库地址 +bili_branch="_develop" # 分支名,空或_develop +prefer_mode=${BILI_MODE:-"dotnet"} # dotnet或bilitool,需要通过环境变量配置 +github_proxy=${BILI_GITHUB_PROXY:-""} # 下载github release包时使用的代理,会拼在地址前面,需要通过环境变量配置 +export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 # 解决抽风问题 + +# Use in the the functions: eval $invocation +invocation='say_verbose "Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}"' + +# standard output may be used as a return value in the functions +# we need a way to write text on the screen in the functions so that +# it won't interfere with the return value. +# Exposing stream 3 as a pipe to standard output of the script itself +exec 3>&1 + +# Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors. +# See if stdout is a terminal +if [ -t 1 ] && command -v tput >/dev/null; then + # see if it supports colors + ncolors=$(tput colors || echo 0) + if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then + bold="$(tput bold || echo)" + normal="$(tput sgr0 || echo)" + black="$(tput setaf 0 || echo)" + red="$(tput setaf 1 || echo)" + green="$(tput setaf 2 || echo)" + yellow="$(tput setaf 3 || echo)" + blue="$(tput setaf 4 || echo)" + magenta="$(tput setaf 5 || echo)" + cyan="$(tput setaf 6 || echo)" + white="$(tput setaf 7 || echo)" + fi +fi + +say_warning() { + printf "%b\n" "${yellow:-}bilitool: Warning: $1${normal:-}" >&3 +} + +say_err() { + printf "%b\n" "${red:-}bilitool: Error: $1${normal:-}" >&2 +} + +say() { + # using stream 3 (defined in the beginning) to not interfere with stdout of functions + # which may be used as return value + printf "%b\n" "${cyan:-}bilitool:${normal:-} $1" >&3 +} + +say_verbose() { + if [ "$verbose" = true ]; then + say "$1" + fi +} + +QL_DIR=${QL_DIR:-"/ql"} +QL_BRANCH=${QL_BRANCH:-"develop"} +DefaultCronRule=${DefaultCronRule:-""} +CpuWarn=${CpuWarn:-""} +MemoryWarn=${MemoryWarn:-""} +DiskWarn=${DiskWarn:-""} + +dir_repo=${dir_repo:-"$QL_DIR/data/repo"} +# 需要兼容老版本青龙,https://github.com/RayWangQvQ/BiliBiliToolPro/issues/728 +if [ ! -d "$dir_repo" ] && [ -d "$QL_DIR/repo" ]; then + dir_repo="$QL_DIR/repo" +fi +dir_shell=$QL_DIR/shell +touch $dir_shell/env.sh && . $dir_shell/env.sh +touch /root/.bashrc && . /root/.bashrc + +# 目录 +say "青龙repo目录: $dir_repo" +qinglong_bili_repo="$(echo "$bili_repo" | sed 's/\//_/g')${bili_branch}" +qinglong_bili_repo_dir="$(find $dir_repo -type d \( -iname $qinglong_bili_repo -o -iname ${qinglong_bili_repo}_main \) | head -1)" +say "bili仓库目录: $qinglong_bili_repo_dir" + +current_linux_os="debian" # 或alpine +current_os="linux" # 或linux-musl +machine_architecture="x64" # 或arm、arm64 + +bilitool_installed_version=0 + +# 以下操作仅在bilitool仓库的根bin文件下执行 +cd $qinglong_bili_repo_dir +mkdir -p bin && cd $qinglong_bili_repo_dir/bin + +# 判断是否存在某指令 +machine_has() { + eval $invocation + + command -v "$1" >/dev/null 2>&1 + return $? +} + +# 判断系统架构 +# 输出:arm、arm64、x64 +get_machine_architecture() { + eval $invocation + + if command -v uname >/dev/null; then + CPUName=$(uname -m) + case $CPUName in + armv*l) + echo "arm" + return 0 + ;; + aarch64 | arm64) + echo "arm64" + return 0 + ;; + esac + fi + + # Always default to 'x64' + echo "x64" + return 0 +} + +# 获取linux系统名称 +# 输出:debian.10、debian.11、debian.12、ubuntu.20.04、ubuntu.22.04、alpine.3.4.3... +get_linux_platform_name() { + eval $invocation + + if [ -e /etc/os-release ]; then + . /etc/os-release + echo "$ID${VERSION_ID:+.${VERSION_ID}}" + return 0 + elif [ -e /etc/redhat-release ]; then + local redhatRelease=$(&1 || true) | grep -q musl +} + +# 获取当前系统名称 +# 输出:linux、linux-musl、osx、freebsd +get_current_os_name() { + eval $invocation + + local uname=$(uname) + if [ "$uname" = "Darwin" ]; then + say_warning "当前系统:osx" + echo "osx" + return 1 + elif [ "$uname" = "FreeBSD" ]; then + say_warning "当前系统:freebsd" + echo "freebsd" + return 1 + elif [ "$uname" = "Linux" ]; then + local linux_platform_name="" + linux_platform_name="$(get_linux_platform_name)" || true + say "当前系统发行版本:$linux_platform_name" + + if [ "$linux_platform_name" = "rhel.6" ]; then + echo $linux_platform_name + return 1 + elif is_musl_based_distro; then + echo "linux-musl" + return 0 + elif [ "$linux_platform_name" = "linux-musl" ]; then + echo "linux-musl" + return 0 + else + echo "linux" + return 0 + fi + fi + + say_err "OS name could not be detected: UName = $uname" + return 1 +} + +# 检查操作系统 +check_os() { + eval $invocation + + current_os="$(get_current_os_name)" + say "当前系统:$current_os" + + machine_architecture="$(get_machine_architecture)" + say "当前架构:$machine_architecture" + + if [ "$current_os" = "linux" ]; then + current_linux_os="debian" # 当前青龙只有debian和aplpine两种 + if ! machine_has curl; then + say "curl未安装,开始安装依赖..." + apt-get update + apt-get install -y curl + fi + else + current_linux_os="alpine" + if ! machine_has curl; then + say "curl未安装,开始安装依赖..." + apk update + apk add -y curl + fi + fi + + say "当前选择的运行方式:$prefer_mode" +} + +# 检查安装jq +check_jq() { + if [ "$current_linux_os" = "debian" ]; then + if ! machine_has jq; then + say "jq未安装,开始安装依赖..." + apt-get update + apt-get install -y jq + fi + else + if ! machine_has jq; then + say "jq未安装,开始安装依赖..." + apk update + apk add -y jq + fi + fi +} + +# 检查安装unzip +check_unzip() { + if [ "$current_linux_os" = "debian" ]; then + if ! machine_has unzip; then + say "unzip未安装,开始安装依赖..." + apt-get update + apt-get install -y unzip + fi + else + if ! machine_has unzip; then + say "jq未安装,开始安装依赖..." + apk update + apk add -y unzip + fi + fi +} + +# 检查dotnet +check_dotnet() { + eval $invocation + + dotnetVersion=$(dotnet --version) + say "当前dotnet版本:$dotnetVersion" + if [[ $(echo "$dotnetVersion" | grep -oE '^[0-9]+') -ge 8 ]]; then + say "已安装,且版本满足" + say "which dotnet: $(which dotnet)" + return 0 + else + say "未安装" + return 1 + fi +} + +# 检查bilitool +check_bilitool() { + eval $invocation + + TAG_FILE="./tag.txt" + touch $TAG_FILE + local STORED_TAG=$(cat $TAG_FILE 2>/dev/null) + + #如果STORED_TAG为空,则返回1 + if [[ -z $STORED_TAG ]]; then + say "tag.txt为空,未安装过" + return 1 + fi + + say "tag.txt记录的版本:$STORED_TAG" + + # 查找当前目录下是否有叫Ray.BiliBiliTool.Console的文件 + if [ -f "./Ray.BiliBiliTool.Console" ]; then + say "bilitool已安装" + bilitool_installed_version=$STORED_TAG + return 0 + else + say "bilitool未安装" + return 1 + fi +} + +# 检查环境 +check_installed() { + eval $invocation + + if [ "$prefer_mode" == "dotnet" ]; then + check_dotnet + return $? + fi + + if [ "$prefer_mode" == "bilitool" ]; then + check_bilitool + return $? + fi + + return 1 +} + +# 使用官方脚本安装dotnet +install_dotnet_by_script() { + eval $invocation + + say "再尝试使用官方脚本安装" + curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 8.0 --verbose + + say "添加到PATH" + local exportFile="/root/.bashrc" + touch $exportFile + echo '' >>$exportFile + echo 'export DOTNET_ROOT=$HOME/.dotnet' >>$exportFile + echo 'export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools' >>$exportFile + . $exportFile +} + +# 安装dotnet环境 +install_dotnet() { + eval $invocation + + say "开始安装dotnet" + say "当前系统:$current_linux_os" + if [[ $current_linux_os == "debian" ]]; then + say "使用apt安装" + + if ! (curl -s -m 5 www.google.com >/dev/nul); then + say "机器位于墙内,切换为包源为国内镜像源" + cp /etc/apt/sources.list /etc/apt/sources.list.bak + sed -i 's/https:\/\/deb.debian.org/https:\/\/mirrors.ustc.edu.cn/g' /etc/apt/sources.list + sed -i 's/http:\/\/deb.debian.org/https:\/\/mirrors.ustc.edu.cn/g' /etc/apt/sources.list + apt-get update + fi + { + . /etc/os-release + curl -o packages-microsoft-prod.deb https://packages.microsoft.com/config/debian/$VERSION_ID/packages-microsoft-prod.deb + dpkg -i packages-microsoft-prod.deb + rm packages-microsoft-prod.deb + apt-get update && apt-get install -y dotnet-sdk-8.0 + } || { + install_dotnet_by_script + } + else + say "使用apk安装" + if ! (curl -s -m 5 www.google.com >/dev/nul); then + say "机器位于墙内,切换为包源为国内镜像源" + cp /etc/apk/repositories /etc/apk/repositories.bak + sed -i 's/https:\/\/dl-cdn.alpinelinux.org/https:\/\/mirrors.ustc.edu.cn/g' /etc/apk/repositories + sed -i 's/http:\/\/dl-cdn.alpinelinux.org/https:\/\/mirrors.ustc.edu.cn/g' /etc/apk/repositories + apk update + fi + { + apk add dotnet8-sdk # https://pkgs.alpinelinux.org/packages + } || { + install_dotnet_by_script + } + fi + dotnet --version && say "which dotnet: $(which dotnet)" && say "安装成功" + return $? +} + +# 从github获取bilitool下载地址 +get_download_url() { + eval $invocation + + tag=$1 + url="${github_proxy}https://github.com/RayWangQvQ/BiliBiliToolPro/releases/download/$tag/bilibili-tool-pro-v$tag-$current_os-$machine_architecture.zip" + say "下载地址:$url" + echo $url + return 0 +} + +# 安装bilitool +install_bilitool() { + eval $invocation + + say "开始安装bilitool" + # 获取最新的release信息 + LATEST_RELEASE=$(curl -s https://api.github.com/repos/$bili_repo/releases/latest) + + # 解析最新的tag名称 + check_jq + LATEST_TAG=$(echo $LATEST_RELEASE | jq -r '.tag_name') + say "最新版本:$LATEST_TAG" + + # 读取之前存储的tag并比较 + if [ "$LATEST_TAG" != "$bilitool_installed_version" ]; then + # 如果不一样,则需要更新安装 + ASSET_URL=$(get_download_url $LATEST_TAG) + + # 使用curl下载文件到当前目录下的test.zip文件 + local zip_file_name="bilitool-$LATEST_TAG.zip" + curl -L -o "$zip_file_name" $ASSET_URL + + # 解压 + check_unzip + unzip -jo "$zip_file_name" -d ./ && + rm "$zip_file_name" && + rm -f appsettings.* + + # 更新tag.txt文件 + echo $LATEST_TAG >./tag.txt + else + say "已经是最新版本,无需下载。" + fi +} + +## 安装dotnet(如果未安装过) +install() { + eval $invocation + + if check_installed; then + say "环境正常,本次无需安装" + else + say "开始安装环境" + if [ "$prefer_mode" == "dotnet" ]; then + install_dotnet || { + say_err "安装失败" + say_err "请根据文档自行在青龙容器中安装dotnet:https://learn.microsoft.com/zh-cn/dotnet/core/install/linux-$current_linux_os" + say_err "或者尝试切换运行模式为bilitool,它不需要安装dotnet:https://github.com/RayWangQvQ/BiliBiliToolPro/blob/develop/qinglong/README.md" + } + fi + + if [ "$prefer_mode" == "bilitool" ]; then + install_bilitool || { + say_err "安装失败,请检查日志并重试" + say_err "或者尝试切换运行模式为dotnet:https://github.com/RayWangQvQ/BiliBiliToolPro/blob/develop/qinglong/README.md" + } + fi + fi +} + +# 运行bilitool任务 +run_task() { + eval $invocation + + local target_code=$1 + + export Ray_PlatformType=QingLong + export Ray_RunTasks=$target_code + + cd $qinglong_bili_repo_dir/src/Ray.BiliBiliTool.Console + + if [ "$prefer_mode" == "dotnet" ]; then + dotnet run --ENVIRONMENT=Production + else + cp -f $qinglong_bili_repo_dir/bin/Ray.BiliBiliTool.Console . + chmod +x ./Ray.BiliBiliTool.Console && ./Ray.BiliBiliTool.Console --ENVIRONMENT=Production + fi +} + +check_os +install diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_charge.sh b/qinglong/DefaultTasks/dev/bili_dev_task_charge.sh new file mode 100644 index 0000000..b6f7c36 --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_charge.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 12 * * * +# new Env("bili免费B币券充电任务[dev先行版]") + +. bili_dev_task_base.sh + +target_task_code="Charge" +run_task "${target_task_code}" diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_daily.sh b/qinglong/DefaultTasks/dev/bili_dev_task_daily.sh new file mode 100644 index 0000000..6e37d5b --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_daily.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:5 9 * * * +# new Env('bili每日任务[dev先行版]'); + +. bili_dev_task_base.sh + +target_task_code="Daily" +run_task "${target_task_code}" diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_liveFansMedal.sh b/qinglong/DefaultTasks/dev/bili_dev_task_liveFansMedal.sh new file mode 100644 index 0000000..05f5f77 --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_liveFansMedal.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:5 0 * * * +# new Env("bili直播粉丝牌[dev先行版]") + +. bili_dev_task_base.sh + +target_task_code="LiveFansMedal" +run_task "${target_task_code}" \ No newline at end of file diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_liveLottery.sh b/qinglong/DefaultTasks/dev/bili_dev_task_liveLottery.sh new file mode 100644 index 0000000..40cade7 --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_liveLottery.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 13 * * * +# new Env("bili天选时刻[dev先行版]") + +. bili_dev_task_base.sh + +target_task_code="LiveLottery" +run_task "${target_task_code}" \ No newline at end of file diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_login.sh b/qinglong/DefaultTasks/dev/bili_dev_task_login.sh new file mode 100644 index 0000000..88976e6 --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_login.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 0 1 1 * +# new Env("bili扫码登录[dev先行版]") + +. bili_dev_task_base.sh + +target_task_code="Login" +run_task "${target_task_code}" \ No newline at end of file diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_manga.sh b/qinglong/DefaultTasks/dev/bili_dev_task_manga.sh new file mode 100644 index 0000000..32dbda7 --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_manga.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 14 * * * +# new Env("bili漫画任务[dev先行版]") + +. bili_dev_task_base.sh + +target_task_code="Manga" +run_task "${target_task_code}" diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_manga_privilege.sh b/qinglong/DefaultTasks/dev/bili_dev_task_manga_privilege.sh new file mode 100644 index 0000000..def173b --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_manga_privilege.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 15 * * * +# new Env("bili领取大会员漫画权益任务[dev先行版]") + +. bili_dev_task_base.sh + +target_task_code="MangaPrivilege" +run_task "${target_task_code}" diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_silver2coin.sh b/qinglong/DefaultTasks/dev/bili_dev_task_silver2coin.sh new file mode 100644 index 0000000..851282a --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_silver2coin.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 8 * * * +# new Env("bili银瓜子兑换硬币任务[dev先行版]") + +. bili_dev_task_base.sh + +target_task_code="Silver2Coin" +run_task "${target_task_code}" diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_test.sh b/qinglong/DefaultTasks/dev/bili_dev_task_test.sh new file mode 100644 index 0000000..711c05e --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_test.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 8 * * * +# new Env("bili测试ck[dev先行版]") + +. bili_dev_task_base.sh + +target_task_code="Test" +run_task "${target_task_code}" \ No newline at end of file diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_tryFix.sh b/qinglong/DefaultTasks/dev/bili_dev_task_tryFix.sh new file mode 100644 index 0000000..56162fd --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_tryFix.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# cron:0 0 1 1 * +# new Env("bili尝试修复异常[dev先行版]") + +dir_shell=$QL_DIR/shell +. $dir_shell/share.sh +. /root/.bashrc + +bili_repo="raywangqvq/bilibilitoolpro" +bili_branch="_develop" + +echo "青龙repo目录: $dir_repo" +qinglong_bili_repo="$(echo "$bili_repo" | sed 's/\//_/g')${bili_branch}" +qinglong_bili_repo_dir="$(find $dir_repo -type d \( -iname $qinglong_bili_repo -o -iname ${qinglong_bili_repo}_main \) | head -1)" +echo "bili仓库目录: $qinglong_bili_repo_dir" + + +echo -e "清理缓存...\n" +cd $qinglong_bili_repo_dir +find . -type d -name "bin" -exec rm -rf {} + +find . -type d -name "obj" -exec rm -rf {} + +echo -e "清理完成\n" + +echo "检测dotnet..." +dotnetVersion=$(dotnet --version) +echo "当前dotnet版本:$dotnetVersion" +if [[ $(echo "$dotnetVersion" | grep -oE '^[0-9]+') -ge 8 ]]; then + echo "已安装,且版本满足" +else + echo "which dotnet: $(which dotnet)" + echo "Path: $PATH" + rm -f /usr/local/bin/dotnet +fi +echo "检测dotnet结束" \ No newline at end of file diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_unfollowBatched.sh b/qinglong/DefaultTasks/dev/bili_dev_task_unfollowBatched.sh new file mode 100644 index 0000000..f1bc528 --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_unfollowBatched.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 12 1 * * +# new Env("bili批量取关主播[dev先行版]") + +. bili_dev_task_base.sh + +target_task_code="UnfollowBatched" +run_task "${target_task_code}" \ No newline at end of file diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_vipBigPoint.sh b/qinglong/DefaultTasks/dev/bili_dev_task_vipBigPoint.sh new file mode 100644 index 0000000..7fadcfb --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_vipBigPoint.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:7 1 * * * +# new Env("bili大会员大积分[dev先行版]") + +. bili_dev_task_base.sh + +target_task_code="VipBigPoint" +run_task "${target_task_code}" \ No newline at end of file diff --git a/qinglong/DefaultTasks/dev/bili_dev_task_vip_privilege.sh b/qinglong/DefaultTasks/dev/bili_dev_task_vip_privilege.sh new file mode 100644 index 0000000..18ee9fe --- /dev/null +++ b/qinglong/DefaultTasks/dev/bili_dev_task_vip_privilege.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# cron:0 1 * * * +# new Env("bili领取大会员福利任务[dev先行版]") + +. bili_dev_task_base.sh + +target_task_code="VipPrivilege" +run_task "${target_task_code}" diff --git a/qinglong/README.md b/qinglong/README.md new file mode 100644 index 0000000..68403b9 --- /dev/null +++ b/qinglong/README.md @@ -0,0 +1,210 @@ +# 在青龙中运行 + +原理是,利用青龙的拉库命令,拉取本仓库源码,自动添加cron定时任务,然后在青龙容器中安装`dotnet`环境或`bilitool`的二进制包,定时运行相应的Task。 + +开始前,请先确保你的青龙面板是运行正常的。 + + + +- [1. 步骤](#1-步骤) + - [1.1. 登录青龙面板并修改配置](#11-登录青龙面板并修改配置) + - [1.2. 在青龙面板中添加拉库定时任务](#12-在青龙面板中添加拉库定时任务) + - [1.2.1. 方式一:订阅管理](#121-方式一订阅管理) + - [1.2.2. 方式二:定时任务拉库](#122-方式二定时任务拉库) + - [1.3. 检查定时任务](#13-检查定时任务) + - [1.4. 配置青龙Client Secret(可选)](#14-配置青龙client-secret可选) + - [1.4.1. 新建 Application](#141-新建-application) + - [1.4.2. 密钥配置到环境变量](#142-密钥配置到环境变量) + - [1.5. Bili登录](#15-bili登录) +- [2. 先行版](#2-先行版) +- [3. GitHub加速](#3-github加速) +- [4. 常见问题](#4-常见问题) + - [4.1. 安装dotnet失败怎么办法](#41-安装dotnet失败怎么办法) + - [4.2. Couldn't find a valid ICU package installed on the system](#42-couldnt-find-a-valid-icu-package-installed-on-the-system) + - [4.3. 提示文件不存在或路径异常,怎么排查](#43-提示文件不存在或路径异常怎么排查) + - [4.4. The configured user limit (128) on the number of inotify instances has been reached](#44-the-configured-user-limit-128-on-the-number-of-inotify-instances-has-been-reached) + + + +## 1. 步骤 + +### 1.1. 登录青龙面板并修改配置 +青龙面板,`配置文件`页。 + +修改 `RepoFileExtensions="js py"` 为 `RepoFileExtensions="js py sh"` + +保存配置。 + +### 1.2. 在青龙面板中添加拉库定时任务 + +两种方式,任选其一即可: + +#### 1.2.1. 方式一:订阅管理 + +``` +名称:Bilibili +类型:公开仓库 +链接:https://github.com/RayWangQvQ/BiliBiliToolPro.git +定时类型:crontab +定时规则:2 2 28 * * +白名单:bili_task_.+\.sh +文件后缀:sh +``` + +没提到的不要动。 + +保存后,点击运行按钮,运行拉库。 + +#### 1.2.2. 方式二:定时任务拉库 +青龙面板,`定时任务`页,右上角`添加任务`,填入以下信息: + +``` +名称:拉取Bili库 +命令:ql repo https://github.com/RayWangQvQ/BiliBiliToolPro.git "bili_task_" +定时规则:2 2 28 * * +``` + +点击确定。 + +保存成功后,找到该定时任务,点击运行按钮,运行拉库。 + +### 1.3. 检查定时任务 + +如果正常,拉库成功后,会自动添加bilibili相关的task任务。 + +![qinglong-tasks.png](../docs/imgs/qinglong-tasks.png) + +### 1.4. 配置青龙Client Secret(可选) + +扫码登录Bili后,需要有权限向青龙的环境变量中持久化Cookie,所以需要添加一个鉴权。 + +青龙官方说明:https://qinglong.online/api/preparation + +#### 1.4.1. 新建 Application + +青龙 -> 系统设置 -> 应用设置,点击新建。 + +![qinglong-application](../docs/imgs/qinglong-application.png) + +#### 1.4.2. 密钥配置到环境变量 + +将上面2个值添加到环境变量中即可。 + +Name分别为: + +- Ray_QingLongConfig__ClientId +- Ray_QingLongConfig__ClientSecret + +![qinglong-app-env](../docs/imgs/qinglong-application-key.png) + + +### 1.5. Bili登录 + +在青龙定时任务中,点击运行`bili扫码登录`任务,查看运行日志,扫描日志中的二维码进行登录。 +![qinglong-login.png](../docs/imgs/qinglong-login.png) + +登录成功后,如果已配置了上述的Application,会将cookie保存到青龙的环境变量中: + +![qinglong-env.png](../docs/imgs/qinglong-env.png) + +如果未配置Application,会打印出cookie,请手动自己到环境变量中添加。 + +首次运行会自动安装环境,时间可能长一点,之后就不需要重复安装了。 + +## 2. 先行版 + +青龙拉库时可以指定分支,develop分支的代码会超前于默认的main分支,包含当前正在开发的新功能。 + +想提前体验新功能,或想要Bug能快速得到解决的朋友,可以尝试切换先行版,但同时也意味着稳定性会相应降低(其实可以忽略不计~🤨)。 + +``` +分支:develop +白名单:bili_dev_task_.+\.sh +``` + +其他选项同上。 + +## 3. GitHub加速 + +拉库时,如果服务器在国内,访问GitHub速度慢,可在仓库地址前加上加速代理进行加速。 + +如: + +``` +https://github.moeyy.xyz/https://github.com/RayWangQvQ/BiliBiliToolPro.git +https://gh-proxy.com/https://github.com/RayWangQvQ/BiliBiliToolPro.git +... +``` + +加速代理地址通常不能保证长期稳定,请自行查找使用。 + +## 4. 常见问题 + +### 4.1. 安装dotnet失败怎么办法 + +首先,青龙有两个版本的镜像: + +- alpine:whyour/qinglong:latest +- debian:whyour/qinglong:debian + +安装dotnet失败的情况,几乎全发生在alpine版上。。。 + +所以,如果你“执迷不悟”,就是一定要用alpine版,那请先通过日志自行排查,不行就根据微软官方文档,进入qinglong容器后,手动安装。 + +如果还不行,那么可以切换到基于`bilitool`的二进制包运行方式,该方式不需要安装`dotnet`,方式: + +编辑青龙面板的`配置文件`,新增如下两行: + +``` +export BILI_MODE="bilitool" # bili运行模式,dotnet或bilitool +export BILI_GITHUB_PROXY="https://github.moeyy.xyz/" # 下载二进制包时使用的加速代理,不要的话则置空 +``` + +![qinglong-login.png](../docs/imgs/qinglong-run-as-bilitool.png) + +bilitool没有先行版的概念,因为只有main分支才会打包,更新会稍慢一点。 + +另外,alpine版的问题,我不建议来提交issue,因为已经大大超出本项目的scope了,建议可以去给alpine官方或微软的dotnet官方提交issue。 + +### 4.2. Couldn't find a valid ICU package installed on the system + +如 #266 ,需要在青龙面板的环境变量添加如下环境变量: + +``` +名称:DOTNET_SYSTEM_GLOBALIZATION_INVARIANT +值:1 +``` + +### 4.3. 提示文件不存在或路径异常,怎么排查 + +需要`docker exec -it qinglong bash`后,查看几个常用路径: + +``` +/ql + /data + /repo + /scripts + /shell +``` + +- `/ql/dada/repo`目录下存储了拉库后,bilitool的源代码 +- `/ql/scripts`目录下存储了bilitool的定时运行脚本 +- `/ql/shell`目录下是青龙的基础脚本 + +请cd到相应目录,查看该目录下文件是否存在,状态是否正常。 + +### 4.4. The configured user limit (128) on the number of inotify instances has been reached + +报错: + +``` +Asp.Net Core - The configured user limit (128) on the number of inotify instances has been reached +``` + +可以尝试添加如下环境变量解决: + +``` +DOTNET_USE_POLLING_FILE_WATCHER=1 +``` + +添加后,对配置变更事件的监听,会从监听 Linux 系统的 inotify 事件,变成定时轮询。 \ No newline at end of file diff --git a/qinglong/bak/bili_dev_task_get_cookie.py.bak b/qinglong/bak/bili_dev_task_get_cookie.py.bak new file mode 100644 index 0000000..7cf0e83 --- /dev/null +++ b/qinglong/bak/bili_dev_task_get_cookie.py.bak @@ -0,0 +1,87 @@ +''' +1 9 11 11 1 bili_dev_task_get_cookie.py +手动运行,查看日志,并使用手机B站app扫描日志中二维码,注意,只能修改第一个cookie +如果产生错误,重新运行并用手机扫描二维码 +有可能识别不出来二维码,我测试了几次都能识别 + +默认环境变量存放位置为/ql/data/config/env.sh +可以自己通过docker命令进入容器查找这个文件位置。docker exec -it qinglong /bin/bash,进入青龙容器,然后查找一下这个文件位置 +filename = '../config/env.sh' +''' + +import qrcode +import requests +import json +import time +import os + +filename = '/ql/data/config/env.sh' + +url_get = 'http://passport.bilibili.com/x/passport-login/web/qrcode/generate' +headers = { + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.42" + } +session = requests.session() +response = session.get(url_get, headers=headers) +json_data = json.loads(response.text) +qr_data = json_data['data']['url'] +qr_code = json_data['data']['qrcode_key'] +# print(qr_data) +# img = qrcode.make(qr_data) +# img.save('../upload/B.png') +# 生成二维码,并且打印,只有invert是True手机才能识别,默认的打印识别不出来 +qr = qrcode.QRCode() +qr.add_data(qr_data) +qr.print_ascii(invert=True) + +url_get_2 = f'http://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key={qr_code}&source=main_mini' +refresh_token = '' +# 尝试次数 +try_time = 8 +while True: + try_time -= 1 + if not try_time: + print('一直没有扫码,退出登录!') + exit(1) + response = session.get(url_get_2, headers=headers) + json_data = json.loads(response.text) + response_data_2 = json_data['data'] + if response_data_2['code'] == 0: + try_time += 5 + refresh_token = response_data_2['refresh_token'] + print(response_data_2, end='') + if response_data_2['message'] == '二维码已失效': + print(response_data_2['message']) + print('-' * 20) + break + print(response_data_2['message']) + print('-' * 20) + time.sleep(5) +session.get('https://api.bilibili.com/x/web-interface/nav') +cookies = requests.utils.dict_from_cookiejar(session.cookies) +lst = [] +for item in cookies.items(): + lst.append(f"{item[0]}={item[1]}") + +cookie_str = ';'.join(lst) +print('=' * 20) +print(cookie_str) +print('=' * 20) +# 修改环境变量 +with open(filename, 'r') as f: + lines = f.readlines() + +flag = True +with open(filename, 'w') as f: + for l in lines: + if 'Ray_BiliBiliCookies__1' in l: + flag = False + l = f'export Ray_BiliBiliCookies__1="{cookie_str}"\n' + print(l) + f.write(l) + if flag: + flag = False + l = f'export Ray_BiliBiliCookies__1="{cookie_str}"\n' + print(l) + f.write(l) +os.popen(f'source {filename}') diff --git a/qinglong/bak/bili_task_get_cookie.py.bak b/qinglong/bak/bili_task_get_cookie.py.bak new file mode 100644 index 0000000..9243a63 --- /dev/null +++ b/qinglong/bak/bili_task_get_cookie.py.bak @@ -0,0 +1,87 @@ +''' +1 9 11 11 1 bili_task_get_cookie.py +手动运行,查看日志,并使用手机B站app扫描日志中二维码,注意,只能修改第一个cookie +如果产生错误,重新运行并用手机扫描二维码 +有可能识别不出来二维码,我测试了几次都能识别 + +默认环境变量存放位置为/ql/data/config/env.sh +可以自己通过docker命令进入容器查找这个文件位置。docker exec -it qinglong /bin/bash,进入青龙容器,然后查找一下这个文件位置 +filename = '../config/env.sh' +''' + +import qrcode +import requests +import json +import time +import os + +filename = '/ql/data/config/env.sh' + +url_get = 'http://passport.bilibili.com/x/passport-login/web/qrcode/generate' +headers = { + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.42" + } +session = requests.session() +response = session.get(url_get, headers=headers) +json_data = json.loads(response.text) +qr_data = json_data['data']['url'] +qr_code = json_data['data']['qrcode_key'] +# print(qr_data) +# img = qrcode.make(qr_data) +# img.save('../upload/B.png') +# 生成二维码,并且打印,只有invert是True手机才能识别,默认的打印识别不出来 +qr = qrcode.QRCode() +qr.add_data(qr_data) +qr.print_ascii(invert=True) + +url_get_2 = f'http://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key={qr_code}&source=main_mini' +refresh_token = '' +# 尝试次数 +try_time = 8 +while True: + try_time -= 1 + if not try_time: + print('一直没有扫码,退出登录!') + exit(1) + response = session.get(url_get_2, headers=headers) + json_data = json.loads(response.text) + response_data_2 = json_data['data'] + if response_data_2['code'] == 0: + try_time += 5 + refresh_token = response_data_2['refresh_token'] + print(response_data_2, end='') + if response_data_2['message'] == '二维码已失效': + print(response_data_2['message']) + print('-' * 20) + break + print(response_data_2['message']) + print('-' * 20) + time.sleep(5) +session.get('https://api.bilibili.com/x/web-interface/nav') +cookies = requests.utils.dict_from_cookiejar(session.cookies) +lst = [] +for item in cookies.items(): + lst.append(f"{item[0]}={item[1]}") + +cookie_str = ';'.join(lst) +print('=' * 20) +print(cookie_str) +print('=' * 20) +# 修改环境变量 +with open(filename, 'r') as f: + lines = f.readlines() + +flag = True +with open(filename, 'w') as f: + for l in lines: + if 'Ray_BiliBiliCookies__1' in l: + flag = False + l = f'export Ray_BiliBiliCookies__1="{cookie_str}"\n' + print(l) + f.write(l) + if flag: + flag = False + l = f'export Ray_BiliBiliCookies__1="{cookie_str}"\n' + print(l) + f.write(l) +os.popen(f'source {filename}') diff --git a/qinglong/dotnet-install.sh b/qinglong/dotnet-install.sh new file mode 100644 index 0000000..096348d --- /dev/null +++ b/qinglong/dotnet-install.sh @@ -0,0 +1,1656 @@ +#!/usr/bin/env bash +# Copyright (c) .NET Foundation and contributors. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +# Stop script on NZEC +set -e +# Stop script if unbound variable found (use ${var:-} if intentional) +set -u +# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success +# This is causing it to fail +set -o pipefail + +# Use in the the functions: eval $invocation +invocation='say_verbose "Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}"' + +# standard output may be used as a return value in the functions +# we need a way to write text on the screen in the functions so that +# it won't interfere with the return value. +# Exposing stream 3 as a pipe to standard output of the script itself +exec 3>&1 + +# Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors. +# See if stdout is a terminal +if [ -t 1 ] && command -v tput > /dev/null; then + # see if it supports colors + ncolors=$(tput colors || echo 0) + if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then + bold="$(tput bold || echo)" + normal="$(tput sgr0 || echo)" + black="$(tput setaf 0 || echo)" + red="$(tput setaf 1 || echo)" + green="$(tput setaf 2 || echo)" + yellow="$(tput setaf 3 || echo)" + blue="$(tput setaf 4 || echo)" + magenta="$(tput setaf 5 || echo)" + cyan="$(tput setaf 6 || echo)" + white="$(tput setaf 7 || echo)" + fi +fi + +say_warning() { + printf "%b\n" "${yellow:-}dotnet_install: Warning: $1${normal:-}" >&3 +} + +say_err() { + printf "%b\n" "${red:-}dotnet_install: Error: $1${normal:-}" >&2 +} + +say() { + # using stream 3 (defined in the beginning) to not interfere with stdout of functions + # which may be used as return value + printf "%b\n" "${cyan:-}dotnet-install:${normal:-} $1" >&3 +} + +say_verbose() { + if [ "$verbose" = true ]; then + say "$1" + fi +} + +# This platform list is finite - if the SDK/Runtime has supported Linux distribution-specific assets, +# then and only then should the Linux distribution appear in this list. +# Adding a Linux distribution to this list does not imply distribution-specific support. +get_legacy_os_name_from_platform() { + eval $invocation + + platform="$1" + case "$platform" in + "centos.7") + echo "centos" + return 0 + ;; + "debian.8") + echo "debian" + return 0 + ;; + "debian.9") + echo "debian.9" + return 0 + ;; + "fedora.23") + echo "fedora.23" + return 0 + ;; + "fedora.24") + echo "fedora.24" + return 0 + ;; + "fedora.27") + echo "fedora.27" + return 0 + ;; + "fedora.28") + echo "fedora.28" + return 0 + ;; + "opensuse.13.2") + echo "opensuse.13.2" + return 0 + ;; + "opensuse.42.1") + echo "opensuse.42.1" + return 0 + ;; + "opensuse.42.3") + echo "opensuse.42.3" + return 0 + ;; + "rhel.7"*) + echo "rhel" + return 0 + ;; + "ubuntu.14.04") + echo "ubuntu" + return 0 + ;; + "ubuntu.16.04") + echo "ubuntu.16.04" + return 0 + ;; + "ubuntu.16.10") + echo "ubuntu.16.10" + return 0 + ;; + "ubuntu.18.04") + echo "ubuntu.18.04" + return 0 + ;; + "alpine.3.4.3") + echo "alpine" + return 0 + ;; + esac + return 1 +} + +get_legacy_os_name() { + eval $invocation + + local uname=$(uname) + if [ "$uname" = "Darwin" ]; then + echo "osx" + return 0 + elif [ -n "$runtime_id" ]; then + echo $(get_legacy_os_name_from_platform "${runtime_id%-*}" || echo "${runtime_id%-*}") + return 0 + else + if [ -e /etc/os-release ]; then + . /etc/os-release + os=$(get_legacy_os_name_from_platform "$ID${VERSION_ID:+.${VERSION_ID}}" || echo "") + if [ -n "$os" ]; then + echo "$os" + return 0 + fi + fi + fi + + say_verbose "Distribution specific OS name and version could not be detected: UName = $uname" + return 1 +} + +get_linux_platform_name() { + eval $invocation + + if [ -n "$runtime_id" ]; then + echo "${runtime_id%-*}" + return 0 + else + if [ -e /etc/os-release ]; then + . /etc/os-release + echo "$ID${VERSION_ID:+.${VERSION_ID}}" + return 0 + elif [ -e /etc/redhat-release ]; then + local redhatRelease=$(&1 || true) | grep -q musl +} + +get_current_os_name() { + eval $invocation + + local uname=$(uname) + if [ "$uname" = "Darwin" ]; then + echo "osx" + return 0 + elif [ "$uname" = "FreeBSD" ]; then + echo "freebsd" + return 0 + elif [ "$uname" = "Linux" ]; then + local linux_platform_name="" + linux_platform_name="$(get_linux_platform_name)" || true + + if [ "$linux_platform_name" = "rhel.6" ]; then + echo $linux_platform_name + return 0 + elif is_musl_based_distro; then + echo "linux-musl" + return 0 + elif [ "$linux_platform_name" = "linux-musl" ]; then + echo "linux-musl" + return 0 + else + echo "linux" + return 0 + fi + fi + + say_err "OS name could not be detected: UName = $uname" + return 1 +} + +machine_has() { + eval $invocation + + command -v "$1" > /dev/null 2>&1 + return $? +} + +check_min_reqs() { + local hasMinimum=false + if machine_has "curl"; then + hasMinimum=true + elif machine_has "wget"; then + hasMinimum=true + fi + + if [ "$hasMinimum" = "false" ]; then + say_err "curl (recommended) or wget are required to download dotnet. Install missing prerequisite to proceed." + return 1 + fi + return 0 +} + +# args: +# input - $1 +to_lowercase() { + #eval $invocation + + echo "$1" | tr '[:upper:]' '[:lower:]' + return 0 +} + +# args: +# input - $1 +remove_trailing_slash() { + #eval $invocation + + local input="${1:-}" + echo "${input%/}" + return 0 +} + +# args: +# input - $1 +remove_beginning_slash() { + #eval $invocation + + local input="${1:-}" + echo "${input#/}" + return 0 +} + +# args: +# root_path - $1 +# child_path - $2 - this parameter can be empty +combine_paths() { + eval $invocation + + # TODO: Consider making it work with any number of paths. For now: + if [ ! -z "${3:-}" ]; then + say_err "combine_paths: Function takes two parameters." + return 1 + fi + + local root_path="$(remove_trailing_slash "$1")" + local child_path="$(remove_beginning_slash "${2:-}")" + say_verbose "combine_paths: root_path=$root_path" + say_verbose "combine_paths: child_path=$child_path" + echo "$root_path/$child_path" + return 0 +} + +get_machine_architecture() { + eval $invocation + + if command -v uname > /dev/null; then + CPUName=$(uname -m) + case $CPUName in + armv*l) + echo "arm" + return 0 + ;; + aarch64|arm64) + echo "arm64" + return 0 + ;; + esac + fi + + # Always default to 'x64' + echo "x64" + return 0 +} + +# args: +# architecture - $1 +get_normalized_architecture_from_architecture() { + eval $invocation + + local architecture="$(to_lowercase "$1")" + + if [[ $architecture == \ ]]; then + echo "$(get_machine_architecture)" + return 0 + fi + + case "$architecture" in + amd64|x64) + echo "x64" + return 0 + ;; + arm) + echo "arm" + return 0 + ;; + arm64) + echo "arm64" + return 0 + ;; + esac + + say_err "Architecture \`$architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues" + return 1 +} + +# args: +# user_defined_os - $1 +get_normalized_os() { + eval $invocation + + local osname="$(to_lowercase "$1")" + if [ ! -z "$osname" ]; then + case "$osname" in + osx | freebsd | rhel.6 | linux-musl | linux) + echo "$osname" + return 0 + ;; + *) + say_err "'$user_defined_os' is not a supported value for --os option, supported values are: osx, linux, linux-musl, freebsd, rhel.6. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues." + return 1 + ;; + esac + else + osname="$(get_current_os_name)" || return 1 + fi + echo "$osname" + return 0 +} + +# args: +# quality - $1 +get_normalized_quality() { + eval $invocation + + local quality="$(to_lowercase "$1")" + if [ ! -z "$quality" ]; then + case "$quality" in + daily | signed | validated | preview) + echo "$quality" + return 0 + ;; + ga) + #ga quality is available without specifying quality, so normalizing it to empty + return 0 + ;; + *) + say_err "'$quality' is not a supported value for --quality option. Supported values are: daily, signed, validated, preview, ga. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues." + return 1 + ;; + esac + fi + return 0 +} + +# args: +# channel - $1 +get_normalized_channel() { + eval $invocation + + local channel="$(to_lowercase "$1")" + + if [[ $channel == release/* ]]; then + say_warning 'Using branch name with -Channel option is no longer supported with newer releases. Use -Quality option with a channel in X.Y format instead.'; + fi + + if [ ! -z "$channel" ]; then + case "$channel" in + lts) + echo "LTS" + return 0 + ;; + *) + echo "$channel" + return 0 + ;; + esac + fi + + return 0 +} + +# args: +# runtime - $1 +get_normalized_product() { + eval $invocation + + local product="" + local runtime="$(to_lowercase "$1")" + if [[ "$runtime" == "dotnet" ]]; then + product="dotnet-runtime" + elif [[ "$runtime" == "aspnetcore" ]]; then + product="aspnetcore-runtime" + elif [ -z "$runtime" ]; then + product="dotnet-sdk" + fi + echo "$product" + return 0 +} + +# The version text returned from the feeds is a 1-line or 2-line string: +# For the SDK and the dotnet runtime (2 lines): +# Line 1: # commit_hash +# Line 2: # 4-part version +# For the aspnetcore runtime (1 line): +# Line 1: # 4-part version + +# args: +# version_text - stdin +get_version_from_latestversion_file_content() { + eval $invocation + + cat | tail -n 1 | sed 's/\r$//' + return 0 +} + +# args: +# install_root - $1 +# relative_path_to_package - $2 +# specific_version - $3 +is_dotnet_package_installed() { + eval $invocation + + local install_root="$1" + local relative_path_to_package="$2" + local specific_version="${3//[$'\t\r\n']}" + + local dotnet_package_path="$(combine_paths "$(combine_paths "$install_root" "$relative_path_to_package")" "$specific_version")" + say_verbose "is_dotnet_package_installed: dotnet_package_path=$dotnet_package_path" + + if [ -d "$dotnet_package_path" ]; then + return 0 + else + return 1 + fi +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +get_version_from_latestversion_file() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + + local version_file_url=null + if [[ "$runtime" == "dotnet" ]]; then + version_file_url="$azure_feed/Runtime/$channel/latest.version" + elif [[ "$runtime" == "aspnetcore" ]]; then + version_file_url="$azure_feed/aspnetcore/Runtime/$channel/latest.version" + elif [ -z "$runtime" ]; then + version_file_url="$azure_feed/Sdk/$channel/latest.version" + else + say_err "Invalid value for \$runtime" + return 1 + fi + say_verbose "get_version_from_latestversion_file: latest url: $version_file_url" + + download "$version_file_url" || return $? + return 0 +} + +# args: +# json_file - $1 +parse_globaljson_file_for_version() { + eval $invocation + + local json_file="$1" + if [ ! -f "$json_file" ]; then + say_err "Unable to find \`$json_file\`" + return 1 + fi + + sdk_section=$(cat $json_file | awk '/"sdk"/,/}/') + if [ -z "$sdk_section" ]; then + say_err "Unable to parse the SDK node in \`$json_file\`" + return 1 + fi + + sdk_list=$(echo $sdk_section | awk -F"[{}]" '{print $2}') + sdk_list=${sdk_list//[\" ]/} + sdk_list=${sdk_list//,/$'\n'} + + local version_info="" + while read -r line; do + IFS=: + while read -r key value; do + if [[ "$key" == "version" ]]; then + version_info=$value + fi + done <<< "$line" + done <<< "$sdk_list" + if [ -z "$version_info" ]; then + say_err "Unable to find the SDK:version node in \`$json_file\`" + return 1 + fi + + unset IFS; + echo "$version_info" + return 0 +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# version - $4 +# json_file - $5 +get_specific_version_from_version() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local version="$(to_lowercase "$4")" + local json_file="$5" + + if [ -z "$json_file" ]; then + if [[ "$version" == "latest" ]]; then + local version_info + version_info="$(get_version_from_latestversion_file "$azure_feed" "$channel" "$normalized_architecture" false)" || return 1 + say_verbose "get_specific_version_from_version: version_info=$version_info" + echo "$version_info" | get_version_from_latestversion_file_content + return 0 + else + echo "$version" + return 0 + fi + else + local version_info + version_info="$(parse_globaljson_file_for_version "$json_file")" || return 1 + echo "$version_info" + return 0 + fi +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# specific_version - $4 +# normalized_os - $5 +construct_download_link() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local specific_version="${4//[$'\t\r\n']}" + local specific_product_version="$(get_specific_product_version "$1" "$4")" + local osname="$5" + + local download_link=null + if [[ "$runtime" == "dotnet" ]]; then + download_link="$azure_feed/Runtime/$specific_version/dotnet-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz" + elif [[ "$runtime" == "aspnetcore" ]]; then + download_link="$azure_feed/aspnetcore/Runtime/$specific_version/aspnetcore-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz" + elif [ -z "$runtime" ]; then + download_link="$azure_feed/Sdk/$specific_version/dotnet-sdk-$specific_product_version-$osname-$normalized_architecture.tar.gz" + else + return 1 + fi + + echo "$download_link" + return 0 +} + +# args: +# azure_feed - $1 +# specific_version - $2 +# download link - $3 (optional) +get_specific_product_version() { + # If we find a 'productVersion.txt' at the root of any folder, we'll use its contents + # to resolve the version of what's in the folder, superseding the specified version. + # if 'productVersion.txt' is missing but download link is already available, product version will be taken from download link + eval $invocation + + local azure_feed="$1" + local specific_version="${2//[$'\t\r\n']}" + local package_download_link="" + if [ $# -gt 2 ]; then + local package_download_link="$3" + fi + local specific_product_version=null + + # Try to get the version number, using the productVersion.txt file located next to the installer file. + local download_links=($(get_specific_product_version_url "$azure_feed" "$specific_version" true "$package_download_link") + $(get_specific_product_version_url "$azure_feed" "$specific_version" false "$package_download_link")) + + for download_link in "${download_links[@]}" + do + say_verbose "Checking for the existence of $download_link" + + if machine_has "curl" + then + specific_product_version=$(curl -s --fail "${download_link}${feed_credential}" 2>&1) + if [ $? = 0 ]; then + echo "${specific_product_version//[$'\t\r\n']}" + return 0 + fi + elif machine_has "wget" + then + specific_product_version=$(wget -qO- "${download_link}${feed_credential}" 2>&1) + if [ $? = 0 ]; then + echo "${specific_product_version//[$'\t\r\n']}" + return 0 + fi + fi + done + + # Getting the version number with productVersion.txt has failed. Try parsing the download link for a version number. + say_verbose "Failed to get the version using productVersion.txt file. Download link will be parsed instead." + specific_product_version="$(get_product_specific_version_from_download_link "$package_download_link" "$specific_version")" + echo "${specific_product_version//[$'\t\r\n']}" + return 0 +} + +# args: +# azure_feed - $1 +# specific_version - $2 +# is_flattened - $3 +# download link - $4 (optional) +get_specific_product_version_url() { + eval $invocation + + local azure_feed="$1" + local specific_version="$2" + local is_flattened="$3" + local package_download_link="" + if [ $# -gt 3 ]; then + local package_download_link="$4" + fi + + local pvFileName="productVersion.txt" + if [ "$is_flattened" = true ]; then + if [ -z "$runtime" ]; then + pvFileName="sdk-productVersion.txt" + elif [[ "$runtime" == "dotnet" ]]; then + pvFileName="runtime-productVersion.txt" + else + pvFileName="$runtime-productVersion.txt" + fi + fi + + local download_link=null + + if [ -z "$package_download_link" ]; then + if [[ "$runtime" == "dotnet" ]]; then + download_link="$azure_feed/Runtime/$specific_version/${pvFileName}" + elif [[ "$runtime" == "aspnetcore" ]]; then + download_link="$azure_feed/aspnetcore/Runtime/$specific_version/${pvFileName}" + elif [ -z "$runtime" ]; then + download_link="$azure_feed/Sdk/$specific_version/${pvFileName}" + else + return 1 + fi + else + download_link="${package_download_link%/*}/${pvFileName}" + fi + + say_verbose "Constructed productVersion link: $download_link" + echo "$download_link" + return 0 +} + +# args: +# download link - $1 +# specific version - $2 +get_product_specific_version_from_download_link() +{ + eval $invocation + + local download_link="$1" + local specific_version="$2" + local specific_product_version="" + + if [ -z "$download_link" ]; then + echo "$specific_version" + return 0 + fi + + #get filename + filename="${download_link##*/}" + + #product specific version follows the product name + #for filename 'dotnet-sdk-3.1.404-linux-x64.tar.gz': the product version is 3.1.404 + IFS='-' + read -ra filename_elems <<< "$filename" + count=${#filename_elems[@]} + if [[ "$count" -gt 2 ]]; then + specific_product_version="${filename_elems[2]}" + else + specific_product_version=$specific_version + fi + unset IFS; + echo "$specific_product_version" + return 0 +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# specific_version - $4 +construct_legacy_download_link() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local specific_version="${4//[$'\t\r\n']}" + + local distro_specific_osname + distro_specific_osname="$(get_legacy_os_name)" || return 1 + + local legacy_download_link=null + if [[ "$runtime" == "dotnet" ]]; then + legacy_download_link="$azure_feed/Runtime/$specific_version/dotnet-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz" + elif [ -z "$runtime" ]; then + legacy_download_link="$azure_feed/Sdk/$specific_version/dotnet-dev-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz" + else + return 1 + fi + + echo "$legacy_download_link" + return 0 +} + +get_user_install_path() { + eval $invocation + + if [ ! -z "${DOTNET_INSTALL_DIR:-}" ]; then + echo "$DOTNET_INSTALL_DIR" + else + echo "$HOME/.dotnet" + fi + return 0 +} + +# args: +# install_dir - $1 +resolve_installation_path() { + eval $invocation + + local install_dir=$1 + if [ "$install_dir" = "" ]; then + local user_install_path="$(get_user_install_path)" + say_verbose "resolve_installation_path: user_install_path=$user_install_path" + echo "$user_install_path" + return 0 + fi + + echo "$install_dir" + return 0 +} + +# args: +# relative_or_absolute_path - $1 +get_absolute_path() { + eval $invocation + + local relative_or_absolute_path=$1 + echo "$(cd "$(dirname "$1")" && pwd -P)/$(basename "$1")" + return 0 +} + +# args: +# input_files - stdin +# root_path - $1 +# out_path - $2 +# override - $3 +copy_files_or_dirs_from_list() { + eval $invocation + + local root_path="$(remove_trailing_slash "$1")" + local out_path="$(remove_trailing_slash "$2")" + local override="$3" + local osname="$(get_current_os_name)" + local override_switch=$( + if [ "$override" = false ]; then + if [ "$osname" = "linux-musl" ]; then + printf -- "-u"; + else + printf -- "-n"; + fi + fi) + + cat | uniq | while read -r file_path; do + local path="$(remove_beginning_slash "${file_path#$root_path}")" + local target="$out_path/$path" + if [ "$override" = true ] || (! ([ -d "$target" ] || [ -e "$target" ])); then + mkdir -p "$out_path/$(dirname "$path")" + if [ -d "$target" ]; then + rm -rf "$target" + fi + cp -R $override_switch "$root_path/$path" "$target" + fi + done +} + +# args: +# zip_path - $1 +# out_path - $2 +extract_dotnet_package() { + eval $invocation + + local zip_path="$1" + local out_path="$2" + + local temp_out_path="$(mktemp -d "$temporary_file_template")" + + local failed=false + tar -xzf "$zip_path" -C "$temp_out_path" > /dev/null || failed=true + + local folders_with_version_regex='^.*/[0-9]+\.[0-9]+[^/]+/' + find "$temp_out_path" -type f | grep -Eo "$folders_with_version_regex" | sort | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" false + find "$temp_out_path" -type f | grep -Ev "$folders_with_version_regex" | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" "$override_non_versioned_files" + + rm -rf "$temp_out_path" + rm -f "$zip_path" && say_verbose "Temporary zip file $zip_path was removed" + + if [ "$failed" = true ]; then + say_err "Extraction failed" + return 1 + fi + return 0 +} + +# args: +# remote_path - $1 +# disable_feed_credential - $2 +get_http_header() +{ + eval $invocation + local remote_path="$1" + local disable_feed_credential="$2" + + local failed=false + local response + if machine_has "curl"; then + get_http_header_curl $remote_path $disable_feed_credential || failed=true + elif machine_has "wget"; then + get_http_header_wget $remote_path $disable_feed_credential || failed=true + else + failed=true + fi + if [ "$failed" = true ]; then + say_verbose "Failed to get HTTP header: '$remote_path'." + return 1 + fi + return 0 +} + +# args: +# remote_path - $1 +# disable_feed_credential - $2 +get_http_header_curl() { + eval $invocation + local remote_path="$1" + local disable_feed_credential="$2" + + remote_path_with_credential="$remote_path" + if [ "$disable_feed_credential" = false ]; then + remote_path_with_credential+="$feed_credential" + fi + + curl_options="-I -sSL --retry 5 --retry-delay 2 --connect-timeout 15 " + curl $curl_options "$remote_path_with_credential" 2>&1 || return 1 + return 0 +} + +# args: +# remote_path - $1 +# disable_feed_credential - $2 +get_http_header_wget() { + eval $invocation + local remote_path="$1" + local disable_feed_credential="$2" + local wget_options="-q -S --spider --tries 5 " + # Store options that aren't supported on all wget implementations separately. + local wget_options_extra="--waitretry 2 --connect-timeout 15 " + local wget_result='' + + remote_path_with_credential="$remote_path" + if [ "$disable_feed_credential" = false ]; then + remote_path_with_credential+="$feed_credential" + fi + + wget $wget_options $wget_options_extra "$remote_path_with_credential" 2>&1 + wget_result=$? + + if [[ $wget_result == 2 ]]; then + # Parsing of the command has failed. Exclude potentially unrecognized options and retry. + wget $wget_options "$remote_path_with_credential" 2>&1 + return $? + fi + + return $wget_result +} + +# args: +# remote_path - $1 +# [out_path] - $2 - stdout if not provided +download() { + eval $invocation + + local remote_path="$1" + local out_path="${2:-}" + + if [[ "$remote_path" != "http"* ]]; then + cp "$remote_path" "$out_path" + return $? + fi + + local failed=false + local attempts=0 + while [ $attempts -lt 3 ]; do + attempts=$((attempts+1)) + failed=false + if machine_has "curl"; then + downloadcurl "$remote_path" "$out_path" || failed=true + elif machine_has "wget"; then + downloadwget "$remote_path" "$out_path" || failed=true + else + say_err "Missing dependency: neither curl nor wget was found." + exit 1 + fi + + if [ "$failed" = false ] || [ $attempts -ge 3 ] || { [ ! -z $http_code ] && [ $http_code = "404" ]; }; then + break + fi + + say "Download attempt #$attempts has failed: $http_code $download_error_msg" + say "Attempt #$((attempts+1)) will start in $((attempts*10)) seconds." + sleep $((attempts*10)) + done + + + + if [ "$failed" = true ]; then + say_verbose "Download failed: $remote_path" + return 1 + fi + return 0 +} + +# Updates global variables $http_code and $download_error_msg +downloadcurl() { + eval $invocation + unset http_code + unset download_error_msg + local remote_path="$1" + local out_path="${2:-}" + # Append feed_credential as late as possible before calling curl to avoid logging feed_credential + # Avoid passing URI with credentials to functions: note, most of them echoing parameters of invocation in verbose output. + local remote_path_with_credential="${remote_path}${feed_credential}" + local curl_options="--retry 20 --retry-delay 2 --connect-timeout 15 -sSL -f --create-dirs " + local failed=false + if [ -z "$out_path" ]; then + curl $curl_options "$remote_path_with_credential" 2>&1 || failed=true + else + curl $curl_options -o "$out_path" "$remote_path_with_credential" 2>&1 || failed=true + fi + if [ "$failed" = true ]; then + local disable_feed_credential=false + local response=$(get_http_header_curl $remote_path $disable_feed_credential) + http_code=$( echo "$response" | awk '/^HTTP/{print $2}' | tail -1 ) + download_error_msg="Unable to download $remote_path." + if [[ $http_code != 2* ]]; then + download_error_msg+=" Returned HTTP status code: $http_code." + fi + say_verbose "$download_error_msg" + return 1 + fi + return 0 +} + + +# Updates global variables $http_code and $download_error_msg +downloadwget() { + eval $invocation + unset http_code + unset download_error_msg + local remote_path="$1" + local out_path="${2:-}" + # Append feed_credential as late as possible before calling wget to avoid logging feed_credential + local remote_path_with_credential="${remote_path}${feed_credential}" + local wget_options="--tries 20 " + # Store options that aren't supported on all wget implementations separately. + local wget_options_extra="--waitretry 2 --connect-timeout 15 " + local wget_result='' + + if [ -z "$out_path" ]; then + wget -q $wget_options $wget_options_extra -O - "$remote_path_with_credential" 2>&1 + wget_result=$? + else + wget $wget_options $wget_options_extra -O "$out_path" "$remote_path_with_credential" 2>&1 + wget_result=$? + fi + + if [[ $wget_result == 2 ]]; then + # Parsing of the command has failed. Exclude potentially unrecognized options and retry. + if [ -z "$out_path" ]; then + wget -q $wget_options -O - "$remote_path_with_credential" 2>&1 + wget_result=$? + else + wget $wget_options -O "$out_path" "$remote_path_with_credential" 2>&1 + wget_result=$? + fi + fi + + if [[ $wget_result != 0 ]]; then + local disable_feed_credential=false + local response=$(get_http_header_wget $remote_path $disable_feed_credential) + http_code=$( echo "$response" | awk '/^ HTTP/{print $2}' | tail -1 ) + download_error_msg="Unable to download $remote_path." + if [[ $http_code != 2* ]]; then + download_error_msg+=" Returned HTTP status code: $http_code." + fi + say_verbose "$download_error_msg" + return 1 + fi + + return 0 +} + +get_download_link_from_aka_ms() { + eval $invocation + + #quality is not supported for LTS or current channel + if [[ ! -z "$normalized_quality" && ("$normalized_channel" == "LTS" || "$normalized_channel" == "current") ]]; then + normalized_quality="" + say_warning "Specifying quality for current or LTS channel is not supported, the quality will be ignored." + fi + + say_verbose "Retrieving primary payload URL from aka.ms for channel: '$normalized_channel', quality: '$normalized_quality', product: '$normalized_product', os: '$normalized_os', architecture: '$normalized_architecture'." + + #construct aka.ms link + aka_ms_link="https://aka.ms/dotnet" + if [ "$internal" = true ]; then + aka_ms_link="$aka_ms_link/internal" + fi + aka_ms_link="$aka_ms_link/$normalized_channel" + if [[ ! -z "$normalized_quality" ]]; then + aka_ms_link="$aka_ms_link/$normalized_quality" + fi + aka_ms_link="$aka_ms_link/$normalized_product-$normalized_os-$normalized_architecture.tar.gz" + say_verbose "Constructed aka.ms link: '$aka_ms_link'." + + #get HTTP response + #do not pass credentials as a part of the $aka_ms_link and do not apply credentials in the get_http_header function + #otherwise the redirect link would have credentials as well + #it would result in applying credentials twice to the resulting link and thus breaking it, and in echoing credentials to the output as a part of redirect link + disable_feed_credential=true + response="$(get_http_header $aka_ms_link $disable_feed_credential)" + + say_verbose "Received response: $response" + # Get results of all the redirects. + http_codes=$( echo "$response" | awk '$1 ~ /^HTTP/ {print $2}' ) + # They all need to be 301, otherwise some links are broken (except for the last, which is not a redirect but 200 or 404). + broken_redirects=$( echo "$http_codes" | sed '$d' | grep -v '301' ) + + # All HTTP codes are 301 (Moved Permanently), the redirect link exists. + if [[ -z "$broken_redirects" ]]; then + aka_ms_download_link=$( echo "$response" | awk '$1 ~ /^Location/{print $2}' | tail -1 | tr -d '\r') + + if [[ -z "$aka_ms_download_link" ]]; then + say_verbose "The aka.ms link '$aka_ms_link' is not valid: failed to get redirect location." + return 1 + fi + + say_verbose "The redirect location retrieved: '$aka_ms_download_link'." + return 0 + else + say_verbose "The aka.ms link '$aka_ms_link' is not valid: received HTTP code: $(echo "$broken_redirects" | paste -sd "," -)." + return 1 + fi +} + +get_feeds_to_use() +{ + feeds=( + "https://dotnetcli.azureedge.net/dotnet" + "https://dotnetbuilds.azureedge.net/public" + ) + + if [[ -n "$azure_feed" ]]; then + feeds=("$azure_feed") + fi + + if [[ "$no_cdn" == "true" ]]; then + feeds=( + "https://dotnetcli.blob.core.windows.net/dotnet" + "https://dotnetbuilds.blob.core.windows.net/public" + ) + + if [[ -n "$uncached_feed" ]]; then + feeds=("$uncached_feed") + fi + fi +} + +# THIS FUNCTION MAY EXIT (if the determined version is already installed). +generate_download_links() { + + download_links=() + specific_versions=() + effective_versions=() + link_types=() + + # If generate_akams_links returns false, no fallback to old links. Just terminate. + # This function may also 'exit' (if the determined version is already installed). + generate_akams_links || return + + # Check other feeds only if we haven't been able to find an aka.ms link. + if [[ "${#download_links[@]}" -lt 1 ]]; then + for feed in ${feeds[@]} + do + # generate_regular_links may also 'exit' (if the determined version is already installed). + generate_regular_links $feed || return + done + fi + + if [[ "${#download_links[@]}" -eq 0 ]]; then + say_err "Failed to resolve the exact version number." + return 1 + fi + + say_verbose "Generated ${#download_links[@]} links." + for link_index in ${!download_links[@]} + do + say_verbose "Link $link_index: ${link_types[$link_index]}, ${effective_versions[$link_index]}, ${download_links[$link_index]}" + done +} + +# THIS FUNCTION MAY EXIT (if the determined version is already installed). +generate_akams_links() { + local valid_aka_ms_link=true; + + normalized_version="$(to_lowercase "$version")" + if [[ -n "$json_file" || "$normalized_version" != "latest" ]]; then + # aka.ms links are not needed when exact version is specified via command or json file + return + fi + + get_download_link_from_aka_ms || valid_aka_ms_link=false + + if [[ "$valid_aka_ms_link" == true ]]; then + say_verbose "Retrieved primary payload URL from aka.ms link: '$aka_ms_download_link'." + say_verbose "Downloading using legacy url will not be attempted." + + download_link=$aka_ms_download_link + + #get version from the path + IFS='/' + read -ra pathElems <<< "$download_link" + count=${#pathElems[@]} + specific_version="${pathElems[count-2]}" + unset IFS; + say_verbose "Version: '$specific_version'." + + #Retrieve effective version + effective_version="$(get_specific_product_version "$azure_feed" "$specific_version" "$download_link")" + + # Add link info to arrays + download_links+=($download_link) + specific_versions+=($specific_version) + effective_versions+=($effective_version) + link_types+=("aka.ms") + + # Check if the SDK version is already installed. + if [[ "$dry_run" != true ]] && is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then + say "$asset_name with version '$effective_version' is already installed." + exit 0 + fi + + return 0 + fi + + # if quality is specified - exit with error - there is no fallback approach + if [ ! -z "$normalized_quality" ]; then + say_err "Failed to locate the latest version in the channel '$normalized_channel' with '$normalized_quality' quality for '$normalized_product', os: '$normalized_os', architecture: '$normalized_architecture'." + say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support." + return 1 + fi + say_verbose "Falling back to latest.version file approach." +} + +# THIS FUNCTION MAY EXIT (if the determined version is already installed) +# args: +# feed - $1 +generate_regular_links() { + local feed="$1" + local valid_legacy_download_link=true + + specific_version=$(get_specific_version_from_version "$feed" "$channel" "$normalized_architecture" "$version" "$json_file") || specific_version='0' + + if [[ "$specific_version" == '0' ]]; then + say_verbose "Failed to resolve the specific version number using feed '$feed'" + return + fi + + effective_version="$(get_specific_product_version "$feed" "$specific_version")" + say_verbose "specific_version=$specific_version" + + download_link="$(construct_download_link "$feed" "$channel" "$normalized_architecture" "$specific_version" "$normalized_os")" + say_verbose "Constructed primary named payload URL: $download_link" + + # Add link info to arrays + download_links+=($download_link) + specific_versions+=($specific_version) + effective_versions+=($effective_version) + link_types+=("primary") + + legacy_download_link="$(construct_legacy_download_link "$feed" "$channel" "$normalized_architecture" "$specific_version")" || valid_legacy_download_link=false + + if [ "$valid_legacy_download_link" = true ]; then + say_verbose "Constructed legacy named payload URL: $legacy_download_link" + + download_links+=($legacy_download_link) + specific_versions+=($specific_version) + effective_versions+=($effective_version) + link_types+=("legacy") + else + legacy_download_link="" + say_verbose "Cound not construct a legacy_download_link; omitting..." + fi + + # Check if the SDK version is already installed. + if [[ "$dry_run" != true ]] && is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then + say "$asset_name with version '$effective_version' is already installed." + exit 0 + fi +} + +print_dry_run() { + + say "Payload URLs:" + + for link_index in "${!download_links[@]}" + do + say "URL #$link_index - ${link_types[$link_index]}: ${download_links[$link_index]}" + done + + resolved_version=${specific_versions[0]} + repeatable_command="./$script_name --version "\""$resolved_version"\"" --install-dir "\""$install_root"\"" --architecture "\""$normalized_architecture"\"" --os "\""$normalized_os"\""" + + if [ ! -z "$normalized_quality" ]; then + repeatable_command+=" --quality "\""$normalized_quality"\""" + fi + + if [[ "$runtime" == "dotnet" ]]; then + repeatable_command+=" --runtime "\""dotnet"\""" + elif [[ "$runtime" == "aspnetcore" ]]; then + repeatable_command+=" --runtime "\""aspnetcore"\""" + fi + + repeatable_command+="$non_dynamic_parameters" + + if [ -n "$feed_credential" ]; then + repeatable_command+=" --feed-credential "\"""\""" + fi + + say "Repeatable invocation: $repeatable_command" +} + +calculate_vars() { + eval $invocation + + script_name=$(basename "$0") + normalized_architecture="$(get_normalized_architecture_from_architecture "$architecture")" + say_verbose "Normalized architecture: '$normalized_architecture'." + + normalized_os="$(get_normalized_os "$user_defined_os")" + say_verbose "Normalized OS: '$normalized_os'." + normalized_quality="$(get_normalized_quality "$quality")" + say_verbose "Normalized quality: '$normalized_quality'." + normalized_channel="$(get_normalized_channel "$channel")" + say_verbose "Normalized channel: '$normalized_channel'." + normalized_product="$(get_normalized_product "$runtime")" + say_verbose "Normalized product: '$normalized_product'." + install_root="$(resolve_installation_path "$install_dir")" + say_verbose "InstallRoot: '$install_root'." + + if [[ "$runtime" == "dotnet" ]]; then + asset_relative_path="shared/Microsoft.NETCore.App" + asset_name=".NET Core Runtime" + elif [[ "$runtime" == "aspnetcore" ]]; then + asset_relative_path="shared/Microsoft.AspNetCore.App" + asset_name="ASP.NET Core Runtime" + elif [ -z "$runtime" ]; then + asset_relative_path="sdk" + asset_name=".NET Core SDK" + fi + + get_feeds_to_use +} + +install_dotnet() { + eval $invocation + local download_failed=false + local download_completed=false + + mkdir -p "$install_root" + zip_path="$(mktemp "$temporary_file_template")" + say_verbose "Zip path: $zip_path" + + for link_index in "${!download_links[@]}" + do + download_link="${download_links[$link_index]}" + specific_version="${specific_versions[$link_index]}" + effective_version="${effective_versions[$link_index]}" + link_type="${link_types[$link_index]}" + + say "Attempting to download using $link_type link $download_link" + + # The download function will set variables $http_code and $download_error_msg in case of failure. + download_failed=false + download "$download_link" "$zip_path" 2>&1 || download_failed=true + + if [ "$download_failed" = true ]; then + case $http_code in + 404) + say "The resource at $link_type link '$download_link' is not available." + ;; + *) + say "Failed to download $link_type link '$download_link': $download_error_msg" + ;; + esac + rm -f "$zip_path" 2>&1 && say_verbose "Temporary zip file $zip_path was removed" + else + download_completed=true + break + fi + done + + if [[ "$download_completed" == false ]]; then + say_err "Could not find \`$asset_name\` with version = $specific_version" + say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support" + return 1 + fi + + say "Extracting zip from $download_link" + extract_dotnet_package "$zip_path" "$install_root" || return 1 + + # Check if the SDK version is installed; if not, fail the installation. + # if the version contains "RTM" or "servicing"; check if a 'release-type' SDK version is installed. + if [[ $specific_version == *"rtm"* || $specific_version == *"servicing"* ]]; then + IFS='-' + read -ra verArr <<< "$specific_version" + release_version="${verArr[0]}" + unset IFS; + say_verbose "Checking installation: version = $release_version" + if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$release_version"; then + return 0 + fi + fi + + # Check if the standard SDK version is installed. + say_verbose "Checking installation: version = $effective_version" + if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then + return 0 + fi + + # Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm. + say_err "Failed to verify the version of installed \`$asset_name\`.\nInstallation source: $download_link.\nInstallation location: $install_root.\nReport the bug at https://github.com/dotnet/install-scripts/issues." + say_err "\`$asset_name\` with version = $effective_version failed to install with an error." + return 1 +} + +args=("$@") + +local_version_file_relative_path="/.version" +bin_folder_relative_path="" +temporary_file_template="${TMPDIR:-/tmp}/dotnet.XXXXXXXXX" + +channel="LTS" +version="Latest" +json_file="" +install_dir="" +architecture="" +dry_run=false +no_path=false +no_cdn=false +azure_feed="" +uncached_feed="" +feed_credential="" +verbose=false +runtime="" +runtime_id="" +quality="" +internal=false +override_non_versioned_files=true +non_dynamic_parameters="" +user_defined_os="" + +while [ $# -ne 0 ] +do + name="$1" + case "$name" in + -c|--channel|-[Cc]hannel) + shift + channel="$1" + ;; + -v|--version|-[Vv]ersion) + shift + version="$1" + ;; + -q|--quality|-[Qq]uality) + shift + quality="$1" + ;; + --internal|-[Ii]nternal) + internal=true + non_dynamic_parameters+=" $name" + ;; + -i|--install-dir|-[Ii]nstall[Dd]ir) + shift + install_dir="$1" + ;; + --arch|--architecture|-[Aa]rch|-[Aa]rchitecture) + shift + architecture="$1" + ;; + --os|-[Oo][SS]) + shift + user_defined_os="$1" + ;; + --shared-runtime|-[Ss]hared[Rr]untime) + say_warning "The --shared-runtime flag is obsolete and may be removed in a future version of this script. The recommended usage is to specify '--runtime dotnet'." + if [ -z "$runtime" ]; then + runtime="dotnet" + fi + ;; + --runtime|-[Rr]untime) + shift + runtime="$1" + if [[ "$runtime" != "dotnet" ]] && [[ "$runtime" != "aspnetcore" ]]; then + say_err "Unsupported value for --runtime: '$1'. Valid values are 'dotnet' and 'aspnetcore'." + if [[ "$runtime" == "windowsdesktop" ]]; then + say_err "WindowsDesktop archives are manufactured for Windows platforms only." + fi + exit 1 + fi + ;; + --dry-run|-[Dd]ry[Rr]un) + dry_run=true + ;; + --no-path|-[Nn]o[Pp]ath) + no_path=true + non_dynamic_parameters+=" $name" + ;; + --verbose|-[Vv]erbose) + verbose=true + non_dynamic_parameters+=" $name" + ;; + --no-cdn|-[Nn]o[Cc]dn) + no_cdn=true + non_dynamic_parameters+=" $name" + ;; + --azure-feed|-[Aa]zure[Ff]eed) + shift + azure_feed="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + ;; + --uncached-feed|-[Uu]ncached[Ff]eed) + shift + uncached_feed="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + ;; + --feed-credential|-[Ff]eed[Cc]redential) + shift + feed_credential="$1" + #feed_credential should start with "?", for it to be added to the end of the link. + #adding "?" at the beginning of the feed_credential if needed. + [[ -z "$(echo $feed_credential)" ]] || [[ $feed_credential == \?* ]] || feed_credential="?$feed_credential" + ;; + --runtime-id|-[Rr]untime[Ii]d) + shift + runtime_id="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + say_warning "Use of --runtime-id is obsolete and should be limited to the versions below 2.1. To override architecture, use --architecture option instead. To override OS, use --os option instead." + ;; + --jsonfile|-[Jj][Ss]on[Ff]ile) + shift + json_file="$1" + ;; + --skip-non-versioned-files|-[Ss]kip[Nn]on[Vv]ersioned[Ff]iles) + override_non_versioned_files=false + non_dynamic_parameters+=" $name" + ;; + -?|--?|-h|--help|-[Hh]elp) + script_name="$(basename "$0")" + echo ".NET Tools Installer" + echo "Usage: $script_name [-c|--channel ] [-v|--version ] [-p|--prefix ]" + echo " $script_name -h|-?|--help" + echo "" + echo "$script_name is a simple command line interface for obtaining dotnet cli." + echo "" + echo "Options:" + echo " -c,--channel Download from the channel specified, Defaults to \`$channel\`." + echo " -Channel" + echo " Possible values:" + echo " - Current - most current release" + echo " - LTS - most current supported release" + echo " - 2-part version in a format A.B - represents a specific release" + echo " examples: 2.0; 1.0" + echo " - 3-part version in a format A.B.Cxx - represents a specific SDK release" + echo " examples: 5.0.1xx, 5.0.2xx." + echo " Supported since 5.0 release" + echo " Note: The version parameter overrides the channel parameter when any version other than 'latest' is used." + echo " -v,--version Use specific VERSION, Defaults to \`$version\`." + echo " -Version" + echo " Possible values:" + echo " - latest - most latest build on specific channel" + echo " - 3-part version in a format A.B.C - represents specific version of build" + echo " examples: 2.0.0-preview2-006120; 1.1.0" + echo " -q,--quality Download the latest build of specified quality in the channel." + echo " -Quality" + echo " The possible values are: daily, signed, validated, preview, GA." + echo " Works only in combination with channel. Not applicable for current and LTS channels and will be ignored if those channels are used." + echo " For SDK use channel in A.B.Cxx format. Using quality for SDK together with channel in A.B format is not supported." + echo " Supported since 5.0 release." + echo " Note: The version parameter overrides the channel parameter when any version other than 'latest' is used, and therefore overrides the quality." + echo " --internal,-Internal Download internal builds. Requires providing credentials via --feed-credential parameter." + echo " --feed-credential Token to access Azure feed. Used as a query string to append to the Azure feed." + echo " -FeedCredential This parameter typically is not specified." + echo " -i,--install-dir Install under specified location (see Install Location below)" + echo " -InstallDir" + echo " --architecture Architecture of dotnet binaries to be installed, Defaults to \`$architecture\`." + echo " --arch,-Architecture,-Arch" + echo " Possible values: x64, arm, and arm64" + echo " --os Specifies operating system to be used when selecting the installer." + echo " Overrides the OS determination approach used by the script. Supported values: osx, linux, linux-musl, freebsd, rhel.6." + echo " In case any other value is provided, the platform will be determined by the script based on machine configuration." + echo " Not supported for legacy links. Use --runtime-id to specify platform for legacy links." + echo " Refer to: https://aka.ms/dotnet-os-lifecycle for more information." + echo " --runtime Installs a shared runtime only, without the SDK." + echo " -Runtime" + echo " Possible values:" + echo " - dotnet - the Microsoft.NETCore.App shared runtime" + echo " - aspnetcore - the Microsoft.AspNetCore.App shared runtime" + echo " --dry-run,-DryRun Do not perform installation. Display download link." + echo " --no-path, -NoPath Do not set PATH for the current process." + echo " --verbose,-Verbose Display diagnostics information." + echo " --azure-feed,-AzureFeed For internal use only." + echo " Allows using a different storage to download SDK archives from." + echo " This parameter is only used if --no-cdn is false." + echo " --uncached-feed,-UncachedFeed For internal use only." + echo " Allows using a different storage to download SDK archives from." + echo " This parameter is only used if --no-cdn is true." + echo " --skip-non-versioned-files Skips non-versioned files if they already exist, such as the dotnet executable." + echo " -SkipNonVersionedFiles" + echo " --no-cdn,-NoCdn Disable downloading from the Azure CDN, and use the uncached feed directly." + echo " --jsonfile Determines the SDK version from a user specified global.json file." + echo " Note: global.json must have a value for 'SDK:Version'" + echo " -?,--?,-h,--help,-Help Shows this help message" + echo "" + echo "Install Location:" + echo " Location is chosen in following order:" + echo " - --install-dir option" + echo " - Environmental variable DOTNET_INSTALL_DIR" + echo " - $HOME/.dotnet" + exit 0 + ;; + *) + say_err "Unknown argument \`$name\`" + exit 1 + ;; + esac + + shift +done + +say "Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" +say "- The SDK needs to be installed without user interaction and without admin rights." +say "- The SDK installation doesn't need to persist across multiple CI runs." +say "To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.\n" + +if [ "$internal" = true ] && [ -z "$(echo $feed_credential)" ]; then + message="Provide credentials via --feed-credential parameter." + if [ "$dry_run" = true ]; then + say_warning "$message" + else + say_err "$message" + exit 1 + fi +fi + +check_min_reqs +calculate_vars +# generate_regular_links call below will 'exit' if the determined version is already installed. +generate_download_links + +if [[ "$dry_run" = true ]]; then + print_dry_run + exit 0 +fi + +install_dotnet + +bin_path="$(get_absolute_path "$(combine_paths "$install_root" "$bin_folder_relative_path")")" +if [ "$no_path" = false ]; then + say "Adding to current process PATH: \`$bin_path\`. Note: This change will be visible only when sourcing script." + export PATH="$bin_path":"$PATH" +else + say "Binaries of dotnet can be found in $bin_path" +fi + +say "Note that the script does not resolve dependencies during installation." +say "To check the list of dependencies, go to https://docs.microsoft.com/dotnet/core/install, select your operating system and check the \"Dependencies\" section." +say "Installation finished successfully." \ No newline at end of file diff --git a/qinglong/extra.sh b/qinglong/extra.sh new file mode 100644 index 0000000..d95651f --- /dev/null +++ b/qinglong/extra.sh @@ -0,0 +1,8 @@ +## 添加你需要重启自动执行的任意命令,比如 ql repo +## 安装node依赖使用 pnpm install -g xxx xxx +## 安装python依赖使用 pip3 install xxx + +# 安装 dotnet 环境 +# dotnet --version || (curl -sSL https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/main/qinglong/ray-dotnet-install.sh | bash /dev/stdin --no-official) && (echo "已安装dotnet") +dotnet --version || (curl -sSL https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/main/qinglong/ray-dotnet-install.sh | bash /dev/stdin) && (echo "已安装dotnet") +# 其他代码... diff --git a/qinglong/ray-dotnet-install.sh b/qinglong/ray-dotnet-install.sh new file mode 100644 index 0000000..7c429aa --- /dev/null +++ b/qinglong/ray-dotnet-install.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +echo -e "\n-------set up dot net env-------" + +## 安装dotnet + +# 安装依赖 +install_dependency() { + echo "安装依赖..." + apk add bash icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib +} + +# 通过官方脚本安装dotnet +install_by_offical() { + echo "install by offical script..." + curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 8.0 --no-cdn --verbose +} + +# 创建软链接 +create_soft_link() { + # echo "创建软链接..." + # rm -f /usr/bin/dotnet + # ln -s ~/.dotnet/dotnet /usr/bin/dotnet + + echo "添加PATH" + local exportFile="/root/.bashrc" + touch $exportFile + echo '' >> $exportFile + echo 'export DOTNET_ROOT=$HOME/.dotnet' >> $exportFile + echo 'export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools' >> $exportFile + . $exportFile +} + +args=("$@") + +install_dependency + +install_by_offical + +create_soft_link + +dotnet --info + +echo -e "\n-------set up dot net env finish-------" \ No newline at end of file diff --git a/scripts/clean.cmd b/scripts/clean.cmd new file mode 100644 index 0000000..65bcc92 --- /dev/null +++ b/scripts/clean.cmd @@ -0,0 +1,12 @@ +@echo off +cd .. +REM start to clean +echo Start to clean all bin and obj folder under Noodle repo +@echo on +@for /d /r %%c in (obj) do @if exist "%%c" (@rd /s /q "%%c" & echo Delete %%c) +@for /d /r %%c in (bin) do @if exist "%%c" (@rd /s /q "%%c" & echo Delete %%c) +@for /d /r %%c in (packages) do @if exist "%%c" (@rd /s /q "%%c" & echo Delete %%c) +@for /d /r %%c in (.vs) do @if exist "%%c" (@rd /s /q "%%c" & echo Delete %%c) +@for /d /r %%c in (temp) do @if exist "%%c" (@rd /s /q "%%c" & echo Delete %%c) +@echo off +pause \ No newline at end of file diff --git a/scripts/publish.bat b/scripts/publish.bat new file mode 100644 index 0000000..7132a6b --- /dev/null +++ b/scripts/publish.bat @@ -0,0 +1,14 @@ +::https://docs.microsoft.com/zh-cn/dotnet/core/tools/dotnet-publish +::关闭回显 +@echo off + +dotnet publish --configuration Release --self-contained false -o ./bin/Publish/net5-dependent +echo "dotnet Ray.BiliBiliTool.Console.dll" > ./bin/Publish/net5-dependent/start.bat + +::dotnet publish --configuration Release --runtime win-x86 --self-contained true -p:PublishTrimmed=true -o ./bin/Publish/win-x86-x64 +::dotnet publish --configuration Release --runtime linux-x64 --self-contained true -p:PublishTrimmed=true -o ./bin/Publish/linux-x64 +::dotnet publish --configuration Release --runtime linux-arm --self-contained true -p:PublishTrimmed=true -o ./bin/Publish/linux-arm +::dotnet publish --configuration Release --runtime linux-arm64 --self-contained true -p:PublishTrimmed=true -o ./bin/Publish/linux-arm64 +::dotnet publish --configuration Release --runtime osx-x64 --self-contained true -p:PublishTrimmed=true -o ./bin/Publish/osx-x64 + +pause diff --git a/scripts/publish.ps1 b/scripts/publish.ps1 new file mode 100644 index 0000000..e21417b --- /dev/null +++ b/scripts/publish.ps1 @@ -0,0 +1,16 @@ +dotnet.exe publish ../src/Ray.BiliBiliTool.Console/Ray.BiliBiliTool.Console.csproj --runtime win-x86 --no-self-contained -c Release -p:PublishSingleFile=true -o ./bin/Publish/win-x86 +dotnet.exe publish ../src/Ray.BiliBiliTool.Console/Ray.BiliBiliTool.Console.csproj --runtime win-x64 --no-self-contained -c Release -p:PublishSingleFile=true -o ./bin/Publish/win-x64 +dotnet.exe publish ../src/Ray.BiliBiliTool.Console/Ray.BiliBiliTool.Console.csproj --runtime win-arm64 --no-self-contained -c Release -p:PublishSingleFile=true -o ./bin/Publish/win-arm64 +dotnet.exe publish ../src/Ray.BiliBiliTool.Console/Ray.BiliBiliTool.Console.csproj --runtime linux-x64 --no-self-contained -c Release -p:PublishSingleFile=true -o ./bin/Publish/linux-x64 +dotnet.exe publish ../src/Ray.BiliBiliTool.Console/Ray.BiliBiliTool.Console.csproj --runtime linux-musl-x64 --no-self-contained -c Release -p:PublishSingleFile=true -o ./bin/Publish/linux-musl-x64 +dotnet.exe publish ../src/Ray.BiliBiliTool.Console/Ray.BiliBiliTool.Console.csproj --runtime linux-arm64 --no-self-contained -c Release -p:PublishSingleFile=true -o ./bin/Publish/linux-arm64 +dotnet.exe publish ../src/Ray.BiliBiliTool.Console/Ray.BiliBiliTool.Console.csproj --runtime linux-arm --no-self-contained -c Release -p:PublishSingleFile=true -o ./bin/Publish/linux-arm +dotnet.exe publish ../src/Ray.BiliBiliTool.Console/Ray.BiliBiliTool.Console.csproj --runtime osx-x64 --no-self-contained -c Release -p:PublishSingleFile=true -o ./bin/Publish/osx-x64 +Remove-Item ./bin/Publish/win-x86/*.pdb +Remove-Item ./bin/Publish/win-x64/*.pdb +Remove-Item ./bin/Publish/win-arm64/*.pdb +Remove-Item ./bin/Publish/linux-x64/*.pdb +Remove-Item ./bin/Publish/linux-musl-x64/*.pdb +Remove-Item ./bin/Publish/linux-arm64/*.pdb +Remove-Item ./bin/Publish/linux-arm/*.pdb +Remove-Item ./bin/Publish/osx-x64/*.pdb diff --git a/scripts/publish.sh b/scripts/publish.sh new file mode 100644 index 0000000..65dcfa1 --- /dev/null +++ b/scripts/publish.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +set -e +set -u +set -o pipefail + +echo ' ____ _ _____ _ ' +echo ' | __ ) _| |_|_ _|__ ___ | | ' +echo ' | _ \(_) (_) | |/ _ \ / _ \| | ' +echo ' | |_) | | | | | | (_) | (_) | | ' +echo ' |____/|_|_|_| |_|\___/ \___/|_| ' +echo '' + +# ------------vars----------- +repoDir=$(dirname $PWD) +consoleDir=$repoDir/src/Ray.BiliBiliTool.Console +publishDir=$consoleDir/bin/Publish +version="" +runTime="" +# -------------------------- + +read_params_from_init_cmd() { + while [ $# -ne 0 ]; do + name="$1" + case "$name" in + -r | --runtime | -[Rr]untime) + shift + runTime="$1" + ;; + *) + say_err "Unknown argument \`$name\`" + exit 1 + ;; + esac + shift + done +} + +read_var_from_user() { + # runTime + if [ -z "$runTime" ]; then + read -p 'please input runTime("all" "win-x86" "win-x64" "win-arm64" "linux-x64" "linux-musl-x64" "linux-arm64" "linux-arm" "osx-x64")' runTime + else + echo "runTime: $runTime" + fi +} + +get_version() { + version=$(grep -oP '(?<=).*?(?=<\/Version>)' $repoDir/common.props) + echo -e "current version: $version \n\n" + + mkdir -p $publishDir + + # 将版本号保存到文件 + echo "$version" > "$publishDir/version.txt" + + echo "Version saved to $publishDir/version.txt" +} + +extract_release_notes() { + echo "Extracting release notes from CHANGELOG.md..." + mkdir -p $publishDir + + # 提取最新的 changelog (从第一个 ## 标题到下一个 ## 标题之间的所有内容) + sed -n '/^## /{p;:a;n;/^## /q;p;ba}' "$repoDir/CHANGELOG.md" > "$publishDir/release_notes.md" + + echo "Release notes saved to $publishDir/release_notes.md" +} + +publish_dotnet_dependent() { + echo "---------start publishing 【dotnet dependent】 release---------" + + echo "clear output dir" + local outputDir=$publishDir/dotnet-dependent + mkdir -p $outputDir + rm -rf $outputDir + + cd $consoleDir + echo "dotnet publish..." + dotnet publish --configuration Release \ + --self-contained false \ + -p:PublishSingleFile=true \ + -p:DebugType=None \ + -p:DebugSymbols=false \ + -o $outputDir + + echo "zip files..." + cd $publishDir + zip -q -r bilibili-tool-pro-v$version-dotnet-dependent.zip ./dotnet-dependent/* + ls -l + echo -e "---------publish successfully---------\n\n" +} + +publish_self_contained() { + local runtime=$1 + echo "---------start publishing 【$runtime】 release---------" + + echo "clear output dir" + local outputDir=$publishDir/$runtime + mkdir -p $outputDir + rm -rf $outputDir + + cd $consoleDir + echo "dotnet publish..." + dotnet publish --configuration Release \ + --self-contained true \ + --runtime $runtime \ + -p:PublishSingleFile=true \ + -p:DebugType=None \ + -p:DebugSymbols=false \ + -o $outputDir + + echo "zip files..." + cd $publishDir + zip -q -r bilibili-tool-pro-v$version-$runtime.zip ./$runtime/* + ls -l + echo -e "---------publish successfully---------\n\n" +} + +publish_tencentScf() { + echo "---------start publishing 【tencent scf】 release---------" + cd $publishDir + cp -r $repoDir/tencentScf/bootstrap $repoDir/tencentScf/index.sh ./linux-x64/ + cd ./linux-x64 + chmod 755 index.sh bootstrap + zip -r ../bilibili-tool-pro-v$version-tencent-scf.zip ./* + cd .. && ls + echo -e "---------publish successfully---------\n\n" +} + +main() { + read_params_from_init_cmd $* + read_var_from_user + + get_version + extract_release_notes + + # dotnet dependent + publish_dotnet_dependent + + # self contained + # https://learn.microsoft.com/zh-cn/dotnet/core/rid-catalog + array=("win-x86" "win-x64" "win-arm64" "linux-x64" "linux-musl-x64" "linux-arm64" "linux-arm" "linux-musl-arm64" "osx-x64") + if [ "$runTime" != "all" ]; then + array=("$runTime") + fi + for i in "${array[@]}"; do + publish_self_contained $i + done + + if [ "$runTime" == "all" ]; then + publish_tencentScf + fi +} + +main $* diff --git a/scripts/ut.ps1 b/scripts/ut.ps1 new file mode 100644 index 0000000..7766857 --- /dev/null +++ b/scripts/ut.ps1 @@ -0,0 +1,19 @@ +Set-Location .. + +# 安装 ReportGenerator 工具 +Write-Output "Installing ReportGenerator tool..." +dotnet tool install -g dotnet-reportgenerator-globaltool + +# 运行单元测试并生成覆盖率报告 +Write-Output "Running unit tests and generating coverage report..." +dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=./TestResults/coverage.opencover.xml + +# 生成html报告 +$coverageFiles = Get-ChildItem -Path . -Recurse -Filter "coverage.cobertura.xml" +$coverageFiles | ForEach-Object { Write-Output $_.FullName } +$reportPaths = ($coverageFiles | ForEach-Object { $_.FullName }) -join ";" +reportgenerator "-reports:$reportPaths" "-targetdir:coveragereport" -reporttypes:Html + +# 检查生成的覆盖率报告文件是否存在 +Write-Output "Coverage report generated successfully." +Start-Process "coveragereport/index.htm" \ No newline at end of file diff --git a/src/BlazingQuartz.Core/BlazingQuartz.Core.csproj b/src/BlazingQuartz.Core/BlazingQuartz.Core.csproj new file mode 100644 index 0000000..4fe87b4 --- /dev/null +++ b/src/BlazingQuartz.Core/BlazingQuartz.Core.csproj @@ -0,0 +1,22 @@ + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/src/BlazingQuartz.Core/BlazingQuartzCoreOptions.cs b/src/BlazingQuartz.Core/BlazingQuartzCoreOptions.cs new file mode 100644 index 0000000..322ae57 --- /dev/null +++ b/src/BlazingQuartz.Core/BlazingQuartzCoreOptions.cs @@ -0,0 +1,28 @@ +using System; + +namespace BlazingQuartz.Core +{ + public class BlazingQuartzCoreOptions + { + /// + /// Assembly files that contain IJob or IJobUI implementation use for creating schedule. + /// Ex. Quartz.Jobs + /// Or Jobs/Quartz.Jobs - if this file is under Job folder + /// + public string[]? AllowedJobAssemblyFiles { get; set; } + + /// + /// Job types that are not allowed to be used for creating new Jobs using UI. + /// Ex. Quartz.Job.NativeJob + /// + public string[]? DisallowedJobTypes { get; set; } + + /// + /// Storage to use to store execution history + /// + public DataStoreProvider DataStoreProvider { get; set; } = DataStoreProvider.Sqlite; + public bool AutoMigrateDb { get; set; } = true; + public string? HousekeepingCronSchedule { get; set; } = "0 0 1 * * ?"; + public int ExecutionLogsDaysToKeep { get; set; } = 21; + } +} diff --git a/src/BlazingQuartz.Core/Constants.cs b/src/BlazingQuartz.Core/Constants.cs new file mode 100644 index 0000000..d91dee0 --- /dev/null +++ b/src/BlazingQuartz.Core/Constants.cs @@ -0,0 +1,153 @@ +using System; +using System.ComponentModel; + +namespace BlazingQuartz; + +public static class Constants +{ + public const string SYSTEM_GROUP = "System"; + public const string DEFAULT_GROUP = "No Group"; +} + +public enum DataStoreProvider +{ + Sqlite, + PostgreSQL, + InMemory, + SqlServer, + Custom, +} + +public enum JobStatus +{ + Running, + Idle, + Paused, + + /// + /// No trigger assigned to this job. This happens when job is durable and triggers ended + /// + NoTrigger, + + /// + /// Not scheduled in scheduler. This happens when job is NOT durable and triggers ended + /// + NoSchedule, + Error, +} + +public enum TriggerType +{ + Cron, + Daily, + Simple, + Calendar, + Unknown, +} + +public enum MisfireAction +{ + [Description( + "Instructs the IScheduler that the ITrigger will never be evaluated for a misfire situation, and that the scheduler will simply try to fire it as soon as it can, and then update the Trigger as if it had fired at the proper time." + )] + IgnoreMisfirePolicy, + + [Description("Instruction not set (yet).")] + InstructionNotSet, + + [Description("Use smart policy.")] + SmartPolicy, + + [Description( + "Fired now by IScheduler. NOTE: This instruction should typically only be used for 'one-shot' (non-repeating) Triggers. If it is used on a trigger with a repeat count > 0 then it is equivalent to the instruction RescheduleNowWithRemainingRepeatCount." + )] + FireNow, + + [Description( + "Re-scheduled to the next scheduled time after 'now' - taking into account any associated ICalendar, and with the repeat count left unchanged." + )] + RescheduleNextWithExistingCount, + + [Description( + "Re-scheduled to the next scheduled time after 'now' - taking into account any associated ICalendar, and with the repeat count set to what it would be, if it had not missed any firings." + )] + RescheduleNextWithRemainingCount, + + [Description( + "Re-scheduled to 'now' (even if the associated ICalendar excludes 'now') with the repeat count left as-is. This does obey the ITrigger end-time however, so if 'now' is after the end-time the ITrigger will not fire again." + )] + RescheduleNowWithExistingRepeatCount, + + [Description( + "Re-scheduled to 'now' (even if the associated ICalendar excludes 'now') with the repeat count set to what it would be, if it had not missed any firings. This does obey the ITrigger end-time however, so if 'now' is after the end-time the ITrigger will not fire again. NOTE: Use of this instruction causes the trigger to 'forget' the start-time and repeat-count that it was originally setup with. Instead, the repeat count on the trigger will be changed to whatever the remaining repeat count is (this is only an issue if you for some reason wanted to be able to tell what the original values were at some later time). NOTE: This instruction could cause the ITrigger to go to the 'COMPLETE' state after firing 'now', if all the repeat-fire-times where missed." + )] + RescheduleNowWithRemainingRepeatCount, + + [Description( + "Instruct to have it's next-fire-time updated to the next time in the schedule after the current time (taking into account any associated ICalendar), but it does not want to be fired now." + )] + DoNothing, + + [Description("Instruct to fire now")] + FireOnceNow, +} + +public enum IntervalUnit +{ + Millisecond, + Second, + Minute, + Hour, + Day, + Week, + Month, + Year, +} + +public enum DataMapType +{ + Bool, + String, + Integer, + Float, + Double, + Decimal, + Long, + Short, + Char, + Object, +} + +public enum JobExecutionStatus +{ + Success, + Failed, + Executing, + Vetoed, +} + +public class BlazingQuartzCoreOptions +{ + /// + /// Assembly files that contain IJob or IJobUI implementation use for creating schedule. + /// Ex. Quartz.Jobs + /// Or Jobs/Quartz.Jobs - if this file is under Job folder + /// + public string[]? AllowedJobAssemblyFiles { get; set; } + + /// + /// Job types that are not allowed to be used for creating new Jobs using UI. + /// Ex. Quartz.Job.NativeJob + /// + public string[]? DisallowedJobTypes { get; set; } + + /// + /// Storage to use to store execution history + /// + public DataStoreProvider DataStoreProvider { get; set; } = DataStoreProvider.Sqlite; + public bool AutoMigrateDb { get; set; } = true; + public string? HousekeepingCronSchedule { get; set; } = "0 0 1 * * ?"; + public int ExecutionLogsDaysToKeep { get; set; } = 21; +} + +public class BlazingQuartzUIOptions : BlazingQuartzCoreOptions { } diff --git a/src/BlazingQuartz.Core/Events/EventArgs.cs b/src/BlazingQuartz.Core/Events/EventArgs.cs new file mode 100644 index 0000000..2f2e8eb --- /dev/null +++ b/src/BlazingQuartz.Core/Events/EventArgs.cs @@ -0,0 +1,16 @@ +using System; + +namespace BlazingQuartz.Core.Events +{ + public class EventArgs : EventArgs + { + public TArgs Args { get; init; } + public CancellationToken CancelToken { get; init; } + + public EventArgs(TArgs args, CancellationToken cancelToken = default(CancellationToken)) + { + Args = args; + CancelToken = cancelToken; + } + } +} diff --git a/src/BlazingQuartz.Core/Events/JobWasExecutedEventArgs.cs b/src/BlazingQuartz.Core/Events/JobWasExecutedEventArgs.cs new file mode 100644 index 0000000..e48544f --- /dev/null +++ b/src/BlazingQuartz.Core/Events/JobWasExecutedEventArgs.cs @@ -0,0 +1,23 @@ +using System; +using Quartz; + +namespace BlazingQuartz.Core.Events +{ + public class JobWasExecutedEventArgs : EventArgs + { + public IJobExecutionContext JobExecutionContext { get; init; } + public JobExecutionException? JobException { get; init; } + public CancellationToken CancelToken { get; set; } + + public JobWasExecutedEventArgs( + IJobExecutionContext context, + JobExecutionException? exception, + CancellationToken cancelToken = default(CancellationToken) + ) + { + JobExecutionContext = context; + JobException = exception; + CancelToken = cancelToken; + } + } +} diff --git a/src/BlazingQuartz.Core/Events/SchedulerErrorEventArgs.cs b/src/BlazingQuartz.Core/Events/SchedulerErrorEventArgs.cs new file mode 100644 index 0000000..5f55b38 --- /dev/null +++ b/src/BlazingQuartz.Core/Events/SchedulerErrorEventArgs.cs @@ -0,0 +1,12 @@ +using System; +using Quartz; + +namespace BlazingQuartz.Core.Events +{ + public class SchedulerErrorEventArgs : EventArgs + { + public string ErrorMessage { get; init; } = null!; + public SchedulerException Exception { get; init; } = null!; + public CancellationToken CancelToken { get; init; } + } +} diff --git a/src/BlazingQuartz.Core/Events/TriggerEventArgs.cs b/src/BlazingQuartz.Core/Events/TriggerEventArgs.cs new file mode 100644 index 0000000..2259e4f --- /dev/null +++ b/src/BlazingQuartz.Core/Events/TriggerEventArgs.cs @@ -0,0 +1,23 @@ +using System; +using Quartz; + +namespace BlazingQuartz.Core.Events +{ + public class TriggerEventArgs : EventArgs + { + public ITrigger Trigger { get; init; } + public IJobExecutionContext JobExecutionContext { get; init; } + public CancellationToken CancelToken { get; init; } + + public TriggerEventArgs( + ITrigger trigger, + IJobExecutionContext context, + CancellationToken cancelToken = default(CancellationToken) + ) + { + Trigger = trigger; + JobExecutionContext = context; + CancelToken = cancelToken; + } + } +} diff --git a/src/BlazingQuartz.Core/Extensions/ModelExtensions.cs b/src/BlazingQuartz.Core/Extensions/ModelExtensions.cs new file mode 100644 index 0000000..8ac792a --- /dev/null +++ b/src/BlazingQuartz.Core/Extensions/ModelExtensions.cs @@ -0,0 +1,97 @@ +using System; +using BlazingQuartz.Core.Models; +using Quartz; + +namespace BlazingQuartz.Core +{ + public static class ModelExtensions + { + public static bool EqualsTriggerKey(this ScheduleModel model, TriggerKey triggerKey) + { + return model.TriggerName == triggerKey.Name && model.TriggerGroup == triggerKey.Group; + } + + public static bool Equals(this ScheduleModel model, JobKey? jobKey, TriggerKey? triggerKey) + { + if (jobKey != null && triggerKey != null) + return model.JobName == jobKey.Name + && model.JobGroup == jobKey.Group + && model.TriggerName == triggerKey.Name + && model.TriggerGroup == triggerKey.Group; + + if (jobKey != null && triggerKey == null) + return model.JobName == jobKey.Name + && model.JobGroup == jobKey.Group + && model.TriggerName == null + && model.TriggerGroup == null; + + // less possible + if (jobKey == null && triggerKey != null) + return model.TriggerName == triggerKey.Name + && model.TriggerGroup == triggerKey.Group + && model.JobName == null + && model.JobGroup == Constants.DEFAULT_GROUP; + + return model.JobName == null && model.TriggerName == null && model.TriggerGroup == null; + } + + public static TriggerType GetTriggerType(this ITrigger trigger) + { + if (trigger is ICronTrigger) + return TriggerType.Cron; + if (trigger is ISimpleTrigger) + return TriggerType.Simple; + if (trigger is ICalendarIntervalTrigger) + return TriggerType.Calendar; + if (trigger is IDailyTimeIntervalTrigger) + return TriggerType.Daily; + + return TriggerType.Unknown; + } + + public static TimeOfDay ToTimeOfDay(this TimeSpan timeSpan) + { + return new TimeOfDay(timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds); + } + + public static Quartz.IntervalUnit ToQuartzIntervalUnit(this IntervalUnit value) + { + return Enum.Parse(value.ToString()); + } + + public static IntervalUnit ToBlazingQuartzIntervalUnit(this Quartz.IntervalUnit value) + { + return Enum.Parse(value.ToString()); + } + + public static JobKey ToJobKey(this Key key) + { + return key.Group == null ? new JobKey(key.Name) : new JobKey(key.Name, key.Group); + } + + public static TriggerKey ToTriggerKey(this Key key) + { + return key.Group == null + ? new TriggerKey(key.Name) + : new TriggerKey(key.Name, key.Group); + } + + /// + /// Return closest non null stack trace of exception. + /// Loop until null InnerException to get stack trace. + /// + /// + /// null if inner exceptions does not have stack trace + public static string? NonNullStackTrace(this Exception exception) + { + Exception? currentException = exception; + while (currentException.StackTrace == null) + { + currentException = currentException.InnerException; + if (currentException == null) + break; + } + return currentException?.StackTrace; + } + } +} diff --git a/src/BlazingQuartz.Core/Helpers/CronExpressionHelper.cs b/src/BlazingQuartz.Core/Helpers/CronExpressionHelper.cs new file mode 100644 index 0000000..9347054 --- /dev/null +++ b/src/BlazingQuartz.Core/Helpers/CronExpressionHelper.cs @@ -0,0 +1,13 @@ +using System; +using Quartz; + +namespace BlazingQuartz.Core.Helpers +{ + public static class CronExpressionHelper + { + public static bool IsValidExpression(string cronExpression) + { + return CronExpression.IsValidExpression(cronExpression); + } + } +} diff --git a/src/BlazingQuartz.Core/History/BaseExecutionLogRawSqlProvider.cs b/src/BlazingQuartz.Core/History/BaseExecutionLogRawSqlProvider.cs new file mode 100644 index 0000000..a8ad674 --- /dev/null +++ b/src/BlazingQuartz.Core/History/BaseExecutionLogRawSqlProvider.cs @@ -0,0 +1,11 @@ +using System; + +namespace BlazingQuartz.Core.History +{ + public class BaseExecutionLogRawSqlProvider : IExecutionLogRawSqlProvider + { + public virtual string DeleteLogsByDays { get; } = + @"DELETE FROM bili_execution_logs +WHERE date_added_utc < {0}"; + } +} diff --git a/src/BlazingQuartz.Core/History/ExecutionLogStore.cs b/src/BlazingQuartz.Core/History/ExecutionLogStore.cs new file mode 100644 index 0000000..3e0126f --- /dev/null +++ b/src/BlazingQuartz.Core/History/ExecutionLogStore.cs @@ -0,0 +1,102 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Ray.BiliBiliTool.Domain; +using Ray.BiliBiliTool.Infrastructure.EF; + +namespace BlazingQuartz.Core.History +{ + public class ExecutionLogStore : IExecutionLogStore + { + private readonly ILogger _logger; + private readonly BiliDbContext _dbContext; + private readonly IExecutionLogRawSqlProvider _sqlProvider; + + public ExecutionLogStore( + ILogger logger, + BiliDbContext dbContext, + IExecutionLogRawSqlProvider sqlProvider + ) + { + _logger = logger; + _dbContext = dbContext; + _sqlProvider = sqlProvider; + } + + public async Task AddExecutionLog(ExecutionLog log, CancellationToken cancelToken = default) + { + await _dbContext.ExecutionLogs.AddAsync(log, cancelToken); + } + + public bool Exists(ExecutionLog log) + { + return _dbContext.ExecutionLogs.Any(l => l.RunInstanceId == log.RunInstanceId); + } + + public async Task DeleteLogsByDays( + int daysToKeep, + CancellationToken cancelToken = default + ) + { + DateTime oldDate = DateTime.UtcNow.Date.AddDays(-(daysToKeep + 1)); + + IEnumerable parameters = new List { oldDate }; + return await _dbContext.Database.ExecuteSqlRawAsync( + _sqlProvider.DeleteLogsByDays, + parameters, + cancelToken + ); + } + + public async Task SaveChangesAsync(CancellationToken cancelToken = default) + { + await _dbContext.SaveChangesAsync(cancelToken); + } + + public ValueTask UpdateExecutionLog(ExecutionLog log) + { + var entry = _dbContext + .ExecutionLogs.Where(l => l.RunInstanceId == log.RunInstanceId) + .FirstOrDefault(); + + if (entry != null) + { + entry.ExecutionLogDetail = log.ExecutionLogDetail; + entry.ErrorMessage = log.ErrorMessage; + entry.ExecutionLogDetail = log.ExecutionLogDetail; + entry.IsVetoed = log.IsVetoed; + entry.JobRunTime = log.JobRunTime; + entry.Result = log.Result; + entry.IsException = log.IsException; + entry.IsSuccess = log.IsSuccess; + entry.ReturnCode = log.ReturnCode; + + _dbContext.ExecutionLogs.Update(entry); + } + else + { + _logger.LogWarning( + "Failed to UpdateExecutionLog. Cannot find run instance id [{runInstanceId}]", + log.RunInstanceId + ); + } + + return ValueTask.CompletedTask; + } + + public async Task MarkExecutingJobAsIncomplete(CancellationToken cancellToken = default) + { + var isSuccessNullJobs = _dbContext.ExecutionLogs.Where(l => + !l.IsSuccess.HasValue && l.LogType == LogType.ScheduleJob + ); + + foreach (var log in isSuccessNullJobs) + { + log.IsSuccess = false; + log.ErrorMessage = "Incomplete execution."; + log.JobRunTime = null; + } + + await _dbContext.SaveChangesAsync(cancellToken); + } + } +} diff --git a/src/BlazingQuartz.Core/History/IExecutionLogRawSqlProvider.cs b/src/BlazingQuartz.Core/History/IExecutionLogRawSqlProvider.cs new file mode 100644 index 0000000..a1d8ac5 --- /dev/null +++ b/src/BlazingQuartz.Core/History/IExecutionLogRawSqlProvider.cs @@ -0,0 +1,7 @@ +namespace BlazingQuartz.Core.History +{ + public interface IExecutionLogRawSqlProvider + { + string DeleteLogsByDays { get; } + } +} diff --git a/src/BlazingQuartz.Core/History/IExecutionLogStore.cs b/src/BlazingQuartz.Core/History/IExecutionLogStore.cs new file mode 100644 index 0000000..ea51396 --- /dev/null +++ b/src/BlazingQuartz.Core/History/IExecutionLogStore.cs @@ -0,0 +1,15 @@ +using System; +using Ray.BiliBiliTool.Domain; + +namespace BlazingQuartz.Core.History +{ + public interface IExecutionLogStore + { + bool Exists(ExecutionLog log); + Task DeleteLogsByDays(int daysToKeep, CancellationToken cancelToken = default); + Task AddExecutionLog(ExecutionLog log, CancellationToken cancelToken = default); + ValueTask UpdateExecutionLog(ExecutionLog log); + Task SaveChangesAsync(CancellationToken cancelToken = default); + Task MarkExecutingJobAsIncomplete(CancellationToken cancellToken = default); + } +} diff --git a/src/BlazingQuartz.Core/History/ISchedulerEventLoggingService.cs b/src/BlazingQuartz.Core/History/ISchedulerEventLoggingService.cs new file mode 100644 index 0000000..07cb77b --- /dev/null +++ b/src/BlazingQuartz.Core/History/ISchedulerEventLoggingService.cs @@ -0,0 +1,6 @@ +using System; + +namespace BlazingQuartz.Core.History +{ + internal interface ISchedulerEventLoggingService { } +} diff --git a/src/BlazingQuartz.Core/History/SchedulerEventLoggingService.cs b/src/BlazingQuartz.Core/History/SchedulerEventLoggingService.cs new file mode 100644 index 0000000..51e7be1 --- /dev/null +++ b/src/BlazingQuartz.Core/History/SchedulerEventLoggingService.cs @@ -0,0 +1,579 @@ +using System; +using System.Globalization; +using System.Threading.Channels; +using BlazingQuartz.Core.Jobs; +using BlazingQuartz.Core.Services; +using BlazingQuartz.Jobs.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Quartz; +using Ray.BiliBiliTool.Domain; + +namespace BlazingQuartz.Core.History; + +internal class SchedulerEventLoggingService : BackgroundService, ISchedulerEventLoggingService +{ + private const int RESULT_MAX_LENGTH = 8000; + private const int MAX_BATCH_SIZE = 50; + + private readonly IServiceProvider _svcProvider; + private readonly ISchedulerListenerService _schLisSvc; + private readonly ISchedulerFactory _schedulerFactory; + private readonly ILogger _logger; + private readonly Channel> _taskQueue; + private readonly BlazingQuartzCoreOptions _options; + + public SchedulerEventLoggingService( + ILogger logger, + IServiceProvider serviceProvider, + ISchedulerListenerService listenerSvc, + ISchedulerFactory schFactory, + IOptions options + ) + { + _logger = logger; + _svcProvider = serviceProvider; + _schLisSvc = listenerSvc; + _schedulerFactory = schFactory; + _options = options.Value; + _taskQueue = Channel.CreateUnbounded< + Func + >(new UnboundedChannelOptions { SingleReader = true }); + + Init(); + } + + public override void Dispose() + { + _schLisSvc.OnJobToBeExecuted -= _schLisSvc_OnJobToBeExecuted; + _schLisSvc.OnJobWasExecuted -= _schLisSvc_OnJobWasExecuted; + _schLisSvc.OnJobExecutionVetoed -= _schLisSvc_OnJobExecutionVetoed; + _schLisSvc.OnJobDeleted -= _schLisSvc_OnJobDeleted; + _schLisSvc.OnJobInterrupted -= _schLisSvc_OnJobInterrupted; + _schLisSvc.OnSchedulerError -= _schLisSvc_OnSchedulerError; + _schLisSvc.OnTriggerMisfired -= _schLisSvc_OnTriggerMisfired; + _schLisSvc.OnTriggerPaused -= _schLisSvc_OnTriggerPaused; + _schLisSvc.OnTriggerResumed -= _schLisSvc_OnTriggerResumed; + _schLisSvc.OnTriggerFinalized -= _schLisSvc_OnTriggerFinalized; + _schLisSvc.OnJobScheduled -= _schLisSvc_OnJobScheduled; + base.Dispose(); + } + + void Init() + { + _schLisSvc.OnJobToBeExecuted += _schLisSvc_OnJobToBeExecuted; + _schLisSvc.OnJobWasExecuted += _schLisSvc_OnJobWasExecuted; + _schLisSvc.OnJobExecutionVetoed += _schLisSvc_OnJobExecutionVetoed; + _schLisSvc.OnJobDeleted += _schLisSvc_OnJobDeleted; + _schLisSvc.OnJobInterrupted += _schLisSvc_OnJobInterrupted; + _schLisSvc.OnSchedulerError += _schLisSvc_OnSchedulerError; + _schLisSvc.OnTriggerMisfired += _schLisSvc_OnTriggerMisfired; + _schLisSvc.OnTriggerPaused += _schLisSvc_OnTriggerPaused; + _schLisSvc.OnTriggerResumed += _schLisSvc_OnTriggerResumed; + _schLisSvc.OnTriggerFinalized += _schLisSvc_OnTriggerFinalized; + _schLisSvc.OnJobScheduled += _schLisSvc_OnJobScheduled; + } + + private void _schLisSvc_OnJobScheduled(object? sender, Events.EventArgs e) + { + var jKey = e.Args.JobKey; + var tKey = e.Args.Key; + var log = new ExecutionLog + { + JobName = jKey.Name, + JobGroup = jKey.Group, + TriggerName = tKey.Name, + TriggerGroup = tKey.Group, + LogType = LogType.Trigger, + Result = "Job scheduled", + }; + QueueInsertTask(log); + } + + private void _schLisSvc_OnTriggerFinalized(object? sender, Events.EventArgs e) + { + var jKey = e.Args.JobKey; + var tKey = e.Args.Key; + var log = new ExecutionLog + { + JobName = jKey.Name, + JobGroup = jKey.Group, + TriggerName = tKey.Name, + TriggerGroup = tKey.Group, + LogType = LogType.Trigger, + Result = "Trigger ended", + }; + QueueInsertTask(log); + } + + private void _schLisSvc_OnTriggerResumed(object? sender, Events.EventArgs e) + { + var tKey = e.Args; + var log = new ExecutionLog + { + TriggerName = tKey.Name, + TriggerGroup = tKey.Group, + LogType = LogType.Trigger, + Result = "Trigger resumed", + }; + QueueGetJobKeyAndInsertTask(log); + } + + private void _schLisSvc_OnTriggerPaused(object? sender, Events.EventArgs e) + { + var tKey = e.Args; + var log = new ExecutionLog + { + TriggerName = tKey.Name, + TriggerGroup = tKey.Group, + LogType = LogType.Trigger, + Result = "Trigger paused", + }; + QueueGetJobKeyAndInsertTask(log); + } + + private void _schLisSvc_OnTriggerMisfired(object? sender, Events.EventArgs e) + { + var jKey = e.Args.JobKey; + var tKey = e.Args.Key; + var log = new ExecutionLog + { + LogType = LogType.Trigger, + JobName = jKey.Name, + JobGroup = jKey.Group, + TriggerName = tKey.Name, + TriggerGroup = tKey.Group, + Result = "Trigger misfired", + }; + QueueInsertTask(log); + } + + private void _schLisSvc_OnSchedulerError(object? sender, Events.SchedulerErrorEventArgs e) + { + var log = new ExecutionLog + { + LogType = LogType.System, + IsException = true, + ErrorMessage = e.ErrorMessage, + ExecutionLogDetail = new() { ErrorStackTrace = e.Exception.NonNullStackTrace() }, + }; + QueueInsertTask(log); + } + + private void _schLisSvc_OnJobInterrupted(object? sender, Events.EventArgs e) + { + var jKey = e.Args; + var log = new ExecutionLog + { + JobName = jKey.Name, + JobGroup = jKey.Group, + LogType = LogType.System, + Result = "Job interrupted", + }; + QueueInsertTask(log); + } + + private void _schLisSvc_OnJobDeleted(object? sender, Events.EventArgs e) + { + JobKey jKey = e.Args; + var log = new ExecutionLog + { + JobName = jKey.Name, + JobGroup = jKey.Group, + LogType = LogType.System, + Result = "Job deleted", + }; + QueueInsertTask(log); + } + + internal void _schLisSvc_OnJobExecutionVetoed( + object? sender, + Events.EventArgs e + ) + { + var log = CreateScheduleJobLogEntry(e.Args, defaultIsSuccess: false); + log.IsVetoed = true; + QueueUpdateTask(log); + } + + internal void _schLisSvc_OnJobWasExecuted(object? sender, Events.JobWasExecutedEventArgs e) + { + QueueUpdateTask(CreateScheduleJobLogEntry(e.JobExecutionContext, e.JobException, true)); + } + + internal void _schLisSvc_OnJobToBeExecuted( + object? sender, + Events.EventArgs e + ) + { + QueueInsertTask(CreateScheduleJobLogEntry(e.Args)); + } + + private async Task AddHousekeepingSchedule(IScheduler scheduler) + { + var housekeepingJobName = "Housekeep ExecutionLogs (bili)"; + var triggerKey = new TriggerKey(housekeepingJobName, Constants.SYSTEM_GROUP); + + if (!string.IsNullOrEmpty(_options.HousekeepingCronSchedule)) + { + var reschedule = true; + + // determine if already exists + if (await scheduler.CheckExists(triggerKey)) + { + // determine if same cron schedule + var trig = await scheduler.GetTrigger(triggerKey); + if (trig != null && trig.GetTriggerType() == TriggerType.Cron) + { + var cronTrigger = (ICronTrigger)trig; + if (cronTrigger.CronExpressionString == _options.HousekeepingCronSchedule) + reschedule = false; + } + } + + if (reschedule) + { + // create new one + IJobDetail job = JobBuilder + .Create() + .WithIdentity(housekeepingJobName, Constants.SYSTEM_GROUP) + .Build(); + + ITrigger trigger = TriggerBuilder + .Create() + .WithIdentity(housekeepingJobName, Constants.SYSTEM_GROUP) + .StartNow() + .WithCronSchedule(_options.HousekeepingCronSchedule) + .Build(); + + ITrigger nowTrigger = TriggerBuilder + .Create() + .WithIdentity("Housekeep ExecutionLogs now (bili)", Constants.SYSTEM_GROUP) + .StartNow() + .Build(); + + await scheduler.ScheduleJob(job, new[] { trigger, nowTrigger }, true); + } + } + else + { + // delete housekeeping schedule + if (await scheduler.CheckExists(triggerKey)) + { + _logger.LogInformation( + "Housekeeping ExecutionLogs has no cron schedule specified. Delete scheduled job" + ); + await scheduler.UnscheduleJob(triggerKey); + } + } + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + var scheduler = await _schedulerFactory.GetScheduler(); + + await AddHousekeepingSchedule(scheduler); + + await base.StartAsync(cancellationToken); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await MarkIncompleteExecution(stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + await ProcessTaskAsync(stoppingToken); + } + } + + internal async Task MarkIncompleteExecution(CancellationToken stoppingToken) + { + try + { + using (IServiceScope scope = _svcProvider.CreateScope()) + { + var repo = scope.ServiceProvider.GetRequiredService(); + + await repo.MarkExecutingJobAsIncomplete(); + } + } + catch (OperationCanceledException) + { + // Prevent throwing if stoppingToken was signaled + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error occurred while updating executing status to incomplete status." + ); + } + } + + internal async Task ProcessTaskAsync(CancellationToken stoppingToken = default) + { + var batch = await GetBatch(stoppingToken); + + _logger.LogInformation( + "Got a batch with {taskCount} task(s). Saving to data store.", + batch.Count + ); + + try + { + using (IServiceScope scope = _svcProvider.CreateScope()) + { + var repo = scope.ServiceProvider.GetRequiredService(); + + foreach (var workItem in batch) + { + await workItem(repo, stoppingToken); + } + + try + { + await repo.SaveChangesAsync(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred while saving execution logs."); + } + } + } + catch (OperationCanceledException) + { + // Prevent throwing if stoppingToken was signaled + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred executing task work item."); + } + } + + private ExecutionLog CreateScheduleJobLogEntry( + IJobExecutionContext context, + JobExecutionException? jobException = null, + bool? defaultIsSuccess = null + ) + { + var log = new ExecutionLog + { + RunInstanceId = context.FireInstanceId, + JobGroup = context.JobDetail.Key.Group, + JobName = context.JobDetail.Key.Name, + TriggerName = context.Trigger.Key.Name, + TriggerGroup = context.Trigger.Key.Group, + FireTimeUtc = context.FireTimeUtc, + ScheduleFireTimeUtc = context.ScheduledFireTimeUtc, + RetryCount = context.RefireCount, + JobRunTime = context.JobRunTime, + LogType = LogType.ScheduleJob, + }; + var logDetail = new ExecutionLogDetail(); + + log.ReturnCode = context.GetReturnCode(); + log.IsSuccess = context.GetIsSuccess(); + + if (log.IsSuccess is null) + log.IsSuccess = defaultIsSuccess; + + var execDetail = context.GetExecutionDetails(); + if (!string.IsNullOrEmpty(execDetail)) + { + logDetail.ExecutionDetails = execDetail; + log.ExecutionLogDetail = logDetail; + } + + if (jobException != null) + { + log.ErrorMessage = jobException.Message; + log.ExecutionLogDetail = logDetail; + logDetail.ErrorCode = jobException.HResult; + logDetail.ErrorStackTrace = jobException.ToString(); + logDetail.ErrorHelpLink = jobException.HelpLink; + + if (log.ReturnCode == null) + log.ReturnCode = jobException.HResult.ToString(); + + log.IsException = true; + log.IsSuccess = false; + } + else + { + if (context.Result != null) + { + var result = Convert.ToString(context.Result, CultureInfo.InvariantCulture); + log.Result = result?.Substring(0, Math.Min(result.Length, RESULT_MAX_LENGTH)); + } + } + + return log; + } + + private async Task>> GetBatch( + CancellationToken cancellationToken + ) + { + await _taskQueue.Reader.WaitToReadAsync(cancellationToken); + + var batch = new List>(); + + while (batch.Count < MAX_BATCH_SIZE && _taskQueue.Reader.TryRead(out var dbTask)) + { + batch.Add(dbTask); + } + + return batch; + } + + void QueueUpdateTask(ExecutionLog log) + { + QueueTask( + async (IExecutionLogStore repo, CancellationToken cancelToken) => + { + try + { + if (!repo.Exists(log)) + await repo.SaveChangesAsync(cancelToken); + + await repo.UpdateExecutionLog(log); + } + catch (Exception ex) + { + if (log.LogType == LogType.ScheduleJob) + { + _logger.LogError( + ex, + "Error occurred while updating execution log with job key [{jobGroup}.{jobName}] " + + "run instance id [{runInstanceId}].", + log.JobGroup, + log.JobName, + log.RunInstanceId + ); + } + else + { + _logger.LogError( + ex, + "Error occurred while updating {logType} execution log with " + + "job key [{jobGroup}.{jobName}] trigger key [{triggerGroup}.{triggerName}].", + log.LogType, + log.JobGroup, + log.JobName, + log.TriggerGroup, + log.TriggerName + ); + } + } + } + ); + } + + void QueueGetJobKeyAndInsertTask(ExecutionLog log) + { + QueueTask( + async (IExecutionLogStore repo, CancellationToken cancelToken) => + { + try + { + if (log.JobName == null && log.TriggerName != null && log.TriggerGroup != null) + { + // when there is no job name but has trigger name + // try to determine the job name + var scheduler = await _schedulerFactory.GetScheduler(); + var trigger = await scheduler.GetTrigger( + new TriggerKey(log.TriggerName, log.TriggerGroup), + cancelToken + ); + if (trigger != null) + { + log.JobName = trigger.JobKey.Name; + log.JobGroup = trigger.JobKey.Group; + } + } + + await repo.AddExecutionLog(log, cancelToken); + } + catch (Exception ex) + { + if (log.LogType == LogType.ScheduleJob) + { + _logger.LogError( + ex, + "Error occurred while adding execution log with job key [{jobGroup}.{jobName}] " + + "run instance id [{runInstanceId}].", + log.JobGroup, + log.JobName, + log.RunInstanceId + ); + } + else + { + _logger.LogError( + ex, + "Error occurred while adding {logType} execution log with " + + "job key [{jobGroup}.{jobName}] trigger key [{triggerGroup}.{triggerName}].", + log.LogType, + log.JobGroup, + log.JobName, + log.TriggerGroup, + log.TriggerName + ); + } + } + } + ); + } + + void QueueInsertTask(ExecutionLog log) + { + QueueTask( + async (IExecutionLogStore repo, CancellationToken cancelToken) => + { + try + { + await repo.AddExecutionLog(log, cancelToken); + } + catch (Exception ex) + { + if (log.LogType == LogType.ScheduleJob) + { + _logger.LogError( + ex, + "Error occurred while adding execution log with job key [{jobGroup}.{jobName}] " + + "run instance id [{runInstanceId}].", + log.JobGroup, + log.JobName, + log.RunInstanceId + ); + } + else + { + _logger.LogError( + ex, + "Error occurred while adding {logType} execution log with " + + "job key [{jobGroup}.{jobName}] trigger key [{triggerGroup}.{triggerName}].", + log.LogType, + log.JobGroup, + log.JobName, + log.TriggerGroup, + log.TriggerName + ); + } + } + } + ); + } + + void QueueTask(Func task) + { + if (!_taskQueue.Writer.TryWrite(task)) + { + // Should not happen since it's unbounded Channel. It 'should' only fail if we call writer.Complete() + throw new InvalidOperationException("Failed to write the log message"); + } + } +} diff --git a/src/BlazingQuartz.Core/Jobs/HousekeepExecutionLogsJob.cs b/src/BlazingQuartz.Core/Jobs/HousekeepExecutionLogsJob.cs new file mode 100644 index 0000000..f1552bb --- /dev/null +++ b/src/BlazingQuartz.Core/Jobs/HousekeepExecutionLogsJob.cs @@ -0,0 +1,37 @@ +using System; +using BlazingQuartz.Core.History; +using BlazingQuartz.Jobs.Abstractions; +using Microsoft.Extensions.Options; +using Quartz; + +namespace BlazingQuartz.Core.Jobs +{ + public class HousekeepExecutionLogsJob : IJob + { + private readonly IExecutionLogStore _logStore; + private readonly BlazingQuartzCoreOptions _options; + + public HousekeepExecutionLogsJob( + IExecutionLogStore logStore, + IOptions options + ) + { + _logStore = logStore; + _options = options.Value; + } + + public async Task Execute(IJobExecutionContext context) + { + try + { + var count = await _logStore.DeleteLogsByDays(_options.ExecutionLogsDaysToKeep); + context.Result = $"Deleted {count} record(s)"; + context.SetIsSuccess(true); + } + catch (Exception ex) + { + throw new JobExecutionException("Failed to delete execution logs", ex); + } + } + } +} diff --git a/src/BlazingQuartz.Core/Models/ExecutionLogFilter.cs b/src/BlazingQuartz.Core/Models/ExecutionLogFilter.cs new file mode 100644 index 0000000..7b36428 --- /dev/null +++ b/src/BlazingQuartz.Core/Models/ExecutionLogFilter.cs @@ -0,0 +1,40 @@ +using System; +using Ray.BiliBiliTool.Domain; + +namespace BlazingQuartz.Core.Models +{ + public class ExecutionLogFilter : ICloneable + { + public string? JobName { get; set; } + public string? JobGroup { get; set; } + public string? TriggerName { get; set; } + public string? TriggerGroup { get; set; } + public HashSet? LogTypes { get; set; } + public string? MessageContains { get; set; } + + /// + /// If only StartUtc specified, means anything after this date + /// + public DateTimeOffset? DateAddedStartUtc { get; set; } + + /// + /// DateAddedUtc before this date (exclusive). + /// If only EndUtc specified, means anything before this date + /// + public DateTimeOffset? DateAddedEndUtc { get; set; } + public bool IsAscending { get; set; } + public bool ErrorOnly { get; set; } + public bool IncludeSystemJobs { get; set; } = false; + + public object Clone() + { + var newObj = (ExecutionLogFilter)this.MemberwiseClone(); + if (this.LogTypes != null) + { + newObj.LogTypes = new HashSet(this.LogTypes); + } + + return newObj; + } + } +} diff --git a/src/BlazingQuartz.Core/Models/JobDetailModel.cs b/src/BlazingQuartz.Core/Models/JobDetailModel.cs new file mode 100644 index 0000000..c3b53b2 --- /dev/null +++ b/src/BlazingQuartz.Core/Models/JobDetailModel.cs @@ -0,0 +1,19 @@ +using System; + +namespace BlazingQuartz.Core.Models +{ + public class JobDetailModel + { + public string Name { get; set; } = string.Empty; + public string Group { get; set; } = Constants.DEFAULT_GROUP; + public string? Description { get; set; } + public Type? JobClass { get; set; } + public IDictionary JobDataMap { get; set; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Flag indicate whether Job should stay in scheduler even there are no more triggers assgined to it. + /// + public bool IsDurable { get; set; } + } +} diff --git a/src/BlazingQuartz.Core/Models/JobExecutionStatusSummaryModel.cs b/src/BlazingQuartz.Core/Models/JobExecutionStatusSummaryModel.cs new file mode 100644 index 0000000..14bd751 --- /dev/null +++ b/src/BlazingQuartz.Core/Models/JobExecutionStatusSummaryModel.cs @@ -0,0 +1,10 @@ +using System; + +namespace BlazingQuartz.Core.Models +{ + public class JobExecutionStatusSummaryModel + { + public DateTime StartDateTimeUtc { get; set; } + public List> Data { get; set; } = new(); + } +} diff --git a/src/BlazingQuartz.Core/Models/Key.cs b/src/BlazingQuartz.Core/Models/Key.cs new file mode 100644 index 0000000..d9a81cc --- /dev/null +++ b/src/BlazingQuartz.Core/Models/Key.cs @@ -0,0 +1,32 @@ +using System; + +namespace BlazingQuartz.Core.Models +{ + public class Key + { + public string Name { get; set; } + public string? Group { get; set; } + + public Key(string name) + { + Name = name; + } + + public Key(string name, string group) + : this(name) + { + Group = group; + } + + public Key(Key key) + { + Name = key.Name; + Group = key.Group; + } + + public bool Equals(string name, string? group) + { + return Name == name && Group == group; + } + } +} diff --git a/src/BlazingQuartz.Core/Models/PagedList.cs b/src/BlazingQuartz.Core/Models/PagedList.cs new file mode 100644 index 0000000..f7e895b --- /dev/null +++ b/src/BlazingQuartz.Core/Models/PagedList.cs @@ -0,0 +1,42 @@ +using System; + +namespace BlazingQuartz.Core.Models +{ + public class PagedList : List + { + public PageMetadata? PageMetadata { get; set; } + + public PagedList(IEnumerable collection) + : this(collection, null) { } + + public PagedList(IEnumerable collection, PageMetadata? metadata) + : base(collection) + { + PageMetadata = metadata; + } + } + + public record PageMetadata + { + /// + /// Page number. Start at 0 + /// + public int Page { get; init; } = 0; + + /// + /// Total number of records + /// + public int TotalCount { get; init; } + + /// + /// Max number of records per page + /// + public int PageSize { get; init; } = 500; + + public PageMetadata(int Page, int PageSize) + { + this.Page = Page; + this.PageSize = PageSize; + } + } +} diff --git a/src/BlazingQuartz.Core/Models/ScheduleJobFilter.cs b/src/BlazingQuartz.Core/Models/ScheduleJobFilter.cs new file mode 100644 index 0000000..bdb5ea3 --- /dev/null +++ b/src/BlazingQuartz.Core/Models/ScheduleJobFilter.cs @@ -0,0 +1,14 @@ +using System; + +namespace BlazingQuartz.Core.Models +{ + public class ScheduleJobFilter : ICloneable + { + public bool IncludeSystemJobs { get; set; } = false; + + public object Clone() + { + return (ScheduleJobFilter)this.MemberwiseClone(); + } + } +} diff --git a/src/BlazingQuartz.Core/Models/ScheduleModel.cs b/src/BlazingQuartz.Core/Models/ScheduleModel.cs new file mode 100644 index 0000000..59176ae --- /dev/null +++ b/src/BlazingQuartz.Core/Models/ScheduleModel.cs @@ -0,0 +1,59 @@ +using System; + +namespace BlazingQuartz.Core.Models +{ + public class ScheduleModel + { + const int SHORT_JOBTYPE_NAME_MAX_LENGTH = 25; + + public string? JobName { get; set; } + public string? JobType { get; set; } + public string? JobDescription { get; set; } + public string JobGroup { get; set; } = Constants.DEFAULT_GROUP; + public string? TriggerName { get; set; } + public string? TriggerGroup { get; set; } + public string? TriggerDescription { get; set; } + public TriggerDetailModel? TriggerDetail { get; set; } + public TriggerType TriggerType { get; set; } + public string? TriggerTypeClassName { get; set; } + public JobStatus JobStatus { get; set; } = JobStatus.Idle; + + public DateTimeOffset? NextTriggerTime { get; set; } + public DateTimeOffset? PreviousTriggerTime { get; set; } + public string? ExceptionMessage { get; set; } + + public void ClearTrigger() + { + TriggerName = null; + TriggerGroup = null; + TriggerDescription = null; + TriggerDetail = null; + TriggerTypeClassName = null; + NextTriggerTime = null; + PreviousTriggerTime = null; + TriggerType = TriggerType.Unknown; + } + + public string? GetJobTypeShortName(int suggestedMaxLength = SHORT_JOBTYPE_NAME_MAX_LENGTH) + { + if (JobType != null) + { + if (JobType.Length <= suggestedMaxLength) + return JobType; + + var dotIndex = JobType.LastIndexOf('.'); + if (dotIndex < 0) + return JobType; + + var className = JobType.Substring(dotIndex + 1); + var classNameLength = className.Length; + if (classNameLength >= suggestedMaxLength) + return className; + + var remainLength = suggestedMaxLength - classNameLength - 3; + return $"{JobType[..remainLength]}...{className}"; + } + return JobType; + } + } +} diff --git a/src/BlazingQuartz.Core/Models/TriggerDetailModel.cs b/src/BlazingQuartz.Core/Models/TriggerDetailModel.cs new file mode 100644 index 0000000..5e4dcbc --- /dev/null +++ b/src/BlazingQuartz.Core/Models/TriggerDetailModel.cs @@ -0,0 +1,165 @@ +using System; +using System.Text; +using Quartz; + +namespace BlazingQuartz.Core.Models +{ + public class TriggerDetailModel + { + public string Name { get; set; } = string.Empty; + public string Group { get; set; } = Constants.DEFAULT_GROUP; + public TriggerType TriggerType { get; set; } + public string? Description { get; set; } + public TimeSpan? StartTimeSpan { get; set; } + public DateTime? StartDate { get; set; } + + /// + /// Combined + /// + public DateTimeOffset? StartDateTimeUtc + { + get + { + if (StartDate.HasValue) + { + DateTimeOffset startTime; + + if (StartTimeSpan.HasValue) + { + var dt = StartDate.Value.Add(StartTimeSpan.Value); + startTime = new DateTimeOffset(dt, StartTimezone.BaseUtcOffset); + } + else + { + startTime = new DateTimeOffset( + StartDate.Value, + StartTimezone.BaseUtcOffset + ); + } + + return startTime; + } + return null; + } + } + public TimeSpan? EndTimeSpan { get; set; } + public DateTime? EndDate { get; set; } + public DateTimeOffset? EndDateTimeUtc + { + get + { + if (EndDate.HasValue) + { + DateTimeOffset endTime; + + if (EndTimeSpan.HasValue) + { + var dt = EndDate.Value.Add(EndTimeSpan.Value); + endTime = new DateTimeOffset(dt, StartTimezone.BaseUtcOffset); + } + else + { + endTime = new DateTimeOffset(EndDate.Value, StartTimezone.BaseUtcOffset); + } + + return endTime; + } + else + { + return null; + } + } + } + + public string? ModifiedByCalendar { get; set; } + + /// + /// Timezone of start time + /// + public TimeZoneInfo StartTimezone { get; set; } = TimeZoneInfo.Utc; + public int Priority { get; set; } = 5; + public string? CronExpression { get; set; } + public bool RepeatForever { get; set; } + public int RepeatCount { get; set; } + + public bool[] DailyDayOfWeek { get; set; } = new bool[7]; + public TimeSpan? StartDailyTime { get; set; } + public TimeSpan? EndDailyTime { get; set; } + + /// + /// The timezone in which to base the scheduled. Used in Cron schedule, Calendar schedule and Daily schedule. + /// + public TimeZoneInfo InTimeZone { get; set; } = TimeZoneInfo.Local; + + public int TriggerInterval { get; set; } = 1; + public IntervalUnit? TriggerIntervalUnit { get; set; } = IntervalUnit.Minute; + public MisfireAction MisfireAction { get; set; } = MisfireAction.SmartPolicy; + + public IDictionary TriggerDataMap { get; set; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + public IReadOnlyCollection GetDailyOnDaysOfWeek() + { + var dayOfWeekCount = 7; + var list = new List(dayOfWeekCount); + for (int i = 0; i < dayOfWeekCount; i++) + { + if (DailyDayOfWeek[i]) + { + list.Add((DayOfWeek)Enum.ToObject(typeof(DayOfWeek), i)); + } + } + + return list; + } + + public string ToSummaryString() + { + var bldr = new StringBuilder(); + switch (TriggerType) + { + case TriggerType.Cron: + bldr.AppendLine( + CronExpressionDescriptor.ExpressionDescriptor.GetDescription(CronExpression) + ); + break; + case TriggerType.Daily: + bldr.AppendJoin( + ", ", + DailyDayOfWeek.Where(f => f).Select((f, i) => (DayOfWeek)i) + ); + if (EndDailyTime.HasValue) + { + bldr.AppendLine( + $" from {StartDailyTime.ToString()} to {EndDailyTime.ToString()} {InTimeZone.DisplayName}" + ); + } + else + { + bldr.AppendLine( + $" at {StartDailyTime.ToString()} {InTimeZone.DisplayName}" + ); + } + break; + case TriggerType.Simple: + bldr.Append($"Every {TriggerInterval} {TriggerIntervalUnit?.ToString()}."); + if (RepeatForever) + { + bldr.AppendLine(" Repeat forever."); + } + else if (RepeatCount > 0) + { + bldr.AppendLine($" Repeat {RepeatCount} time(s)."); + } + break; + case TriggerType.Calendar: + bldr.Append( + $"{ModifiedByCalendar} calendar. Start at {StartDate} {StartTimeSpan} {InTimeZone}. Repeat every {TriggerInterval} {TriggerIntervalUnit?.ToString()}" + ); + break; + } + + return bldr.ToString(); + } + } +} diff --git a/src/BlazingQuartz.Core/ServiceCollectionExtensions.cs b/src/BlazingQuartz.Core/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..81f81c5 --- /dev/null +++ b/src/BlazingQuartz.Core/ServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +using System; +using System.Reflection; +using BlazingQuartz.Core.History; +using BlazingQuartz.Core.Services; +using BlazingQuartz.Jobs; +using BlazingQuartz.Jobs.Abstractions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Quartz; +using Ray.BiliBiliTool.Infrastructure.EF; + +namespace BlazingQuartz.Core +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddBlazingQuartz(this IServiceCollection services) + { + services.AddBlazingQuartzJobs(); + + services.TryAddSingleton(); + services.AddTransient(); + + var schListenerSvc = new SchedulerListenerService(); + services.TryAddSingleton(schListenerSvc); + services.AddSingleton(schListenerSvc); + services.AddSingleton(schListenerSvc); + services.AddSingleton(schListenerSvc); + + services.AddScoped(); + services.AddScoped(); + + services.AddSingleton(); + + services.AddHostedService(); + + return services; + } + } +} diff --git a/src/BlazingQuartz.Core/Services/ExecutionLogService.cs b/src/BlazingQuartz.Core/Services/ExecutionLogService.cs new file mode 100644 index 0000000..ab57df5 --- /dev/null +++ b/src/BlazingQuartz.Core/Services/ExecutionLogService.cs @@ -0,0 +1,326 @@ +using System; +using BlazingQuartz.Core.Models; +using Microsoft.EntityFrameworkCore; +using Ray.BiliBiliTool.Domain; +using Ray.BiliBiliTool.Infrastructure.EF; + +namespace BlazingQuartz.Core.Services +{ + public class ExecutionLogService : IExecutionLogService + { + private readonly IDbContextFactory _contextFactory; + + public ExecutionLogService(IDbContextFactory contextFactory) + { + _contextFactory = contextFactory; + } + + public async Task> GetLatestExecutionLog( + string jobName, + string jobGroup, + string? triggerName, + string? triggerGroup, + PageMetadata? pageMetadata = null, + long firstLogId = 0, + HashSet? logTypes = null + ) + { + using (var context = _contextFactory.CreateDbContext()) + { + var q = context.ExecutionLogs.Where(l => + l.JobName == jobName && l.JobGroup == jobGroup + ); + + if (triggerName is not null) + { + q = q.Where(l => + l.TriggerName == triggerName && l.TriggerGroup == triggerGroup + ); + } + + if (firstLogId > 0) + { + // to avoid incorrect page data + q = q.Where(l => l.LogId <= firstLogId); + } + + if (logTypes != null) + { + q = q = q.Where(l => logTypes.Contains(l.LogType)); + } + + var ordered = q.OrderByDescending(l => l.DateAddedUtc) + .ThenByDescending(l => l.FireTimeUtc); + if (pageMetadata == null) + { + return new PagedList(await ordered.ToListAsync()); + } + + PageMetadata newPageMetadata = pageMetadata; + if (pageMetadata.Page == 0) + { + // if first page, get the total records + var totalRecords = await q.CountAsync(); + newPageMetadata = pageMetadata with { TotalCount = totalRecords }; + } + + var result = await ordered + .Skip(pageMetadata.Page * pageMetadata.PageSize) + .Take(pageMetadata.PageSize) + .ToListAsync(); + return new PagedList(result, newPageMetadata); + } + } + + public async Task> GetExecutionLogs( + ExecutionLogFilter? filter = null, + PageMetadata? pageMetadata = null, + long firstLogId = 0 + ) + { + using (var context = _contextFactory.CreateDbContext()) + { + IQueryable q = context.ExecutionLogs; + if (filter != null) + { + if (filter.JobName != null) + { + q = q.Where(l => l.JobName == filter.JobName); + } + + if (filter.JobGroup != null) + { + q = q.Where(l => l.JobGroup == filter.JobGroup); + } + + if (filter.TriggerName != null) + { + q = q.Where(l => l.TriggerName == filter.TriggerName); + } + + if (filter.TriggerGroup != null) + { + q = q.Where(l => l.TriggerGroup == filter.TriggerGroup); + } + + if (filter.LogTypes != null && filter.LogTypes.Any()) + { + q = q.Where(l => filter.LogTypes.Contains(l.LogType)); + } + + if (filter.DateAddedStartUtc != null) + { + q = q.Where(l => l.DateAddedUtc >= filter.DateAddedStartUtc); + } + + if (filter.DateAddedEndUtc != null) + { + q = q.Where(l => l.DateAddedUtc < filter.DateAddedEndUtc); + } + + if (filter.ErrorOnly) + { + q = q.Where(l => + (l.IsException ?? false) || (l.IsSuccess.HasValue && !l.IsSuccess.Value) + ); + } + + if (filter.MessageContains != null) + { + var likeStr = $"%{filter.MessageContains}%"; + q = q.Where(l => + EF.Functions.Like(l.JobName ?? string.Empty, likeStr) + || EF.Functions.Like(l.TriggerName ?? string.Empty, likeStr) + || EF.Functions.Like(l.Result ?? string.Empty, likeStr) + || EF.Functions.Like(l.ErrorMessage ?? string.Empty, likeStr) + || ( + l.ExecutionLogDetail != null + && ( + EF.Functions.Like( + l.ExecutionLogDetail.ExecutionDetails ?? string.Empty, + likeStr + ) + || EF.Functions.Like( + l.ExecutionLogDetail.ErrorStackTrace ?? string.Empty, + likeStr + ) + || ( + l.ExecutionLogDetail.ErrorCode != null + && l.ExecutionLogDetail.ErrorCode.Value.ToString() + == filter.MessageContains + ) + ) + ) + ); + } + + if (!filter.IncludeSystemJobs) + { + q = q.Where(l => + !( + l.TriggerGroup == Constants.SYSTEM_GROUP + || l.JobGroup == Constants.SYSTEM_GROUP + ) + ); + } + } + + IOrderedQueryable ordered; + if (filter != null && filter.IsAscending) + { + ordered = q.OrderBy(l => l.DateAddedUtc).ThenBy(l => l.FireTimeUtc); + } + else + { + if (firstLogId > 0) + { + // to avoid incorrect page data for descing order + q = q.Where(l => l.LogId <= firstLogId); + } + ordered = q.OrderByDescending(l => l.DateAddedUtc) + .ThenByDescending(l => l.FireTimeUtc); + } + + if (pageMetadata == null) + { + return new PagedList(await ordered.ToListAsync()); + } + + PageMetadata newPageMetadata = pageMetadata; + if (pageMetadata.Page == 0) + { + // if first page, get the total records + var totalRecords = await q.CountAsync(); + newPageMetadata = pageMetadata with { TotalCount = totalRecords }; + } + + var result = await ordered + .Skip(pageMetadata.Page * pageMetadata.PageSize) + .Take(pageMetadata.PageSize) + .ToListAsync(); + return new PagedList(result, newPageMetadata); + } + } + + public async Task> GetJobNames() + { + using (var context = _contextFactory.CreateDbContext()) + { + return await context + .ExecutionLogs.Where(l => l.LogType != LogType.System) + .Select(l => l.JobName ?? string.Empty) + .Distinct() + .OrderBy(l => l) + .ToListAsync(); + } + } + + public async Task> GetJobGroups() + { + using (var context = _contextFactory.CreateDbContext()) + { + return await context + .ExecutionLogs.Where(l => l.LogType != LogType.System) + .Select(l => l.JobGroup ?? string.Empty) + .Distinct() + .OrderBy(l => l) + .ToListAsync(); + } + } + + public async Task> GetTriggerNames() + { + using (var context = _contextFactory.CreateDbContext()) + { + return await context + .ExecutionLogs.Where(l => l.LogType != LogType.System) + .Select(l => l.TriggerName ?? string.Empty) + .Distinct() + .OrderBy(l => l) + .ToListAsync(); + } + } + + public async Task> GetTriggerGroups() + { + using (var context = _contextFactory.CreateDbContext()) + { + return await context + .ExecutionLogs.Where(l => l.LogType != LogType.System) + .Select(l => l.TriggerGroup ?? string.Empty) + .Distinct() + .OrderBy(l => l) + .ToListAsync(); + } + } + + public async Task GetJobExecutionStatusSummary( + DateTimeOffset? startTimeUtc, + DateTimeOffset? endTimeUtc = null + ) + { + using (var context = _contextFactory.CreateDbContext()) + { + var q = context.ExecutionLogs.Where(l => l.LogType == LogType.ScheduleJob); + if (startTimeUtc.HasValue) + { + q = q.Where(l => l.DateAddedUtc >= startTimeUtc.Value); + } + if (endTimeUtc.HasValue) + { + q = q.Where(l => l.DateAddedUtc < endTimeUtc.Value); + } + + var statusList = q.Select(l => new + { + DateAddedUtc = l.DateAddedUtc, + ExecutionStatus = (l.IsException ?? false) + ? + // has exception + JobExecutionStatus.Failed + : + // vetoed? + ( + (l.IsVetoed ?? false) + ? JobExecutionStatus.Vetoed + : + // is success null? + ( + l.IsSuccess.HasValue + ? ( + l.IsSuccess.Value + ? JobExecutionStatus.Success + : JobExecutionStatus.Failed + ) + : JobExecutionStatus.Executing + ) + ), + }); + + var statusGroup = await statusList + .GroupBy(l => l.ExecutionStatus) + .Select(g => new + { + EarliestDateAdded = g.Min(l => l.DateAddedUtc), + ExecutionStatus = g.Key, + Count = g.Count(), + }) + .ToListAsync(); + + if (!statusGroup.Any()) + return new(); + + return new JobExecutionStatusSummaryModel + { + StartDateTimeUtc = statusGroup.Min(s => s.EarliestDateAdded).DateTime, + Data = statusGroup + .Select(s => new KeyValuePair( + s.ExecutionStatus, + s.Count + )) + .ToList(), + }; + } + } + } +} diff --git a/src/BlazingQuartz.Core/Services/IExecutionLogService.cs b/src/BlazingQuartz.Core/Services/IExecutionLogService.cs new file mode 100644 index 0000000..e5d363d --- /dev/null +++ b/src/BlazingQuartz.Core/Services/IExecutionLogService.cs @@ -0,0 +1,39 @@ +using BlazingQuartz.Core.Models; +using Ray.BiliBiliTool.Domain; + +namespace BlazingQuartz.Core.Services +{ + public interface IExecutionLogService + { + Task> GetLatestExecutionLog( + string jobName, + string jobGroup, + string? triggerName, + string? triggerGroup, + PageMetadata? pageMetadata = null, + long firstLogId = 0, + HashSet? logTypes = null + ); + Task> GetExecutionLogs( + ExecutionLogFilter? filter = null, + PageMetadata? pageMetadata = null, + long firstLogId = 0 + ); + Task> GetJobNames(); + Task> GetJobGroups(); + Task> GetTriggerNames(); + Task> GetTriggerGroups(); + + /// + /// Returns job execution summary. Number of success, failed, + /// executing and interrupted jobs of given date range. + /// + /// + /// inclusive + /// + Task GetJobExecutionStatusSummary( + DateTimeOffset? startTimeUtc, + DateTimeOffset? endTimeUtc = null + ); + } +} diff --git a/src/BlazingQuartz.Core/Services/ISchedulerDefinitionService.cs b/src/BlazingQuartz.Core/Services/ISchedulerDefinitionService.cs new file mode 100644 index 0000000..6f3cf9f --- /dev/null +++ b/src/BlazingQuartz.Core/Services/ISchedulerDefinitionService.cs @@ -0,0 +1,17 @@ +using System; + +namespace BlazingQuartz.Core.Services +{ + public interface ISchedulerDefinitionService + { + IEnumerable GetTriggerIntervalUnits(TriggerType triggerType); + IEnumerable GetMisfireActions(TriggerType triggerType); + + /// + /// Return available IJob implementations + /// + /// + /// + IEnumerable GetJobTypes(bool reload = false); + } +} diff --git a/src/BlazingQuartz.Core/Services/ISchedulerListenerService.cs b/src/BlazingQuartz.Core/Services/ISchedulerListenerService.cs new file mode 100644 index 0000000..c0605e0 --- /dev/null +++ b/src/BlazingQuartz.Core/Services/ISchedulerListenerService.cs @@ -0,0 +1,61 @@ +using System; +using BlazingQuartz.Core.Events; +using Quartz; + +namespace BlazingQuartz.Core.Services +{ + public interface ISchedulerListenerService + { + event EventHandler>? OnJobAdded; + event EventHandler>? OnJobDeleted; + + /// + /// From IJobListener + /// + event EventHandler>? OnJobExecutionVetoed; + event EventHandler>? OnJobInterrupted; + event EventHandler>? OnJobPaused; + event EventHandler>? OnJobResumed; + event EventHandler>? OnJobScheduled; + event EventHandler>? OnJobsPaused; + event EventHandler>? OnJobsResumed; + + /// + /// From IJobListener + /// + event EventHandler>? OnJobToBeExecuted; + event EventHandler>? OnJobUnscheduled; + + /// + /// From IJobListener + /// + event EventHandler? OnJobWasExecuted; + event EventHandler? OnSchedulerError; + event EventHandler? OnSchedulerInStandbyMode; + event EventHandler? OnSchedulerShutdown; + event EventHandler? OnSchedulerShuttingdown; + event EventHandler? OnSchedulerStarted; + event EventHandler? OnSchedulerStarting; + event EventHandler? OnSchedulingDataCleared; + event EventHandler>? OnTriggerFinalized; + + /// + /// From ITriggerListener + /// + event EventHandler>? OnTriggerMisfired; + event EventHandler>? OnTriggerPaused; + event EventHandler>? OnTriggerResumed; + event EventHandler>? OnTriggerGroupPaused; + event EventHandler>? OnTriggerGroupResumed; + + /// + /// From ITriggerListener + /// + event EventHandler? OnTriggerComplete; + + /// + /// From ITriggerListener + /// + event EventHandler? OnTriggerFired; + } +} diff --git a/src/BlazingQuartz.Core/Services/ISchedulerService.cs b/src/BlazingQuartz.Core/Services/ISchedulerService.cs new file mode 100644 index 0000000..768c54b --- /dev/null +++ b/src/BlazingQuartz.Core/Services/ISchedulerService.cs @@ -0,0 +1,36 @@ +using BlazingQuartz.Core.Models; +using Quartz; + +namespace BlazingQuartz.Core.Services +{ + public interface ISchedulerService + { + Task GetScheduleModelAsync(ITrigger trigger); + IAsyncEnumerable GetAllJobsAsync(ScheduleJobFilter? filter = null); + Task CreateSchedule(JobDetailModel jobDetailModel, TriggerDetailModel triggerDetailModel); + Task> GetJobGroups(); + Task> GetTriggerGroups(); + Task GetJobDetail(string jobName, string groupName); + Task GetTriggerDetail(string triggerName, string triggerGroup); + Task ContainsTriggerKey(string triggerName, string triggerGroup); + Task ContainsJobKey(string jobName, string jobGroup); + Task> GetCalendarNames(CancellationToken cancelToken = default); + Task PauseTrigger(string triggerName, string? triggerGroup); + Task ResumeTrigger(string triggerName, string? triggerGroup); + Task TriggerJob(string jobName, string jobGroup); + Task DeleteSchedule(ScheduleModel model); + Task UpdateSchedule( + Key oldJobKey, + Key? oldTriggerKey, + JobDetailModel newJobModel, + TriggerDetailModel newTriggerModel + ); + Task GetMetadataAsync(); + Task>> GetScheduledJobSummary(); + Task PauseAllSchedules(); + Task ResumeAllSchedules(); + Task ShutdownScheduler(); + Task StartScheduler(); + Task StandbyScheduler(); + } +} diff --git a/src/BlazingQuartz.Core/Services/SchedulerDefinitionService.cs b/src/BlazingQuartz.Core/Services/SchedulerDefinitionService.cs new file mode 100644 index 0000000..580f459 --- /dev/null +++ b/src/BlazingQuartz.Core/Services/SchedulerDefinitionService.cs @@ -0,0 +1,176 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Quartz; + +namespace BlazingQuartz.Core.Services +{ + internal class SchedulerDefinitionService : ISchedulerDefinitionService + { + private readonly ISchedulerFactory _schedulerFactory; + private IEnumerable _calendarIntervalUnits; + private IEnumerable _simpleIntervalUnits; + private IEnumerable? _cronCalDailyMisfireActions; + private IEnumerable? _simpleMisfireActions; + private readonly BlazingQuartzCoreOptions _options; + private readonly ILogger _logger; + private List? _allowedJobTypes; + + public SchedulerDefinitionService( + ILogger logger, + ISchedulerFactory schedulerFactory, + IOptions options + ) + { + _logger = logger; + _schedulerFactory = schedulerFactory; + _options = options.Value; + Init(); + } + + [MemberNotNull(nameof(_calendarIntervalUnits))] + [MemberNotNull(nameof(_simpleIntervalUnits))] + private void Init() + { + _calendarIntervalUnits = new List + { + IntervalUnit.Second, + IntervalUnit.Minute, + IntervalUnit.Hour, + IntervalUnit.Day, + IntervalUnit.Week, + IntervalUnit.Month, + IntervalUnit.Year, + }; + _simpleIntervalUnits = new List + { + IntervalUnit.Second, + IntervalUnit.Minute, + IntervalUnit.Hour, + }; + } + + public IEnumerable GetTriggerIntervalUnits(TriggerType triggerType) + { + switch (triggerType) + { + case TriggerType.Calendar: + return _calendarIntervalUnits; + case TriggerType.Daily: + case TriggerType.Simple: + return _simpleIntervalUnits; + default: + return Enumerable.Empty(); + } + } + + public IEnumerable GetMisfireActions(TriggerType triggerType) + { + switch (triggerType) + { + case TriggerType.Cron: + case TriggerType.Daily: + case TriggerType.Calendar: + if (_cronCalDailyMisfireActions == null) + { + _cronCalDailyMisfireActions = new List + { + MisfireAction.SmartPolicy, + MisfireAction.DoNothing, + MisfireAction.IgnoreMisfirePolicy, + MisfireAction.FireOnceNow, + }; + } + return _cronCalDailyMisfireActions; + case TriggerType.Simple: + if (_simpleMisfireActions == null) + { + _simpleMisfireActions = new List + { + MisfireAction.SmartPolicy, + MisfireAction.FireNow, + MisfireAction.IgnoreMisfirePolicy, + MisfireAction.RescheduleNextWithExistingCount, + MisfireAction.RescheduleNextWithRemainingCount, + MisfireAction.RescheduleNowWithExistingRepeatCount, + MisfireAction.RescheduleNowWithRemainingRepeatCount, + }; + } + return _simpleMisfireActions; + } + + return Enumerable.Empty(); + } + + public IEnumerable GetJobTypes(bool reload = false) + { + if (_options.AllowedJobAssemblyFiles == null) + return Enumerable.Empty(); + + // use cached job types if already loaded + if (_allowedJobTypes != null && !reload) + return _allowedJobTypes; + + HashSet disallowedJobs = new( + _options.DisallowedJobTypes ?? Enumerable.Empty() + ); + + if (_options.DisallowedJobTypes != null) + _logger.LogInformation( + "{disallowedVar} was set. Will not load following job types {jobTypes}", + nameof(_options.DisallowedJobTypes), + _options.DisallowedJobTypes + ); + + var path = + Path.GetDirectoryName( + Assembly.GetAssembly(typeof(SchedulerDefinitionService))!.Location + ) ?? String.Empty; + List jobTypes = new(); + foreach (var assemblyStr in _options.AllowedJobAssemblyFiles) + { + string assemblyPath = Path.Combine(path, assemblyStr + ".dll"); + try + { + Assembly assembly = Assembly.LoadFrom(assemblyPath); + if (assembly == null) + { + _logger.LogWarning( + "Cannot load allowed job assembly name '{assembly}'", + assemblyStr + ); + continue; + } + + jobTypes.AddRange( + assembly + .GetExportedTypes() + .Where(x => + x.IsPublic + && x.IsClass + && !x.IsAbstract + && typeof(IJob).IsAssignableFrom(x) + && !disallowedJobs.Contains(x.FullName ?? string.Empty) + ) + ); + } + catch (Exception ex) + { + _logger.LogWarning( + ex, + "Failed to load allowed job assembly filename '{assembly}'", + assemblyStr + ); + continue; + } + } + if (!jobTypes.Any()) + return jobTypes; + + _allowedJobTypes = jobTypes; + return _allowedJobTypes.AsReadOnly(); + } + } +} diff --git a/src/BlazingQuartz.Core/Services/SchedulerListenerService.cs b/src/BlazingQuartz.Core/Services/SchedulerListenerService.cs new file mode 100644 index 0000000..bca44cd --- /dev/null +++ b/src/BlazingQuartz.Core/Services/SchedulerListenerService.cs @@ -0,0 +1,290 @@ +using System; +using BlazingQuartz.Core.Events; +using Quartz; + +namespace BlazingQuartz.Core.Services +{ + public class SchedulerListenerService + : ISchedulerListenerService, + IJobListener, + ITriggerListener, + ISchedulerListener + { + public event EventHandler>? OnJobAdded; + public event EventHandler>? OnJobDeleted; + public event EventHandler>? OnJobExecutionVetoed; + public event EventHandler>? OnJobInterrupted; + public event EventHandler>? OnJobPaused; + public event EventHandler>? OnJobResumed; + public event EventHandler>? OnJobScheduled; + public event EventHandler>? OnJobsPaused; + public event EventHandler>? OnJobsResumed; + public event EventHandler>? OnJobToBeExecuted; + public event EventHandler>? OnJobUnscheduled; + public event EventHandler? OnJobWasExecuted; + public event EventHandler? OnSchedulerError; + public event EventHandler? OnSchedulerInStandbyMode; + public event EventHandler? OnSchedulerShutdown; + public event EventHandler? OnSchedulerShuttingdown; + public event EventHandler? OnSchedulerStarted; + public event EventHandler? OnSchedulerStarting; + public event EventHandler? OnSchedulingDataCleared; + public event EventHandler>? OnTriggerFinalized; + public event EventHandler>? OnTriggerMisfired; + public event EventHandler>? OnTriggerPaused; + public event EventHandler>? OnTriggerResumed; + public event EventHandler>? OnTriggerGroupPaused; + public event EventHandler>? OnTriggerGroupResumed; + public event EventHandler? OnTriggerComplete; + public event EventHandler? OnTriggerFired; + + public string Name => "BlazingQuartzNetUI"; + + public Task JobAdded(IJobDetail jobDetail, CancellationToken cancellationToken = default) + { + OnJobAdded?.Invoke(this, new EventArgs(jobDetail, cancellationToken)); + return Task.CompletedTask; + } + + public Task JobDeleted(JobKey jobKey, CancellationToken cancellationToken = default) + { + OnJobDeleted?.Invoke(this, new EventArgs(jobKey, cancellationToken)); + return Task.CompletedTask; + } + + public Task JobExecutionVetoed( + IJobExecutionContext context, + CancellationToken cancellationToken = default + ) + { + OnJobExecutionVetoed?.Invoke( + this, + new EventArgs(context, cancellationToken) + ); + return Task.CompletedTask; + } + + public Task JobInterrupted(JobKey jobKey, CancellationToken cancellationToken = default) + { + OnJobInterrupted?.Invoke(this, new EventArgs(jobKey, cancellationToken)); + return Task.CompletedTask; + } + + public Task JobPaused(JobKey jobKey, CancellationToken cancellationToken = default) + { + OnJobPaused?.Invoke(this, new EventArgs(jobKey, cancellationToken)); + return Task.CompletedTask; + } + + public Task JobResumed(JobKey jobKey, CancellationToken cancellationToken = default) + { + OnJobResumed?.Invoke(this, new EventArgs(jobKey, cancellationToken)); + return Task.CompletedTask; + } + + public Task JobScheduled(ITrigger trigger, CancellationToken cancellationToken = default) + { + OnJobScheduled?.Invoke(this, new EventArgs(trigger, cancellationToken)); + return Task.CompletedTask; + } + + public Task JobsPaused(string jobGroup, CancellationToken cancellationToken = default) + { + OnJobsPaused?.Invoke(this, new EventArgs(jobGroup, cancellationToken)); + return Task.CompletedTask; + } + + public Task JobsResumed(string jobGroup, CancellationToken cancellationToken = default) + { + OnJobsResumed?.Invoke(this, new EventArgs(jobGroup, cancellationToken)); + return Task.CompletedTask; + } + + public Task JobToBeExecuted( + IJobExecutionContext context, + CancellationToken cancellationToken = default + ) + { + OnJobToBeExecuted?.Invoke( + this, + new EventArgs(context, cancellationToken) + ); + return Task.CompletedTask; + } + + public Task JobUnscheduled( + TriggerKey triggerKey, + CancellationToken cancellationToken = default + ) + { + OnJobUnscheduled?.Invoke( + this, + new EventArgs(triggerKey, cancellationToken) + ); + return Task.CompletedTask; + } + + public Task JobWasExecuted( + IJobExecutionContext context, + JobExecutionException? jobException, + CancellationToken cancellationToken = default + ) + { + OnJobWasExecuted?.Invoke( + this, + new JobWasExecutedEventArgs(context, jobException, cancellationToken) + { + JobException = jobException, + } + ); + return Task.CompletedTask; + } + + public Task SchedulerError( + string msg, + SchedulerException cause, + CancellationToken cancellationToken = default + ) + { + OnSchedulerError?.Invoke( + this, + new SchedulerErrorEventArgs + { + ErrorMessage = msg, + Exception = cause, + CancelToken = cancellationToken, + } + ); + return Task.CompletedTask; + } + + public Task SchedulerInStandbyMode(CancellationToken cancellationToken = default) + { + OnSchedulerInStandbyMode?.Invoke(this, cancellationToken); + return Task.CompletedTask; + } + + public Task SchedulerShutdown(CancellationToken cancellationToken = default) + { + OnSchedulerShutdown?.Invoke(this, cancellationToken); + return Task.CompletedTask; + } + + public Task SchedulerShuttingdown(CancellationToken cancellationToken = default) + { + OnSchedulerShuttingdown?.Invoke(this, cancellationToken); + return Task.CompletedTask; + } + + public Task SchedulerStarted(CancellationToken cancellationToken = default) + { + OnSchedulerStarted?.Invoke(this, cancellationToken); + return Task.CompletedTask; + } + + public Task SchedulerStarting(CancellationToken cancellationToken = default) + { + OnSchedulerStarting?.Invoke(this, cancellationToken); + return Task.CompletedTask; + } + + public Task SchedulingDataCleared(CancellationToken cancellationToken = default) + { + OnSchedulingDataCleared?.Invoke(this, cancellationToken); + return Task.CompletedTask; + } + + public Task TriggerComplete( + ITrigger trigger, + IJobExecutionContext context, + SchedulerInstruction triggerInstructionCode, + CancellationToken cancellationToken = default + ) + { + OnTriggerComplete?.Invoke( + this, + new TriggerEventArgs(trigger, context, cancellationToken) + ); + return Task.CompletedTask; + } + + public Task TriggerFinalized( + ITrigger trigger, + CancellationToken cancellationToken = default + ) + { + OnTriggerFinalized?.Invoke(this, new EventArgs(trigger, cancellationToken)); + return Task.CompletedTask; + } + + public Task TriggerFired( + ITrigger trigger, + IJobExecutionContext context, + CancellationToken cancellationToken = default + ) + { + OnTriggerFired?.Invoke(this, new TriggerEventArgs(trigger, context, cancellationToken)); + return Task.CompletedTask; + } + + public Task TriggerMisfired(ITrigger trigger, CancellationToken cancellationToken = default) + { + OnTriggerMisfired?.Invoke(this, new EventArgs(trigger, cancellationToken)); + return Task.CompletedTask; + } + + public Task TriggerPaused( + TriggerKey triggerKey, + CancellationToken cancellationToken = default + ) + { + OnTriggerPaused?.Invoke(this, new EventArgs(triggerKey, cancellationToken)); + return Task.CompletedTask; + } + + public Task TriggerResumed( + TriggerKey triggerKey, + CancellationToken cancellationToken = default + ) + { + OnTriggerResumed?.Invoke( + this, + new EventArgs(triggerKey, cancellationToken) + ); + return Task.CompletedTask; + } + + public Task TriggersPaused( + string? triggerGroup, + CancellationToken cancellationToken = default + ) + { + OnTriggerGroupPaused?.Invoke( + this, + new EventArgs(triggerGroup, cancellationToken) + ); + return Task.CompletedTask; + } + + public Task TriggersResumed( + string? triggerGroup, + CancellationToken cancellationToken = default + ) + { + OnTriggerGroupResumed?.Invoke( + this, + new EventArgs(triggerGroup, cancellationToken) + ); + return Task.CompletedTask; + } + + public Task VetoJobExecution( + ITrigger trigger, + IJobExecutionContext context, + CancellationToken cancellationToken = default + ) + { + return Task.FromResult(false); + } + } +} diff --git a/src/BlazingQuartz.Core/Services/SchedulerService.cs b/src/BlazingQuartz.Core/Services/SchedulerService.cs new file mode 100644 index 0000000..d080d55 --- /dev/null +++ b/src/BlazingQuartz.Core/Services/SchedulerService.cs @@ -0,0 +1,832 @@ +using System; +using BlazingQuartz.Core.Models; +using BlazingQuartz.Jobs; +using Microsoft.Extensions.Logging; +using Quartz; +using Quartz.Impl.Matchers; + +namespace BlazingQuartz.Core.Services; + +public class SchedulerService(ILogger logger, ISchedulerFactory schedulerFactory) + : ISchedulerService +{ + public async IAsyncEnumerable GetAllJobsAsync(ScheduleJobFilter? filter = null) + { + IScheduler scheduler = await schedulerFactory.GetScheduler(); + IReadOnlyCollection jobGroupNames = await scheduler.GetJobGroupNames(); + + foreach (var jobGrp in jobGroupNames) + { + if (filter is { IncludeSystemJobs: false } && jobGrp == Constants.SYSTEM_GROUP) + continue; + + var jobKeys = await scheduler.GetJobKeys(GroupMatcher.GroupEquals(jobGrp)); + + foreach (var jobKey in jobKeys) + { + await foreach (var job in GetScheduleModelsAsync(jobKey)) + { + yield return job; + } + } + } + } + + public async Task GetScheduleModelAsync(ITrigger trigger) + { + var scheduler = await schedulerFactory.GetScheduler(); + + var jobDetail = await scheduler.GetJobDetail(trigger.JobKey); + + return await CreateScheduleModel(jobDetail, trigger); + } + + public async Task> GetJobGroups() + { + var scheduler = await schedulerFactory.GetScheduler(); + return (await scheduler.GetJobGroupNames()) + .Where(n => n != Constants.SYSTEM_GROUP) + .ToList(); + } + + public async Task> GetTriggerGroups() + { + var scheduler = await schedulerFactory.GetScheduler(); + return (await scheduler.GetTriggerGroupNames()) + .Where(n => n != Constants.SYSTEM_GROUP) + .ToList(); + } + + public async Task>> GetScheduledJobSummary() + { + var scheduler = await schedulerFactory.GetScheduler(); + var executingCount = (await scheduler.GetCurrentlyExecutingJobs()).Count; + var jobCount = (await scheduler.GetJobKeys(GroupMatcher.AnyGroup())).Count; + var triggerCount = ( + await scheduler.GetTriggerKeys(GroupMatcher.AnyGroup()) + ).Count; + var sysJobCount = ( + await scheduler.GetJobKeys(GroupMatcher.GroupEquals(Constants.SYSTEM_GROUP)) + ).Count; + var sysTriggerCount = ( + await scheduler.GetTriggerKeys( + GroupMatcher.GroupEquals(Constants.SYSTEM_GROUP) + ) + ).Count; + + return new List> + { + new KeyValuePair("Jobs", jobCount), + new KeyValuePair("Triggers", triggerCount), + new KeyValuePair("Executing", executingCount), + new KeyValuePair("System Jobs", sysJobCount), + new KeyValuePair("System Triggers", sysTriggerCount), + }; + } + + public async Task GetMetadataAsync() + { + var scheduler = await schedulerFactory.GetScheduler(); + return await scheduler.GetMetaData(); + } + + private async Task CreateScheduleModel( + IJobDetail? jobDetail, + ITrigger trigger, + CancellationToken cancellationToken = default + ) + { + var scheduler = await schedulerFactory.GetScheduler(); + var triggerState = (await scheduler.GetTriggerState(trigger.Key)); + var runningTrigger = (await scheduler.GetCurrentlyExecutingJobs(cancellationToken)) + .Where(context => context.Trigger.Equals(trigger)) + .FirstOrDefault(); + + return new ScheduleModel + { + JobName = jobDetail?.Key.Name, + JobGroup = jobDetail?.Key.Group ?? "No Group", + JobType = jobDetail?.JobType.ToString(), + JobDescription = jobDetail?.Description, + TriggerName = trigger.Key.Name, + TriggerGroup = trigger.Key.Group, + TriggerDescription = trigger.Description, + TriggerType = trigger.GetTriggerType(), + TriggerTypeClassName = trigger.GetType().Name, + NextTriggerTime = trigger.GetNextFireTimeUtc(), + PreviousTriggerTime = trigger.GetPreviousFireTimeUtc(), + JobStatus = + runningTrigger != null + ? JobStatus.Running + : triggerState switch + { + TriggerState.Paused => JobStatus.Paused, + TriggerState.None => JobStatus.NoTrigger, + TriggerState.Error => JobStatus.Error, + _ => JobStatus.Idle, + }, + TriggerDetail = CreateTriggerDetailModel(trigger), + }; + } + + public async Task CreateSchedule( + JobDetailModel jobDetailModel, + TriggerDetailModel triggerDetailModel + ) + { + var scheduler = await schedulerFactory.GetScheduler(); + + var trigger = BuildTrigger(triggerDetailModel); + + // Determine if job already exists + if (await ContainsJobKey(jobDetailModel.Name, jobDetailModel.Group)) + { + var existingJob = await scheduler.GetJobDetail( + new JobKey(jobDetailModel.Name, jobDetailModel.Group) + ); + if (existingJob != null) + { + //await scheduler.GetTriggersOfJob(job.Key) + var jobTriggers = new List(1); + jobTriggers.Add(trigger); + + await scheduler.ScheduleJob(existingJob, jobTriggers.AsReadOnly(), true); + return; + } + } + + var job = CreateJobDetail(jobDetailModel); + + await scheduler.ScheduleJob(job, trigger); + } + + public async Task UpdateSchedule( + Key oldJobKey, + Key? oldTriggerKey, + JobDetailModel newJobModel, + TriggerDetailModel newTriggerModel + ) + { + var scheduler = await schedulerFactory.GetScheduler().ConfigureAwait(false); + var oJobKey = oldJobKey.ToJobKey(); + + var newJob = CreateJobDetail(newJobModel); + var trigger = BuildTrigger(newTriggerModel, newJob.Key); + // determine if old triggerKey exists + if ( + oldTriggerKey != null + && await scheduler.CheckExists(oldTriggerKey.ToTriggerKey()).ConfigureAwait(false) + ) + { + await scheduler.UnscheduleJob(oldTriggerKey.ToTriggerKey()).ConfigureAwait(false); + } + + var existingTriggers = await scheduler.GetTriggersOfJob(oJobKey).ConfigureAwait(false); + + // assign new job to all triggers + var triggers = existingTriggers + .Select(t => + { + var b = t.GetTriggerBuilder().ForJob(newJob.Key); + if (t.StartTimeUtc < DateTimeOffset.UtcNow) + b.StartNow(); + return b.Build(); + }) + .ToList(); + triggers.Add(trigger); + + // delete old job + await scheduler.DeleteJob(oJobKey).ConfigureAwait(false); + + // save new job with triggers + await scheduler.ScheduleJob(newJob, triggers, replace: true).ConfigureAwait(false); + } + + public async Task GetJobDetail(string jobName, string groupName) + { + var scheduler = await schedulerFactory.GetScheduler(); + var jd = await scheduler.GetJobDetail(new JobKey(jobName, groupName)); + + if (jd == null) + return null; + + return new JobDetailModel + { + Name = jd.Key.Name, + Group = jd.Key.Group, + Description = jd.Description, + JobDataMap = jd.JobDataMap, + JobClass = jd.JobType, + IsDurable = jd.Durable, + }; + } + + public async Task GetTriggerDetail(string triggerName, string triggerGroup) + { + var scheduler = await schedulerFactory.GetScheduler(); + var trigger = await scheduler.GetTrigger(new TriggerKey(triggerName, triggerGroup)); + + if (trigger == null) + return null; + + return CreateTriggerDetailModel(trigger); + } + + public async Task ContainsTriggerKey(string triggerName, string triggerGroup) + { + var scheduler = await schedulerFactory.GetScheduler(); + return await scheduler.CheckExists(new TriggerKey(triggerName, triggerGroup)); + } + + public async Task ContainsJobKey(string jobName, string jobGroup) + { + var scheduler = await schedulerFactory.GetScheduler(); + return await scheduler.CheckExists(new JobKey(jobName, jobGroup)); + } + + public async Task> GetCalendarNames( + CancellationToken cancelToken = default + ) + { + var scheduler = await schedulerFactory.GetScheduler(cancelToken); + + return await scheduler.GetCalendarNames(cancelToken); + } + + public async Task PauseTrigger(string triggerName, string? triggerGroup) + { + var scheduler = await schedulerFactory.GetScheduler(); + await scheduler.PauseTrigger( + triggerGroup == null + ? new TriggerKey(triggerName) + : new TriggerKey(triggerName, triggerGroup) + ); + } + + public async Task ResumeTrigger(string triggerName, string? triggerGroup) + { + var scheduler = await schedulerFactory.GetScheduler(); + await scheduler.ResumeTrigger( + triggerGroup == null + ? new TriggerKey(triggerName) + : new TriggerKey(triggerName, triggerGroup) + ); + } + + public async Task DeleteSchedule(ScheduleModel model) + { + var scheduler = await schedulerFactory.GetScheduler(); + + if (model.JobName == null) + return false; + + if (model.JobStatus == JobStatus.NoSchedule) + return true; + + var jobKey = new JobKey(model.JobName, model.JobGroup); + + if (model.JobStatus == JobStatus.Error && model.TriggerName == null) + { + logger.LogInformation( + "Job [{jobGroup}.{jobName}] has no trigger name. " + + "Cannot UncheduleJob by trigger, will delete job directly.", + jobKey.Group, + jobKey.Name + ); + return await scheduler.DeleteJob(jobKey); + } + + if (model.JobStatus == JobStatus.NoTrigger) + { + var triggers = await scheduler.GetTriggersOfJob(jobKey); + if (!triggers.Any()) + return await scheduler.DeleteJob(jobKey); + else + { + logger.LogWarning( + "Cannot delete Job [{jobGroup}.{jobName}]. There are still {triggerCount}" + + " trigger(s) assigned to this job.", + jobKey.Group, + jobKey.Name, + triggers.Count + ); + return false; + } + } + + if (model.TriggerName == null) + return false; + + var success = await scheduler.UnscheduleJob( + model.TriggerGroup == null + ? new TriggerKey(model.TriggerName) + : new TriggerKey(model.TriggerName, model.TriggerGroup) + ); + + if (success) + { + var triggers = await scheduler.GetTriggersOfJob(jobKey); + if (!triggers.Any()) + { + logger.LogInformation( + "UnscheduleJob [{jobGroup}.{jobName}] has no more triggers. " + + "Determine if job was deleted.", + jobKey.Group, + jobKey.Name + ); + + if (await scheduler.CheckExists(jobKey)) + { + logger.LogInformation( + "Manually delete job [{jobGroup}.{jobName}].", + jobKey.Group, + jobKey.Name + ); + return await scheduler.DeleteJob(jobKey); + } + } + } + + return success; + } + + public async Task TriggerJob(string jobName, string jobGroup) + { + var scheduler = await schedulerFactory.GetScheduler(); + await scheduler.TriggerJob(new JobKey(jobName, jobGroup)); + } + + #region Private methods + + private async IAsyncEnumerable GetScheduleModelsAsync(JobKey jobkey) + { + var scheduler = await schedulerFactory.GetScheduler(); + + IJobDetail? jobDetail = null; + IReadOnlyCollection? jobTriggers = null; + ScheduleModel? exceptionJob = null; + try + { + jobDetail = await scheduler.GetJobDetail(jobkey); + jobTriggers = await scheduler.GetTriggersOfJob(jobkey); + } + catch (Exception ex) + { + logger.LogWarning( + ex, + "Cannot GetScheduleModel of job [{jobGroup}.{jobName}]", + jobkey.Group, + jobkey.Name + ); + exceptionJob = new ScheduleModel + { + JobName = jobkey.Name, + JobGroup = jobkey.Group, + JobStatus = JobStatus.Error, + ExceptionMessage = ex.Message, + }; + } + + if (exceptionJob != null) + { + // job with exception + if (jobTriggers == null || !jobTriggers.Any()) + { + exceptionJob.TriggerType = TriggerType.Unknown; + yield return exceptionJob; + } + else + { + foreach (var trigger in jobTriggers) + { + var jobModel = await CreateScheduleModel(null, trigger); + jobModel.JobName = exceptionJob.JobName; + jobModel.JobGroup = exceptionJob.JobGroup; + jobModel.JobStatus = exceptionJob.JobStatus; + jobModel.ExceptionMessage = exceptionJob.ExceptionMessage; + yield return jobModel; + } + } + } + else if (jobTriggers == null || !jobTriggers.Any()) + { + yield return new ScheduleModel + { + JobName = jobkey.Name, + JobGroup = jobkey.Group, + JobType = jobDetail?.JobType.ToString(), + JobStatus = JobStatus.NoTrigger, + }; + } + else + { + foreach (var trigger in jobTriggers) + { + yield return await CreateScheduleModel(jobDetail, trigger); + } + } + } + + private TriggerDetailModel CreateTriggerDetailModel(ITrigger trigger) + { + var triggerType = trigger.GetTriggerType(); + + var model = new TriggerDetailModel + { + Name = trigger.Key.Name, + Group = trigger.Key.Group, + Description = trigger.Description, + TriggerDataMap = trigger.JobDataMap, + EndDate = trigger.EndTimeUtc?.Date, + EndTimeSpan = trigger.EndTimeUtc?.TimeOfDay, + StartDate = trigger.StartTimeUtc.Date, + StartTimeSpan = trigger.StartTimeUtc.TimeOfDay, + StartTimezone = TimeZoneInfo.Utc, + TriggerType = triggerType, + ModifiedByCalendar = trigger.CalendarName, + Priority = trigger.Priority, + }; + + switch (trigger.MisfireInstruction) + { + case MisfireInstruction.IgnoreMisfirePolicy: + model.MisfireAction = MisfireAction.IgnoreMisfirePolicy; + break; + // comment out same as SmartPolicy + //case MisfireInstruction.InstructionNotSet: + // model.MisfireAction = MisfireAction.InstructionNotSet; + // break; + case MisfireInstruction.SmartPolicy: + model.MisfireAction = MisfireAction.SmartPolicy; + break; + } + + switch (triggerType) + { + case TriggerType.Cron: + var cron = (ICronTrigger)trigger; + model.CronExpression = cron.CronExpressionString; + model.InTimeZone = cron.TimeZone; + switch (cron.MisfireInstruction) + { + case MisfireInstruction.CronTrigger.DoNothing: + model.MisfireAction = MisfireAction.DoNothing; + break; + case MisfireInstruction.CronTrigger.FireOnceNow: + model.MisfireAction = MisfireAction.FireOnceNow; + break; + } + break; + case TriggerType.Daily: + var daily = (IDailyTimeIntervalTrigger)trigger; + foreach (var dow in daily.DaysOfWeek) + { + model.DailyDayOfWeek[(int)dow] = true; + } + switch (daily.MisfireInstruction) + { + case MisfireInstruction.DailyTimeIntervalTrigger.DoNothing: + model.MisfireAction = MisfireAction.DoNothing; + break; + case MisfireInstruction.DailyTimeIntervalTrigger.FireOnceNow: + model.MisfireAction = MisfireAction.FireOnceNow; + break; + } + model.RepeatCount = daily.RepeatCount; + model.TriggerInterval = daily.RepeatInterval; + model.TriggerIntervalUnit = daily.RepeatIntervalUnit.ToBlazingQuartzIntervalUnit(); + model.InTimeZone = daily.TimeZone; + model.StartDailyTime = new TimeSpan( + daily.StartTimeOfDay.Hour, + daily.StartTimeOfDay.Minute, + daily.StartTimeOfDay.Second + ); + model.EndDailyTime = new TimeSpan( + daily.EndTimeOfDay.Hour, + daily.EndTimeOfDay.Minute, + daily.EndTimeOfDay.Second + ); + break; + case TriggerType.Simple: + var simple = (ISimpleTrigger)trigger; + model = PopulateSimpleTrigger(simple, model); + break; + case TriggerType.Calendar: + var calTrigger = (ICalendarIntervalTrigger)trigger; + switch (calTrigger.MisfireInstruction) + { + case MisfireInstruction.CalendarIntervalTrigger.DoNothing: + model.MisfireAction = MisfireAction.DoNothing; + break; + case MisfireInstruction.CalendarIntervalTrigger.FireOnceNow: + model.MisfireAction = MisfireAction.FireOnceNow; + break; + } + model.TriggerInterval = calTrigger.RepeatInterval; + model.TriggerIntervalUnit = + calTrigger.RepeatIntervalUnit.ToBlazingQuartzIntervalUnit(); + model.InTimeZone = calTrigger.TimeZone; + break; + } + + return model; + } + + private IJobDetail CreateJobDetail(JobDetailModel jobDetailModel) + { + ArgumentNullException.ThrowIfNull(jobDetailModel.JobClass); + + return JobBuilder + .Create(jobDetailModel.JobClass) + .WithIdentity(jobDetailModel.Name, jobDetailModel.Group) + .WithDescription(jobDetailModel.Description) + .UsingJobData(new JobDataMap(jobDetailModel.JobDataMap)) + .StoreDurably(jobDetailModel.IsDurable) + .Build(); + } + + private ITrigger BuildTrigger(TriggerDetailModel triggerDetailModel, JobKey? jobKey = null) + { + var tbldr = TriggerBuilder + .Create() + .WithIdentity(triggerDetailModel.Name, triggerDetailModel.Group) + .WithDescription(triggerDetailModel.Description) + .WithPriority(triggerDetailModel.Priority) + .UsingJobData(new JobDataMap(triggerDetailModel.TriggerDataMap)) + .ModifiedByCalendar(triggerDetailModel.ModifiedByCalendar); + + if (jobKey != null) + { + tbldr.ForJob(jobKey); + } + + var startTime = triggerDetailModel.StartDateTimeUtc; + if (startTime.HasValue) + { + tbldr = tbldr.StartAt(startTime.Value); + } + else + { + tbldr = tbldr.StartNow(); + } + + tbldr.EndAt(triggerDetailModel.EndDateTimeUtc); + + switch (triggerDetailModel.TriggerType) + { + case TriggerType.Cron: + ArgumentNullException.ThrowIfNull(triggerDetailModel.CronExpression); + tbldr = tbldr.WithCronSchedule( + triggerDetailModel.CronExpression, + x => + { + switch (triggerDetailModel.MisfireAction) + { + case MisfireAction.DoNothing: + x.WithMisfireHandlingInstructionDoNothing(); + break; + case MisfireAction.FireOnceNow: + x.WithMisfireHandlingInstructionFireAndProceed(); + break; + case MisfireAction.IgnoreMisfirePolicy: + x.WithMisfireHandlingInstructionIgnoreMisfires(); + break; + } + x.InTimeZone(triggerDetailModel.InTimeZone); + } + ); + break; + case TriggerType.Daily: + tbldr = tbldr.WithDailyTimeIntervalSchedule(x => + { + switch (triggerDetailModel.MisfireAction) + { + case MisfireAction.DoNothing: + x.WithMisfireHandlingInstructionDoNothing(); + break; + case MisfireAction.FireOnceNow: + x.WithMisfireHandlingInstructionFireAndProceed(); + break; + case MisfireAction.IgnoreMisfirePolicy: + x.WithMisfireHandlingInstructionIgnoreMisfires(); + break; + } + x.OnDaysOfTheWeek(triggerDetailModel.GetDailyOnDaysOfWeek()); + if (triggerDetailModel.StartDailyTime.HasValue) + { + x.StartingDailyAt(triggerDetailModel.StartDailyTime.Value.ToTimeOfDay()); + } + if (triggerDetailModel.EndDailyTime.HasValue) + { + x.EndingDailyAt(triggerDetailModel.EndDailyTime.Value.ToTimeOfDay()); + } + x.InTimeZone(triggerDetailModel.InTimeZone); + if ( + triggerDetailModel.TriggerInterval > 0 + && triggerDetailModel.TriggerIntervalUnit.HasValue + ) + { + x.WithInterval( + triggerDetailModel.TriggerInterval, + triggerDetailModel.TriggerIntervalUnit.Value.ToQuartzIntervalUnit() + ); + } + if (triggerDetailModel.RepeatCount > 0) + x.WithRepeatCount(triggerDetailModel.RepeatCount); + }); + break; + case TriggerType.Simple: + tbldr = tbldr.WithSimpleSchedule(x => + { + switch (triggerDetailModel.MisfireAction) + { + case MisfireAction.FireNow: + x.WithMisfireHandlingInstructionFireNow(); + break; + case MisfireAction.RescheduleNextWithExistingCount: + x.WithMisfireHandlingInstructionNextWithExistingCount(); + break; + case MisfireAction.RescheduleNextWithRemainingCount: + x.WithMisfireHandlingInstructionNextWithRemainingCount(); + break; + case MisfireAction.RescheduleNowWithExistingRepeatCount: + x.WithMisfireHandlingInstructionNowWithExistingCount(); + break; + case MisfireAction.RescheduleNowWithRemainingRepeatCount: + x.WithMisfireHandlingInstructionNowWithRemainingCount(); + break; + case MisfireAction.IgnoreMisfirePolicy: + x.WithMisfireHandlingInstructionIgnoreMisfires(); + break; + } + + if ( + triggerDetailModel.TriggerInterval > 0 + && triggerDetailModel.TriggerIntervalUnit.HasValue + ) + { + TimeSpan timeSpan; + switch (triggerDetailModel.TriggerIntervalUnit.Value) + { + case IntervalUnit.Millisecond: + timeSpan = TimeSpan.FromMilliseconds( + triggerDetailModel.TriggerInterval + ); + break; + case IntervalUnit.Second: + timeSpan = TimeSpan.FromSeconds(triggerDetailModel.TriggerInterval); + break; + case IntervalUnit.Minute: + timeSpan = TimeSpan.FromMinutes(triggerDetailModel.TriggerInterval); + break; + case IntervalUnit.Hour: + timeSpan = TimeSpan.FromHours(triggerDetailModel.TriggerInterval); + break; + case IntervalUnit.Day: + timeSpan = TimeSpan.FromDays(triggerDetailModel.TriggerInterval); + break; + default: + throw new NotSupportedException( + $"Interval unit {triggerDetailModel.TriggerIntervalUnit} is not supported for SimpleTrigger." + ); + } + x.WithInterval(timeSpan); + } + + if (triggerDetailModel.RepeatForever) + x.RepeatForever(); + else + x.WithRepeatCount(triggerDetailModel.RepeatCount); + }); + break; + case TriggerType.Calendar: + tbldr = tbldr.WithCalendarIntervalSchedule(x => + { + switch (triggerDetailModel.MisfireAction) + { + case MisfireAction.DoNothing: + x.WithMisfireHandlingInstructionDoNothing(); + break; + case MisfireAction.FireOnceNow: + x.WithMisfireHandlingInstructionFireAndProceed(); + break; + case MisfireAction.IgnoreMisfirePolicy: + x.WithMisfireHandlingInstructionIgnoreMisfires(); + break; + } + + x.InTimeZone(triggerDetailModel.InTimeZone); + if ( + triggerDetailModel.TriggerInterval > 0 + && triggerDetailModel.TriggerIntervalUnit.HasValue + ) + { + x.WithInterval( + triggerDetailModel.TriggerInterval, + triggerDetailModel.TriggerIntervalUnit.Value.ToQuartzIntervalUnit() + ); + } + }); + break; + } + + return tbldr.Build(); + } + + private TriggerDetailModel PopulateSimpleTrigger( + ISimpleTrigger simple, + TriggerDetailModel model + ) + { + switch (simple.MisfireInstruction) + { + case MisfireInstruction.SimpleTrigger.RescheduleNextWithExistingCount: + model.MisfireAction = MisfireAction.RescheduleNextWithExistingCount; + break; + case MisfireInstruction.SimpleTrigger.RescheduleNextWithRemainingCount: + model.MisfireAction = MisfireAction.RescheduleNextWithRemainingCount; + break; + case MisfireInstruction.SimpleTrigger.RescheduleNowWithExistingRepeatCount: + model.MisfireAction = MisfireAction.RescheduleNowWithExistingRepeatCount; + break; + case MisfireInstruction.SimpleTrigger.RescheduleNowWithRemainingRepeatCount: + model.MisfireAction = MisfireAction.RescheduleNowWithRemainingRepeatCount; + break; + case MisfireInstruction.SimpleTrigger.FireNow: + model.MisfireAction = MisfireAction.FireNow; + break; + } + if (simple.RepeatCount >= 0) + model.RepeatCount = simple.RepeatCount; + else + model.RepeatForever = true; + + var total = simple.RepeatInterval.TotalHours; + if (Math.Round(total) == total) + { + model.TriggerInterval = Convert.ToInt32(total); + model.TriggerIntervalUnit = IntervalUnit.Hour; + } + else + { + total = simple.RepeatInterval.TotalMinutes; + if (Math.Round(total) == total) + { + model.TriggerInterval = Convert.ToInt32(total); + model.TriggerIntervalUnit = IntervalUnit.Minute; + } + else + { + total = simple.RepeatInterval.TotalSeconds; + if (Math.Round(total) == total) + { + model.TriggerInterval = Convert.ToInt32(total); + model.TriggerIntervalUnit = IntervalUnit.Second; + } + //else + //{ + // total = simple.RepeatInterval.TotalMilliseconds; + // if (Math.Round(total) == total) + // { + // model.TriggerInterval = Convert.ToInt32(total); + // model.TriggerIntervalUnit = IntervalUnit.Millisecond; + // } + //} + } + } + + return model; + } + + public async Task PauseAllSchedules() + { + var scheduler = await schedulerFactory.GetScheduler(); + await scheduler.PauseAll(); + } + + public async Task ResumeAllSchedules() + { + var scheduler = await schedulerFactory.GetScheduler(); + await scheduler.ResumeAll(); + } + + public async Task ShutdownScheduler() + { + var scheduler = await schedulerFactory.GetScheduler(); + await scheduler.Shutdown(); + } + + public async Task StartScheduler() + { + var scheduler = await schedulerFactory.GetScheduler(); + await scheduler.Start(); + } + + public async Task StandbyScheduler() + { + var scheduler = await schedulerFactory.GetScheduler(); + await scheduler.Standby(); + } + + #endregion Private methods +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/BlazingQuartz.Jobs.Abstractions.csproj b/src/BlazingQuartz.Jobs.Abstractions/BlazingQuartz.Jobs.Abstractions.csproj new file mode 100644 index 0000000..0c317ca --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/BlazingQuartz.Jobs.Abstractions.csproj @@ -0,0 +1,25 @@ + + + net8.0 + enable + enable + + + + + + + + + + + + + + <_Parameter1>$(AssemblyName).Test + + + + + + diff --git a/src/BlazingQuartz.Jobs.Abstractions/DataMapValue.cs b/src/BlazingQuartz.Jobs.Abstractions/DataMapValue.cs new file mode 100644 index 0000000..18c2cc4 --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/DataMapValue.cs @@ -0,0 +1,72 @@ +using System; +using System.Globalization; +using System.Text.Json; + +namespace BlazingQuartz.Jobs.Abstractions +{ + public class DataMapValue + { + public DataMapValueType Type { get; set; } + public string? Value { get; set; } + public int Version { get; set; } + + public DataMapValue() + : this(DataMapValueType.InterpolatedString, 1) { } + + public DataMapValue(DataMapValueType type, int version) + : this(type, null, version) { } + + public DataMapValue(DataMapValueType type, string? value = null, int version = 1) + { + Type = type; + Value = value; + Version = version; + } + + public override string ToString() + { + return JsonSerializer.Serialize(this); + } + + /// + /// Create DataMapValue instance based from specified value + /// + /// + /// + public static DataMapValue? Create(object? dataMapValue) + { + var value = Convert.ToString(dataMapValue, CultureInfo.InvariantCulture); + if (value == null) + return null; + + return JsonSerializer.Deserialize(value); + } + + /// + /// Create DataMapValue instance based from specified value + /// + /// + /// + public static DataMapValue? Create(string? dataMapValue) + { + if (dataMapValue == null) + return null; + + return JsonSerializer.Deserialize(dataMapValue); + } + + public static DataMapValue Create( + object? dataMapValue, + DataMapValueType defaultType, + int defaultVersion, + string? defaultValue = null + ) + { + var dmv = Create(dataMapValue); + if (dmv != null) + return dmv; + else + return new(defaultType, defaultValue, defaultVersion); + } + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/DataMapValueResolver.cs b/src/BlazingQuartz.Jobs.Abstractions/DataMapValueResolver.cs new file mode 100644 index 0000000..70c9b2f --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/DataMapValueResolver.cs @@ -0,0 +1,44 @@ +using BlazingQuartz.Jobs.Abstractions.Processors; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace BlazingQuartz.Jobs.Abstractions +{ + public class DataMapValueResolver : IDataMapValueResolver + { + private readonly IServiceProvider? _svcProvider; + + public DataMapValueResolver(IServiceProvider? svcProvider) + { + _svcProvider = svcProvider; + } + + public string? Resolve(DataMapValue? dmv) + { + if (dmv == null) + return null; + + switch (dmv.Type) + { + case DataMapValueType.InterpolatedString: + switch (dmv.Version) + { + case 1: + var logger = _svcProvider?.GetRequiredService< + ILogger + >(); + var processor = new InterpolatedStringV1Processor(logger); + return processor.Process(dmv); + default: + throw new NotSupportedException( + $"DataMapValue {dmv.Type} version {dmv.Version} is not supported." + ); + } + default: + throw new NotSupportedException( + $"DataMapValueType {dmv.Type} is not supported." + ); + } + } + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/DataMapValueType.cs b/src/BlazingQuartz.Jobs.Abstractions/DataMapValueType.cs new file mode 100644 index 0000000..cdcfe92 --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/DataMapValueType.cs @@ -0,0 +1,9 @@ +using System; + +namespace BlazingQuartz.Jobs.Abstractions +{ + public enum DataMapValueType + { + InterpolatedString, + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/IDataMapValueResolver.cs b/src/BlazingQuartz.Jobs.Abstractions/IDataMapValueResolver.cs new file mode 100644 index 0000000..282cb60 --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/IDataMapValueResolver.cs @@ -0,0 +1,7 @@ +namespace BlazingQuartz.Jobs.Abstractions +{ + public interface IDataMapValueResolver + { + string? Resolve(DataMapValue? dmv); + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/IJobUI.cs b/src/BlazingQuartz.Jobs.Abstractions/IJobUI.cs new file mode 100644 index 0000000..11d89ed --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/IJobUI.cs @@ -0,0 +1,25 @@ +using System; + +namespace BlazingQuartz.Jobs.Abstractions +{ + public interface IJobUI + { + string JobClass { get; } + + bool IsReadOnly { get; set; } + + IDictionary JobDataMap { get; set; } + + /// + /// Remove all the JobUI keys that was added to JobDataMap + /// + /// + Task ClearChanges(); + + /// + /// Apply the changes to JobDataMap + /// + /// true if no validation error and all changes applied + Task ApplyChanges(); + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/JobDataMapKeys.cs b/src/BlazingQuartz.Jobs.Abstractions/JobDataMapKeys.cs new file mode 100644 index 0000000..d2bb196 --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/JobDataMapKeys.cs @@ -0,0 +1,11 @@ +using System; + +namespace BlazingQuartz.Jobs.Abstractions +{ + public static class JobDataMapKeys + { + public const string ExecutionDetails = "__execDetails"; + public const string IsSuccess = "__isSuccess"; + public const string ReturnCode = "__returnCode"; + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/JobExecutionContextExtensions.cs b/src/BlazingQuartz.Jobs.Abstractions/JobExecutionContextExtensions.cs new file mode 100644 index 0000000..f966e69 --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/JobExecutionContextExtensions.cs @@ -0,0 +1,104 @@ +using System; +using System.Globalization; +using System.Text; +using System.Text.Json; +using Quartz; + +namespace BlazingQuartz.Jobs.Abstractions +{ + public static class JobExecutionContextExtensions + { + public static IJobExecutionContext SetReturnCode( + this IJobExecutionContext context, + string value + ) + { + context.Put(JobDataMapKeys.ReturnCode, value); + return context; + } + + public static IJobExecutionContext SetReturnCode( + this IJobExecutionContext context, + int value + ) + { + context.Put(JobDataMapKeys.ReturnCode, value.ToString()); + return context; + } + + public static IJobExecutionContext SetExecutionDetails( + this IJobExecutionContext context, + string execDetails + ) + { + context.Put(JobDataMapKeys.ExecutionDetails, execDetails); + return context; + } + + public static IJobExecutionContext SetIsSuccess( + this IJobExecutionContext context, + bool success + ) + { + context.Put(JobDataMapKeys.IsSuccess, success); + return context; + } + + public static string? GetReturnCode(this IJobExecutionContext context) + { + var val = context.Get(JobDataMapKeys.ReturnCode); + if (val != null) + return Convert.ToString(val, CultureInfo.InvariantCulture); + return null; + } + + public static string? GetExecutionDetails(this IJobExecutionContext context) + { + var val = context.Get(JobDataMapKeys.ExecutionDetails); + if (val != null) + return Convert.ToString(val, CultureInfo.InvariantCulture); + + return null; + } + + public static bool? GetIsSuccess(this IJobExecutionContext context) + { + var value = context.Get(JobDataMapKeys.IsSuccess); + if (value == null) + return null; + return Convert.ToBoolean(value); + } + + public static DataMapValue? GetDataMapValue(this IJobExecutionContext context, string key) + { + var value = context.MergedJobDataMap.GetString(key); + return DataMapValue.Create(value); + } + + public static DataMapValue? GetDataMapValue(this JobDataMap dataMap, string key) + { + if (dataMap.TryGetString(key, out var value)) + { + return DataMapValue.Create(value); + } + + return null; + } + + public static string? GetReturnCodeAndResult(this IJobExecutionContext context) + { + var returnCode = context.GetReturnCode(); + var strBldr = new StringBuilder(); + if (!string.IsNullOrEmpty(returnCode)) + { + strBldr.Append($"Return {returnCode}. "); + } + var result = context.Result?.ToString(); + if (!string.IsNullOrEmpty(result)) + { + strBldr.Append(result); + } + return strBldr.ToString(); + } + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/Processors/InterpolatedStringV1Processor.cs b/src/BlazingQuartz.Jobs.Abstractions/Processors/InterpolatedStringV1Processor.cs new file mode 100644 index 0000000..a7e3c4b --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/Processors/InterpolatedStringV1Processor.cs @@ -0,0 +1,66 @@ +using System; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; + +namespace BlazingQuartz.Jobs.Abstractions.Processors +{ + public class InterpolatedStringV1Processor + { + const string VariableRegex = @"\{{2}(\$.+?)\}{2}"; + + private readonly ILogger? _logger; + + public InterpolatedStringV1Processor(ILogger? logger) + { + _logger = logger; + } + + public string? Process(DataMapValue interpolatedString) + { + if (interpolatedString.Type != DataMapValueType.InterpolatedString) + throw new ArgumentException( + $"Invalid DataMapValue type {interpolatedString.Type}. Expected type {DataMapValueType.InterpolatedString}." + ); + + if (string.IsNullOrEmpty(interpolatedString.Value)) + return interpolatedString.Value; + + StringBuilder strBldr = new StringBuilder(); + int lastIndex = 0; + + var provider = new SystemVariableV1Provider(); + + _logger?.LogDebug( + "Processing interpolated string [{string}]", + interpolatedString.Value + ); + foreach ( + Match match in Regex.Matches( + interpolatedString.Value, + VariableRegex, + RegexOptions.None + ) + ) + { + _logger?.LogDebug( + "Matched '{matchValue}' on index {index} length {length}.", + match.Value, + match.Index, + match.Length + ); + strBldr.Append( + interpolatedString.Value.Substring(lastIndex, match.Index - lastIndex) + ); + lastIndex = match.Index + match.Length; + + strBldr.Append(provider.Resolve(match.Value)); + } + + // append remaining + strBldr.Append(interpolatedString.Value.Substring(lastIndex)); + + return strBldr.ToString(); + } + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/Processors/SystemVariableV1Provider.cs b/src/BlazingQuartz.Jobs.Abstractions/Processors/SystemVariableV1Provider.cs new file mode 100644 index 0000000..2c3f5cb --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/Processors/SystemVariableV1Provider.cs @@ -0,0 +1,49 @@ +using System; +using System.Text.RegularExpressions; +using BlazingQuartz.Jobs.Abstractions.Resolvers; +using BlazingQuartz.Jobs.Abstractions.Resolvers.V1; + +namespace BlazingQuartz.Jobs.Abstractions.Processors +{ + internal class SystemVariableV1Provider + { + char[] separators = new char[] { ' ', '}' }; + + private static Dictionary Resolvers; + + static SystemVariableV1Provider() + { + Resolvers = new() + { + { VariableNameContants.DateTime, new DateTimeVariableResolver() }, + { VariableNameContants.LocalDateTime, new LocalDateTimeVariableResolver() }, + { VariableNameContants.Guid, new GuidVariableResolver() }, + }; + } + + public string Resolve(string varBlock) + { + var varName = varBlock.Split(separators, 2).First().Substring(2); + + IResolver? resolver; + if (!Resolvers.TryGetValue(varName, out resolver)) + { + return varBlock; + } + + return resolver.Resolve(varBlock); + } + + /// + /// Add variable resolver + /// + /// + /// + /// false if not added + public static bool AddResolver(string key, IResolver resolver) + { + // don't allow overwrite exiting value + return Resolvers.TryAdd(key, resolver); + } + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/Resolvers/IResolver.cs b/src/BlazingQuartz.Jobs.Abstractions/Resolvers/IResolver.cs new file mode 100644 index 0000000..1000176 --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/Resolvers/IResolver.cs @@ -0,0 +1,9 @@ +using System; + +namespace BlazingQuartz.Jobs.Abstractions.Resolvers +{ + public interface IResolver + { + string Resolve(string varBlock); + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/Resolvers/V1/DateTimeVariableResolver.cs b/src/BlazingQuartz.Jobs.Abstractions/Resolvers/V1/DateTimeVariableResolver.cs new file mode 100644 index 0000000..d6e43de --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/Resolvers/V1/DateTimeVariableResolver.cs @@ -0,0 +1,93 @@ +using System; +using System.Text.RegularExpressions; + +namespace BlazingQuartz.Jobs.Abstractions.Resolvers.V1 +{ + internal class DateTimeVariableResolver : IResolver + { + const string DatetimeRegex = + $"\\{VariableNameContants.DateTime}\\s(rfc1123|iso8601|\'.+\'|\\\".+\\\")(?:\\s(\\-?\\d+)\\s(y|M|d|h|m|s|ms))?"; + + public string Resolve(string varBlock) + { + var result = Regex.Match(varBlock, GetVariableRegex()); + + if (!result.Success || result.Index != 2) + throw new FormatException( + $"Invalid {GetVariableName()} format. Expected format is " + + "{{$datetime rfc1123|iso8601|'date format'|\"date format\" [integer y|M|w|d|h|m|s|ms]}}." + ); + + var dt = GetDateTimeOffset(); + + var format = result.Groups[1].Value; + var rawOffset = result.Groups[2].Value; + if (!string.IsNullOrEmpty(rawOffset)) + { + int offset; + if (!int.TryParse(rawOffset, out offset)) + { + throw new FormatException( + $"Invalid {GetVariableName()} pattern. Offset '{rawOffset}' should be numeric value." + ); + } + var offsetOption = result.Groups[3].Value; + + switch (offsetOption) + { + case "y": + dt = dt.AddYears(offset); + break; + case "M": + dt = dt.AddMonths(offset); + break; + case "d": + dt = dt.AddDays(offset); + break; + case "h": + dt = dt.AddHours(offset); + break; + case "m": + dt = dt.AddMinutes(offset); + break; + case "s": + dt = dt.AddSeconds(offset); + break; + case "ms": + dt = dt.AddMilliseconds(offset); + break; + default: + throw new FormatException( + $"Invalid {GetVariableName()} pattern. Need to provide valid offset option." + ); + } + } + + switch (format) + { + case "rfc1123": + return dt.ToString("r"); + case "iso8601": + return dt.ToString("u"); + default: + // value was enclosed in quotes, ignore first and last character + return dt.ToString(format.Substring(1, format.Length - 2)); + } + } + + internal virtual string GetVariableRegex() + { + return DatetimeRegex; + } + + internal virtual string GetVariableName() + { + return VariableNameContants.DateTime; + } + + internal virtual DateTimeOffset GetDateTimeOffset() + { + return DateTimeOffset.UtcNow; + } + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/Resolvers/V1/GuidVariableResolver.cs b/src/BlazingQuartz.Jobs.Abstractions/Resolvers/V1/GuidVariableResolver.cs new file mode 100644 index 0000000..9d3c941 --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/Resolvers/V1/GuidVariableResolver.cs @@ -0,0 +1,20 @@ +using System; + +namespace BlazingQuartz.Jobs.Abstractions.Resolvers.V1 +{ + internal class GuidVariableResolver : IResolver + { + const string Format = $"{{{{{VariableNameContants.Guid}}}}}"; + + public GuidVariableResolver() { } + + public string Resolve(string varBlock) + { + if (varBlock != Format) + throw new FormatException( + $"Invalid {VariableNameContants.Guid} format. Expected format is {Format}." + ); + return Guid.NewGuid().ToString(); + } + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/Resolvers/V1/LocalDateTimeVariableResolver.cs b/src/BlazingQuartz.Jobs.Abstractions/Resolvers/V1/LocalDateTimeVariableResolver.cs new file mode 100644 index 0000000..f63fed4 --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/Resolvers/V1/LocalDateTimeVariableResolver.cs @@ -0,0 +1,25 @@ +using System; + +namespace BlazingQuartz.Jobs.Abstractions.Resolvers.V1 +{ + internal class LocalDateTimeVariableResolver : DateTimeVariableResolver + { + const string DatetimeRegex = + $"\\{VariableNameContants.LocalDateTime}\\s(rfc1123|iso8601|\'.+\'|\\\".+\\\")(?:\\s(\\-?\\d+)\\s(y|M|d|h|m|s|ms))?"; + + internal override string GetVariableName() + { + return VariableNameContants.LocalDateTime; + } + + internal override string GetVariableRegex() + { + return DatetimeRegex; + } + + internal override DateTimeOffset GetDateTimeOffset() + { + return DateTimeOffset.Now; + } + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/ServiceCollectionExtensions.cs b/src/BlazingQuartz.Jobs.Abstractions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..d548360 --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/ServiceCollectionExtensions.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace BlazingQuartz.Jobs.Abstractions +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddBlazingQuartzJobs(this IServiceCollection services) + { + services.TryAddTransient(); + + return services; + } + } +} diff --git a/src/BlazingQuartz.Jobs.Abstractions/VariableNameContants.cs b/src/BlazingQuartz.Jobs.Abstractions/VariableNameContants.cs new file mode 100644 index 0000000..e91307a --- /dev/null +++ b/src/BlazingQuartz.Jobs.Abstractions/VariableNameContants.cs @@ -0,0 +1,11 @@ +using System; + +namespace BlazingQuartz.Jobs.Abstractions +{ + public static class VariableNameContants + { + public const string DateTime = "$datetime"; + public const string LocalDateTime = "$localDatetime"; + public const string Guid = "$guid"; + } +} diff --git a/src/BlazingQuartz.Jobs/BlazingQuartz.Jobs.csproj b/src/BlazingQuartz.Jobs/BlazingQuartz.Jobs.csproj new file mode 100644 index 0000000..95ae450 --- /dev/null +++ b/src/BlazingQuartz.Jobs/BlazingQuartz.Jobs.csproj @@ -0,0 +1,13 @@ + + + net8.0 + enable + enable + + + + + + + + diff --git a/src/BlazingQuartz.Jobs/Constants.cs b/src/BlazingQuartz.Jobs/Constants.cs new file mode 100644 index 0000000..763318c --- /dev/null +++ b/src/BlazingQuartz.Jobs/Constants.cs @@ -0,0 +1,9 @@ +using System; + +namespace BlazingQuartz.Jobs +{ + public abstract class Constants + { + public const string HttpClientIgnoreVerifySsl = "IgnoreSsl"; + } +} diff --git a/src/BlazingQuartz.Jobs/HttpAction.cs b/src/BlazingQuartz.Jobs/HttpAction.cs new file mode 100644 index 0000000..fe57f5a --- /dev/null +++ b/src/BlazingQuartz.Jobs/HttpAction.cs @@ -0,0 +1,12 @@ +using System; + +namespace BlazingQuartz.Jobs +{ + public enum HttpAction + { + Get, + Post, + Put, + Delete, + } +} diff --git a/src/BlazingQuartz.Jobs/HttpJob.cs b/src/BlazingQuartz.Jobs/HttpJob.cs new file mode 100644 index 0000000..8666445 --- /dev/null +++ b/src/BlazingQuartz.Jobs/HttpJob.cs @@ -0,0 +1,191 @@ +using System; +using System.Text; +using System.Text.Json; +using BlazingQuartz.Jobs.Abstractions; +using Microsoft.Extensions.Logging; +using Quartz; +using static System.Net.Mime.MediaTypeNames; + +namespace BlazingQuartz.Jobs +{ + public class HttpJob : IJob + { + public const string PropertyRequestAction = "requestAction"; + public const string PropertyRequestUrl = "requestUrl"; + public const string PropertyRequestParameters = "requestParams"; + public const string PropertyRequestHeaders = "requestHeaders"; + public const string PropertyIgnoreVerifySsl = "ignoreSsl"; + + /// + /// HTTP request timeout. Negative value to indicate infinite timeout. + /// + public const string PropertyRequestTimeoutInSec = "requestTimeout"; + + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + private readonly IDataMapValueResolver _dmvResolver; + + public HttpJob( + IHttpClientFactory httpClientFactory, + ILogger logger, + IDataMapValueResolver dmvResolver + ) + { + _httpClientFactory = httpClientFactory; + _logger = logger; + _dmvResolver = dmvResolver; + } + + public async Task Execute(IJobExecutionContext context) + { + try + { + var data = context.MergedJobDataMap; + + int? timeoutInSec = data.TryGetInt(PropertyRequestTimeoutInSec, out var x) + ? x + : null; + var dmvUrl = data.GetDataMapValue(PropertyRequestUrl); + var url = _dmvResolver.Resolve(dmvUrl); + if (string.IsNullOrEmpty(url)) + { + _logger.LogWarning( + "[{runInstanceId}]. Cannot run HttpJob. No request url specified.", + context.FireInstanceId + ); + throw new JobExecutionException("No request url specified"); + } + url = url.StartsWith("http") ? url : "http://" + url; + + var parameters = _dmvResolver.Resolve( + data.GetDataMapValue(PropertyRequestParameters) + ); + var strHeaders = _dmvResolver.Resolve(data.GetDataMapValue(PropertyRequestHeaders)); + var headers = string.IsNullOrEmpty(strHeaders) + ? null + : JsonSerializer.Deserialize>(strHeaders.Trim()); + + var strAction = data.GetString(PropertyRequestAction); + HttpAction action; + if (strAction == null) + { + _logger.LogWarning( + "[{runInstanceId}]. Cannot run HttpJob. No http action specified.", + context.FireInstanceId + ); + throw new JobExecutionException("No http action specified"); + } + action = Enum.Parse(strAction); + + _logger.LogDebug( + "[{runInstanceId}]. Creating HttpClient...", + context.FireInstanceId + ); + HttpClient httpClient; + if ( + data.TryGetBoolean(PropertyIgnoreVerifySsl, out var IgnoreVerifySsl) + && IgnoreVerifySsl + ) + { + httpClient = _httpClientFactory.CreateClient( + Constants.HttpClientIgnoreVerifySsl + ); + _logger.LogInformation( + "[{runInstanceId}]. Created ignore SSL validation HttpClient.", + context.FireInstanceId + ); + } + else + { + httpClient = _httpClientFactory.CreateClient(); + _logger.LogInformation( + "[{runInstanceId}]. Created HttpClient.", + context.FireInstanceId + ); + } + + // configure time out. Default 100 secs + if (timeoutInSec.HasValue) + { + if (timeoutInSec > 0) + { + httpClient.Timeout = TimeSpan.FromSeconds(timeoutInSec.Value); + } + else + { + httpClient.Timeout = Timeout.InfiniteTimeSpan; + } + } + + if (headers != null) + { + foreach (var header in headers) + { + httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + + HttpContent? reqParam = null; + if (!string.IsNullOrEmpty(parameters)) + reqParam = new StringContent(parameters, Encoding.UTF8, Application.Json); + + HttpResponseMessage response = new HttpResponseMessage(); + _logger.LogInformation( + "[{runInstanceId}]. Sending '{action}' request to specified url '{url}'.", + context.FireInstanceId, + action, + url + ); + switch (action) + { + case HttpAction.Get: + response = await httpClient.GetAsync(url, context.CancellationToken); + break; + case HttpAction.Post: + response = await httpClient.PostAsync( + url, + reqParam, + context.CancellationToken + ); + break; + case HttpAction.Put: + response = await httpClient.PutAsync( + url, + reqParam, + context.CancellationToken + ); + break; + case HttpAction.Delete: + response = await httpClient.DeleteAsync(url, context.CancellationToken); + break; + } + + var result = await response.Content.ReadAsStringAsync(context.CancellationToken); + _logger.LogInformation( + "[{runInstanceId}]. Response tatus code '{code}'.", + context.FireInstanceId, + response.StatusCode + ); + context.Result = result; + context.SetIsSuccess(response.IsSuccessStatusCode); + context.SetReturnCode((int)response.StatusCode); + context.SetExecutionDetails($"Request: [{response.RequestMessage}]"); + } + catch (JobExecutionException) + { + context.SetIsSuccess(false); + throw; + } + catch (Exception ex) + { + _logger.LogWarning( + ex, + "Failed to run HttpJob. [{runInstanceId}]", + context.FireInstanceId + ); + context.SetIsSuccess(false); + throw new JobExecutionException("Failed to execute http job", ex); + } + } + } +} diff --git a/src/BlazingQuartz.Jobs/ServiceCollectionExtensions.cs b/src/BlazingQuartz.Jobs/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..4517a9f --- /dev/null +++ b/src/BlazingQuartz.Jobs/ServiceCollectionExtensions.cs @@ -0,0 +1,26 @@ +using System; +using BlazingQuartz.Jobs.Abstractions; +using Microsoft.Extensions.DependencyInjection; + +namespace BlazingQuartz.Jobs +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddBlazingQuartzJobs(this IServiceCollection services) + { + // require to run BlazingQuartz.Jobs.HttpJob + services.AddHttpClient(); + services + .AddHttpClient(Constants.HttpClientIgnoreVerifySsl) + .ConfigurePrimaryHttpMessageHandler(() => + new HttpClientHandler + { + ServerCertificateCustomValidationCallback = + HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, + } + ); + + return Abstractions.ServiceCollectionExtensions.AddBlazingQuartzJobs(services); + } + } +} diff --git a/src/Ray.BiliBiliTool.Agent/Attributes/AppendHeaderAttribute.cs b/src/Ray.BiliBiliTool.Agent/Attributes/AppendHeaderAttribute.cs new file mode 100644 index 0000000..4ee733d --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/Attributes/AppendHeaderAttribute.cs @@ -0,0 +1,75 @@ +using System.Diagnostics; +using System.Net.Http.Headers; +using WebApiClientCore; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.Attributes; + +[DebuggerDisplay("{name} = {value}")] +[AttributeUsage( + AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Parameter, + AllowMultiple = true +)] +public class AppendHeaderAttribute( + string name, + string? value, + AppendHeaderType appendHeaderType = AppendHeaderType.AddOrReplace +) : ApiActionAttribute, IApiParameterAttribute +{ + //添加的顺序为:子类、基类、子类函数、子类函数入参 + + private readonly string? _aliasName; + + public AppendHeaderAttribute( + string aliasName, + AppendHeaderType appendHeaderType = AppendHeaderType.AddOrReplace + ) + : this("", null, appendHeaderType) + { + _aliasName = aliasName; + } + + public override Task OnRequestAsync(ApiRequestContext context) + { + AddByAppendType(context.HttpContext.RequestMessage.Headers, name, value); + return Task.CompletedTask; + } + + public Task OnRequestAsync(ApiParameterContext context) + { + var parameterName = _aliasName; + if (string.IsNullOrEmpty(parameterName)) + { + parameterName = context.ParameterName; + } + + var text = context.ParameterValue?.ToString(); + if (!string.IsNullOrEmpty(text)) + { + AddByAppendType(context.HttpContext.RequestMessage.Headers, parameterName, text); + } + + return Task.CompletedTask; + } + + private void AddByAppendType(HttpRequestHeaders headers, string key, string? value) + { + switch (appendHeaderType) + { + case AppendHeaderType.Add: + headers.TryAddWithoutValidation(key, value); + break; + case AppendHeaderType.AddIfNotExist: + if (!headers.Contains(key)) + headers.TryAddWithoutValidation(key, value); + break; + case AppendHeaderType.AddOrReplace: + if (headers.Contains(key)) + headers.Remove(key); + headers.TryAddWithoutValidation(key, value); + break; + default: + break; + } + } +} diff --git a/src/Ray.BiliBiliTool.Agent/Attributes/AppendHeaderType.cs b/src/Ray.BiliBiliTool.Agent/Attributes/AppendHeaderType.cs new file mode 100644 index 0000000..04586de --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/Attributes/AppendHeaderType.cs @@ -0,0 +1,8 @@ +namespace Ray.BiliBiliTool.Agent.Attributes; + +public enum AppendHeaderType +{ + Add, + AddIfNotExist, + AddOrReplace, +} diff --git a/src/Ray.BiliBiliTool.Agent/Attributes/LogFilterAttribute.cs b/src/Ray.BiliBiliTool.Agent/Attributes/LogFilterAttribute.cs new file mode 100644 index 0000000..9db9082 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/Attributes/LogFilterAttribute.cs @@ -0,0 +1,45 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using WebApiClientCore; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.Attributes; + +public class LogFilterAttribute(bool logError = true) : LoggingFilterAttribute +{ + protected override Task WriteLogAsync(ApiResponseContext context, LogMessage logMessage) + { + var loggerFactory = context.HttpContext.ServiceProvider.GetService(); + if (loggerFactory == null) + { + return Task.CompletedTask; + } + + MethodInfo member = context.ActionDescriptor.Member; + var strArray = new string?[5]; + var declaringType1 = member.DeclaringType; + strArray[0] = declaringType1?.Namespace; + strArray[1] = "."; + var declaringType2 = member.DeclaringType; + strArray[2] = declaringType2?.Name; + strArray[3] = "."; + strArray[4] = member.Name; + string categoryName = string.Concat(strArray); + ILogger logger = loggerFactory.CreateLogger(categoryName); + + if (logMessage.Exception == null) + { + logger.LogDebug(logMessage.ToString()); + } + else + { + if (logError) + logger.LogError(logMessage.ToString()); + else + logger.LogDebug(logMessage.ToString()); + } + + return Task.CompletedTask; + } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Attributes/WbiParameterAttribute.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Attributes/WbiParameterAttribute.cs new file mode 100644 index 0000000..c4662cd --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Attributes/WbiParameterAttribute.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DependencyInjection; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Services; +using Ray.BiliBiliTool.Infrastructure.Cookie; +using WebApiClientCore; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Attributes; + +[AttributeUsage(AttributeTargets.Parameter)] +public class WbiParameterAttribute : Attribute, IApiParameterAttribute +{ + public async Task OnRequestAsync(ApiParameterContext context) + { + // 只处理实现了IWrid接口的参数 + if (context.ParameterValue is IWrid wridRequest) + { + // 从依赖注入获取WbiService + var wbiService = context.HttpContext.ServiceProvider.GetRequiredService(); + + // 从函数参数中获取Cookie + var cookieStr = string.Empty; + var allParameters = context.ActionDescriptor.Parameters; + foreach (var parameter in allParameters) + { + var cookieHeader = parameter.Attributes.FirstOrDefault(a => + a is HeaderAttribute header + && (string)header.GetFieldValue("aliasName") == "Cookie" + ); + if (cookieHeader != null) + { + cookieStr = context.Arguments[parameter.Index]?.ToString(); + break; + } + } + + var cookie = CookieStrFactory.CreateNew(cookieStr ?? ""); + + // 设置w_rid和wts值 + await wbiService.SetWridAsync(wridRequest, cookie); + } + } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/AddCoinRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/AddCoinRequest.cs new file mode 100644 index 0000000..7c96473 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/AddCoinRequest.cs @@ -0,0 +1,28 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class AddCoinRequest +{ + public AddCoinRequest(long aid, string csrf) + { + Aid = aid; + Csrf = csrf; + } + + public long Aid { get; set; } + + public int Multiply { get; set; } = 1; + + public int Select_like { get; set; } = 1; + + public string Cross_domain { get; set; } = "true"; + + public string Csrf { get; set; } + + public string Eab_x { get; set; } = "2"; + + public string Ramval { get; set; } = "3"; + + public string Source { get; set; } = "web_normal"; + + public string Ga { get; set; } = "1"; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/AddCoinForArticleRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/AddCoinForArticleRequest.cs new file mode 100644 index 0000000..e2b465f --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/AddCoinForArticleRequest.cs @@ -0,0 +1,22 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Article; + +public class AddCoinForArticleRequest +{ + public AddCoinForArticleRequest(long cvid, long mid, string csrf) + { + Aid = cvid; + Upid = mid; + Csrf = csrf; + } + + public long Aid { get; set; } + + public long Upid { get; set; } + + public int Multiply { get; set; } = 1; + + // 必须为2 + public int Avtype { get; private set; } = 2; + + public string Csrf { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/SearchArticleInfoResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/SearchArticleInfoResponse.cs new file mode 100644 index 0000000..22b9b37 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/SearchArticleInfoResponse.cs @@ -0,0 +1,10 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Article; + +public class SearchArticleInfoResponse +{ + public int Like { get; set; } + + public int Coin { get; set; } + + public long Mid { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/SearchArticlesByUpIdFullFto.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/SearchArticlesByUpIdFullFto.cs new file mode 100644 index 0000000..65a4169 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/SearchArticlesByUpIdFullFto.cs @@ -0,0 +1,21 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Services; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Article; + +public class SearchArticlesByUpIdDto : IWrid +{ + public long mid { get; set; } + + public int pn { get; set; } = 1; + + public int ps { get; set; } = 12; + + public string sort { get; set; } = "publish_time"; + + public long web_location { get; set; } = 1550101; + + public string platform { get; set; } = "web"; + + public string? w_rid { get; set; } + public long wts { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/SearchUpArticlesResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/SearchUpArticlesResponse.cs new file mode 100644 index 0000000..c9c4ab9 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/SearchUpArticlesResponse.cs @@ -0,0 +1,14 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Article; + +public class SearchUpArticlesResponse +{ + public List Articles { get; set; } = []; + public int Count { get; set; } +} + +public class ArticleInfo +{ + public long Id { get; set; } + + public required string Title { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/BaseAppRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/BaseAppRequest.cs new file mode 100644 index 0000000..b78e0cb --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/BaseAppRequest.cs @@ -0,0 +1,32 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class BaseAppRequest +{ + // public string access_key { get; set; } + // public string appkey { get; set; } + + public string build { get; } = "8451100"; + + public int disable_rcmd { get; } = 0; + + public string mobi_app { get; } = "android"; + + public string platform { get; } = "android"; + + public string statistics { get; } = + "{\"appId\":1,\"platform\":3,\"version\":\"8.45.1\",\"abtest\":\"\"}"; + + /// + /// 当前时间(毫秒) + /// + /// 1748445354567 + public long t { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + /// + /// 当前时间(秒) + /// + /// 1748445354 + public long ts => t / 1000; + + public string? sign { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/BiliApiResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/BiliApiResponse.cs new file mode 100644 index 0000000..d0f4f8a --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/BiliApiResponse.cs @@ -0,0 +1,13 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class BiliApiResponse +{ + public int Code { get; set; } = int.MinValue; + + public string? Message { get; set; } +} + +public class BiliApiResponse : BiliApiResponse +{ + public required TData Data { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/BiliPageResult.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/BiliPageResult.cs new file mode 100644 index 0000000..692109a --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/BiliPageResult.cs @@ -0,0 +1,19 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class BiliPageResult +{ + /// + /// 视频总数量 + /// + public int Count { get; set; } + + /// + /// 页码 + /// + public int Pn { get; set; } + + /// + /// 每页条数 + /// + public int Ps { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ChargeCommentRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ChargeCommentRequest.cs new file mode 100644 index 0000000..77a4af5 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ChargeCommentRequest.cs @@ -0,0 +1,17 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class ChargeCommentRequest +{ + public ChargeCommentRequest(string order_id, string message, string csrf) + { + Order_id = order_id; + Message = message; + Csrf = csrf; + } + + public string Order_id { get; set; } + + public string Message { get; set; } + + public string Csrf { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ChargeRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ChargeRequest.cs new file mode 100644 index 0000000..b0ab411 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ChargeRequest.cs @@ -0,0 +1,39 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class ChargeRequest +{ + public ChargeRequest(decimal bp_num, long upId, string csrf) + { + Bp_num = bp_num; + Up_mid = upId; + Oid = upId; + Csrf = csrf; + } + + /// + /// B币个数 + /// + public decimal Bp_num { get; set; } + + public string Is_bp_remains_prior { get; set; } = "true"; + + /// + /// 对方Id + /// + public long Up_mid { get; set; } + + /// + /// + /// + public string Otype { get; set; } = "up"; + + /// + /// 对方来源代码(空间充电:充电对象用户UID;视频充电:稿件avID) + /// + public long Oid { get; set; } + + /// + /// 自己的bili_jct + /// + public string Csrf { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ChargeResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ChargeResponse.cs new file mode 100644 index 0000000..af6e259 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ChargeResponse.cs @@ -0,0 +1,25 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class ChargeResponse +{ + public int Status { get; set; } + + public required string Order_no { get; set; } +} + +public class ChargeV2Response +{ + public required string Bp_num { get; set; } + + public decimal Exp { get; set; } + + public long Mid { get; set; } + + public string? Msg { get; set; } + + public required string Order_no { get; set; } + + public int Status { get; set; } + + public long Up_mid { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/CoinBalance.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/CoinBalance.cs new file mode 100644 index 0000000..9f6363b --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/CoinBalance.cs @@ -0,0 +1,9 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +/// +/// 硬币余额 +/// +public class CoinBalance +{ + public decimal? Money { get; set; } = int.MinValue; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/DailyTaskInfo.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/DailyTaskInfo.cs new file mode 100644 index 0000000..e4817d3 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/DailyTaskInfo.cs @@ -0,0 +1,20 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class DailyTaskInfo +{ + public bool Login { get; set; } + + public bool Watch { get; set; } + + public long Coins { get; set; } + + public bool Share { get; set; } + + public bool Email { get; set; } + + public bool Tel { get; set; } + + public bool Safe_question { get; set; } + + public bool Identify_card { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/DonatedCoinsForVideo.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/DonatedCoinsForVideo.cs new file mode 100644 index 0000000..19aa455 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/DonatedCoinsForVideo.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class DonatedCoinsForVideo +{ + public int Multiply { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetAlreadyDonatedCoinsRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetAlreadyDonatedCoinsRequest.cs new file mode 100644 index 0000000..664d9af --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetAlreadyDonatedCoinsRequest.cs @@ -0,0 +1,15 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class GetAlreadyDonatedCoinsRequest +{ + public GetAlreadyDonatedCoinsRequest(long aid) + { + Aid = aid; + } + + public string Jsonp { get; set; } = "jsonp"; + + public long Aid { get; set; } + + //public string Callback { get; set; } = $"jsonCallback_bili_{new Random().Next(10000, 99999)}{new Random().Next(10000, 99999)}"; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetSpaceInfoFullDto.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetSpaceInfoFullDto.cs new file mode 100644 index 0000000..8ddd00b --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetSpaceInfoFullDto.cs @@ -0,0 +1,11 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Services; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class GetSpaceInfoDto : IWrid +{ + public long mid { get; set; } + + public string? w_rid { get; set; } + public long wts { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetSpaceInfoResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetSpaceInfoResponse.cs new file mode 100644 index 0000000..061c7fc --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetSpaceInfoResponse.cs @@ -0,0 +1,17 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class GetSpaceInfoResponse +{ + public long Mid { get; set; } + + public required string Name { get; set; } + + public required SpaceLiveRoomInfoDto Live_room { get; set; } +} + +public class SpaceLiveRoomInfoDto +{ + public required string Title { get; set; } + + public long Roomid { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetSpecialFollowingsRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetSpecialFollowingsRequest.cs new file mode 100644 index 0000000..e15a2de --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetSpecialFollowingsRequest.cs @@ -0,0 +1,31 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class GetSpecialFollowingsRequest +{ + public GetSpecialFollowingsRequest(long userId) + { + Mid = userId; + } + + public GetSpecialFollowingsRequest(long userId, long tagId) + { + Mid = userId; + Tagid = tagId; + } + + public long Mid { get; set; } + + /// + /// TagId + /// + /// -10:特别关注 + public long Tagid { get; set; } = -10; + + public int Pn { get; set; } = 1; + + public int Ps { get; set; } = 20; + + public string Jsonp { get; set; } = "jsonp"; + + //public string Callback { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetVideosResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetVideosResponse.cs new file mode 100644 index 0000000..3a55123 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetVideosResponse.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class GetVideosResponse +{ + public List Media_list { get; set; } = []; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/AreaDto.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/AreaDto.cs new file mode 100644 index 0000000..b5c4cd9 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/AreaDto.cs @@ -0,0 +1,13 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class GetArteaListResponse +{ + public List Data { get; set; } = []; +} + +public class AreaDto +{ + public long Id { get; set; } + + public required string Name { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/CheckTianXuanDto.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/CheckTianXuanDto.cs new file mode 100644 index 0000000..9cbfba8 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/CheckTianXuanDto.cs @@ -0,0 +1,133 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class CheckTianXuanDto +{ + public long Id { get; set; } + + public long Room_id { get; set; } + + /// + /// 状态 + /// + public TianXuanStatus Status { get; set; } + + /// + /// 奖励名称 + /// + public required string Award_name { get; set; } + + /// + /// 奖励数量 + /// + public int Award_num { get; set; } + + /// + /// 弹幕内容 + /// + public string? Danmu { get; set; } + + public int Join_type { get; set; } + + /// + /// 要求条件类型 + /// + public RequireType? Require_type { get; set; } + + /// + /// 要求值 + /// + public int Require_value { get; set; } + + /// + /// 要求名称 + /// + public string? Require_text { get; set; } + + #region 礼物 + public long Gift_id { get; set; } + + public string? Gift_name { get; set; } + + public int Gift_num { get; set; } + + public int Gift_price { get; set; } + + public int Cur_gift_num { get; set; } + + public string GiftDesc => $"价值{Gift_price}的{Gift_name}{Gift_num}个"; + #endregion + + public int Send_gift_ensure { get; set; } + + public bool AwardNameIsSatisfied(List includeKeys, List excludeKeys) + { + //只要包含了排除的关键字,就排除 + if (excludeKeys.Any()) + { + foreach (var item in excludeKeys) + { + if (Award_name.Contains(item)) + return false; + } + } + + //遍历所有包含关键字,包含其一就确认,否则保持排除 + bool isInclude = true; + if (includeKeys.Any()) + { + isInclude = false; + foreach (var item in includeKeys) + { + if (Award_name.Contains(item)) + { + isInclude = true; + break; + } + } + } + + return isInclude; + } +} + +/// +/// 天选抽奖状态 +/// +public enum TianXuanStatus +{ + /// + /// 可参与抽奖 + /// + Enable = 1, + + /// + /// 已结束 + /// + End = 2, +} + +/// +/// 天选抽奖条件 +/// +public enum RequireType +{ + /// + /// 无 + /// + None = 0, + + /// + /// 关注主播 + /// + Follow = 1, + + /// + /// 粉丝勋章级数要求 + /// + FansLevel = 2, + + /// + /// 至少成为提督舰长等 + /// + TiDuOrJianZhang = 3, +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/EnterRoomRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/EnterRoomRequest.cs new file mode 100644 index 0000000..a0999d6 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/EnterRoomRequest.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class EnterRoomRequest +{ + public EnterRoomRequest( + long roomId, + long parentId, + long areaID, + int seqNumber, // 心跳包编号 + long timestamp, + string userAgent, + string csrf, + long ruid, + string device + ) + { + Id = JsonConvert.SerializeObject(new[] { parentId, areaID, seqNumber, roomId }); + Ts = timestamp; + Ua = userAgent; + Csrf = csrf; + Ruid = ruid; + + Is_patch = 0; + Heart_beat = "[]"; + Visit_id = ""; + Device = device; + } + + public string Id { get; set; } + + public long Ruid { get; set; } + + public long Ts { get; set; } + + public int Is_patch { get; set; } + + public string Heart_beat { get; set; } + + public string Ua { get; set; } + + public string Csrf_token => Csrf; + + public string Csrf { get; set; } + + public string Visit_id { get; set; } + + public string Device { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/ExchangeSilverStatusResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/ExchangeSilverStatusResponse.cs new file mode 100644 index 0000000..7cd92ac --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/ExchangeSilverStatusResponse.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class ExchangeSilverStatusResponse +{ + public int Silver { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/GetListRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/GetListRequest.cs new file mode 100644 index 0000000..a74f3f0 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/GetListRequest.cs @@ -0,0 +1,14 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Services; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class GetListRequest : IWrid +{ + public string platform { get; set; } = "web"; + public long parent_area_id { get; set; } + public long area_id { get; set; } + public string? sort_type { get; set; } + public int page { get; set; } + public long wts { get; set; } + public string? w_rid { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/GetLiveRoomInfoResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/GetLiveRoomInfoResponse.cs new file mode 100644 index 0000000..96b9b62 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/GetLiveRoomInfoResponse.cs @@ -0,0 +1,14 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class GetLiveRoomInfoResponse +{ + public long Room_id { get; set; } + + public long Area_id { get; set; } + + public long Parent_area_id { get; set; } + + public int Live_Status { get; set; } + + public long Uid { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/HeartBeatRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/HeartBeatRequest.cs new file mode 100644 index 0000000..0100d04 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/HeartBeatRequest.cs @@ -0,0 +1,74 @@ +using Newtonsoft.Json; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Utils; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class HeartBeatRequest +{ + public HeartBeatRequest( + long roomId, + long parentId, + long areaID, + int seqNumber, // 心跳包编号 + string buvid, // cookie['LIVE_BUVID'] + long timestamp, + long ets, // 由后端返回的 timestamp + string userAgent, + ICollection secretRule, + string secretKey, + string csrf, + string uuid, + string device + ) + { + Id = JsonConvert.SerializeObject(new[] { parentId, areaID, seqNumber, roomId }); + Ets = ets; + Benchmark = secretKey; + Time = 60; + Ts = timestamp; + Ua = userAgent; + Csrf = csrf; + Device = device; + + // 构造哈希值 + var json = new + { + platform = "web", + parent_id = parentId, + area_id = areaID, + seq_id = seqNumber, + room_id = roomId, + buvid, + uuid, + ets, + time = 60, + ts = timestamp, + }; + string jsonString = JsonConvert.SerializeObject(json); + S = LiveHeartBeatCrypto.Sypder(jsonString, secretRule, secretKey); + + Visit_id = ""; + } + + public string S { get; set; } + + public string Id { get; set; } + + public long Ets { get; set; } + + public string Benchmark { get; set; } + + public long Time { get; set; } + + public long Ts { get; set; } + + public string Ua { get; set; } + + public string Csrf_token => Csrf; + + public string Csrf { get; set; } + + public string Visit_id { get; set; } + + public string Device { get; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/HeartBeatResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/HeartBeatResponse.cs new file mode 100644 index 0000000..cea88a6 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/HeartBeatResponse.cs @@ -0,0 +1,12 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class HeartBeatResponse +{ + public int Heartbeat_interval { get; set; } + + public string? Secret_key { get; set; } + + public List Secret_rule { get; set; } = []; + + public long Timestamp { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/JoinTianXuanRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/JoinTianXuanRequest.cs new file mode 100644 index 0000000..25cf9a1 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/JoinTianXuanRequest.cs @@ -0,0 +1,53 @@ +using Ray.BiliBiliTool.Infrastructure.Helpers; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class JoinTianXuanRequest +{ + /// + /// Id(从Check接口获取) + /// + public long Id { get; set; } + + /// + /// 礼物Id(从Check接口获取) + /// + public long Gift_id { get; set; } + + /// + /// 礼物数量(从Check接口获取) + /// + public int Gift_num { get; set; } + + /// + /// bili_jct(取自Cookie) + /// + public required string Csrf { get; set; } + + public string Csrf_token => Csrf; + + /// + /// + /// + /// 8u0w3cesz1o0 + /// 33moy4vugle0 + /// 9zys612vo0c0 + /// 3uu2mkxt21c0 + /// 8orqn5vf4i00 + public string Visit_id { get; set; } = _visitId; //todo + + public string Platform { get; set; } = "pc"; + + public static string GetRandomVisitId() + { + var ran = new Random(); + int first = ran.Next(1, 10); + int last = 0; + + var s = new RandomHelper().GenerateCode(10).ToLower(); + + return $"{first}{s}{last}"; + } + + private static string _visitId = GetRandomVisitId(); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/JoinTianXuanResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/JoinTianXuanResponse.cs new file mode 100644 index 0000000..3eb3136 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/JoinTianXuanResponse.cs @@ -0,0 +1,16 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class JoinTianXuanResponse +{ + public long Discount_id { get; set; } + + public long Gold { get; set; } + + public long Silver { get; set; } + + public long Cur_gift_num { get; set; } + + public long Goods_id { get; set; } + + public required string New_order_id { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/LikeLiveRoomRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/LikeLiveRoomRequest.cs new file mode 100644 index 0000000..b6a5e7d --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/LikeLiveRoomRequest.cs @@ -0,0 +1,30 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class LikeLiveRoomRequest +{ + public LikeLiveRoomRequest(long roomid, string csrf, int clickTime, long anchorId, string uid) + { + Roomid = roomid; + Csrf = csrf; + Click_Time = clickTime; + Anchor_Id = anchorId; + Uid = uid; + } + + public long Roomid { get; set; } + + public string Csrf { get; set; } + + public string Csrf_Token => Csrf; + + public int Click_Time { get; set; } + + public long Anchor_Id { get; set; } + + public string Uid { get; set; } + + public string RawTextBuild() + { + return $"click_time={Click_Time.ToString()}&room_id={Roomid.ToString()}&uid={Uid}&anchor_id={Anchor_Id}&csrf_token={Csrf_Token}&csrf={Csrf}"; + } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/ListItemDto.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/ListItemDto.cs new file mode 100644 index 0000000..bb8124c --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/ListItemDto.cs @@ -0,0 +1,73 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class GetListResponse +{ + public List New_tags { get; set; } = []; + + public List List { get; set; } = []; + + public int Has_more { get; set; } +} + +public class LiveSortTag +{ + public long Id { get; set; } + + public required string Name { get; set; } + + public string? Sort_type { get; set; } +} + +public class ListItemDto +{ + public long Roomid { get; set; } + + public long Uid { get; set; } + + public required string Title { get; set; } + + public string ShortTitle + { + get + { + if (string.IsNullOrWhiteSpace(Title) || Title.Length <= 10) + return Title; + + return Title.Substring(0, 7) + "..."; + } + } + + public required string Uname { get; set; } + + public long Parent_id { get; set; } + + public required string Parent_name { get; set; } + + public long Area_id { get; set; } + + public string? Area_name { get; set; } + + /// + /// + /// + /// 1:百人成就 + /// 2:天选时刻、新星主播 + public Dictionary? Pendant_info { get; set; } +} + +public class PendantInfo +{ + /// + /// Id + /// + /// 504:天选 + /// 426:百人成就 + /// 397:新星主播 + public long Pendent_id { get; set; } + + /// + /// 内容 + /// + /// 天选时刻 + public string? Content { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/LiveSignResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/LiveSignResponse.cs new file mode 100644 index 0000000..f6908c3 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/LiveSignResponse.cs @@ -0,0 +1,8 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class LiveSignResponse +{ + public string? Text { get; set; } + + public string? SpecialText { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/LiveWalletStatusResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/LiveWalletStatusResponse.cs new file mode 100644 index 0000000..8d7c33f --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/LiveWalletStatusResponse.cs @@ -0,0 +1,29 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class LiveWalletStatusResponse +{ + /// + /// 硬币余额 + /// + public decimal Coin { get; set; } + + /// + /// 金瓜子余额 + /// + public decimal Gold { get; set; } + + /// + /// 银瓜子余额 + /// + public decimal Silver { get; set; } + + /// + /// 银瓜子兑换硬币剩余次数 + /// + public int Silver_2_coin_left { get; set; } + + /// + /// 硬币兑换银瓜子剩余次数 + /// + public int Coin_2_silver_left { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/MedalWallDto.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/MedalWallDto.cs new file mode 100644 index 0000000..7170e02 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/MedalWallDto.cs @@ -0,0 +1,28 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class MedalWallResponse +{ + public List List { get; set; } = []; +} + +public class MedalWallDto +{ + public int Live_status { get; set; } + + public required string Target_name { get; set; } + + public required string Link { get; set; } + + public required MedalInfoDto Medal_info { get; set; } +} + +public class MedalInfoDto +{ + public required string Medal_name { get; set; } + + public long Medal_id { get; set; } + + public long Target_id { get; set; } + + public int Level { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/SendLiveDanmukuRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/SendLiveDanmukuRequest.cs new file mode 100644 index 0000000..de46ee6 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/SendLiveDanmukuRequest.cs @@ -0,0 +1,34 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class SendLiveDanmukuRequest +{ + public SendLiveDanmukuRequest(string csrf, long room_id, string message) + { + this.Csrf = csrf; + this.Msg = message; + this.Roomid = room_id; + this.Bubble = "0"; + this.Mode = "1"; + this.Fontsize = "25"; + this.Rnd = "1672305761"; + this.Color = "16777215"; + } + + public string Bubble { get; set; } + + public string Msg { get; set; } + + public string Color { get; set; } + + public string Mode { get; set; } + + public string Fontsize { get; set; } + + public string Rnd { get; set; } + + public long Roomid { get; set; } + + public string Csrf { get; set; } + + public string Csrf_token => Csrf; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/Silver2CoinRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/Silver2CoinRequest.cs new file mode 100644 index 0000000..75830df --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/Silver2CoinRequest.cs @@ -0,0 +1,38 @@ +using Ray.BiliBiliTool.Infrastructure.Helpers; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class Silver2CoinRequest +{ + public Silver2CoinRequest(string csrf) + { + Csrf = csrf; + } + + public string Csrf { get; set; } + + public string Csrf_token => Csrf; + + /// + /// + /// + /// 8u0w3cesz1o0 + /// 33moy4vugle0 + /// 9zys612vo0c0 + /// 3uu2mkxt21c0 + /// 8orqn5vf4i00 + public string Visit_id { get; set; } = _visitId; //todo + + public static string GetRandomVisitId() + { + var ran = new Random(); + int first = ran.Next(1, 10); + int last = 0; + + var s = new RandomHelper().GenerateCode(10).ToLower(); + + return $"{first}{s}{last}"; + } + + private static string _visitId = GetRandomVisitId(); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/Silver2CoinResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/Silver2CoinResponse.cs new file mode 100644 index 0000000..2256538 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/Silver2CoinResponse.cs @@ -0,0 +1,12 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class Silver2CoinResponse +{ + public long Coin { get; set; } + + public long Gold { get; set; } + + public long Silver { get; set; } + + public string? Tid { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/WearMedalWallRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/WearMedalWallRequest.cs new file mode 100644 index 0000000..081094f --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/WearMedalWallRequest.cs @@ -0,0 +1,41 @@ +using Ray.BiliBiliTool.Infrastructure.Helpers; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class WearMedalWallRequest +{ + public WearMedalWallRequest(string csrf, int medal_id) + { + Csrf = csrf; + Medal_id = medal_id; + } + + public int Medal_id { get; set; } + + public string Csrf { get; set; } + + public string Csrf_token => Csrf; + + /// + /// + /// + /// 8u0w3cesz1o0 + /// 33moy4vugle0 + /// 9zys612vo0c0 + /// 3uu2mkxt21c0 + /// 8orqn5vf4i00 + public string Visit_id { get; set; } = _visitId; //todo + + public static string GetRandomVisitId() + { + var ran = new Random(); + int first = ran.Next(1, 10); + int last = 0; + + var s = new RandomHelper().GenerateCode(10).ToLower(); + + return $"{first}{s}{last}"; + } + + private static string _visitId = GetRandomVisitId(); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/WebHeartBeatRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/WebHeartBeatRequest.cs new file mode 100644 index 0000000..94409fc --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/WebHeartBeatRequest.cs @@ -0,0 +1,22 @@ +using System.Text; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class WebHeartBeatRequest +{ + public WebHeartBeatRequest(int room_id, int next_interval) + { + this.RoomId = room_id; + this.NextInterval = next_interval; + } + + public long RoomId { set; get; } + + public int NextInterval { set; get; } + + public override string ToString() + { + string arg = $"{this.NextInterval}|{this.RoomId}|1|0"; + return Convert.ToBase64String(Encoding.UTF8.GetBytes(arg)); + } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/WebHeartBeatResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/WebHeartBeatResponse.cs new file mode 100644 index 0000000..44ae8fc --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/WebHeartBeatResponse.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +public class WebHeartBeatResponse +{ + public int Next_interval { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/GetCombineRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/GetCombineRequest.cs new file mode 100644 index 0000000..40c7771 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/GetCombineRequest.cs @@ -0,0 +1,12 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall; + +public class GetCombineRequest : BaseAppRequest +{ + public required string buvid { get; set; } + public required string csrf { get; set; } + + public string brand { get; set; } = "Samsung"; + public string channel { get; set; } = "bili"; + public string containerName { get; set; } = "AbstractWebActivity"; + public string device { get; set; } = "phone"; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/PointInfo.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/PointInfo.cs new file mode 100644 index 0000000..bc6bce2 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/PointInfo.cs @@ -0,0 +1,3 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall; + +public record PointInfo(int point, int expire_point, int expire_time, int expire_days); diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/Sign2Request.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/Sign2Request.cs new file mode 100644 index 0000000..0aab5f3 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/Sign2Request.cs @@ -0,0 +1,18 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall; + +public class Sign2Request +{ + public string device { get; set; } = "phone"; + + /// + /// 当前时间(毫秒) + /// + /// 1748445354567 + public long t { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + /// + /// 当前时间(秒) + /// + /// 1748445354 + public long ts => t / 1000; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/Sign2RequestPath.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/Sign2RequestPath.cs new file mode 100644 index 0000000..5e670e3 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/Sign2RequestPath.cs @@ -0,0 +1,10 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall; + +public class Sign2RequestPath(string csrf) +{ + public string mobi_app { get; set; } = "android"; + + public string csrf { get; set; } = csrf; + + public string platform { get; set; } = "android"; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/Sign2Response.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/Sign2Response.cs new file mode 100644 index 0000000..2eab04e --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/Sign2Response.cs @@ -0,0 +1,23 @@ +using System.Text; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall; + +public class Sign2Response +{ + public int count { get; set; } + public int countdown { get; set; } + public int duration { get; set; } + public bool hasCoupon { get; set; } + public int score { get; set; } + public int vipScore { get; set; } + public int vipStatus { get; set; } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendLine($"获得经验:{score}"); + sb.AppendLine($"累计签到:{count}/{duration} 天"); + + return sb.ToString(); + } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/TaskInfo.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/TaskInfo.cs new file mode 100644 index 0000000..29e2548 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/TaskInfo.cs @@ -0,0 +1,67 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall; + +public class TaskInfo +{ + public int Score_month { get; set; } + + public int Score_limit { get; set; } + + public List Modules { get; set; } = []; + + [Obsolete( + "The sign result comes from combine API is not correct, use IVipBigPointApi.GetThreeDaySignAsync instead." + )] + public required SingTaskItem Sing_task_item { get; set; } +} + +public class SingTaskItem +{ + public int Count { get; set; } + + public int Base_score { get; set; } + + public List Histories { get; set; } = []; + + public Histtory? TodayHistory => Histories.FirstOrDefault(x => x.Is_today); + + public bool IsTodaySigned => TodayHistory?.Signed == true; +} + +public class ModuleItem +{ + public required string module_title { get; set; } + + public List common_task_item { get; set; } = []; +} + +public class CommonTaskItem +{ + public required string title { get; set; } + + public string? subtitle { get; set; } + + public string? explain { get; set; } + + public required string task_code { get; set; } + + public int state { get; set; } + + public int vip_limit { get; set; } + + public int complete_times { get; set; } + + public int max_times { get; set; } + + public int recall_num { get; set; } +} + +public class Histtory +{ + public DateTime Day { get; set; } + + public bool Signed { get; set; } + + public int Score { get; set; } + + public bool Is_today { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/VipBigPointCombine.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/VipBigPointCombine.cs new file mode 100644 index 0000000..d69162b --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/VipBigPointCombine.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Logging; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall; + +public class VipBigPointCombine +{ + public required PointInfo point_info { get; set; } + public required TaskInfo Task_info { get; set; } + + public void LogFullInfo(ILogger logger) + { + logger.LogInformation("当前经验:{point}", point_info.point); + // logger.LogInformation("打卡:{signed}", Task_info.Sing_task_item.IsTodaySigned ? "√" : "X"); + foreach (var moduleItem in Task_info.Modules) + { + logger.LogInformation("-{title}", moduleItem.module_title); + foreach (var commonTaskItem in moduleItem.common_task_item) + { + logger.LogInformation( + "---{title}:{status}", + commonTaskItem.title, + commonTaskItem.state == 3 ? "√" : "X" + ); + } + } + } + + public void LogPointInfo(ILogger logger) + { + logger.LogInformation("当前经验:{point}", point_info.point); + } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/MangaVipRewardResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/MangaVipRewardResponse.cs new file mode 100644 index 0000000..1213d02 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/MangaVipRewardResponse.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class MangaVipRewardResponse +{ + public int Amount { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Passport/GetSsoListResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Passport/GetSsoListResponse.cs new file mode 100644 index 0000000..8616aec --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Passport/GetSsoListResponse.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Passport; + +public class GetSsoListResponse +{ + public List sso { get; set; } = []; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Passport/QrCodeDto.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Passport/QrCodeDto.cs new file mode 100644 index 0000000..2deb088 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Passport/QrCodeDto.cs @@ -0,0 +1,8 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Passport; + +public class QrCodeDto +{ + public required string Qrcode_key { get; set; } + + public required string Url { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Passport/TokenDto.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Passport/TokenDto.cs new file mode 100644 index 0000000..3f91078 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Passport/TokenDto.cs @@ -0,0 +1,10 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Passport; + +public class TokenDto +{ + public required string Url { get; set; } + public required string Refresh_token { get; set; } + public long Timestamp { get; set; } + public int Code { get; set; } + public string? Message { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/RankingInfo.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/RankingInfo.cs new file mode 100644 index 0000000..c43f402 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/RankingInfo.cs @@ -0,0 +1,29 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class Ranking +{ + public List List { get; set; } = []; +} + +/// +/// 排行榜信息 +/// +public class RankingInfo +{ + public long Aid { get; set; } + + public required string Bvid { get; set; } + + public long Cid { get; set; } + + public required string Title { get; set; } + + /// + /// 是否转载 + /// 1:原创 + /// 2:转载 + /// + public int Copyright { get; set; } + + public int Duration { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/CopyUserToGroupRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/CopyUserToGroupRequest.cs new file mode 100644 index 0000000..77b63c4 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/CopyUserToGroupRequest.cs @@ -0,0 +1,19 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation; + +public class CopyUserToGroupRequest +{ + public CopyUserToGroupRequest(List fids, string tagid, string csrf) + { + Fids = string.Join(",", fids); + Tagids = tagid; + Csrf = csrf; + } + + public string Fids { get; set; } + + public string Tagids { get; set; } + + public string Csrf { get; set; } + + public string Jsonp { get; set; } = "jsonp"; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/CreateTagRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/CreateTagRequest.cs new file mode 100644 index 0000000..fba0273 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/CreateTagRequest.cs @@ -0,0 +1,8 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation; + +public class CreateTagRequest +{ + public required string Tag { get; set; } + + public required string Csrf { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/CreateTagResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/CreateTagResponse.cs new file mode 100644 index 0000000..f9345a2 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/CreateTagResponse.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation; + +public class CreateTagResponse +{ + public long Tagid { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/GetFollowingsRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/GetFollowingsRequest.cs new file mode 100644 index 0000000..180d9e8 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/GetFollowingsRequest.cs @@ -0,0 +1,29 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation; + +public class GetFollowingsRequest +{ + public GetFollowingsRequest( + long userId, + FollowingsOrderType followingsOrder = FollowingsOrderType.AttentionDesc + ) + { + Vmid = userId; + Order_type = followingsOrder.DefaultValue(); + } + + public long Vmid { get; set; } + + public string Order_type { get; set; } + + public int Pn { get; set; } = 1; + + public int Ps { get; set; } = 20; + + public string Order { get; set; } = "desc"; + + public string Jsonp { get; set; } = "jsonp"; + + //public string Callback { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/GetFollowingsResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/GetFollowingsResponse.cs new file mode 100644 index 0000000..2eb1857 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/GetFollowingsResponse.cs @@ -0,0 +1,8 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation; + +public class GetFollowingsResponse +{ + public List List { get; set; } = []; + + public int Total { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/ModifyRelationRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/ModifyRelationRequest.cs new file mode 100644 index 0000000..402f174 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/ModifyRelationRequest.cs @@ -0,0 +1,24 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation; + +public class ModifyRelationRequest +{ + public ModifyRelationRequest(long fid, string csrf) + { + Fid = fid; + Csrf = csrf; + } + + public long Fid { get; set; } + + public string Csrf { get; set; } + + /// + /// 动作 + /// + /// 2:取关 + public int Act { get; set; } = 2; + + public int Re_src { get; set; } = 11; + + public string Jsonp { get; set; } = "jsonp"; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/TagDto.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/TagDto.cs new file mode 100644 index 0000000..cfd1be3 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/TagDto.cs @@ -0,0 +1,15 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation; + +public class TagDto +{ + public long Tagid { get; set; } + + public required string Name { get; set; } + + /// + /// 关注up个数 + /// + public int Count { get; set; } + + public string? Tip { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/SearchUpVideosResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/SearchUpVideosResponse.cs new file mode 100644 index 0000000..f14adeb --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/SearchUpVideosResponse.cs @@ -0,0 +1,55 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class SearchUpVideosResponse +{ + public UpContent? List { get; set; } + + public required BiliPageResult Page { get; set; } +} + +public class UpContent +{ + public List Vlist { get; set; } = []; +} + +public class UpVideoInfo +{ + public long Aid { get; set; } + + public string? Author { get; set; } + + public required string Bvid { get; set; } + + public required string Title { get; set; } + + /// + /// 视频时长 + /// 61:05 + /// 00:15 + /// + public required string Length { get; set; } + + /// + /// 视频时长的秒数 + /// + public int? Duration + { + get + { + int? result = null; + + try + { + var list = Length.Split(':'); + var min = int.Parse(list[0]); + var sec = int.Parse(list[1]); + return min * 60 + sec; + } + catch (Exception) + { + //throw; + } + return result; + } + } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ShareVideoRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ShareVideoRequest.cs new file mode 100644 index 0000000..ca5c6d8 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ShareVideoRequest.cs @@ -0,0 +1,22 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class ShareVideoRequest +{ + public ShareVideoRequest(long aid, string csrf) + { + Aid = aid; + Csrf = csrf; + } + + public long Aid { get; set; } + + public string Csrf { get; set; } + + public string Eab_x { get; set; } = "1"; + + public string Ramval { get; set; } = $"{new Random().Next(3, 20)}"; + + public string Source { get; set; } = "web_normal"; + + public string Ga { get; set; } = "1"; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/UpInfo.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/UpInfo.cs new file mode 100644 index 0000000..49e9ab5 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/UpInfo.cs @@ -0,0 +1,8 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class UpInfo +{ + public long Mid { get; set; } + + public required string Uname { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/UploadVideoHeartbeatRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/UploadVideoHeartbeatRequest.cs new file mode 100644 index 0000000..a045680 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/UploadVideoHeartbeatRequest.cs @@ -0,0 +1,68 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class UploadVideoHeartbeatRequest +{ + public long Aid { get; set; } + + /// + /// 视频CID,用于识别分P + /// + public long? Cid { get; set; } + + public required string Bvid { get; set; } + + public long? Epid { get; set; } + + public long? Sid { get; set; } + + /// + /// 当前用户UID + /// + public long Mid { get; set; } + + public required string Csrf { get; set; } + + /// + /// 视频播放进度(即视频进度条的当前秒数),单位为秒,默认为0 + /// + public int Played_time { get; set; } + + public int Real_played_time { get; set; } + + /// + /// 总计播放时间,单位为秒 + /// + public int Realtime { get; set; } + + /// + /// 开始播放时刻,时间戳 + /// + public long Start_ts { get; set; } = DateTime.Now.ToTimeStamp(); + + /// + /// 视频类型 + /// 3:投稿视频 + /// 4:剧集 + /// 10:课程 + /// + public int Type { get; set; } = 3; + + /// + /// 剧集副类型 + /// + public int? Sub_type { get; set; } + + /// + /// 2 + /// + public int Dt { get; set; } = 2; + + /// + /// 播放动作 + /// 0:播放中 + /// 1:开始播放 + /// 2:暂停 + /// 3:继续播放 + /// + public int Play_type { get; set; } = 3; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/UserInfo.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/UserInfo.cs new file mode 100644 index 0000000..f1b103b --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/UserInfo.cs @@ -0,0 +1,172 @@ +using System.Text; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +/// +/// 账户信息 +/// +public class UserInfo +{ + /// + /// 用户Id + /// + public long Mid { get; set; } + + /// + /// 是否登录 + /// + public bool IsLogin { get; set; } + + /// + /// 等级信息 + /// + public LevelInfo? Level_info { get; set; } + + public decimal? Money { get; set; } + + public string? Uname { get; set; } + + public Wallet? Wallet { get; set; } + + /// + /// 会员状态 + /// 只有VipStatus为1的时候获取到VipType才是有效的 + /// + public VipStatus VipStatus { get; set; } + + public VipType VipType { get; set; } + + /// + /// 获取隐私处理后的用户名 + /// + /// + public string GetFuzzyUname() + { + if (Uname == null) + { + return ""; + } + + var sb = new StringBuilder(); + int s1 = Uname.Length / 2; + int s2 = (s1 + 1) / 2; + for (int i = 0; i < Uname.Length; i++) + { + if (i >= s2 && i < s1 + s2) + sb.Append("x"); + else + sb.Append(Uname[i]); + } + + return sb.ToString(); + } + + /// + /// 返回会员类型 + /// + /// + /// 0:无会员(会员过期、当前不是会员) + /// 1:月会员 + /// 2:年会员 + /// + public VipType GetVipType() + { + if (VipStatus == VipStatus.Enable) + { + //只有VipStatus为1的时候获取到VipType才是有效的。 + return VipType; + } + else + { + return VipType.None; + } + } + + /// + /// 防爬加密用的 + /// + public required WbiImg Wbi_img { get; set; } +} + +/// +/// 会员等级 +/// +public class LevelInfo +{ + /// + /// 当前等级 + /// + public int Current_level { get; set; } + + //public long Current_min { get; set; } + + /// + /// 当前经验值 + /// + public long Current_exp { get; set; } + + private long _next_exp; + + /// + /// 下一升级经验值(因为Lv6的带佬会返回字符串“--”,所以这里只能用string接收) + /// + public object Next_exp + { + get { return _next_exp; } + set + { + bool isLong = long.TryParse(value.ToString(), out long exp); + if (isLong) + { + _next_exp = exp; + } + else + _next_exp = long.MinValue; + } + } + + /// + /// 获取下一升级经验值 + /// + /// + public long GetNext_expLong() + { + if (Current_level == 6) + return long.MaxValue; + else + return _next_exp; + } +} + +/// +/// 钱包 +/// +public class Wallet +{ + //public long Mid { get; set; } + + //public int Bcoin_balance { get; set; } + + public decimal Coupon_balance { get; set; } + + //public int Coupon_due_time { get; set; } +} + +public class WbiImg +{ + /// + /// img url + /// + /// https://i0.hdslb.com/bfs/wbi/9cd4224d4fe74c7e9d6963e2ef891688.png + public required string img_url { get; set; } + + /// + /// sub url + /// + /// https://i0.hdslb.com/bfs/wbi/263655ae2cad4cce95c9c401981b044a.png + public required string sub_url { get; set; } + + public string ImgKey => img_url.Split("wbi/").ToList().Last().Replace(".png", ""); + + public string SubKey => sub_url.Split("wbi/").ToList().Last().Replace(".png", ""); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Video/GetBangumiBySsidResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Video/GetBangumiBySsidResponse.cs new file mode 100644 index 0000000..8f53b29 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Video/GetBangumiBySsidResponse.cs @@ -0,0 +1,36 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Video; + +public class GetBangumiBySsidResponse +{ + public int Code { get; set; } = int.MinValue; + + public string? Message { get; set; } + + public required Result Result { get; set; } +} + +public class Result +{ + public List episodes { get; set; } = []; +} + +public class Episode +{ + public int aid { get; set; } + + public required string bvid { get; set; } + + public int cid { get; set; } + + public int duration { get; set; } + + public int ep_id { get; set; } + + public int id { get; set; } + + public required string long_title { get; set; } + + public required string share_copy { get; set; } + + public int status { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Video/SearchVideosByUpIdFullDto.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Video/SearchVideosByUpIdFullDto.cs new file mode 100644 index 0000000..df5ee30 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Video/SearchVideosByUpIdFullDto.cs @@ -0,0 +1,37 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Services; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Video; + +public class SearchVideosByUpIdDto : IWrid +{ + /// + /// upId + /// + public long mid { get; set; } + + /// + /// pageSize + /// + public int ps { get; set; } = 30; + + /// + /// pageNumber + /// + public int pn { get; set; } = 1; + + public int tid { get; set; } = 0; + + public string keyword { get; set; } = ""; + + public string order { get; set; } = "pubdate"; + + public string platform { get; set; } = "web"; + + public int web_location { get; set; } = 1550101; + + public string order_avoided { get; set; } = "true"; + + public string? w_rid { get; set; } + + public long wts { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VideoDetail.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VideoDetail.cs new file mode 100644 index 0000000..89c5d49 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VideoDetail.cs @@ -0,0 +1,65 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class VideoDetail +{ + public required string Bvid { get; set; } + + public long Aid { get; set; } + + /// + /// 稿件分P总数 + /// + public int Videos { get; set; } + + /// + /// 子分区名称 + /// + public string? Tname { get; set; } + + /// + /// 是否转载 + /// 1:原创 + /// 2:转载 + /// + public int Copyright { get; set; } + + /// + /// 稿件封面图片url + /// + public string? Pic { get; set; } + + /// + /// 稿件标题 + /// + public required string Title { get; set; } + + /// + /// 稿件发布时间(时间戳) + /// + public long Pubdate { get; set; } + + /// + /// 用户提交稿件的时间(时间戳) + /// + public long Ctime { get; set; } + + /// + /// 视频简介 + /// + public string? Desc { get; set; } + + /// + /// 视频状态 + /// + public int State { get; set; } + + /// + /// 稿件总时长(所有分P)(单位为秒) + /// + public long Duration { get; set; } +} + +/* + * 样例 + * {"code":0,"message":"0","ttl":1,"data":{"bvid":"BV1Bv411s7xN","aid":246364184,"videos":1,"tid":183,"tname":"影视剪辑","copyright":1,"pic":"http://i0.hdslb.com/bfs/archive/0355ba75255d4cfa67762ccd388fc6f90f010a3a.jpg","title":"美战?你说的是越战吧?","pubdate":1611656338,"ctime":1611656338,"desc":"小谢尔顿第一次邀请新朋友丹来家里吃饭。","state":0,"duration":164,"rights":{"bp":0,"elec":0,"download":1,"movie":0,"pay":0,"hd5":0,"no_reprint":1,"autoplay":1,"ugc_pay":0,"is_cooperation":0,"ugc_pay_preview":0,"no_background":0,"clean_mode":0,"is_stein_gate":0},"owner":{"mid":220893216,"name":"在7楼","face":"http://i0.hdslb.com/bfs/face/24cbaa418600c7872379d3f4cdb9e06cbb27c985.jpg"},"stat":{"aid":246364184,"view":52325,"danmaku":88,"reply":315,"favorite":200,"coin":1157,"share":412,"now_rank":0,"his_rank":0,"like":2251,"dislike":0,"evaluation":"","argue_msg":""},"dynamic":"","cid":287928175,"dimension":{"width":1280,"height":720,"rotate":0},"no_cache":false,"pages":[{"cid":287928175,"page":1,"from":"vupload","part":"welcome!你们一家为什么来德州呢?","duration":164,"vid":"","weblink":"","dimension":{"width":1280,"height":720,"rotate":0}}],"subtitle":{"allow_submit":false,"list":[]},"user_garb":{"url_image_ani_cut":""}}} + */ diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VideoInfo.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VideoInfo.cs new file mode 100644 index 0000000..f101949 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VideoInfo.cs @@ -0,0 +1,10 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public class VideoInfo +{ + public long Id { get; set; } + + public required string Bv_id { get; set; } + + public required string Title { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ViewMall/ViewvipMallRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ViewMall/ViewvipMallRequest.cs new file mode 100644 index 0000000..00a9b29 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ViewMall/ViewvipMallRequest.cs @@ -0,0 +1,7 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.ViewMall; + +public class ViewVipMallRequest +{ + public required string Csrf { get; set; } + public string EventId { get; set; } = "hevent_oy4b7h3epeb"; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipPrivilegeType.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipPrivilegeType.cs new file mode 100644 index 0000000..44b3488 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipPrivilegeType.cs @@ -0,0 +1,7 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public enum VipPrivilegeType +{ + BCoinCoupon = 1, + MembershipBenefits = 2, +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipStatus.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipStatus.cs new file mode 100644 index 0000000..92f4d1d --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipStatus.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public enum VipStatus +{ + [Description("无/过期")] + Disable = 0, + + [Description("正常")] + Enable = 1, +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/CompleteOgvWatchRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/CompleteOgvWatchRequest.cs new file mode 100644 index 0000000..8cae0de --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/CompleteOgvWatchRequest.cs @@ -0,0 +1,23 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask; + +public class CompleteOgvWatchRequest : BaseAppRequest +{ + public CompleteOgvWatchRequest(long taskId, string token) + { + task_id = taskId; + this.token = token; + } + + public long task_id { get; set; } + + public string token { get; set; } + + public string? task_sign { get; set; } + + public long timestamp { get; set; } + + public string c_locale { get; } = "zh_CN"; + public string channel { get; } = Constants.Channel; + public string s_locale { get; } = "zh_CN"; + public string from_spmid { get; } = "united.player-video-detail.player.continue"; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ReceiveOrCompleteTaskRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ReceiveOrCompleteTaskRequest.cs new file mode 100644 index 0000000..7185131 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ReceiveOrCompleteTaskRequest.cs @@ -0,0 +1,11 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask; + +public class ReceiveOrCompleteTaskRequest +{ + public ReceiveOrCompleteTaskRequest(string taskCode) + { + TaskCode = taskCode; + } + + public string TaskCode { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/SignRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/SignRequest.cs new file mode 100644 index 0000000..d81121a --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/SignRequest.cs @@ -0,0 +1,9 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask; + +public class SignRequest +{ + public string? csrf { get; set; } + + public string statistics { get; set; } = + "{\"appId\":1,\"platform\":3,\"version\":\"6.85.0\",\"abtest\":\"\"}"; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/StartOgvWatchRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/StartOgvWatchRequest.cs new file mode 100644 index 0000000..f8d3d35 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/StartOgvWatchRequest.cs @@ -0,0 +1,17 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask; + +public class StartOgvWatchRequest : BaseAppRequest +{ + public long ep_id { get; } = 328482; + + public long season_id { get; } = 12548; + + public string Activity_code { get; } = ""; + + public string spmid { get; } = "united.player-video-detail.0.0"; + + public string c_locale { get; } = "zh_CN"; + public string channel { get; } = Constants.Channel; + public string s_locale { get; } = "zh_CN"; + public string from_spmid { get; } = "united.player-video-detail.player.continue"; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/StartOgvWatchResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/StartOgvWatchResponse.cs new file mode 100644 index 0000000..7edfbfe --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/StartOgvWatchResponse.cs @@ -0,0 +1,12 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask; + +public class StartOgvWatchResponse +{ + public string? closeType { get; set; } + + public string? showTime { get; set; } + + public long task_id { get; set; } + + public string? token { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/BigPointDto.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/BigPointDto.cs new file mode 100644 index 0000000..bdce169 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/BigPointDto.cs @@ -0,0 +1,3 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask.ThreeDaysSign; + +public record BigPointDto(int point, int expire_point, int expire_time, int expire_days); diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/ThreeDaySignDto.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/ThreeDaySignDto.cs new file mode 100644 index 0000000..9fec8ce --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/ThreeDaySignDto.cs @@ -0,0 +1,33 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask.ThreeDaysSign; + +/// +/// ThreeDaySignDto +/// +/// +/// +/// 周期内的第几天 +/// 今日是否已签到 +/// 已累计签到天数 +/// +/// +/// +/// +/// +/// +/// +/// 累计签到周期 +public record ThreeDaySignDto( + int previous_vip_status, + int vip_status, + int day, + bool signed, + int count, + bool has_coupon, + int countdown, + int score, + int vip_score, + string explain, + int exp_value, + bool received_coupon, + int duration +); diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/ThreeDaySignRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/ThreeDaySignRequest.cs new file mode 100644 index 0000000..bf364ae --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/ThreeDaySignRequest.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask.ThreeDaysSign; + +public class ThreeDaySignRequest : BaseAppRequest +{ + public required string csrf { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/ThreeDaySignResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/ThreeDaySignResponse.cs new file mode 100644 index 0000000..e3dcea9 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/ThreeDaySignResponse.cs @@ -0,0 +1,36 @@ +using System.Text; +using Microsoft.Extensions.Logging; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask.ThreeDaysSign; + +public class ThreeDaySignResponse +{ + public required BigPointDto big_point { get; set; } + + public required ThreeDaySignDto three_day_sign { get; set; } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendLine($"今日获得签到积分: {three_day_sign.score}"); + sb.AppendLine($"累计签到: {three_day_sign.count}/{three_day_sign.duration} 天"); + + if (three_day_sign.count < 3) + { + sb.AppendLine( + $"{three_day_sign.duration} 天内累计签到 3 天,可额外获取 {three_day_sign.exp_value} 经验" + ); + } + else + { + sb.AppendLine($"满 3 天,已获得额外 {three_day_sign.vip_score} 经验"); + } + + return sb.ToString(); + } + + public void LogPointInfo(ILogger logger) + { + logger.LogInformation("当前经验:{point}", big_point.point); + } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ViewRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ViewRequest.cs new file mode 100644 index 0000000..137d804 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ViewRequest.cs @@ -0,0 +1,17 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask; + +public class ViewRequest : BaseAppRequest +{ + public ViewRequest(string position) + { + this.position = position; + } + + public string position { get; } + + public string c_locale { get; } = "zh_CN"; + + public string channel { get; } = Constants.Channel; + + public string s_locale { get; } = "zh_CN"; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/VipExperienceRequest.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/VipExperienceRequest.cs new file mode 100644 index 0000000..4289757 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/VipExperienceRequest.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask; + +public class VipExperienceRequest +{ + public required string csrf { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/VouchersInfoResponse.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/VouchersInfoResponse.cs new file mode 100644 index 0000000..4f3b7f4 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/VouchersInfoResponse.cs @@ -0,0 +1,24 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask; + +public class VouchersInfoResponse +{ + public List List { get; set; } = []; + public bool IsShortVip { get; set; } + public bool IsFreightOpen { get; set; } + public int Level { get; set; } + public int CurExp { get; set; } + public int NextExp { get; set; } + public bool IsVip { get; set; } + public int IsSeniorMember { get; set; } + public int Format060102 { get; set; } +} + +public class List +{ + public int Type { get; set; } + public int State { get; set; } + public int ExpireTime { get; set; } + public int VipType { get; set; } + public int NextReceiveDays { get; set; } + public int PeriodEndUnix { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipType.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipType.cs new file mode 100644 index 0000000..2018551 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipType.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +public enum VipType +{ + [Description("无")] + None = 0, + + [Description("月度大会员")] + Mensual = 1, + + [Description("年度大会员")] + Annual = 2, +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IAccountApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IAccountApi.cs new file mode 100644 index 0000000..637bd61 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IAccountApi.cs @@ -0,0 +1,16 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +[Header("Host", "account.bilibili.com")] +public interface IAccountApi : IBiliBiliApi +{ + /// + /// 获取硬币余额 + /// + /// + [Header("Referer", "https://account.bilibili.com/account/coin")] + [HttpGet("/site/getCoin")] + Task> GetCoinBalanceAsync([Header("Cookie")] string ck); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IArticleApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IArticleApi.cs new file mode 100644 index 0000000..adc0eb3 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IArticleApi.cs @@ -0,0 +1,56 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Article; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +[Header("Host", "api.bilibili.com")] +public interface IArticleApi : IBiliBiliApi +{ + [Header("Referer", "https://www.bilibili.com/")] + [Header("Origin", "https://space.bilibili.com")] + [HttpGet("/x/space/wbi/article")] + Task> SearchUpArticlesByUpIdAsync( + [PathQuery] SearchArticlesByUpIdDto request + ); + + /// + /// 获取专栏详情 + /// + /// + /// + [HttpGet("/x/article/viewinfo?id={cvid}")] + Task> SearchArticleInfoAsync(long cvid); + + /// + /// 为专栏文章投币 + /// + /// + /// + /// + [Header("Content-Type", "application/x-www-form-urlencoded")] + [Header("Origin", "https://www.bilibili.com")] + [HttpPost("/x/web-interface/coin/add")] + Task AddCoinForArticleAsync( + [FormContent] AddCoinForArticleRequest request, + [Header("Cookie")] string ck, + [Header("referer")] + string refer = + "https://www.bilibili.com/read/cv5806746/?from=search&spm_id_from=333.337.0.0" + ); + + /// + /// 为专栏文章点赞 + /// + /// + /// + /// + [Header("Content-Type", "application/x-www-form-urlencoded")] + [Header( + "Referer", + "https://www.bilibili.com/read/cv{cvid}/?from=search&spm_id_from=333.337.0.0" + )] + [Header("Origin", "https://www.bilibili.com")] + [HttpPost("/x/article/like?id={cvid}&type=1&csrf={csrf}")] + Task LikeAsync(long cvid, string csrf, [Header("Cookie")] string ck); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IBiliBiliApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IBiliBiliApi.cs new file mode 100644 index 0000000..a3b53ce --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IBiliBiliApi.cs @@ -0,0 +1,17 @@ +using Ray.BiliBiliTool.Agent.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +[AppendHeader("Accept", "application/json, text/plain, */*", AppendHeaderType.AddIfNotExist)] +//[Header("Accept-Encoding", "gzip, deflate, br")] +[AppendHeader( + "Accept-Language", + "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + AppendHeaderType.AddIfNotExist +)] +[AppendHeader("Sec-Fetch-Dest", "empty", AppendHeaderType.AddIfNotExist)] +[AppendHeader("Sec-Fetch-Mode", "cors", AppendHeaderType.AddIfNotExist)] +[AppendHeader("Sec-Fetch-Site", "same-site", AppendHeaderType.AddIfNotExist)] +[AppendHeader("Connection", "keep-alive", AppendHeaderType.AddIfNotExist)] +[LogFilter] +public interface IBiliBiliApi; diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IChargeApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IChargeApi.cs new file mode 100644 index 0000000..01a43be --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IChargeApi.cs @@ -0,0 +1,65 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +/// +/// 充电相关接口 +/// +[Header("Host", "api.bilibili.com")] +public interface IChargeApi : IBiliBiliApi +{ + /// + /// 充电 + /// + /// 充电电池数量(B币*10),必须在20-99990之间 + /// 充电对象用户UID + /// 充电来源代码(空间充电:充电对象用户UID;视频充电:稿件avID) + /// + /// + [HttpPost( + "/x/ugcpay/trade/elec/pay/quick?elec_num={elec_num}&up_mid={up_mid}&otype=up&oid={oid}&csrf={csrf}" + )] + [Obsolete] + Task> Charge( + int elec_num, + string up_mid, + string oid, + string csrf, + [Header("Cookie")] string ck + ); + + /// + /// 充电V2 + /// + /// B币个数 + /// 对方Id + /// 对方来源代码(空间充电:充电对象用户UID;视频充电:稿件avID) + /// 自己的bili_jct + /// + [Header("Content-Type", "application/x-www-form-urlencoded")] + [Header("Referer", "https://www.bilibili.com/")] + [Header("Origin", "https://www.bilibili.com")] + [HttpPost("/x/ugcpay/web/v2/trade/elec/pay/quick")] + Task> ChargeV2Async( + [FormContent] ChargeRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 充电后留言 + /// + /// + /// + /// + /// + /// + [Header("Content-Type", "application/x-www-form-urlencoded")] + [Header("Referer", "https://www.bilibili.com/")] + [Header("Origin", "https://www.bilibili.com")] + [HttpPost("/x/ugcpay/trade/elec/message")] + Task> ChargeCommentAsync( + [FormContent] ChargeCommentRequest request, + [Header("Cookie")] string ck + ); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IDailyTaskApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IDailyTaskApi.cs new file mode 100644 index 0000000..1c4e4de --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IDailyTaskApi.cs @@ -0,0 +1,42 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +/// +/// BiliBili每日任务相关接口 +/// +[Header("Host", "api.bilibili.com")] +public interface IDailyTaskApi : IBiliBiliApi +{ + /// + /// 获取每日任务的完成情况 + /// + /// + [Header("Referer", "https://account.bilibili.com/account/home")] + [Header("Origin", "https://account.bilibili.com")] + [HttpGet("/x/member/web/exp/reward")] + Task> GetDailyTaskRewardInfoAsync([Header("Cookie")] string ck); + + /// + /// 获取通过投币已获取的经验值 + /// + /// + [Header("Referer", "https://www.bilibili.com/")] + [Header("Origin", "https://www.bilibili.com")] + [HttpGet("/x/web-interface/coin/today/exp")] + Task> GetDonateCoinExpAsync([Header("Cookie")] string ck); + + /// + /// 获取VIP特权 + /// + /// + /// + /// + [HttpPost("/x/vip/privilege/receive?type={type}&csrf={csrf}")] + Task ReceiveVipPrivilegeAsync( + int type, + string csrf, + [Header("Cookie")] string ck + ); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IHomeApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IHomeApi.cs new file mode 100644 index 0000000..fb79cef --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IHomeApi.cs @@ -0,0 +1,12 @@ +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +/// +/// 主站首页接口API +/// +public interface IHomeApi : IBiliBiliApi +{ + [HttpGet("")] + Task GetHomePageAsync([Header("Cookie")] string ck); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/ILiveApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/ILiveApi.cs new file mode 100644 index 0000000..751e8fe --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/ILiveApi.cs @@ -0,0 +1,172 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +/// +/// 直播相关接口 +/// +[Header("Host", "api.live.bilibili.com")] +public interface ILiveApi : IBiliBiliApi +{ + /// + /// 直播签到 + /// + /// + [Header("Referer", "https://link.bilibili.com/")] + [Header("Origin", "https://link.bilibili.com")] + [HttpGet("/xlive/web-ucenter/v1/sign/DoSign")] + Task> Sign([Header("Cookie")] string ck); + + /// + /// 银瓜子兑换硬币 + /// + /// + [Header("Referer", "https://link.bilibili.com/")] + [Header("Origin", "https://link.bilibili.com")] + [Header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")] + [HttpGet("/pay/v1/Exchange/silver2coin")] + [Obsolete] + Task ExchangeSilver2Coin([Header("Cookie")] string ck); + + /// + /// 获取银瓜子余额 + /// + /// + [Header("Referer", "https://link.bilibili.com/")] + [Header("Origin", "https://link.bilibili.com")] + [HttpGet("/pay/v1/Exchange/getStatus")] + [Obsolete] + Task> GetExchangeSilverStatus( + [Header("Cookie")] string ck + ); + + /// + /// 银瓜子兑换硬币 + /// + /// + /// + //[Header("Referer", "https://link.bilibili.com/p/center/index?visit_id=1ddo4yl01q00")] + [Header("Content-Type", "application/x-www-form-urlencoded")] + [Header("Origin", "https://link.bilibili.com")] + [HttpPost("/xlive/revenue/v1/wallet/silver2coin")] + Task> Silver2Coin( + [FormContent] Silver2CoinRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 获取直播中心钱包状态 + /// + /// + //[Header("Referer", "https://link.bilibili.com/p/center/index?visit_id=1ddo4yl01q00")] + [Header("Origin", "https://link.bilibili.com")] + [HttpGet("/xlive/revenue/v1/wallet/getStatus")] + Task> GetLiveWalletStatus( + [Header("Cookie")] string ck + ); + + [HttpGet("/xlive/web-interface/v1/index/getWebAreaList?source_id=2")] + Task> GetAreaList([Header("Cookie")] string ck); + + /// + /// 获取直播列表 + /// + /// + /// + /// + [Header("Referer", "https://live.bilibili.com/")] + [Header("Origin", "https://live.bilibili.com")] + [HttpGet("/xlive/web-interface/v1/second/getList")] + Task> GetList( + [PathQuery] GetListRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 检查天选时刻抽奖 + /// + /// + /// + [Header("Referer", "https://live.bilibili.com/")] + [Header("Origin", "https://live.bilibili.com")] + [HttpGet("/xlive/lottery-interface/v1/Anchor/Check?roomid={roomId}")] + Task> CheckTianXuan( + long roomId, + [Header("Cookie")] string ck + ); + + /// + /// 参加天选时刻抽奖 + /// + /// + /// + [HttpPost("/xlive/lottery-interface/v1/Anchor/Join")] + Task> Join( + [FormContent] JoinTianXuanRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 获取用户的粉丝勋章 + /// + /// uid + /// + [Header("Referer", "https://live.bilibili.com/")] + [Header("Origin", "https://live.bilibili.com")] + [HttpGet("/xlive/web-ucenter/user/MedalWall?target_id={userId}")] + Task> GetMedalWall( + string userId, + [Header("Cookie")] string ck + ); + + /// + /// 佩戴粉丝勋章 + /// + /// uid + /// + [Header("Referer", "https://live.bilibili.com/")] + [Header("Origin", "https://live.bilibili.com")] + [HttpPost("/xlive/app-ucenter/v1/fansMedal/wear")] + Task WearMedalWall( + [FormContent] WearMedalWallRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 发送弹幕 + /// + /// request + /// + [HttpPost("/msg/send")] + Task SendLiveDanmuku( + [FormContent] SendLiveDanmukuRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 获取直播间信息 + /// + /// roomId + /// + [HttpGet("/room/v1/Room/get_info?room_id={roomId}&from=room")] + Task> GetLiveRoomInfo(long roomId); + + /// + /// 请求直播主页用于配置直播相关 Cookie + /// + [HttpGet("/news/v1/notice/recom?product=live")] + Task GetLiveHome([Header("Cookie")] string ck); + + /// + /// 点赞直播间 + /// + [HttpPost("/xlive/app-ucenter/v1/like_info_v3/like/likeReportV3")] + [Header("Referer", "https://live.bilibili.com/")] + [Header("Origin", "https://live.bilibili.com")] + Task LikeLiveRoom( + [RawFormContent] string request, + [Header("Cookie")] string ck + ); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/ILiveTraceApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/ILiveTraceApi.cs new file mode 100644 index 0000000..2de9b8b --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/ILiveTraceApi.cs @@ -0,0 +1,27 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +[Header("Host", "live-trace.bilibili.com")] +public interface ILiveTraceApi : IBiliBiliApi +{ + [HttpGet("/xlive/rdata-interface/v1/heartbeat/webHeartBeat?hb={request}&pf=web")] + Task> WebHeartBeat( + WebHeartBeatRequest request, + [Header("Cookie")] string ck + ); + + [HttpPost("/xlive/data-interface/v1/x25Kn/E")] + Task> EnterRoom( + [FormContent] EnterRoomRequest request, + [Header("Cookie")] string ck + ); + + [HttpPost("/xlive/data-interface/v1/x25Kn/X")] + Task> HeartBeat( + [FormContent] HeartBeatRequest request, + [Header("Cookie")] string ck + ); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IMallApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IMallApi.cs new file mode 100644 index 0000000..e68808f --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IMallApi.cs @@ -0,0 +1,41 @@ +using Ray.BiliBiliTool.Agent.Attributes; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +/// +/// 大会员大积分 +/// +[LogFilter] +[Header("Host", "api.bilibili.com")] +public interface IMallApi +{ + /// + /// 签到任务 + /// + /// + /// + /// + /// + [Header("Referer", "https://big.bilibili.com/mobile/index")] + [HttpPost("/pgc/activity/score/task/sign2")] + Task> Sign2Async( + [PathQuery] Sign2RequestPath requestPath, + [JsonContent] Sign2Request request, + [Header("Cookie")] string ck + ); + + /// + /// 获取任务 combine 信息 + /// + /// 里面的登录信息是错误的,阿B特色 + /// + [Header("Referer", "https://big.bilibili.com/mobile/bigPoint/task")] + [HttpGet("/x/vip_point/task/combine")] + Task> GetCombineAsync( + [PathQuery] GetCombineRequest request, + [Header("Cookie")] string ck + ); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IMangaApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IMangaApi.cs new file mode 100644 index 0000000..8031efd --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IMangaApi.cs @@ -0,0 +1,48 @@ +using Ray.BiliBiliTool.Agent.Attributes; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +/// +/// 漫画相关接口 +/// +[Header("Origin", "https://manga.bilibili.com")] +[Header("Host", "manga.bilibili.com")] +public interface IMangaApi : IBiliBiliApi +{ + /// + /// 漫画签到 + /// + /// + /// + [LogFilter(false)] + [HttpPost("/twirp/activity.v1.Activity/ClockIn?platform={platform}")] + Task ClockIn(string platform, [Header("Cookie")] string ck); + + /// + /// 漫画阅读 + /// + /// + /// + [HttpPost( + "/twirp/bookshelf.v1.Bookshelf/AddHistory?platform={platform}&comic_id={comic_id}&ep_id={ep_id}" + )] + Task ReadManga( + string platform, + long comic_id, + long ep_id, + [Header("Cookie")] string ck + ); + + /// + /// 获取会员漫画奖励 + /// + /// + /// + [HttpPost("/twirp/user.v1.User/GetVipReward?reason_id={reason_id}")] + Task> ReceiveMangaVipReward( + int reason_id, + [Header("Cookie")] string ck + ); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IPassportApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IPassportApi.cs new file mode 100644 index 0000000..965c2eb --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IPassportApi.cs @@ -0,0 +1,19 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Passport; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +[Header("Host", "passport.bilibili.com")] +public interface IPassportApi : IBiliBiliApi +{ + [HttpGet("/x/passport-login/web/qrcode/generate")] + Task> GenerateQrCode(); + + [HttpGet("/x/passport-login/web/qrcode/poll?qrcode_key={qrcode_key}&source=main_mini")] + //Task> CheckQrCodeHasScaned(string qrcode_key); + Task CheckQrCodeHasScaned(string qrcode_key); + + [HttpGet("/x/passport-login/web/sso/list?biliCSRF={csrf}")] + Task> GetSsoListAsync(string csrf); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IRelationApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IRelationApi.cs new file mode 100644 index 0000000..fe140b1 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IRelationApi.cs @@ -0,0 +1,123 @@ +using System.ComponentModel; +using Ray.BiliBiliTool.Agent.Attributes; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +/// +/// 关注相关接口 +/// +[AppendHeader("Host", "api.bilibili.com", AppendHeaderType.AddIfNotExist)] +[AppendHeader("Referer", "https://space.bilibili.com/", AppendHeaderType.AddIfNotExist)] +public interface IRelationApi : IBiliBiliApi +{ + /// + /// 获取关注列表 + /// + /// + [HttpGet("/x/relation/followings")] + Task> GetFollowings( + GetFollowingsRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 获取特别关注列表 + /// + /// + [Header("Cache-Control", "no-cache")] + [Header("Pragma", "no-cache")] + [JsonReturn(EnsureMatchAcceptContentType = false)] + [HttpGet("/x/relation/tag")] + Task>> GetFollowingsByTag( + GetSpecialFollowingsRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 获取关注分组 + /// + /// + [AppendHeader("Sec-Fetch-Mode", "no-cors")] + [AppendHeader("Sec-Fetch-Dest", "script")] + [HttpGet("/x/relation/tags?jsonp=jsonp")] + Task>> GetTags( + [Header("Cookie")] string ck, + [AppendHeader("Referer")] string referer = RelationApiConstant.GetTagsReferer + ); + + /// + /// 添加关注分组(tag) + /// + /// + /// + [AppendHeader("Origin", "https://space.bilibili.com")] + [HttpPost("/x/relation/tag/create?cross_domain=true")] + Task> CreateTag( + [FormContent] CreateTagRequest request, + [Header("Cookie")] string ck, + [AppendHeader("Referer")] string referer = RelationApiConstant.GetTagsReferer + ); + + /// + /// 批量拷贝关注up到某指定分组 + /// + /// + /// + [AppendHeader("Origin", "https://space.bilibili.com")] + [HttpPost("/x/relation/tags/copyUsers")] + Task CopyUpsToGroup( + [FormContent] CopyUserToGroupRequest request, + [Header("Cookie")] string ck, + [AppendHeader("Referer")] string referer = RelationApiConstant.CopyReferer + ); + + /// + /// 修改关系 + /// + /// + [AppendHeader("Origin", "https://space.bilibili.com")] + [HttpPost("/x/relation/modify")] + Task ModifyRelation( + [FormContent] ModifyRelationRequest request, + [Header("Cookie")] string ck, + [AppendHeader("Referer")] string referer = RelationApiConstant.ModifyReferer + ); +} + +public enum FollowingsOrderType +{ + /// + /// 最常访问频率倒序 + /// + [DefaultValue("attention")] + AttentionDesc, + + /// + /// 关注时间倒序 + /// + [DefaultValue("")] + TimeDesc, +} + +public class RelationApiConstant +{ + /// + /// GetTags接口中的Referer + /// {0}为UserId + /// + public const string GetTagsReferer = "https://space.bilibili.com/{0}/fans/follow"; + + /// + /// CopyUpsToGroup接口中的Referer + /// {0}为UserId + /// + public const string CopyReferer = "https://space.bilibili.com/{0}/fans/follow?tagid=-1"; + + /// + /// ModifyRelation接口种的Referer + /// + public const string ModifyReferer = "https://space.bilibili.com/{0}/fans/follow?tagid={1}"; +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IUpInfoApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IUpInfoApi.cs new file mode 100644 index 0000000..307ff45 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IUpInfoApi.cs @@ -0,0 +1,24 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +/// +/// 用户信息接口API +/// +[Header("Referer", "https://www.bilibili.com/")] +[Header("Origin", "https://www.bilibili.com")] +[Header("Host", "api.bilibili.com")] +public interface IUpInfoApi : IBiliBiliApi +{ + /// + /// 获取用户空间信息 + /// + /// uid + /// + [HttpGet("/x/space/wbi/acc/info")] + Task> GetSpaceInfo( + [PathQuery] GetSpaceInfoDto request, + [Header("Cookie")] string ck + ); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IUserInfoApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IUserInfoApi.cs new file mode 100644 index 0000000..ff2d921 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IUserInfoApi.cs @@ -0,0 +1,20 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +/// +/// 用户信息接口API +/// +[Header("Referer", "https://www.bilibili.com/")] +[Header("Origin", "https://www.bilibili.com")] +[Header("Host", "api.bilibili.com")] +public interface IUserInfoApi : IBiliBiliApi +{ + /// + /// 登录 + /// + /// + [HttpGet("/x/web-interface/nav")] + Task> LoginByCookie([Header("Cookie")] string ck); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IVideoApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IVideoApi.cs new file mode 100644 index 0000000..a8e2deb --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IVideoApi.cs @@ -0,0 +1,135 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Video; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +/// +/// 视频相关接口 +/// +[Header("Host", "api.bilibili.com")] +public interface IVideoApi : IBiliBiliApi +{ + /// + /// 分享视频 + /// + /// + /// ck中必须要有buvid3,否则几率性-403 + /// + [Header("Origin", "https://www.bilibili.com")] + [HttpPost("/x/web-interface/share/add")] + Task ShareVideo( + [FormContent] ShareVideoRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 上传视频观看进度 + /// 每15秒上报一次 + /// + /// + //[Header("Content-Length", "186")] + [Header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")] + [Header("Referer", "https://www.bilibili.com/")] + [Header("Origin", "https://www.bilibili.com")] + [HttpPost("/x/click-interface/web/heartbeat?aid={aid}&played_time={playedTime}")] + Task UploadVideoHeartbeat( + [FormContent] UploadVideoHeartbeatRequest request, + [Header("Cookie")] string ck + ); + + #region 投币相关 + /// + /// 为视频投币 + /// + /// + /// + /// + /// + /// + [Header("Content-Type", "application/x-www-form-urlencoded")] + //[Header("Referer", "https://www.bilibili.com/")] + [Header("Origin", "https://www.bilibili.com")] + [HttpPost("/x/web-interface/coin/add")] + Task AddCoinForVideo( + [FormContent] AddCoinRequest request, + [Header("Cookie")] string ck, + [Header("referer")] + string refer = + "https://www.bilibili.com/video/BV123456/?spm_id_from=333.1007.tianma.1-1-1.click&vd_source=80c1601a7003934e7a90709c18dfcffd" + ); + + /// + /// 获取当前用户对视频的投币信息 + /// + /// + /// + [Header("Referer", "https://www.bilibili.com/")] + [HttpGet("/x/web-interface/archive/coins")] + Task> GetDonatedCoinsForVideo( + GetAlreadyDonatedCoinsRequest request, + [Header("Cookie")] string ck + ); + #endregion + + /// + /// 搜索指定Up的视频列表 + /// + /// + /// [1,100]验证不通过接口会报异常 + /// + /// + /// + [Header("Referer", "https://www.bilibili.com/")] + [Header("Origin", "https://space.bilibili.com")] + //[HttpGet("/x/space/wbi/arc/search?mid={upId}&ps={pageSize}&tid=0&pn={pageNumber}&keyword={keyword}&order=pubdate&platform=web&web_location=1550101&order_avoided=true&w_rid=5df06b1c48e2be86a96e9d0f99bf06f4&wts=1684854929")] + [HttpGet("/x/space/wbi/arc/search")] + Task> SearchVideosByUpId( + [PathQuery] SearchVideosByUpIdDto request, + [Header("Cookie")] string ck + ); + + /// + /// 通过ssid获取番剧的具体信息 + /// + /// + /// + [HttpGet("/pgc/view/web/season?season_id={ssid}")] + Task GetBangumiBySsid(long ssid, [Header("Cookie")] string ck); +} + +/// +/// 不需要传递Cookie的接口 +/// +public interface IVideoWithoutCookieApi : IVideoApi +{ + /// + /// 获取视频详情 + /// + /// + /// + [HttpGet("/x/web-interface/view?aid={aid}")] + Task> GetVideoDetail(string aid); + + /// + /// 获取某分区下X日内排行榜 + /// + /// + /// + /// + [Header("Referer", "https://www.bilibili.com/")] + [Header("Origin", "https://www.bilibili.com")] + [HttpGet("/x/web-interface/ranking/region?rid={rid}&day={day}")] + [Obsolete] + Task>> GetRegionRankingVideos(int rid, int day); + + /// + /// 获取排行榜 + /// + /// + [Header("Referer", "https://www.bilibili.com/")] + [Header("Origin", "https://www.bilibili.com")] + [Header("dnt", "1")] + [HttpGet("/x/web-interface/ranking/v2?rid=0&type=all")] + Task> GetRegionRankingVideosV2(); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IVipBigPointApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IVipBigPointApi.cs new file mode 100644 index 0000000..1579410 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IVipBigPointApi.cs @@ -0,0 +1,141 @@ +using Ray.BiliBiliTool.Agent.Attributes; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask.ThreeDaysSign; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +/// +/// 大会员大积分 +/// +[Header("Host", "api.bilibili.com")] +[Header("Referer", "https://big.bilibili.com/mobile/bigPoint/task")] +[LogFilter] +public interface IVipBigPointApi +{ + /// + /// 获取签到信息 + /// + /// + /// + /// + [HttpGet("/x/vip/vip_center/sign_in/three_days_sign")] + Task> GetThreeDaySignAsync( + [PathQuery] ThreeDaySignRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 获取任务列表 + /// + /// 里面的登录信息是错误的,阿B特色 + /// + [Obsolete("Using IMallApi.GetCombineAsync instead.")] + [HttpGet("/x/vip_point/task/combine")] + Task> GetCombineAsync([Header("Cookie")] string ck); + + /// + /// 签到任务 + /// + /// + /// + [Obsolete("Using IMallApi.Sign2Async instead.")] + [HttpPost("/pgc/activity/score/task/sign")] + Task SignAsync( + [FormContent] SignRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 领取任务 + /// + /// + /// + [Obsolete] + [HttpPost("/pgc/activity/score/task/receive")] + Task Receive( + [JsonContent] ReceiveOrCompleteTaskRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 领取任务 + /// + /// + /// + [HttpPost("/pgc/activity/score/task/receive/v2")] + Task ReceiveV2( + [FormContent] ReceiveOrCompleteTaskRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 完成任务 + /// + /// + /// + [HttpPost("/pgc/activity/score/task/complete")] + Task CompleteAsync( + [JsonContent] ReceiveOrCompleteTaskRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 完成任务 + /// + /// + /// + [HttpPost("/pgc/activity/score/task/complete/v2")] + Task CompleteV2( + [FormContent] ReceiveOrCompleteTaskRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 完成浏览页面任务 + /// + /// + /// + /// + [HttpPost("/pgc/activity/deliver/task/complete")] + Task ViewComplete( + [FormContent] ViewRequest request, + [Header("Cookie")] string ck + ); + + [HttpGet("/x/vip/privilege/my")] + Task> GetVouchersInfoAsync([Header("Cookie")] string ck); + + /// + /// 兑换大会员经验 + /// + /// + /// + [HttpPost("/x/vip/experience/add")] + Task ObtainVipExperienceAsync( + [FormContent] VipExperienceRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 开始观看剧集任务 + /// + /// + /// + Task> StartOgvWatchAsync( + StartOgvWatchRequest request, + [Header("Cookie")] string ck + ); + + /// + /// 完成观看剧集任务 + /// + /// + /// + Task CompleteOgvWatchAsync( + CompleteOgvWatchRequest request, + [Header("Cookie")] string ck + ); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IVipMallApi.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IVipMallApi.cs new file mode 100644 index 0000000..fb67132 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IVipMallApi.cs @@ -0,0 +1,17 @@ +using Ray.BiliBiliTool.Agent.Attributes; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.ViewMall; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; + +[Header("Host", "show.bilibili.com")] +[LogFilter] +public interface IVipMallApi +{ + [HttpPost("/api/activity/fire/common/event/dispatch")] + Task ViewVipMallAsync( + [JsonContent] ViewVipMallRequest request, + [Header("Cookie")] string ck + ); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Services/IWbiService.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Services/IWbiService.cs new file mode 100644 index 0000000..a6a153d --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Services/IWbiService.cs @@ -0,0 +1,23 @@ +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Services; + +/// +/// 防爬 +/// +public interface IWbiService +{ + Task GetWridAsync(Dictionary parameters, BiliCookie ck); + + /// + /// 获取WbiKey + /// + /// + Task SetWridAsync(T ob, BiliCookie ck) + where T : IWrid; + + WridDto EncWbi( + Dictionary parameters, + string imgKey, + string subKey, + long timespan = 0 + ); +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Services/WbiService.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Services/WbiService.cs new file mode 100644 index 0000000..01a2996 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Services/WbiService.cs @@ -0,0 +1,217 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Helpers; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Services; + +/// +/// 防爬 +/// +public class WbiService(ILogger logger, IUserInfoApi userInfoApi) : IWbiService +{ + private Dictionary _cache = new(); + + public async Task GetWridAsync(Dictionary parameters, BiliCookie ck) + { + parameters.Remove(nameof(IWrid.wts)); + parameters.Remove(nameof(IWrid.w_rid)); + + WbiImg wbi = await GetWbiKeysAsync(ck); + + return EncWbi(parameters, wbi.ImgKey, wbi.SubKey); + } + + public async Task SetWridAsync(T request, BiliCookie ck) + where T : IWrid + { + //生成字典 + Dictionary parameters = ObjectHelper + .ObjectToDictionary(request) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? ""); + + //生成 + var re = await GetWridAsync(parameters, ck); + + request.w_rid = re.w_rid; + request.wts = re.wts; + } + + /// + /// 为请求参数进行 wbi 签名 + /// + /// + /// + /// + /// + /// + public WridDto EncWbi( + Dictionary parameters, + string imgKey, + string subKey, + long timespan = 0 + ) + { + var re = new WridDto(); + + var mixinKey = GetMixinKey(imgKey + subKey); + + if (timespan == 0) + { + re.wts = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; + } + else + { + re.wts = timespan; + } + + var chrFilter = new Regex("[!'()*]"); + + var dic = new Dictionary { { "wts", re.wts.ToString() } }; + + foreach (var entry in parameters) + { + var key = entry.Key; + var value = entry.Value; + + var encodedValue = chrFilter.Replace(value, ""); + + dic.Add(Uri.EscapeDataString(key), Uri.EscapeDataString(encodedValue)); + } + + var keyList = dic.Keys.ToList(); + keyList.Sort(); + + var queryList = ( + from item in keyList + let value = dic[item] + select $"{item}={value}" + ).ToList(); + + var queryString = string.Join("&", queryList); + var hashStr = queryString + mixinKey; + var hashedQueryString = MD5.HashData(Encoding.UTF8.GetBytes(hashStr)); + var wbiSign = BitConverter.ToString(hashedQueryString).Replace("-", "").ToLower(); + + re.w_rid = wbiSign; + + return re; + } + + private async Task GetWbiKeysAsync(BiliCookie ck) + { + _cache.TryGetValue(ck, out var wbiImg); + + if (wbiImg != null) + return wbiImg; + + BiliApiResponse apiResponse = await userInfoApi.LoginByCookie(ck.ToString()); + UserInfo useInfo = apiResponse.Data!; + logger.LogDebug("【img_url】{0}", useInfo.Wbi_img.img_url); + logger.LogDebug("【sub_url】{0}", useInfo.Wbi_img.sub_url); + + wbiImg = useInfo.Wbi_img; + _cache[ck] = wbiImg; + return wbiImg; + } + + /// + /// 对 imgKey 和 subKey 进行字符顺序打乱编码 + /// + /// + /// + private string GetMixinKey(string orig) + { + int[] mixinKeyEncTab = new int[] + { + 46, + 47, + 18, + 2, + 53, + 8, + 23, + 32, + 15, + 50, + 10, + 31, + 58, + 3, + 45, + 35, + 27, + 43, + 5, + 49, + 33, + 9, + 42, + 19, + 29, + 28, + 14, + 39, + 12, + 38, + 41, + 13, + 37, + 48, + 7, + 16, + 24, + 55, + 40, + 61, + 26, + 17, + 0, + 1, + 60, + 51, + 30, + 4, + 22, + 25, + 54, + 21, + 56, + 59, + 6, + 63, + 57, + 62, + 11, + 36, + 20, + 34, + 44, + 52, + }; + + var temp = new StringBuilder(); + foreach (var index in mixinKeyEncTab) + { + temp.Append(orig[index]); + } + return temp.ToString().Substring(0, 32); + } +} + +public class WridDto : IWrid +{ + public long wts { get; set; } + + public string? w_rid { get; set; } +} + +public interface IWrid +{ + public long wts { get; set; } + + public string? w_rid { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Utils/LiveHeartBeatCrypto.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Utils/LiveHeartBeatCrypto.cs new file mode 100644 index 0000000..5b89efa --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Utils/LiveHeartBeatCrypto.cs @@ -0,0 +1,54 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Utils; + +public class LiveHeartBeatCrypto +{ + public static string Sypder(string text, ICollection rules, string key) + { + string result = text; + foreach (var rule in rules) + { + switch (rule) + { + case 0: + result = Hash(result, key, "HMACMD5"); + break; + case 1: + result = Hash(result, key, "HMACSHA1"); + break; + case 2: + result = Hash(result, key, "HMACSHA256"); + break; + case 3: + result = Hash(result, key, "HMACSHA224"); + break; + case 4: + result = Hash(result, key, "HMACSHA512"); + break; + case 5: + result = Hash(result, key, "HMACSHA384"); + break; + default: + break; + } + } + return result; + } + + private static string Hash(string text, string key, string algorithmName) + { + HMAC hamc = algorithmName.ToUpperInvariant() switch + { + "HMACSHA256" => new HMACSHA256(Encoding.UTF8.GetBytes(key)), + "HMACSHA1" => new HMACSHA1(Encoding.UTF8.GetBytes(key)), + "HMACMD5" => new HMACMD5(Encoding.UTF8.GetBytes(key)), + _ => throw new ArgumentException($"Unsupported algorithm: {algorithmName}"), + }; + + using HMAC hmac = hamc; + byte[] hashBytes = hamc.ComputeHash(Encoding.UTF8.GetBytes(text)); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/WridEncryptionDelegatingHandler.cs b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/WridEncryptionDelegatingHandler.cs new file mode 100644 index 0000000..cf887e4 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliBiliAgent/WridEncryptionDelegatingHandler.cs @@ -0,0 +1,74 @@ +using System.Collections.Specialized; +using System.Web; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Services; +using Ray.BiliBiliTool.Infrastructure.Cookie; + +namespace Ray.BiliBiliTool.Agent.BiliBiliAgent; + +public class WridEncryptionDelegatingHandler(IWbiService wbiService) : DelegatingHandler +{ + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken + ) + { + if (request.Content is FormUrlEncodedContent originalFormContent) + { + var originalFormDataString = await originalFormContent.ReadAsStringAsync( + cancellationToken + ); + var formData = HttpUtility.ParseQueryString(originalFormDataString); + + await TrySetWridAync(request, formData, cancellationToken); + + var newFormKeyValuePairs = formData + .AllKeys.Select(key => new KeyValuePair(key!, formData[key] ?? "")) + .ToList(); + request.Content = new FormUrlEncodedContent(newFormKeyValuePairs); + } + + if (request.RequestUri?.Query != null) + { + var queryParameters = HttpUtility.ParseQueryString(request.RequestUri.Query); + + await TrySetWridAync(request, queryParameters, cancellationToken); + + var uriBuilder = new UriBuilder(request.RequestUri) + { + Query = queryParameters?.ToString() ?? "", + }; + request.RequestUri = uriBuilder.Uri; + } + + return await base.SendAsync(request, cancellationToken); + } + + private async Task TrySetWridAync( + HttpRequestMessage request, + NameValueCollection formData, + CancellationToken cancellationToken + ) + { + var paramsToSign = new Dictionary(); + foreach (var key in formData.AllKeys) + { + paramsToSign[key!] = formData[key] ?? ""; + } + + if (paramsToSign.All(x => x.Key != "w_rid")) + { + return; + } + + var ckStr = request + .Headers.FirstOrDefault(x => x.Key == "Cookie") + .Value.FirstOrDefault() + ?.ToString(); + var ck = CookieStrFactory.CreateNew(ckStr ?? ""); + + var wbi = await wbiService.GetWridAsync(paramsToSign, ck); + + formData["w_rid"] = wbi.w_rid; + formData["wts"] = wbi.wts.ToString(); + } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliCookie.cs b/src/Ray.BiliBiliTool.Agent/BiliCookie.cs new file mode 100644 index 0000000..ad026f9 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliCookie.cs @@ -0,0 +1,114 @@ +using System.ComponentModel; +using Ray.BiliBiliTool.Infrastructure.Cookie; +using Ray.BiliBiliTool.Infrastructure.Extensions; + +namespace Ray.BiliBiliTool.Agent; + +public class BiliCookie(Dictionary cookieDic) : CookieInfo(cookieDic) +{ + protected override string CkValueBuild(string value) + { + value = base.CkValueBuild(value); + + if (value.Contains(',')) + { + value = Uri.EscapeDataString(value); + } + + return value; + } + + #region 扩充属性 + + [Description("DedeUserID")] + public string UserId => + CookieItemDictionary.TryGetValue(GetPropertyDescription(nameof(UserId)), out string? userId) + ? userId + : ""; + + /// + /// SESSDATA + /// + [Description("SESSDATA")] + public string SessData => + CookieItemDictionary.TryGetValue(GetPropertyDescription(nameof(SessData)), out string? sess) + ? sess + : ""; + + [Description("bili_jct")] + public string BiliJct => + CookieItemDictionary.TryGetValue(GetPropertyDescription(nameof(BiliJct)), out string? jct) + ? jct + : ""; + + [Description("LIVE_BUVID")] + public string LiveBuvid => + CookieItemDictionary.TryGetValue( + GetPropertyDescription(nameof(LiveBuvid)), + out string? liveBuvid + ) + ? liveBuvid + : ""; + + [Description("buvid3")] + public string Buvid => + CookieItemDictionary.TryGetValue(GetPropertyDescription(nameof(Buvid)), out string? buvid) + ? buvid + : ""; + + #endregion + + + /// + /// 检查是否已配置 + /// + /// + public override void Check() + { + base.Check(); + + if (CookieItemDictionary.Count == 0) + throw new Exception("Cookie字符串格式异常,内部无等号"); + + bool result = true; + string msg = "Cookie字符串异常,无[{1}]项"; + + //UserId为空,抛异常 + if (string.IsNullOrWhiteSpace(UserId)) + { + throw new Exception(string.Format(msg, GetPropertyDescription(nameof(UserId)))); + } + else if (!long.TryParse(UserId, out long uid)) //不为空,但不能转换为long,警告 + { + throw new Exception( + string.Format( + "[{uidKey}]={uid} 不能转换为long型,请确认配置的是正确的Cookie值", + GetPropertyDescription(nameof(UserId)), + UserId + ) + ); + } + + //SessData为空,抛异常 + if (string.IsNullOrWhiteSpace(SessData)) + { + throw new Exception(string.Format(msg, GetPropertyDescription(nameof(SessData)))); + } + + //BiliJct为空,抛异常 + if (string.IsNullOrWhiteSpace(BiliJct)) + { + throw new Exception(string.Format(msg, GetPropertyDescription(nameof(BiliJct)))); + } + + if (!result) + throw new Exception( + $"请正确配置Cookie后再运行,配置方式见 {Config.Constants.SourceCodeUrl}" + ); + } + + private string GetPropertyDescription(string propertyName) + { + return GetType().GetPropertyDescription(propertyName); + } +} diff --git a/src/Ray.BiliBiliTool.Agent/BiliHosts.cs b/src/Ray.BiliBiliTool.Agent/BiliHosts.cs new file mode 100644 index 0000000..3b6c804 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/BiliHosts.cs @@ -0,0 +1,15 @@ +namespace Ray.BiliBiliTool.Agent; + +public static class BiliHosts +{ + public const string Api = "https://api.bilibili.com"; + public const string App = "https://app.bilibili.com"; + public const string Show = "https://show.bilibili.com"; + public const string Passport = "http://passport.bilibili.com"; + public const string LiveTrace = "https://live-trace.bilibili.com"; + public const string Www = "https://www.bilibili.com"; + public const string Manga = "https://manga.bilibili.com"; + public const string Account = "https://account.bilibili.com"; + public const string Live = "https://api.live.bilibili.com"; + public const string Mall = "https://mall.bilibili.com"; +} diff --git a/src/Ray.BiliBiliTool.Agent/Constants.cs b/src/Ray.BiliBiliTool.Agent/Constants.cs new file mode 100644 index 0000000..4f96b17 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/Constants.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Agent; + +public static class Constants +{ + public const string Channel = "bili"; +} diff --git a/src/Ray.BiliBiliTool.Agent/Extensions/ServiceCollectionExtension.cs b/src/Ray.BiliBiliTool.Agent/Extensions/ServiceCollectionExtension.cs new file mode 100644 index 0000000..cf3a5bf --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/Extensions/ServiceCollectionExtension.cs @@ -0,0 +1,191 @@ +using System.Net; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Polly; +using Polly.Extensions.Http; +using Ray.BiliBiliTool.Agent.BiliBiliAgent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Services; +using Ray.BiliBiliTool.Agent.HttpClientDelegatingHandlers; +using Ray.BiliBiliTool.Agent.QingLong; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.Infrastructure.Cookie; + +namespace Ray.BiliBiliTool.Agent.Extensions; + +public static class ServiceCollectionExtension +{ + /// + /// 注册强类型api客户端 + /// + /// + /// + public static IServiceCollection AddBiliBiliClientApi( + this IServiceCollection services, + IConfiguration configuration + ) + { + //Cookie + services.AddSingleton>(); + + //全局代理 + services.SetGlobalProxy(configuration); + + //DelegatingHandler + services.Scan(scan => + scan.FromAssemblyOf() + .AddClasses(classes => classes.AssignableTo()) + .AsSelf() + .WithTransientLifetime() + ); + + //服务 + services.AddScoped(); + + //bilibli + Action config = (sp, c) => + { + c.DefaultRequestHeaders.Add( + "User-Agent", + sp.GetRequiredService>().CurrentValue.UserAgent + ); + }; + Action configApp = (sp, c) => + { + c.DefaultRequestHeaders.Add( + "User-Agent", + sp.GetRequiredService>().CurrentValue.UserAgentApp + ); + }; + + services.AddBiliBiliClientApi(BiliHosts.Api, config, true); + + services.AddBiliBiliClientApi(BiliHosts.Api, config); + services.AddBiliBiliClientApi(BiliHosts.Api, config); + services.AddBiliBiliClientApi(BiliHosts.Api, config); + services.AddBiliBiliClientApi(BiliHosts.Api, config); + services.AddBiliBiliClientApi(BiliHosts.Api, config); + services.AddBiliBiliClientApi(BiliHosts.Api, config); + services.AddBiliBiliClientApi(BiliHosts.Api, config); + + services.AddBiliBiliClientApi(BiliHosts.Show, config); + services.AddBiliBiliClientApi(BiliHosts.Passport, config); + services.AddBiliBiliClientApi(BiliHosts.LiveTrace, config); + services.AddBiliBiliClientApi(BiliHosts.Www, config); + services.AddBiliBiliClientApi(BiliHosts.Manga, config); + services.AddBiliBiliClientApi(BiliHosts.Account, config); + services.AddBiliBiliClientApi(BiliHosts.Live, config); + + services.AddBiliBiliClientApi(BiliHosts.App, configApp); + services.AddBiliBiliClientApi(BiliHosts.Mall, configApp); + + //qinglong + var qinglongHost = configuration["QL_URL"] ?? "http://localhost:5600"; + services + .AddHttpApi(o => + { + o.HttpHost = new Uri(qinglongHost); + o.UseDefaultUserAgent = false; + }) + .ConfigureHttpClient( + (sp, c) => + { + c.DefaultRequestHeaders.Add( + "User-Agent", + sp.GetRequiredService< + IOptionsMonitor + >().CurrentValue.UserAgent + ); + } + ) + .AddPolicyHandler(GetRetryPolicy()); + + return services; + } + + /// + /// 封装Refit,默认将Cookie添加到Header中 + /// + /// + /// + /// + /// + private static IServiceCollection AddBiliBiliClientApi( + this IServiceCollection services, + string host, + Action config, + bool ignorWrid = false + ) + where TInterface : class + { + var uri = new Uri(host); + IHttpClientBuilder httpClientBuilder = services + .AddHttpApi(o => + { + o.HttpHost = uri; + o.UseDefaultUserAgent = false; + }) + .ConfigureHttpClient(config) + .AddHttpMessageHandler() + .AddPolicyHandler(GetRetryPolicy()); + + if (!ignorWrid) + { + httpClientBuilder.AddHttpMessageHandler(); + } + + return services; + } + + /// + /// 设置全局代理(如果配置了代理) + /// + /// + /// + private static IServiceCollection SetGlobalProxy( + this IServiceCollection services, + IConfiguration configuration + ) + { + var proxyAddress = configuration["Security:WebProxy"]; + if (!string.IsNullOrWhiteSpace(proxyAddress)) + { + WebProxy webProxy = new WebProxy(); + + //user:password@host:port http proxy only .Tested with tinyproxy-1.11.0-rc1 + if (proxyAddress!.Contains("@")) + { + string userPass = proxyAddress.Split("@")[0]; + string address = proxyAddress.Split("@")[1]; + + string proxyUser = ""; + string proxyPass = ""; + if (userPass.Contains(":")) + { + proxyUser = userPass.Split(":")[0]; + proxyPass = userPass.Split(":")[1]; + } + + webProxy.Address = new Uri("http://" + address); + webProxy.Credentials = new NetworkCredential(proxyUser, proxyPass); + } + else + { + webProxy.Address = new Uri(proxyAddress); + } + + HttpClient.DefaultProxy = webProxy; + } + + return services; + } + + static IAsyncPolicy GetRetryPolicy() + { + return HttpPolicyExtensions + .HandleTransientHttpError() + .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound) + .WaitAndRetryAsync(1, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); + } +} diff --git a/src/Ray.BiliBiliTool.Agent/HttpClientDelegatingHandlers/IntervalDelegatingHandler.cs b/src/Ray.BiliBiliTool.Agent/HttpClientDelegatingHandlers/IntervalDelegatingHandler.cs new file mode 100644 index 0000000..ccb0f1b --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/HttpClientDelegatingHandlers/IntervalDelegatingHandler.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Config.Options; + +namespace Ray.BiliBiliTool.Agent.HttpClientDelegatingHandlers; + +public class IntervalDelegatingHandler(IOptionsMonitor securityOptions) + : DelegatingHandler +{ + private readonly Dictionary _special = new() + { + { "/xlive/lottery-interface/v1/Anchor/Join", 3 }, //天选抽奖,有时效,不能间隔过久,使用默认3秒 + { "/xlive/data-interface/v1/x25Kn/E", 1 }, + { "/xlive/data-interface/v1/x25Kn/X", 1 }, + }; + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken + ) + { + await IntervalForSecurityAsync(request, cancellationToken); + return await base.SendAsync(request, cancellationToken); + } + + private async Task IntervalForSecurityAsync( + HttpRequestMessage request, + CancellationToken cancellationToken + ) + { + if (securityOptions.CurrentValue.IntervalSecondsBetweenRequestApi <= 0) + return; + if (!securityOptions.CurrentValue.GetIntervalMethods().Contains(request.Method)) + return; + + int seconds = 0; + //需要特殊处理的接口 + if (_special.TryGetValue(request.RequestUri?.AbsolutePath ?? "", out int s)) + { + seconds = s; + } + else + { + int maxSeconds = securityOptions.CurrentValue.IntervalSecondsBetweenRequestApi; + seconds = new Random().Next(maxSeconds / 2, maxSeconds + 1); + } + + await Task.Delay(seconds * 1000, cancellationToken); + } +} diff --git a/src/Ray.BiliBiliTool.Agent/HttpClientDelegatingHandlers/LogDelegatingHandler.cs b/src/Ray.BiliBiliTool.Agent/HttpClientDelegatingHandlers/LogDelegatingHandler.cs new file mode 100644 index 0000000..76b7dd0 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/HttpClientDelegatingHandlers/LogDelegatingHandler.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Logging; + +namespace Ray.BiliBiliTool.Agent.HttpClientDelegatingHandlers; + +public class LogDelegatingHandler(ILogger logger) : DelegatingHandler +{ + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken + ) + { + //记录请求内容 + logger.LogDebug("发起请求:[{method}] {uri}", request.Method, request.RequestUri); + + if (request.Content != null) + { + var requestContent = await request.Content.ReadAsStringAsync(cancellationToken); + logger.LogDebug("请求Content: {content}", requestContent); + } + + HttpResponseMessage response = await base.SendAsync(request, cancellationToken); + + var content = await response.Content.ReadAsStringAsync(cancellationToken); + logger.LogDebug("返回Content:{content}", content); + + return response; + } +} diff --git a/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/AddQingLongEnv.cs b/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/AddQingLongEnv.cs new file mode 100644 index 0000000..feac96f --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/AddQingLongEnv.cs @@ -0,0 +1,8 @@ +namespace Ray.BiliBiliTool.Agent.QingLong.Dtos; + +public class AddQingLongEnv +{ + public required string name { get; set; } + public required string value { get; set; } + public string? remarks { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/QingLongEnv.cs b/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/QingLongEnv.cs new file mode 100644 index 0000000..07e6c74 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/QingLongEnv.cs @@ -0,0 +1,11 @@ +namespace Ray.BiliBiliTool.Agent.QingLong.Dtos; + +public class QingLongEnv : UpdateQingLongEnv +{ + public required string timestamp { get; set; } + public int status { get; set; } + + //public long position { get; set; } + public DateTime createdAt { get; set; } + public DateTime updatedAt { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/QingLongGenericResponse.cs b/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/QingLongGenericResponse.cs new file mode 100644 index 0000000..10d5c4c --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/QingLongGenericResponse.cs @@ -0,0 +1,8 @@ +namespace Ray.BiliBiliTool.Agent.QingLong.Dtos; + +public class QingLongGenericResponse +{ + public int Code { get; set; } + + public required T Data { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/TokenResponse.cs b/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/TokenResponse.cs new file mode 100644 index 0000000..76c0b3c --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/TokenResponse.cs @@ -0,0 +1,8 @@ +namespace Ray.BiliBiliTool.Agent.QingLong.Dtos; + +public class TokenResponse +{ + public required string token { get; set; } + + public required string token_type { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/UpdateQingLongEnv.cs b/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/UpdateQingLongEnv.cs new file mode 100644 index 0000000..39a8476 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/QingLong/Dtos/UpdateQingLongEnv.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Agent.QingLong.Dtos; + +public class UpdateQingLongEnv : AddQingLongEnv +{ + public long id { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Agent/QingLong/IQingLongApi.cs b/src/Ray.BiliBiliTool.Agent/QingLong/IQingLongApi.cs new file mode 100644 index 0000000..b87405c --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/QingLong/IQingLongApi.cs @@ -0,0 +1,33 @@ +using Ray.BiliBiliTool.Agent.Attributes; +using Ray.BiliBiliTool.Agent.QingLong.Dtos; +using WebApiClientCore.Attributes; + +namespace Ray.BiliBiliTool.Agent.QingLong; + +[LogFilter] +public interface IQingLongApi +{ + [HttpGet("/open/auth/token")] + Task> GetTokenAsync( + string client_id, + string client_secret + ); + + [HttpGet("/open/envs")] + Task>> GetEnvsAsync( + string searchValue, + [Header("Authorization")] string token + ); + + [HttpPost("/open/envs")] + Task>> AddEnvsAsync( + [JsonContent] List envs, + [Header("Authorization")] string token + ); + + [HttpPut("/open/envs")] + Task> UpdateEnvsAsync( + [JsonContent] UpdateQingLongEnv env, + [Header("Authorization")] string token + ); +} diff --git a/src/Ray.BiliBiliTool.Agent/Ray.BiliBiliTool.Agent.csproj b/src/Ray.BiliBiliTool.Agent/Ray.BiliBiliTool.Agent.csproj new file mode 100644 index 0000000..0f75511 --- /dev/null +++ b/src/Ray.BiliBiliTool.Agent/Ray.BiliBiliTool.Agent.csproj @@ -0,0 +1,16 @@ + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/Ray.BiliBiliTool.Application.Contracts/IAppService.cs b/src/Ray.BiliBiliTool.Application.Contracts/IAppService.cs new file mode 100644 index 0000000..3abab77 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/IAppService.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Application.Contracts; + +public interface IAppService +{ + Task DoTaskAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Ray.BiliBiliTool.Application.Contracts/IChargeTaskAppService.cs b/src/Ray.BiliBiliTool.Application.Contracts/IChargeTaskAppService.cs new file mode 100644 index 0000000..456d6fa --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/IChargeTaskAppService.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Application.Contracts; + +/// +/// 免费B币券充电任务 +/// +[Description("Charge")] +public interface IChargeTaskAppService : IAppService; diff --git a/src/Ray.BiliBiliTool.Application.Contracts/IDailyTaskAppService.cs b/src/Ray.BiliBiliTool.Application.Contracts/IDailyTaskAppService.cs new file mode 100644 index 0000000..3fea62a --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/IDailyTaskAppService.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Application.Contracts; + +/// +/// 每日自动任务 +/// +[Description("Daily")] +public interface IDailyTaskAppService : IAppService; diff --git a/src/Ray.BiliBiliTool.Application.Contracts/ILiveFansMedalAppService.cs b/src/Ray.BiliBiliTool.Application.Contracts/ILiveFansMedalAppService.cs new file mode 100644 index 0000000..01a9b1e --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/ILiveFansMedalAppService.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Application.Contracts; + +/// +/// 直播粉丝牌任务 +/// +[Description("LiveFansMedal")] +public interface ILiveFansMedalAppService : IAppService; diff --git a/src/Ray.BiliBiliTool.Application.Contracts/ILiveLotteryTaskAppService.cs b/src/Ray.BiliBiliTool.Application.Contracts/ILiveLotteryTaskAppService.cs new file mode 100644 index 0000000..3f6108a --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/ILiveLotteryTaskAppService.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Application.Contracts; + +/// +/// 每日自动任务 +/// +[Description("LiveLottery")] +public interface ILiveLotteryTaskAppService : IAppService; diff --git a/src/Ray.BiliBiliTool.Application.Contracts/ILoginTaskAppService.cs b/src/Ray.BiliBiliTool.Application.Contracts/ILoginTaskAppService.cs new file mode 100644 index 0000000..c50a105 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/ILoginTaskAppService.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Application.Contracts; + +/// +/// 登录任务 +/// +[Description("Login")] +public interface ILoginTaskAppService : IAppService; diff --git a/src/Ray.BiliBiliTool.Application.Contracts/IMangaPrivilegeTaskAppService.cs b/src/Ray.BiliBiliTool.Application.Contracts/IMangaPrivilegeTaskAppService.cs new file mode 100644 index 0000000..d6d5b78 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/IMangaPrivilegeTaskAppService.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Application.Contracts; + +/// +/// 领取大会员漫画权益任务 +/// +[Description("MangaPrivilege")] +public interface IMangaPrivilegeTaskAppService : IAppService; diff --git a/src/Ray.BiliBiliTool.Application.Contracts/IMangaTaskAppService.cs b/src/Ray.BiliBiliTool.Application.Contracts/IMangaTaskAppService.cs new file mode 100644 index 0000000..16a9537 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/IMangaTaskAppService.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Application.Contracts; + +/// +/// 漫画任务 +/// +[Description("Manga")] +public interface IMangaTaskAppService : IAppService; diff --git a/src/Ray.BiliBiliTool.Application.Contracts/ISilver2CoinTaskAppService.cs b/src/Ray.BiliBiliTool.Application.Contracts/ISilver2CoinTaskAppService.cs new file mode 100644 index 0000000..268b8a4 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/ISilver2CoinTaskAppService.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Application.Contracts; + +/// +/// 银瓜子兑换硬币 +/// +[Description("Silver2Coin")] +public interface ISilver2CoinTaskAppService : IAppService; diff --git a/src/Ray.BiliBiliTool.Application.Contracts/ITestAppService.cs b/src/Ray.BiliBiliTool.Application.Contracts/ITestAppService.cs new file mode 100644 index 0000000..377c008 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/ITestAppService.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Application.Contracts; + +/// +/// 每日自动任务 +/// +[Description("Test")] +public interface ITestAppService : IAppService; diff --git a/src/Ray.BiliBiliTool.Application.Contracts/IUnfollowBatchedTaskAppService.cs b/src/Ray.BiliBiliTool.Application.Contracts/IUnfollowBatchedTaskAppService.cs new file mode 100644 index 0000000..0b725d7 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/IUnfollowBatchedTaskAppService.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Application.Contracts; + +/// +/// 每日自动任务 +/// +[Description("UnfollowBatched")] +public interface IUnfollowBatchedTaskAppService : IAppService; diff --git a/src/Ray.BiliBiliTool.Application.Contracts/IVipBigPointAppService.cs b/src/Ray.BiliBiliTool.Application.Contracts/IVipBigPointAppService.cs new file mode 100644 index 0000000..3ae07e1 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/IVipBigPointAppService.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Application.Contracts; + +/// +/// 每日自动任务 +/// +[Description("VipBigPoint")] +public interface IVipBigPointAppService : IAppService; diff --git a/src/Ray.BiliBiliTool.Application.Contracts/IVipPrivilegeTaskAppService.cs b/src/Ray.BiliBiliTool.Application.Contracts/IVipPrivilegeTaskAppService.cs new file mode 100644 index 0000000..ac35212 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/IVipPrivilegeTaskAppService.cs @@ -0,0 +1,6 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Application.Contracts; + +[Description("VipPrivilege")] +public interface IVipPrivilegeTaskAppService : IAppService; diff --git a/src/Ray.BiliBiliTool.Application.Contracts/Ray.BiliBiliTool.Application.Contracts.csproj b/src/Ray.BiliBiliTool.Application.Contracts/Ray.BiliBiliTool.Application.Contracts.csproj new file mode 100644 index 0000000..281bb2a --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/Ray.BiliBiliTool.Application.Contracts.csproj @@ -0,0 +1,10 @@ + + + net8.0 + enable + enable + + + + + diff --git a/src/Ray.BiliBiliTool.Application.Contracts/TaskTypeFactory.cs b/src/Ray.BiliBiliTool.Application.Contracts/TaskTypeFactory.cs new file mode 100644 index 0000000..23591dd --- /dev/null +++ b/src/Ray.BiliBiliTool.Application.Contracts/TaskTypeFactory.cs @@ -0,0 +1,68 @@ +using System.ComponentModel; +using System.Reflection; +using Microsoft.Extensions.Logging; + +namespace Ray.BiliBiliTool.Application.Contracts; + +public static class TaskTypeFactory +{ + private static readonly List TypeList = + [ + typeof(ILoginTaskAppService), + typeof(ITestAppService), + typeof(IDailyTaskAppService), + typeof(IMangaTaskAppService), + typeof(IMangaPrivilegeTaskAppService), + typeof(IVipPrivilegeTaskAppService), + typeof(ISilver2CoinTaskAppService), + typeof(IChargeTaskAppService), + typeof(ILiveFansMedalAppService), + typeof(ILiveLotteryTaskAppService), + typeof(IVipBigPointAppService), + typeof(IUnfollowBatchedTaskAppService), + ]; + + private static readonly List All = []; + + static TaskTypeFactory() + { + for (int i = 0; i < TypeList.Count; i++) + { + All.Add( + new TaskTypeItem( + i + 1, + TypeList[i].GetCustomAttribute()?.Description + ?? "Unknown", + TypeList[i] + ) + ); + } + } + + public static Type Get(string code) + { + return All.First(x => x.Code == code).Type; + } + + public static void Show(ILogger logger) + { + foreach (var item in All) + { + logger.LogInformation("{id}):{code}", item.Id, item.Code); + } + } + + public static string GetCodeByIndex(int index) + { + return All.First(x => x.Id == index).Code; + } +} + +public class TaskTypeItem(int id, string code, Type type) +{ + public int Id { get; } = id; + + public string Code { get; } = code; + + public Type Type { get; } = type; +} diff --git a/src/Ray.BiliBiliTool.Application/AppService.cs b/src/Ray.BiliBiliTool.Application/AppService.cs new file mode 100644 index 0000000..6ed92ae --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/AppService.cs @@ -0,0 +1,8 @@ +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Application; + +public abstract class AppService : IAppService +{ + public abstract Task DoTaskAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Ray.BiliBiliTool.Application/Attributes/TaskInterceptorAttribute.cs b/src/Ray.BiliBiliTool.Application/Attributes/TaskInterceptorAttribute.cs new file mode 100644 index 0000000..3170305 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/Attributes/TaskInterceptorAttribute.cs @@ -0,0 +1,79 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Ray.BiliBiliTool.Infrastructure; +using Rougamo; +using Rougamo.Context; + +namespace Ray.BiliBiliTool.Application.Attributes; + +/// +/// 任务拦截器 +/// +public class TaskInterceptorAttribute( + string? taskName = null, + TaskLevel taskLevel = TaskLevel.Two, + bool rethrowWhenException = true +) : MoAttribute +{ + private readonly ILogger _logger = Global.ServiceProviderRoot!.GetRequiredService< + ILogger + >(); + + public override void OnEntry(MethodContext context) + { + if (taskName == null) + return; + string end = taskLevel == TaskLevel.One ? Environment.NewLine : ""; + string delimiter = GetDelimiters(); + _logger.LogInformation(delimiter + "开始 {taskName} " + delimiter + end, taskName); + } + + public override void OnExit(MethodContext context) + { + if (taskName == null) + return; + + string delimiter = GetDelimiters(); + var append = new string(GetDelimiter(), taskName.Length); + + _logger.LogInformation( + delimiter + append + "结束" + append + delimiter + Environment.NewLine + ); + } + + public override void OnException(MethodContext context) + { + if (rethrowWhenException) + { + _logger.LogError("程序发生异常:{msg}", context.Exception?.Message ?? ""); + base.OnException(context); + return; + } + + _logger.LogError( + "{task}失败,继续其他任务。失败信息:{msg}" + Environment.NewLine, + taskName, + context.Exception?.Message ?? "" + ); + context.HandledException(this, null); + } + + private string GetDelimiters() + { + char delimiter = GetDelimiter(); + + int count = Convert.ToInt32(taskLevel.DefaultValue()); + return new string(delimiter, count); + } + + private char GetDelimiter() + { + return taskLevel switch + { + TaskLevel.One => '=', + TaskLevel.Two => '-', + TaskLevel.Three => '-', + _ => throw new ArgumentOutOfRangeException(nameof(taskLevel), taskLevel, null), + }; + } +} diff --git a/src/Ray.BiliBiliTool.Application/Attributes/TaskLevel.cs b/src/Ray.BiliBiliTool.Application/Attributes/TaskLevel.cs new file mode 100644 index 0000000..8594c67 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/Attributes/TaskLevel.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Application.Attributes; + +public enum TaskLevel +{ + [DefaultValue(5)] + One, + + [DefaultValue(3)] + Two, + + [DefaultValue(2)] + Three, +} diff --git a/src/Ray.BiliBiliTool.Application/BaseMultiAccountsAppService.cs b/src/Ray.BiliBiliTool.Application/BaseMultiAccountsAppService.cs new file mode 100644 index 0000000..76895c6 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/BaseMultiAccountsAppService.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Logging; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Infrastructure.Cookie; + +namespace Ray.BiliBiliTool.Application; + +public abstract class BaseMultiAccountsAppService( + ILogger logger, + CookieStrFactory cookieStrFactory +) : AppService +{ + public override async Task DoTaskAsync(CancellationToken cancellationToken = default) + { + logger.LogInformation( + "【账号个数】{count}个" + Environment.NewLine, + cookieStrFactory.Count + ); + for (int i = 0; i < cookieStrFactory.Count; i++) + { + logger.LogInformation("######### 账号 {num} #########" + Environment.NewLine, i); + var ck = cookieStrFactory.GetCookie(i); + try + { + await DoTaskAccountAsync(ck, cancellationToken); + } + catch (Exception e) + { + //ignore + logger.LogWarning("异常:{msg}", e); + } + } + } + + protected abstract Task DoTaskAccountAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ); +} diff --git a/src/Ray.BiliBiliTool.Application/ChargeTaskAppService.cs b/src/Ray.BiliBiliTool.Application/ChargeTaskAppService.cs new file mode 100644 index 0000000..0a82f41 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/ChargeTaskAppService.cs @@ -0,0 +1,96 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Application.Attributes; +using Ray.BiliBiliTool.Application.Contracts; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Cookie; +using Ray.BiliBiliTool.Infrastructure.Enums; + +namespace Ray.BiliBiliTool.Application; + +public class ChargeTaskAppService( + ILogger logger, + IOptionsMonitor chargeTaskOptions, + IAccountDomainService accountDomainService, + IChargeDomainService chargeDomainService, + ILoginDomainService loginDomainService, + IConfiguration configuration, + CookieStrFactory cookieStrFactory +) : BaseMultiAccountsAppService(logger, cookieStrFactory), IChargeTaskAppService +{ + [TaskInterceptor("免费B币券充电任务", TaskLevel.One)] + protected override async Task DoTaskAccountAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + if (!chargeTaskOptions.CurrentValue.IsEnable) + { + logger.LogInformation("已配置为关闭,跳过"); + return; + } + + await SetCookiesAsync(ck, cancellationToken); + UserInfo userInfo = await Login(ck); + await Charge(userInfo, ck); + } + + [TaskInterceptor("Set Cookie")] + private async Task SetCookiesAsync(BiliCookie biliCookie, CancellationToken cancellationToken) + { + //判断cookie是否完整 + if (!string.IsNullOrWhiteSpace(biliCookie.Buvid)) + { + logger.LogInformation("Cookie完整,不需要Set Cookie"); + return; + } + + //Set + logger.LogInformation("开始Set Cookie"); + var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken); + + //持久化 + logger.LogInformation("持久化Cookie"); + await SaveCookieAsync(ck, cancellationToken); + } + + /// + /// 登录 + /// + /// + [TaskInterceptor("登录")] + private async Task Login(BiliCookie ck) + { + UserInfo userInfo = await accountDomainService.LoginByCookie(ck); + return userInfo; + } + + /// + /// 每月为自己充电 + /// + [TaskInterceptor("B币券充电", rethrowWhenException: false)] + private async Task Charge(UserInfo userInfo, BiliCookie ck) + { + await chargeDomainService.Charge(userInfo, ck); + } + + private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken) + { + var platformType = configuration.GetSection("PlatformType").Get(); + logger.LogInformation("当前运行平台:{platform}", platformType); + + //更新cookie到青龙env + if (platformType == PlatformType.QingLong) + { + await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken); + return; + } + + //更新cookie到json + await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken); + } +} diff --git a/src/Ray.BiliBiliTool.Application/DailyTaskAppService.cs b/src/Ray.BiliBiliTool.Application/DailyTaskAppService.cs new file mode 100644 index 0000000..fe5ed73 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/DailyTaskAppService.cs @@ -0,0 +1,180 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Application.Attributes; +using Ray.BiliBiliTool.Application.Contracts; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Cookie; +using Ray.BiliBiliTool.Infrastructure.Enums; + +namespace Ray.BiliBiliTool.Application; + +public class DailyTaskAppService( + ILogger logger, + IAccountDomainService accountDomainService, + IVideoDomainService videoDomainService, + IArticleDomainService articleDomainService, + IDonateCoinDomainService donateCoinDomainService, + IVipPrivilegeDomainService vipPrivilegeDomainService, + IOptionsMonitor dailyTaskOptions, + ILoginDomainService loginDomainService, + IConfiguration configuration, + CookieStrFactory cookieStrFactory +) : BaseMultiAccountsAppService(logger, cookieStrFactory), IDailyTaskAppService +{ + private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue; + private readonly Dictionary _expDic = Config.Constants.ExpDic; + + [TaskInterceptor("每日任务", TaskLevel.One)] + protected override async Task DoTaskAccountAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + if (!_dailyTaskOptions.IsEnable) + { + logger.LogInformation("已配置为关闭,跳过"); + return; + } + + await SetCookiesAsync(ck, cancellationToken); + + //每日任务赚经验: + UserInfo userInfo = await Login(ck); + + DailyTaskInfo dailyTaskInfo = await GetDailyTaskStatus(ck); + await WatchAndShareVideo(dailyTaskInfo, ck); + + await AddCoins(userInfo, ck); + + await ReceiveVipPrivilege(userInfo, ck); + } + + [TaskInterceptor("Set Cookie")] + private async Task SetCookiesAsync(BiliCookie biliCookie, CancellationToken cancellationToken) + { + //判断cookie是否完整 + if (!string.IsNullOrWhiteSpace(biliCookie.Buvid)) + { + logger.LogInformation("Cookie完整,不需要Set Cookie"); + return; + } + + //Set + logger.LogInformation("开始Set Cookie"); + var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken); + + //持久化 + logger.LogInformation("持久化Cookie"); + await SaveCookieAsync(ck, cancellationToken); + } + + /// + /// 登录 + /// + /// + [TaskInterceptor("登录")] + private async Task Login(BiliCookie ck) + { + UserInfo userInfo = await accountDomainService.LoginByCookie(ck); + + _expDic.TryGetValue("每日登录", out int exp); + logger.LogInformation("登录成功,经验+{exp} √", exp); + + return userInfo; + } + + /// + /// 获取任务完成情况 + /// + /// + [TaskInterceptor(rethrowWhenException: false)] + private async Task GetDailyTaskStatus(BiliCookie ck) + { + return await accountDomainService.GetDailyTaskStatus(ck); + } + + /// + /// 观看、分享视频 + /// + [TaskInterceptor("观看、分享视频", rethrowWhenException: false)] + private async Task WatchAndShareVideo(DailyTaskInfo dailyTaskInfo, BiliCookie ck) + { + if (!_dailyTaskOptions.IsWatchVideo && !_dailyTaskOptions.IsShareVideo) + { + logger.LogInformation("已配置为关闭,跳过任务"); + return; + } + + await videoDomainService.WatchAndShareVideo(dailyTaskInfo, ck); + } + + /// + /// 投币任务 + /// + [TaskInterceptor("投币", rethrowWhenException: false)] + private async Task AddCoins(UserInfo userInfo, BiliCookie ck) + { + if (_dailyTaskOptions.SaveCoinsWhenLv6 && userInfo.Level_info?.Current_level >= 6) + { + logger.LogInformation("已经为LV6大佬,开始白嫖"); + return; + } + + if (_dailyTaskOptions.IsDonateCoinForArticle) + { + logger.LogInformation("专栏投币已开启"); + + if (!await articleDomainService.AddCoinForArticles(ck)) + { + logger.LogInformation("专栏投币结束,转入视频投币"); + await donateCoinDomainService.AddCoinsForVideos(ck); + } + } + else + { + await donateCoinDomainService.AddCoinsForVideos(ck); + } + } + + /// + /// 每月领取大会员福利 + /// + [TaskInterceptor("领取大会员福利", rethrowWhenException: false)] + private async Task ReceiveVipPrivilege(UserInfo userInfo, BiliCookie ck) + { + var suc = await vipPrivilegeDomainService.ReceiveVipPrivilege(userInfo, ck); + + //如果领取成功,需要刷新账户信息(比如B币余额) + if (suc) + { + try + { + await accountDomainService.LoginByCookie(ck); + } + catch (Exception ex) + { + logger.LogError("领取福利成功,但之后刷新用户信息时异常,信息:{msg}", ex.Message); + } + } + } + + private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken) + { + var platformType = configuration.GetSection("PlatformType").Get(); + logger.LogInformation("当前运行平台:{platform}", platformType); + + //更新cookie到青龙env + if (platformType == PlatformType.QingLong) + { + await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken); + return; + } + + //更新cookie到json + await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken); + } +} diff --git a/src/Ray.BiliBiliTool.Application/Extensions/ServiceCollectionExtension.cs b/src/Ray.BiliBiliTool.Application/Extensions/ServiceCollectionExtension.cs new file mode 100644 index 0000000..b54cb48 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/Extensions/ServiceCollectionExtension.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Application.Extensions; + +public static class ServiceCollectionExtension +{ + public static IServiceCollection AddAppServices(this IServiceCollection services) + { + services.Scan(scan => + scan.FromAssemblyOf() + .AddClasses(classes => classes.AssignableTo()) + .AsImplementedInterfaces() + .WithTransientLifetime() + ); + + return services; + } +} diff --git a/src/Ray.BiliBiliTool.Application/FodyWeavers.xml b/src/Ray.BiliBiliTool.Application/FodyWeavers.xml new file mode 100644 index 0000000..e958c64 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/FodyWeavers.xml @@ -0,0 +1,6 @@ + + + diff --git a/src/Ray.BiliBiliTool.Application/FodyWeavers.xsd b/src/Ray.BiliBiliTool.Application/FodyWeavers.xsd new file mode 100644 index 0000000..1a5a0c8 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/FodyWeavers.xsd @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + The assembly name of the aspect type, which does not contain the '.dll' suffix. + + + + + The aspect type full name. + + + + + An AspectN pattern. Apply the aspect type to methods matched by the pattern. This pattern will override the pointcut settings of the aspect type. + + + + + + + + + + + Set to false to disable Rougamo. The default is true. + + + + + Set to true to use the type and method composite accessibility. The default is false. Etc, an internal type has a public method, public for default(false) and internal for true. + + + + + Set to true to skip saving ref struct parameters and return value into MethodContext. The default is false. + + + + + Set to false to prevent generating the StackTraceHiddenAttribute for the proxy method. The default is true. + + + + + Set to true to save the items that the iterator returns. This will take up additional memory space. The default is false. + + + + + Set to false to make the execution order of the OnSuccess, OnException, and OnExit methods the same as OnEntry. The default is true. + + + + + Regex expressions for the type's full name, separated by ',' or ';'. All types matching any of these regex expressions will be ignored by Rougamo. + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/src/Ray.BiliBiliTool.Application/LiveFansMedalAppService.cs b/src/Ray.BiliBiliTool.Application/LiveFansMedalAppService.cs new file mode 100644 index 0000000..293eaac --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/LiveFansMedalAppService.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Application.Attributes; +using Ray.BiliBiliTool.Application.Contracts; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Cookie; + +namespace Ray.BiliBiliTool.Application; + +public class LiveFansMedalAppService( + ILogger logger, + IOptionsMonitor liveFansMedalTaskOptions, + ILiveDomainService liveDomainService, + CookieStrFactory cookieStrFactory +) : BaseMultiAccountsAppService(logger, cookieStrFactory), ILiveFansMedalAppService +{ + [TaskInterceptor("直播间互动", TaskLevel.One)] + protected override async Task DoTaskAccountAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + if (!liveFansMedalTaskOptions.CurrentValue.IsEnable) + { + logger.LogInformation("已配置为关闭,跳过"); + return; + } + + await SendDanmaku(ck); + await Like(ck); + await HeartBeat(ck); + } + + [TaskInterceptor("发送弹幕", TaskLevel.Two, false)] + private async Task SendDanmaku(BiliCookie ck) + { + await liveDomainService.SendDanmakuToFansMedalLive(ck); + } + + [TaskInterceptor("点赞直播间", TaskLevel.Two, false)] + private async Task Like(BiliCookie ck) + { + await liveDomainService.LikeFansMedalLive(ck); + } + + [TaskInterceptor("直播时长挂机", TaskLevel.Two, false)] + private async Task HeartBeat(BiliCookie ck) + { + await liveDomainService.SendHeartBeatToFansMedalLive(ck); + } +} diff --git a/src/Ray.BiliBiliTool.Application/LiveLotteryTaskAppService.cs b/src/Ray.BiliBiliTool.Application/LiveLotteryTaskAppService.cs new file mode 100644 index 0000000..de19b23 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/LiveLotteryTaskAppService.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Application.Attributes; +using Ray.BiliBiliTool.Application.Contracts; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Cookie; + +namespace Ray.BiliBiliTool.Application; + +public class LiveLotteryTaskAppService( + ILiveDomainService liveDomainService, + IOptionsMonitor liveLotteryTaskOptions, + ILogger logger, + IAccountDomainService accountDomainService, + CookieStrFactory cookieStrFactory +) : BaseMultiAccountsAppService(logger, cookieStrFactory), ILiveLotteryTaskAppService +{ + private readonly LiveLotteryTaskOptions _liveLotteryTaskOptions = + liveLotteryTaskOptions.CurrentValue; + + [TaskInterceptor("天选时刻抽奖", TaskLevel.One)] + protected override async Task DoTaskAccountAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + if (!liveLotteryTaskOptions.CurrentValue.IsEnable) + { + logger.LogInformation("已配置为关闭,跳过"); + return; + } + + await LogUserInfo(ck); + await LotteryTianXuan(ck); + await AutoGroupFollowings(ck); + } + + [TaskInterceptor("打印用户信息")] + private async Task LogUserInfo(BiliCookie ck) + { + await accountDomainService.LoginByCookie(ck); + } + + [TaskInterceptor("抽奖")] + private async Task LotteryTianXuan(BiliCookie ck) + { + await liveDomainService.TianXuan(ck); + } + + [TaskInterceptor("自动分组关注的主播")] + private async Task AutoGroupFollowings(BiliCookie ck) + { + if (_liveLotteryTaskOptions.AutoGroupFollowings) + { + await liveDomainService.GroupFollowing(ck); + } + else + { + logger.LogInformation("配置未开启,跳过"); + } + } +} diff --git a/src/Ray.BiliBiliTool.Application/LoginTaskAppService.cs b/src/Ray.BiliBiliTool.Application/LoginTaskAppService.cs new file mode 100644 index 0000000..55cebae --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/LoginTaskAppService.cs @@ -0,0 +1,65 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Application.Attributes; +using Ray.BiliBiliTool.Application.Contracts; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Enums; + +namespace Ray.BiliBiliTool.Application; + +public class LoginTaskAppService( + IConfiguration configuration, + ILogger logger, + ILoginDomainService loginDomainService +) : AppService, ILoginTaskAppService +{ + [TaskInterceptor("扫码登录", TaskLevel.One)] + public override async Task DoTaskAsync(CancellationToken cancellationToken = default) + { + //扫码登录 + var cookieInfo = await QrCodeLoginAsync(cancellationToken); + if (cookieInfo == null) + return; + + //set cookie + cookieInfo = await SetCookiesAsync(cookieInfo, cancellationToken); + + //持久化cookie + await SaveCookieAsync(cookieInfo, cancellationToken); + } + + [TaskInterceptor("获取二维码")] + private async Task QrCodeLoginAsync(CancellationToken cancellationToken) + { + var biliCookie = await loginDomainService.LoginByQrCodeAsync(cancellationToken); + return biliCookie; + } + + [TaskInterceptor("Set Cookie")] + private async Task SetCookiesAsync( + BiliCookie biliCookie, + CancellationToken cancellationToken + ) + { + var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken); + return ck; + } + + [TaskInterceptor("持久化Cookie")] + private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken) + { + var platformType = configuration.GetSection("PlatformType").Get(); + logger.LogInformation("当前运行平台:{platform}", platformType); + + //更新cookie到青龙env + if (platformType == PlatformType.QingLong) + { + await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken); + return; + } + + //更新cookie到json + await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken); + } +} diff --git a/src/Ray.BiliBiliTool.Application/MangaPrivilegeTaskAppService.cs b/src/Ray.BiliBiliTool.Application/MangaPrivilegeTaskAppService.cs new file mode 100644 index 0000000..6d61919 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/MangaPrivilegeTaskAppService.cs @@ -0,0 +1,97 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Application.Attributes; +using Ray.BiliBiliTool.Application.Contracts; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Cookie; +using Ray.BiliBiliTool.Infrastructure.Enums; + +namespace Ray.BiliBiliTool.Application; + +public class MangaPrivilegeTaskAppService( + ILogger logger, + IOptionsMonitor mangaPrivilegeTaskOptions, + IAccountDomainService accountDomainService, + IMangaDomainService mangaDomainService, + ILoginDomainService loginDomainService, + IConfiguration configuration, + CookieStrFactory cookieStrFactory +) : BaseMultiAccountsAppService(logger, cookieStrFactory), IMangaPrivilegeTaskAppService +{ + [TaskInterceptor("每月领取大会员漫画权益任务", TaskLevel.One)] + protected override async Task DoTaskAccountAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + if (!mangaPrivilegeTaskOptions.CurrentValue.IsEnable) + { + logger.LogInformation("已配置为关闭,跳过"); + return; + } + + await SetCookiesAsync(ck, cancellationToken); + UserInfo userInfo = await Login(ck); + await ReceiveMangaVipReward(userInfo, ck); + } + + [TaskInterceptor("Set Cookie")] + private async Task SetCookiesAsync(BiliCookie biliCookie, CancellationToken cancellationToken) + { + //判断cookie是否完整 + if (!string.IsNullOrWhiteSpace(biliCookie.Buvid)) + { + logger.LogInformation("Cookie完整,不需要Set Cookie"); + return; + } + + //Set + logger.LogInformation("开始Set Cookie"); + var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken); + + //持久化 + logger.LogInformation("持久化Cookie"); + await SaveCookieAsync(ck, cancellationToken); + } + + /// + /// 登录 + /// + /// + [TaskInterceptor("登录")] + private async Task Login(BiliCookie ck) + { + UserInfo userInfo = await accountDomainService.LoginByCookie(ck); + + return userInfo; + } + + /// + /// 每月获取大会员漫画权益 + /// + [TaskInterceptor("领取大会员漫画权益", rethrowWhenException: false)] + private async Task ReceiveMangaVipReward(UserInfo userInfo, BiliCookie ck) + { + await mangaDomainService.ReceiveMangaVipReward(1, userInfo, ck); + } + + private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken) + { + var platformType = configuration.GetSection("PlatformType").Get(); + logger.LogInformation("当前运行平台:{platform}", platformType); + + //更新cookie到青龙env + if (platformType == PlatformType.QingLong) + { + await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken); + return; + } + + //更新cookie到json + await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken); + } +} diff --git a/src/Ray.BiliBiliTool.Application/MangaTaskAppService.cs b/src/Ray.BiliBiliTool.Application/MangaTaskAppService.cs new file mode 100644 index 0000000..139582e --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/MangaTaskAppService.cs @@ -0,0 +1,105 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Application.Attributes; +using Ray.BiliBiliTool.Application.Contracts; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Cookie; +using Ray.BiliBiliTool.Infrastructure.Enums; + +namespace Ray.BiliBiliTool.Application; + +public class MangaTaskAppService( + ILogger logger, + IOptionsMonitor mangaTaskOptions, + IAccountDomainService accountDomainService, + IMangaDomainService mangaDomainService, + ILoginDomainService loginDomainService, + IConfiguration configuration, + CookieStrFactory cookieStrFactory +) : BaseMultiAccountsAppService(logger, cookieStrFactory), IMangaTaskAppService +{ + [TaskInterceptor("漫画任务", TaskLevel.One)] + protected override async Task DoTaskAccountAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + if (!mangaTaskOptions.CurrentValue.IsEnable) + { + logger.LogInformation("已配置为关闭,跳过"); + return; + } + + await SetCookiesAsync(ck, cancellationToken); + await Login(ck); + + await MangaSign(ck); + await MangaRead(ck); + } + + [TaskInterceptor("Set Cookie")] + private async Task SetCookiesAsync(BiliCookie biliCookie, CancellationToken cancellationToken) + { + //判断cookie是否完整 + if (!string.IsNullOrWhiteSpace(biliCookie.Buvid)) + { + logger.LogInformation("Cookie完整,不需要Set Cookie"); + return; + } + + //Set + logger.LogInformation("开始Set Cookie"); + var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken); + + //持久化 + logger.LogInformation("持久化Cookie"); + await SaveCookieAsync(ck, cancellationToken); + } + + /// + /// 登录 + /// + /// + [TaskInterceptor("登录")] + private async Task Login(BiliCookie ck) + { + await accountDomainService.LoginByCookie(ck); + } + + /// + /// 漫画签到 + /// + [TaskInterceptor("漫画签到", rethrowWhenException: false)] + private async Task MangaSign(BiliCookie ck) + { + await mangaDomainService.MangaSign(ck); + } + + /// + /// 漫画阅读 + /// + [TaskInterceptor("漫画阅读", rethrowWhenException: false)] + private async Task MangaRead(BiliCookie ck) + { + await mangaDomainService.MangaRead(ck); + } + + private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken) + { + var platformType = configuration.GetSection("PlatformType").Get(); + logger.LogInformation("当前运行平台:{platform}", platformType); + + //更新cookie到青龙env + if (platformType == PlatformType.QingLong) + { + await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken); + return; + } + + //更新cookie到json + await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken); + } +} diff --git a/src/Ray.BiliBiliTool.Application/Ray.BiliBiliTool.Application.csproj b/src/Ray.BiliBiliTool.Application/Ray.BiliBiliTool.Application.csproj new file mode 100644 index 0000000..7e7d813 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/Ray.BiliBiliTool.Application.csproj @@ -0,0 +1,21 @@ + + + net8.0 + enable + enable + $(NoWarn);CS8600;CS8601 + + + + + + + + + + + + + + + diff --git a/src/Ray.BiliBiliTool.Application/Silver2CoinTaskAppService.cs b/src/Ray.BiliBiliTool.Application/Silver2CoinTaskAppService.cs new file mode 100644 index 0000000..350887d --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/Silver2CoinTaskAppService.cs @@ -0,0 +1,102 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Application.Attributes; +using Ray.BiliBiliTool.Application.Contracts; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Cookie; +using Ray.BiliBiliTool.Infrastructure.Enums; + +namespace Ray.BiliBiliTool.Application; + +public class Silver2CoinTaskAppService( + ILogger logger, + IOptionsMonitor silver2CoinTaskOptions, + IAccountDomainService accountDomainService, + ILoginDomainService loginDomainService, + IConfiguration configuration, + ILiveDomainService liveDomainService, + ICoinDomainService coinDomainService, + CookieStrFactory cookieStrFactory +) : BaseMultiAccountsAppService(logger, cookieStrFactory), ISilver2CoinTaskAppService +{ + [TaskInterceptor("银瓜子兑换硬币任务", TaskLevel.One)] + protected override async Task DoTaskAccountAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + if (!silver2CoinTaskOptions.CurrentValue.IsEnable) + { + logger.LogInformation("已配置为关闭,跳过"); + return; + } + + await SetCookiesAsync(ck, cancellationToken); + await Login(ck); + + await ExchangeSilver2Coin(ck); + } + + [TaskInterceptor("Set Cookie")] + private async Task SetCookiesAsync(BiliCookie biliCookie, CancellationToken cancellationToken) + { + //判断cookie是否完整 + if (!string.IsNullOrWhiteSpace(biliCookie.Buvid)) + { + logger.LogInformation("Cookie完整,不需要Set Cookie"); + return; + } + + //Set + logger.LogInformation("开始Set Cookie"); + var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken); + + //持久化 + logger.LogInformation("持久化Cookie"); + await SaveCookieAsync(ck, cancellationToken); + } + + /// + /// 登录 + /// + /// + [TaskInterceptor("登录")] + private async Task Login(BiliCookie ck) + { + await accountDomainService.LoginByCookie(ck); + } + + /// + /// 直播中心的银瓜子兑换硬币 + /// + [TaskInterceptor("银瓜子兑换硬币", rethrowWhenException: false)] + private async Task ExchangeSilver2Coin(BiliCookie ck) + { + var success = await liveDomainService.ExchangeSilver2Coin(ck); + if (!success) + return; + + //如果兑换成功,则打印硬币余额 + var coinBalance = coinDomainService.GetCoinBalance(ck); + logger.LogInformation("【硬币余额】 {coin}", coinBalance); + } + + private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken) + { + var platformType = configuration.GetSection("PlatformType").Get(); + logger.LogInformation("当前运行平台:{platform}", platformType); + + //更新cookie到青龙env + if (platformType == PlatformType.QingLong) + { + await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken); + return; + } + + //更新cookie到json + await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken); + } +} diff --git a/src/Ray.BiliBiliTool.Application/TestAppService.cs b/src/Ray.BiliBiliTool.Application/TestAppService.cs new file mode 100644 index 0000000..7faeb20 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/TestAppService.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Logging; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Application.Attributes; +using Ray.BiliBiliTool.Application.Contracts; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Cookie; + +namespace Ray.BiliBiliTool.Application; + +public class TestAppService( + ILogger logger, + IAccountDomainService accountDomainService, + CookieStrFactory cookieStrFactory +) : BaseMultiAccountsAppService(logger, cookieStrFactory), ITestAppService +{ + [TaskInterceptor("测试Cookie")] + protected override async Task DoTaskAccountAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + await accountDomainService.LoginByCookie(ck); + } +} diff --git a/src/Ray.BiliBiliTool.Application/UnfollowBatchedTaskAppService.cs b/src/Ray.BiliBiliTool.Application/UnfollowBatchedTaskAppService.cs new file mode 100644 index 0000000..558b695 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/UnfollowBatchedTaskAppService.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Application.Attributes; +using Ray.BiliBiliTool.Application.Contracts; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Cookie; + +namespace Ray.BiliBiliTool.Application; + +public class UnfollowBatchedTaskAppService( + ILogger logger, + IOptionsMonitor unfollowBatchedTaskOptions, + IAccountDomainService accountDomainService, + CookieStrFactory cookieStrFactory +) : BaseMultiAccountsAppService(logger, cookieStrFactory), IUnfollowBatchedTaskAppService +{ + [TaskInterceptor("批量取关", TaskLevel.One)] + protected override async Task DoTaskAccountAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + if (!unfollowBatchedTaskOptions.CurrentValue.IsEnable) + { + logger.LogInformation("已配置为关闭,跳过"); + return; + } + + await accountDomainService.UnfollowBatched(ck); + } +} diff --git a/src/Ray.BiliBiliTool.Application/VipBigPointAppService.cs b/src/Ray.BiliBiliTool.Application/VipBigPointAppService.cs new file mode 100644 index 0000000..8310213 --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/VipBigPointAppService.cs @@ -0,0 +1,259 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall; +using Ray.BiliBiliTool.Application.Attributes; +using Ray.BiliBiliTool.Application.Contracts; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Cookie; + +namespace Ray.BiliBiliTool.Application; + +public class VipBigPointAppService( + ILogger logger, + IOptionsMonitor vipBigPointOptions, + IAccountDomainService loginDomainService, + IVipBigPointDomainService vipBigPointDomainService, + CookieStrFactory cookieFactory +) : BaseMultiAccountsAppService(logger, cookieFactory), IVipBigPointAppService +{ + [TaskInterceptor("大会员大积分", TaskLevel.One)] + protected override async Task DoTaskAccountAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + if (!vipBigPointOptions.CurrentValue.IsEnable) + { + logger.LogInformation("已配置为关闭,跳过"); + return; + } + + bool isVip = await LoginAndCheckVipStatusAsync(ck, cancellationToken); + if (!isVip) + { + return; + } + + await ExpressAsync(ck, cancellationToken); + await SignAsync(ck, cancellationToken); + var combine = await CheckCombineAsync(ck, cancellationToken); + + // 2 个一次性任务 + await BonusMissionAsync(combine, ck, cancellationToken); + await PrivilegeMissionAsync(combine, ck, cancellationToken); + + // 日常任务 + await ReceiveMissionsAsync(combine, ck, cancellationToken); + await DailyMissionsAsync(combine, ck, cancellationToken); + + await CheckCombineAsync(ck, cancellationToken); + } + + [TaskInterceptor("登录并检测会员状态")] + private async Task LoginAndCheckVipStatusAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + UserInfo userInfo = await loginDomainService.LoginByCookie(ck); + if (userInfo.GetVipType() == VipType.None) + { + logger.LogInformation("当前不是大会员,跳过任务"); + return false; + } + + return true; + } + + [TaskInterceptor("查看大会员大积分状态")] + private async Task CheckCombineAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + VipBigPointCombine combine = await vipBigPointDomainService.GetCombineAsync(ck); + combine.LogFullInfo(logger); + return combine; + } + + /// + /// 领经验(专属等级加速包),观看视频 1 分钟领取 10 经验 + /// + /// + /// + [TaskInterceptor("大会员经验观看任务", rethrowWhenException: false)] + private async Task ExpressAsync(BiliCookie ck, CancellationToken cancellationToken = default) + { + await vipBigPointDomainService.VipExpressAsync(ck); + } + + [TaskInterceptor("签到任务", rethrowWhenException: false)] + private async Task SignAsync(BiliCookie ck, CancellationToken cancellationToken = default) + { + await vipBigPointDomainService.SignAsync(ck); + } + + [TaskInterceptor("领取日常任务", rethrowWhenException: false)] + private async Task ReceiveMissionsAsync( + VipBigPointCombine combine, + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + await vipBigPointDomainService.ReceiveDailyMissionsAsync(combine, ck); + } + + [TaskInterceptor("福利任务", rethrowWhenException: false)] + private async Task BonusMissionAsync( + VipBigPointCombine combine, + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + await vipBigPointDomainService.ReceiveAndCompleteAsync( + combine, + "福利任务", + "bonus", + ck, + async (_, _) => await vipBigPointDomainService.CompleteAsync("bonus", ck) + ); + } + + [TaskInterceptor("体验任务", rethrowWhenException: false)] + private async Task PrivilegeMissionAsync( + VipBigPointCombine combine, + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + await vipBigPointDomainService.ReceiveAndCompleteAsync( + combine, + "体验任务", + "privilege", + ck, + async (_, _) => await vipBigPointDomainService.CompleteAsync("privilege", ck) + ); + } + + [TaskInterceptor("日常任务", rethrowWhenException: false)] + private async Task DailyMissionsAsync( + VipBigPointCombine combine, + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + await DailyDressViewMissionAsync(combine, ck, cancellationToken); + await DailyVipMallViewMissionAsync(combine, ck, cancellationToken); + await DailyVipMallBuyMissionAsync(cancellationToken); + await DailyAnimateTabMissionAsync(combine, ck, cancellationToken); + await DailyFilmTabMissionAsync(combine, ck, cancellationToken); + await DailyOgvWatchMissionAsync(combine, ck, cancellationToken); + await DailyTvOdBuyMissionAsync(cancellationToken); + await DailyDressBuyAmountMissionAsync(cancellationToken); + } + + [TaskInterceptor("日常1:浏览装扮商城", TaskLevel.Three, rethrowWhenException: false)] + private async Task DailyDressViewMissionAsync( + VipBigPointCombine combine, + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + await vipBigPointDomainService.ReceiveAndCompleteAsync( + combine, + "日常任务", + "dress-view", + ck, + async (_, _) => await vipBigPointDomainService.CompleteV2Async("dress-view", ck) + ); + } + + [TaskInterceptor("日常2:浏览会员购", TaskLevel.Three, rethrowWhenException: false)] + private async Task DailyVipMallViewMissionAsync( + VipBigPointCombine combine, + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + await vipBigPointDomainService.ReceiveAndCompleteAsync( + combine, + "日常任务", + "vipmallview", + ck, + async (_, _) => + await vipBigPointDomainService.CompleteViewVipMallAsync("vipmallview", ck) + ); + } + + [TaskInterceptor("日常3:购买会员购", TaskLevel.Three, rethrowWhenException: false)] + private Task DailyVipMallBuyMissionAsync(CancellationToken cancellationToken = default) + { + logger.LogInformation("需购买,跳过"); + return Task.CompletedTask; + } + + [TaskInterceptor("日常4:浏览追番频道", TaskLevel.Three, rethrowWhenException: false)] + private async Task DailyAnimateTabMissionAsync( + VipBigPointCombine combine, + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + await vipBigPointDomainService.ReceiveAndCompleteAsync( + combine, + "日常任务", + "animatetab", + ck, + async (_, _) => await vipBigPointDomainService.CompleteViewAsync("animatetab", ck) + ); + } + + [TaskInterceptor("日常5:浏览影视频道", TaskLevel.Three, rethrowWhenException: false)] + private async Task DailyFilmTabMissionAsync( + VipBigPointCombine combine, + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + await vipBigPointDomainService.ReceiveAndCompleteAsync( + combine, + "日常任务", + "filmtab", + ck, + async (_, _) => await vipBigPointDomainService.CompleteViewAsync("filmtab", ck) + ); + } + + [TaskInterceptor("日常6:观看剧集", TaskLevel.Three, rethrowWhenException: false)] + private async Task DailyOgvWatchMissionAsync( + VipBigPointCombine combine, + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + await vipBigPointDomainService.ReceiveAndCompleteAsync( + combine, + "日常任务", + "ogvwatchnew", + ck, + async (_, _) => await vipBigPointDomainService.CompleteV2Async("ogvwatchnew", ck) + ); + } + + [TaskInterceptor("日常7:购买影片", TaskLevel.Three, rethrowWhenException: false)] + private Task DailyTvOdBuyMissionAsync(CancellationToken cancellationToken = default) + { + logger.LogInformation("需购买,跳过"); + return Task.CompletedTask; + } + + [TaskInterceptor("日常8:购买装扮", TaskLevel.Three, rethrowWhenException: false)] + private Task DailyDressBuyAmountMissionAsync(CancellationToken cancellationToken = default) + { + logger.LogInformation("需购买,跳过"); + return Task.CompletedTask; + } +} diff --git a/src/Ray.BiliBiliTool.Application/VipPrivilegeTaskAppService.cs b/src/Ray.BiliBiliTool.Application/VipPrivilegeTaskAppService.cs new file mode 100644 index 0000000..a89f77e --- /dev/null +++ b/src/Ray.BiliBiliTool.Application/VipPrivilegeTaskAppService.cs @@ -0,0 +1,110 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Application.Attributes; +using Ray.BiliBiliTool.Application.Contracts; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Cookie; +using Ray.BiliBiliTool.Infrastructure.Enums; + +namespace Ray.BiliBiliTool.Application; + +public class VipPrivilegeTaskAppService( + ILogger logger, + IOptionsMonitor vipPrivilegeOptions, + IAccountDomainService accountDomainService, + IVipPrivilegeDomainService vipPrivilegeDomainService, + ILoginDomainService loginDomainService, + IConfiguration configuration, + CookieStrFactory cookieStrFactory +) : BaseMultiAccountsAppService(logger, cookieStrFactory), IVipPrivilegeTaskAppService +{ + [TaskInterceptor("领取大会员福利任务", TaskLevel.One)] + protected override async Task DoTaskAccountAsync( + BiliCookie ck, + CancellationToken cancellationToken = default + ) + { + if (!vipPrivilegeOptions.CurrentValue.IsEnable) + { + logger.LogInformation("已配置为关闭,跳过"); + return; + } + + await SetCookiesAsync(ck, cancellationToken); + UserInfo userInfo = await Login(ck); + + await ReceiveVipPrivilege(userInfo, ck); + } + + [TaskInterceptor("Set Cookie")] + private async Task SetCookiesAsync(BiliCookie biliCookie, CancellationToken cancellationToken) + { + //判断cookie是否完整 + if (!string.IsNullOrWhiteSpace(biliCookie.Buvid)) + { + logger.LogInformation("Cookie完整,不需要Set Cookie"); + return; + } + + //Set + logger.LogInformation("开始Set Cookie"); + var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken); + + //持久化 + logger.LogInformation("持久化Cookie"); + await SaveCookieAsync(ck, cancellationToken); + } + + /// + /// 登录 + /// + /// + [TaskInterceptor("登录")] + private async Task Login(BiliCookie ck) + { + UserInfo userInfo = await accountDomainService.LoginByCookie(ck); + return userInfo; + } + + /// + /// 每月领取大会员福利 + /// + [TaskInterceptor("领取", rethrowWhenException: false)] + private async Task ReceiveVipPrivilege(UserInfo userInfo, BiliCookie ck) + { + var suc = await vipPrivilegeDomainService.ReceiveVipPrivilege(userInfo, ck); + + //如果领取成功,需要刷新账户信息(比如B币余额) + if (suc) + { + try + { + await accountDomainService.LoginByCookie(ck); + } + catch (Exception ex) + { + logger.LogError("领取福利成功,但之后刷新用户信息时异常,信息:{msg}", ex.Message); + } + } + } + + private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken) + { + var platformType = configuration.GetSection("PlatformType").Get(); + logger.LogInformation("当前运行平台:{platform}", platformType); + + //更新cookie到青龙env + if (platformType == PlatformType.QingLong) + { + await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken); + return; + } + + //更新cookie到json + await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken); + } +} diff --git a/src/Ray.BiliBiliTool.Config/Constants.cs b/src/Ray.BiliBiliTool.Config/Constants.cs new file mode 100644 index 0000000..f3580da --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Constants.cs @@ -0,0 +1,77 @@ +namespace Ray.BiliBiliTool.Config; + +public static class Constants +{ + /// + /// 每天的最大投币数,优先级最高,默认每天最多投5个币(包含已投过的数量) + /// + public static int MaxNumberOfDonateCoins = 5; + + /// + /// 每天可获取的满额经验值 + /// + public static int EveryDayExp = 65; + + /// + /// 开源地址 + /// + public static string SourceCodeUrl = "https://github.com/RayWangQvQ/BiliBiliToolPro"; + + public static string FallbackAutoChargeUpId = "220893216"; + + /// + /// 每日任务exp + /// + /// + public static readonly Dictionary ExpDic = new() + { + { "每日登录", 5 }, + { "每日观看视频", 5 }, + { "每日分享视频", 5 }, + { "每日投币", 10 }, + }; + + /// + /// 投币接口的data.code返回以下这些状态码,则可以继续尝试投币 + /// 如返回除这些之外的状态码,则终止投币流程,不进行无意义的尝试 + /// (比如返回-101:账号未登录;-102:账号被封停;-111:csrf校验失败等) + /// + /// + public static readonly Dictionary DonateCoinCanContinueStatusDic = new() + { + { "0", "成功" }, + { "-400", "请求错误" }, + { "10003", "不存在该稿件" }, + { "34002", "不能给自己投币" }, + { "34003", "非法的投币数量" }, + { "34004", "投币间隔太短" }, + { "34005", "超过投币上限" }, + }; + + public static readonly Dictionary CommandLineMappingsDic = new() + { + { "--cookieStr1", "BiliBiliCookies:1" }, + { "--runTasks", "RunTasks" }, + { "--randomSleep", "Security:RandomSleepMaxMin" }, + { "--numberOfCoins", "DailyTaskConfig:NumberOfCoins" }, + { "--numberOfProtectedCoins", "DailyTaskConfig:NumberOfProtectedCoins" }, + { "--saveCoinsWhenLv6", "DailyTaskConfig:SaveCoinsWhenLv6" }, + { "--selectLike", "DailyTaskConfig:SelectLike" }, + { "--supportUpIds", "DailyTaskConfig:SupportUpIds" }, + { "--dayOfAutoCharge", "DailyTaskConfig:DayOfAutoCharge" }, + { "--autoChargeUpId", "DailyTaskConfig:AutoChargeUpId" }, + { "--dayOfReceiveVipPrivilege", "DailyTaskConfig:DayOfReceiveVipPrivilege" }, + { "--isExchangeSilver2Coin", "DailyTaskConfig:IsExchangeSilver2Coin" }, + { "--devicePlatform", "DailyTaskConfig:DevicePlatform" }, + { "--excludeAwardNames", "LiveLotteryTaskConfig:ExcludeAwardNames" }, + { "--includeAwardNames", "LiveLotteryTaskConfig:INCLUDEAWARDNAMES" }, + { "--unfollowGroup", "UnfollowBatchedTaskConfig:GroupName" }, + { "--unfollowCount", "UnfollowBatchedTaskConfig:Count" }, + { "--intervalSecondsBetweenRequestApi", "Security:IntervalSecondsBetweenRequestApi" }, + { "--intervalMethodTypes", "Security:IntervalMethodTypes" }, + { "--pushScKey", "Serilog:WriteTo:6:Args:scKey" }, + { "--proxy", "WebProxy" }, + }; + + public const string SqliteTableName = "bili_appsettings"; +} diff --git a/src/Ray.BiliBiliTool.Config/Extensions/ServiceCollectionExtension.cs b/src/Ray.BiliBiliTool.Config/Extensions/ServiceCollectionExtension.cs new file mode 100644 index 0000000..e17dae1 --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Extensions/ServiceCollectionExtension.cs @@ -0,0 +1,47 @@ +using System.Text.Json; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.Infrastructure; + +namespace Ray.BiliBiliTool.Config.Extensions; + +public static class ServiceCollectionExtension +{ + /// + /// 注册配置 + /// + /// + /// + public static IServiceCollection AddBiliBiliConfigs( + this IServiceCollection services, + IConfiguration configuration + ) + { + //Options + services + .AddOptions() + .Configure(o => o = JsonSerializerOptionsBuilder.DefaultOptions) + .Configure(configuration.GetSection("BiliBiliCookie")) + .Configure(configuration.GetSection("DailyTaskConfig")) + .Configure(configuration.GetSection("MangaTaskConfig")) + .Configure( + configuration.GetSection("MangaPrivilegeTaskConfig") + ) + .Configure(configuration.GetSection("Silver2CoinTaskConfig")) + .Configure(configuration.GetSection("ChargeTaskConfig")) + .Configure(configuration.GetSection("LiveLotteryTaskConfig")) + .Configure( + configuration.GetSection("UnfollowBatchedTaskConfig") + ) + .Configure(configuration.GetSection("VipBigPointConfig")) + .Configure(configuration.GetSection("Security")) + .Configure(configuration.GetSection("VipPrivilegeConfig")) + .Configure( + configuration.GetSection("LiveFansMedalTaskConfig") + ) + .Configure(configuration.GetSection("QingLongConfig")); + + return services; + } +} diff --git a/src/Ray.BiliBiliTool.Config/IHasCron.cs b/src/Ray.BiliBiliTool.Config/IHasCron.cs new file mode 100644 index 0000000..dc70ba3 --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/IHasCron.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Config; + +public interface IHasCron +{ + public string? Cron { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Config/Options/BaseConfigOptions.cs b/src/Ray.BiliBiliTool.Config/Options/BaseConfigOptions.cs new file mode 100644 index 0000000..ee511b7 --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/BaseConfigOptions.cs @@ -0,0 +1,57 @@ +namespace Ray.BiliBiliTool.Config.Options; + +/// +/// 基础配置选项类,包含所有配置共有的属性 +/// +public abstract class BaseConfigOptions : IHasCron, IConfigOptions +{ + /// + /// 定时任务Cron表达式 + /// + public string? Cron { get; set; } + + /// + /// 是否启用该任务 + /// + public bool IsEnable { get; set; } = true; + + /// + /// 配置节名称,由子类实现 + /// + public abstract string SectionName { get; } + + /// + /// 转换为配置字典,子类可以重写以添加更多配置项 + /// + public virtual Dictionary ToConfigDictionary() + { + return GetBaseConfigDictionary(); + } + + /// + /// 获取基础配置字典,避免循环调用 + /// + protected Dictionary GetBaseConfigDictionary() + { + return new Dictionary + { + { $"{SectionName}:{nameof(Cron)}", Cron ?? "" }, + { $"{SectionName}:{nameof(IsEnable)}", IsEnable.ToString().ToLower() }, + }; + } + + /// + /// 合并配置字典,用于子类添加额外配置项 + /// + protected Dictionary MergeConfigDictionary( + Dictionary additionalConfig + ) + { + var baseConfig = GetBaseConfigDictionary(); + foreach (var kvp in additionalConfig) + { + baseConfig[kvp.Key] = kvp.Value; + } + return baseConfig; + } +} diff --git a/src/Ray.BiliBiliTool.Config/Options/BiliBiliCookieOptions.cs b/src/Ray.BiliBiliTool.Config/Options/BiliBiliCookieOptions.cs new file mode 100644 index 0000000..105ab21 --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/BiliBiliCookieOptions.cs @@ -0,0 +1,9 @@ +namespace Ray.BiliBiliTool.Config.Options; + +/// +/// Cookie信息 +/// +public class BiliBiliCookieOptions +{ + public string? CookieStr { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Config/Options/ChargeTaskOptions.cs b/src/Ray.BiliBiliTool.Config/Options/ChargeTaskOptions.cs new file mode 100644 index 0000000..856a5bb --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/ChargeTaskOptions.cs @@ -0,0 +1,59 @@ +namespace Ray.BiliBiliTool.Config.Options; + +public class ChargeTaskOptions : BaseConfigOptions +{ + public override string SectionName => "ChargeTaskConfig"; + + /// + /// 充电Up主Id + /// + public string? AutoChargeUpId { get; set; } + + private string? _chargeComment; + + /// + /// 充电后留言 + /// + public string ChargeComment + { + get => + string.IsNullOrWhiteSpace(_chargeComment) + ? DefaultComments[new Random().Next(0, DefaultComments.Count)] + : _chargeComment; + set => _chargeComment = value; + } + + private static readonly List DefaultComments = + [ + "棒", + "棒唉", + "棒耶", + "加油~", + "UP加油!", + "支持~", + "支持支持!", + "催更啦", + "顶顶", + "留下脚印~", + "干杯", + "bilibili干杯", + "o(* ̄▽ ̄*)o", + "(。・∀・)ノ゙嗨", + "(●ˇ∀ˇ●)", + "( •̀ ω •́ )y", + "(ง •_•)ง", + ">.<", + "^_~", + ]; + + public override Dictionary ToConfigDictionary() + { + return MergeConfigDictionary( + new Dictionary + { + { $"{SectionName}:{nameof(AutoChargeUpId)}", AutoChargeUpId ?? "" }, + { $"{SectionName}:{nameof(ChargeComment)}", _chargeComment ?? "" }, + } + ); + } +} diff --git a/src/Ray.BiliBiliTool.Config/Options/DailyTaskOptions.cs b/src/Ray.BiliBiliTool.Config/Options/DailyTaskOptions.cs new file mode 100644 index 0000000..2a8678f --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/DailyTaskOptions.cs @@ -0,0 +1,121 @@ +namespace Ray.BiliBiliTool.Config.Options; + +/// +/// 程序自定义个性化配置 +/// +public class DailyTaskOptions : BaseConfigOptions +{ + public override string SectionName => "DailyTaskConfig"; + + /// + /// 是否观看视频 + /// + public bool IsWatchVideo { get; set; } + + /// + /// 是否分享视频 + /// + public bool IsShareVideo { get; set; } + + /// + /// 是否开启专栏投币模式 + /// + public bool IsDonateCoinForArticle { get; set; } + + /// + /// 每日设定的投币数 [0,5] + /// + public int NumberOfCoins { get; set; } = 5; + + /// + /// 要保留的硬币数量 [0,int_max] + /// + public int NumberOfProtectedCoins { get; set; } = 0; + + /// + /// 达到六级后是否开始白嫖 + /// + public bool SaveCoinsWhenLv6 { get; set; } = false; + + /// + /// 投币时是否点赞[false,true] + /// + public bool SelectLike { get; set; } = false; + + /// + /// 优先选择支持的up主Id集合,配置后会优先从指定的up主下挑选视频进行观看、分享和投币,不配置则从排行耪随机获取支持视频 + /// + public string? SupportUpIds { get; set; } + + /// + /// 执行客户端操作时的平台 [ios,android] + /// + public string DevicePlatform { get; set; } = "android"; + + public List SupportUpIdList + { + get + { + List re = []; + if (string.IsNullOrWhiteSpace(SupportUpIds) | SupportUpIds == "-1") + return re; + + string[] array = SupportUpIds?.Split(',') ?? []; + foreach (string item in array) + { + re.Add(long.TryParse(item.Trim(), out long upId) ? upId : long.MinValue); + } + return re; + } + } + + private static readonly List DefaultComments = + [ + "棒", + "棒唉", + "棒耶", + "加油~", + "UP加油!", + "支持~", + "支持支持!", + "催更啦", + "顶顶", + "留下脚印~", + "干杯", + "bilibili干杯", + "o(* ̄▽ ̄*)o", + "(。・∀・)ノ゙嗨", + "(●ˇ∀ˇ●)", + "( •̀ ω •́ )y", + "(ง •_•)ง", + ">.<", + "^_~", + ]; + + public override Dictionary ToConfigDictionary() + { + return MergeConfigDictionary( + new Dictionary + { + { $"{SectionName}:{nameof(IsWatchVideo)}", IsWatchVideo.ToString().ToLower() }, + { $"{SectionName}:{nameof(IsShareVideo)}", IsShareVideo.ToString().ToLower() }, + { + $"{SectionName}:{nameof(IsDonateCoinForArticle)}", + IsDonateCoinForArticle.ToString().ToLower() + }, + { $"{SectionName}:{nameof(NumberOfCoins)}", NumberOfCoins.ToString() }, + { + $"{SectionName}:{nameof(NumberOfProtectedCoins)}", + NumberOfProtectedCoins.ToString() + }, + { + $"{SectionName}:{nameof(SaveCoinsWhenLv6)}", + SaveCoinsWhenLv6.ToString().ToLower() + }, + { $"{SectionName}:{nameof(SelectLike)}", SelectLike.ToString().ToLower() }, + { $"{SectionName}:{nameof(SupportUpIds)}", SupportUpIds ?? "" }, + { $"{SectionName}:{nameof(DevicePlatform)}", DevicePlatform }, + } + ); + } +} diff --git a/src/Ray.BiliBiliTool.Config/Options/IConfigOptions.cs b/src/Ray.BiliBiliTool.Config/Options/IConfigOptions.cs new file mode 100644 index 0000000..6868493 --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/IConfigOptions.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Config.Options; + +public interface IConfigOptions +{ + Dictionary ToConfigDictionary(); +} diff --git a/src/Ray.BiliBiliTool.Config/Options/LiveFansMedalTaskOptions.cs b/src/Ray.BiliBiliTool.Config/Options/LiveFansMedalTaskOptions.cs new file mode 100644 index 0000000..8fb3f05 --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/LiveFansMedalTaskOptions.cs @@ -0,0 +1,71 @@ +namespace Ray.BiliBiliTool.Config.Options; + +/// +/// 粉丝牌等级任务相关配置 +/// +public class LiveFansMedalTaskOptions : BaseConfigOptions +{ + public override string SectionName => "LiveFansMedalTaskConfig"; + + /// + /// 自定义发送弹幕内容,如 "打卡" 等来触发直播间内机器人关键词 + /// + public string DanmakuContent { get; set; } = "OvO"; + + /// + /// 心跳包发送的个数 / 挂机的时间,单位为分钟 + /// + public int HeartBeatNumber { get; set; } = 70; + + /// + /// 当心跳包发送连续失败多少次时放弃 + /// + public int HeartBeatSendGiveUpThreshold { get; set; } = 5; + + /// + /// 对于直播时长任务是否跳过粉丝牌等级大于等于 20 的 + /// + public bool IsSkipLevel20Medal { get; set; } = true; + + public const int HeartBeatInterval = 60; + + /// + /// 点赞次数,默认值为30(用于点亮粉丝勋章) + /// + public int LikeNumber { get; set; } = 30; + + /// + /// 发送弹幕次数 + /// + public int SendDanmakuNumber { get; set; } = 1; + + /// + /// 弹幕发送失败多少次时放弃 + /// + public int SendDanmakugiveUpThreshold { get; set; } = 3; + + public override Dictionary ToConfigDictionary() + { + return MergeConfigDictionary( + new Dictionary + { + { $"{SectionName}:{nameof(DanmakuContent)}", DanmakuContent }, + { $"{SectionName}:{nameof(HeartBeatNumber)}", HeartBeatNumber.ToString() }, + { + $"{SectionName}:{nameof(HeartBeatSendGiveUpThreshold)}", + HeartBeatSendGiveUpThreshold.ToString() + }, + { + $"{SectionName}:{nameof(IsSkipLevel20Medal)}", + IsSkipLevel20Medal.ToString().ToLower() + }, + { $"{SectionName}:{nameof(LikeNumber)}", LikeNumber.ToString() }, + { $"{SectionName}:{nameof(SendDanmakuNumber)}", SendDanmakuNumber.ToString() }, + { + $"{SectionName}:{nameof(SendDanmakugiveUpThreshold)}", + SendDanmakugiveUpThreshold.ToString() + }, + } + ); + } +} diff --git a/src/Ray.BiliBiliTool.Config/Options/LiveLotteryTaskOptions.cs b/src/Ray.BiliBiliTool.Config/Options/LiveLotteryTaskOptions.cs new file mode 100644 index 0000000..71c725f --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/LiveLotteryTaskOptions.cs @@ -0,0 +1,45 @@ +namespace Ray.BiliBiliTool.Config.Options; + +public class LiveLotteryTaskOptions : BaseConfigOptions +{ + public override string SectionName => "LiveLotteryTaskConfig"; + + public string? IncludeAwardNames { get; set; } + + public string? ExcludeAwardNames { get; set; } + + public List IncludeAwardNameList => + IncludeAwardNames + ?.Split("|", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .ToList() ?? new List(); + + public List ExcludeAwardNameList => + ExcludeAwardNames + ?.Split("|", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .ToList() ?? new List(); + + public bool AutoGroupFollowings { get; set; } = true; + + public string? DenyUids { get; set; } + + public List DenyUidList => + DenyUids + ?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .ToList() ?? new List(); + + public override Dictionary ToConfigDictionary() + { + return MergeConfigDictionary( + new Dictionary + { + { $"{SectionName}:{nameof(IncludeAwardNames)}", IncludeAwardNames ?? "" }, + { $"{SectionName}:{nameof(ExcludeAwardNames)}", ExcludeAwardNames ?? "" }, + { + $"{SectionName}:{nameof(AutoGroupFollowings)}", + AutoGroupFollowings.ToString().ToLower() + }, + { $"{SectionName}:{nameof(DenyUids)}", DenyUids ?? "" }, + } + ); + } +} diff --git a/src/Ray.BiliBiliTool.Config/Options/MangaPrivilegeTaskOptions.cs b/src/Ray.BiliBiliTool.Config/Options/MangaPrivilegeTaskOptions.cs new file mode 100644 index 0000000..599ae3b --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/MangaPrivilegeTaskOptions.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Config.Options; + +public class MangaPrivilegeTaskOptions : BaseConfigOptions +{ + public override string SectionName => "MangaPrivilegeTaskConfig"; +} diff --git a/src/Ray.BiliBiliTool.Config/Options/MangaTaskOptions.cs b/src/Ray.BiliBiliTool.Config/Options/MangaTaskOptions.cs new file mode 100644 index 0000000..c64c3d0 --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/MangaTaskOptions.cs @@ -0,0 +1,27 @@ +namespace Ray.BiliBiliTool.Config.Options; + +public class MangaTaskOptions : BaseConfigOptions +{ + public override string SectionName => "MangaTaskConfig"; + + /// + /// 自定义漫画阅读 comic_id + /// + public long CustomComicId { get; set; } = 27355; + + /// + /// 自定义漫画阅读 ep_id + /// + public long CustomEpId { get; set; } = 381662; + + public override Dictionary ToConfigDictionary() + { + return MergeConfigDictionary( + new Dictionary + { + { $"{SectionName}:{nameof(CustomComicId)}", CustomComicId.ToString() }, + { $"{SectionName}:{nameof(CustomEpId)}", CustomEpId.ToString() }, + } + ); + } +} diff --git a/src/Ray.BiliBiliTool.Config/Options/QingLongOptions.cs b/src/Ray.BiliBiliTool.Config/Options/QingLongOptions.cs new file mode 100644 index 0000000..315e269 --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/QingLongOptions.cs @@ -0,0 +1,8 @@ +namespace Ray.BiliBiliTool.Config.Options; + +public class QingLongOptions +{ + public string? ClientId { get; set; } + + public string? ClientSecret { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Config/Options/SecurityOptions.cs b/src/Ray.BiliBiliTool.Config/Options/SecurityOptions.cs new file mode 100644 index 0000000..c0d68ce --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/SecurityOptions.cs @@ -0,0 +1,69 @@ +namespace Ray.BiliBiliTool.Config.Options; + +/// +/// 安全相关配置 +/// +public class SecurityOptions +{ + /// + /// 是否跳过执行任务,用于特殊情况下,通过配置灵活的开启和关闭任务 + /// + public bool IsSkipDailyTask { get; set; } = false; + + /// + /// 随机睡眠的最大时长,用于使每天运行时间在范围内相对随机 + /// + public int RandomSleepMaxMin { get; set; } = 10; + + /// + /// 两次调用api之间间隔的秒数[0,+] + /// 有人担心在几秒内连续调用api会被b站安全机制发现,所以为不放心的朋友添加了间隔秒数配置,两次调用Api之间会大于该秒数 + /// + public int IntervalSecondsBetweenRequestApi { get; set; } = 3; + + /// + /// 间隔秒数所针对的HttpMethod,多个用英文逗号隔开,当前有GET和POST两种,可配置如“GET,POST” + /// 服务器一般对GET请求不是很敏感,建议只针对POST请求做间隔就可以了 + /// + public string IntervalMethodTypes { get; set; } = "GET,POST"; + + public List GetIntervalMethods() + { + List result = new List(); + if (string.IsNullOrWhiteSpace(IntervalMethodTypes)) + return result; + + foreach (var item in IntervalMethodTypes.Split(',')) + { + try + { + HttpMethod method = new HttpMethod(item); + if (method != null && !result.Contains(method)) + result.Add(method); + } + catch (Exception) + { + //ignore + } + } + + return result; + } + + /// + /// 请求B站接口时头部传递的User-Agent + /// + public string UserAgent { get; set; } = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36 Edg/87.0.664.41"; + + /// + /// App请求B站接口时头部传递的User-Agent + /// + public string UserAgentApp { get; set; } = + "Mozilla/5.0 (Linux; Android 12; SM-S9080 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Mobile Safari/537.36 os/android model/SM-S9080 build/7760700 osVer/12 sdkInt/32 network/2 BiliApp/7760700 mobi_app/android channel/bili innerVer/7760710 c_locale/zh_CN s_locale/zh_CN disable_rcmd/0 7.76.0 os/android model/SM-S9080 mobi_app/android build/7760700 channel/bili innerVer/7760710 osVer/12 network/2"; + + /// + /// 代理 + /// + public string? WebProxy { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Config/Options/Silver2CoinTaskOptions.cs b/src/Ray.BiliBiliTool.Config/Options/Silver2CoinTaskOptions.cs new file mode 100644 index 0000000..e779dcb --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/Silver2CoinTaskOptions.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Config.Options; + +public class Silver2CoinTaskOptions : BaseConfigOptions +{ + public override string SectionName => "Silver2CoinTaskConfig"; +} diff --git a/src/Ray.BiliBiliTool.Config/Options/UnfollowBatchedTaskOptions.cs b/src/Ray.BiliBiliTool.Config/Options/UnfollowBatchedTaskOptions.cs new file mode 100644 index 0000000..eebb0ec --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/UnfollowBatchedTaskOptions.cs @@ -0,0 +1,30 @@ +namespace Ray.BiliBiliTool.Config.Options; + +public class UnfollowBatchedTaskOptions : BaseConfigOptions +{ + public override string SectionName => "UnfollowBatchedTaskConfig"; + private const string DefaultGroupName = "天选时刻"; + + public string GroupName { get; set; } = DefaultGroupName; + + public int Count { get; set; } + + public string? RetainUids { get; set; } + + public List RetainUidList => + RetainUids + ?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .ToList() ?? new List(); + + public override Dictionary ToConfigDictionary() + { + return MergeConfigDictionary( + new Dictionary + { + { $"{SectionName}:{nameof(GroupName)}", GroupName }, + { $"{SectionName}:{nameof(Count)}", Count.ToString() }, + { $"{SectionName}:{nameof(RetainUids)}", RetainUids ?? "" }, + } + ); + } +} diff --git a/src/Ray.BiliBiliTool.Config/Options/VipBigPointOptions.cs b/src/Ray.BiliBiliTool.Config/Options/VipBigPointOptions.cs new file mode 100644 index 0000000..8917025 --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/VipBigPointOptions.cs @@ -0,0 +1,38 @@ +namespace Ray.BiliBiliTool.Config.Options; + +public class VipBigPointOptions : BaseConfigOptions +{ + public override string SectionName => "VipBigPointConfig"; + + public string? ViewBangumis { get; set; } + + public List ViewBangumiList + { + get + { + List re = []; + if (string.IsNullOrWhiteSpace(ViewBangumis) | ViewBangumis == "-1") + return re; + + string[] array = ViewBangumis?.Split(',') ?? []; + foreach (string item in array) + { + if (long.TryParse(item.Trim(), out long upId)) + re.Add(upId); + else + re.Add(long.MinValue); + } + return re; + } + } + + public override Dictionary ToConfigDictionary() + { + return MergeConfigDictionary( + new Dictionary + { + { $"{SectionName}:{nameof(ViewBangumis)}", ViewBangumis ?? "" }, + } + ); + } +} diff --git a/src/Ray.BiliBiliTool.Config/Options/VipPrivilegeOptions.cs b/src/Ray.BiliBiliTool.Config/Options/VipPrivilegeOptions.cs new file mode 100644 index 0000000..5b49ae3 --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Options/VipPrivilegeOptions.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Config.Options; + +public class VipPrivilegeOptions : BaseConfigOptions +{ + public override string SectionName => "VipPrivilegeConfig"; +} diff --git a/src/Ray.BiliBiliTool.Config/Ray.BiliBiliTool.Config.csproj b/src/Ray.BiliBiliTool.Config/Ray.BiliBiliTool.Config.csproj new file mode 100644 index 0000000..91cd60c --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/Ray.BiliBiliTool.Config.csproj @@ -0,0 +1,16 @@ + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/Ray.BiliBiliTool.Config/SQLite/SqliteConfigurationExtensions.cs b/src/Ray.BiliBiliTool.Config/SQLite/SqliteConfigurationExtensions.cs new file mode 100644 index 0000000..94d0f3e --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/SQLite/SqliteConfigurationExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Configuration; + +namespace Ray.BiliBiliTool.Config.SQLite; + +public static class SqliteConfigurationExtensions +{ + public static IConfigurationBuilder AddSqlite( + this IConfigurationBuilder builder, + string connectionString, + string tableName = "AppSettings", + string keyColumnName = "Key", + string valueColumnName = "Value" + ) + { + return builder.Add( + new SqliteConfigurationSource + { + ConnectionString = connectionString, + TableName = tableName, + KeyColumnName = keyColumnName, + ValueColumnName = valueColumnName, + } + ); + } +} diff --git a/src/Ray.BiliBiliTool.Config/SQLite/SqliteConfigurationProvider.cs b/src/Ray.BiliBiliTool.Config/SQLite/SqliteConfigurationProvider.cs new file mode 100644 index 0000000..a0e679e --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/SQLite/SqliteConfigurationProvider.cs @@ -0,0 +1,98 @@ +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; + +namespace Ray.BiliBiliTool.Config.SQLite; + +public class SqliteConfigurationProvider(SqliteConfigurationSource source) : ConfigurationProvider +{ + private readonly string _connectionString = + source.ConnectionString ?? throw new ArgumentNullException(nameof(source.ConnectionString)); + private readonly string _tableName = source.TableName ?? "AppSettings"; + private readonly string _keyColumnName = source.KeyColumnName ?? "Key"; + private readonly string _valueColumnName = source.ValueColumnName ?? "Value"; + + public override void Load() + { + Data = new Dictionary(StringComparer.OrdinalIgnoreCase); + + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + + EnsureTableExists(connection); + + using var command = connection.CreateCommand(); + command.CommandText = + $"SELECT [{_keyColumnName}], [{_valueColumnName}] FROM [{_tableName}]"; + + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + string key = reader.GetString(0); + string value = reader.GetString(1); + Data[key] = value; + } + } + + private void EnsureTableExists(SqliteConnection connection) + { + using var command = connection.CreateCommand(); + command.CommandText = + $@" + CREATE TABLE IF NOT EXISTS [{_tableName}] ( + [{_keyColumnName}] TEXT PRIMARY KEY, + [{_valueColumnName}] TEXT NOT NULL + )"; + command.ExecuteNonQuery(); + } + + public override void Set(string key, string? value) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = + $@" + INSERT OR REPLACE INTO [{_tableName}] ([{_keyColumnName}], [{_valueColumnName}]) + VALUES (@key, @value)"; + command.Parameters.AddWithValue("@key", key); + command.Parameters.AddWithValue("@value", value); + command.ExecuteNonQuery(); + + Data[key] = value; + } + + public void BatchSet(Dictionary configValues) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + + using var transaction = connection.BeginTransaction(); + using var command = connection.CreateCommand(); + command.Transaction = transaction; + + try + { + foreach (var kvp in configValues) + { + command.CommandText = + $@" + INSERT OR REPLACE INTO [{_tableName}] ([{_keyColumnName}], [{_valueColumnName}]) + VALUES (@key, @value)"; + command.Parameters.Clear(); + command.Parameters.AddWithValue("@key", kvp.Key); + command.Parameters.AddWithValue("@value", kvp.Value ?? (object)DBNull.Value); + command.ExecuteNonQuery(); + + Data[kvp.Key] = kvp.Value; + } + + transaction.Commit(); + } + catch + { + transaction.Rollback(); + throw; + } + } +} diff --git a/src/Ray.BiliBiliTool.Config/SQLite/SqliteConfigurationSource.cs b/src/Ray.BiliBiliTool.Config/SQLite/SqliteConfigurationSource.cs new file mode 100644 index 0000000..0ede17b --- /dev/null +++ b/src/Ray.BiliBiliTool.Config/SQLite/SqliteConfigurationSource.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Configuration; + +namespace Ray.BiliBiliTool.Config.SQLite; + +public class SqliteConfigurationSource : IConfigurationSource +{ + public string? ConnectionString { get; set; } + public string? TableName { get; set; } + public string? KeyColumnName { get; set; } + public string? ValueColumnName { get; set; } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new SqliteConfigurationProvider(this); + } +} diff --git a/src/Ray.BiliBiliTool.Console/BiliBiliToolHostedService.cs b/src/Ray.BiliBiliTool.Console/BiliBiliToolHostedService.cs new file mode 100644 index 0000000..1855031 --- /dev/null +++ b/src/Ray.BiliBiliTool.Console/BiliBiliToolHostedService.cs @@ -0,0 +1,153 @@ +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Application.Contracts; +using Ray.BiliBiliTool.Config.Options; +using Ray.Serilog.Sinks.Batched; +using Constants = Ray.BiliBiliTool.Config.Constants; + +namespace Ray.BiliBiliTool.Console; + +public class BiliBiliToolHostedService( + IHostApplicationLifetime applicationLifetime, + IServiceProvider serviceProvider, + IHostEnvironment environment, + IConfiguration configuration, + ILogger logger, + IOptionsMonitor securityOptions +) : IHostedService +{ + private readonly SecurityOptions _securityOptions = securityOptions.CurrentValue; + + public async Task StartAsync(CancellationToken cancellationToken) + { + try + { + logger.LogInformation("BiliBiliToolPro 开始运行..." + Environment.NewLine); + + bool pass = await PreCheckAsync(cancellationToken); + if (!pass) + return; + + await RandomSleepAsync(cancellationToken); + + string[] tasks = await ReadTargetTasksAsync(cancellationToken); + logger.LogInformation("【目标任务】{tasks}", string.Join(",", tasks)); + await DoTasksAsync(tasks, cancellationToken); + } + catch (Exception ex) + { + logger.LogError("程序异常终止,原因:{msg}", ex.Message); + throw; + } + finally + { + LogAppInfo(); + + //环境 + logger.LogInformation("运行环境:{env}", environment.EnvironmentName); + logger.LogInformation( + "应用目录:{path}" + Environment.NewLine, + environment.ContentRootPath + ); + logger.LogInformation("运行结束"); + + //自动退出 + applicationLifetime.StopApplication(); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + private Task PreCheckAsync(CancellationToken cancellationToken) + { + //是否跳过 + if (_securityOptions.IsSkipDailyTask) + { + logger.LogWarning("已配置为跳过任务" + Environment.NewLine); + return Task.FromResult(false); + } + + return Task.FromResult(true); + } + + private async Task RandomSleepAsync(CancellationToken cancellationToken) + { + if ( + configuration["RunTasks"]!.Contains("Login") + || configuration["RunTasks"]!.Contains("Test") + ) + return; + + if (_securityOptions.RandomSleepMaxMin > 0) + { + int randomMin = new Random().Next(1, ++_securityOptions.RandomSleepMaxMin); + logger.LogInformation("随机休眠{min}分钟" + Environment.NewLine, randomMin); + await Task.Delay(randomMin * 1000 * 60, cancellationToken); + } + } + + /// + /// 读取目标任务 + /// + /// + /// + private Task ReadTargetTasksAsync(CancellationToken cancellationToken) + { + string[] tasks = configuration["RunTasks"]!.Split( + "&", + options: StringSplitOptions.RemoveEmptyEntries + ); + if (tasks.Any()) + { + return Task.FromResult(tasks); + } + + logger.LogInformation("未指定目标任务,请选择要运行的任务:"); + TaskTypeFactory.Show(logger); + logger.LogInformation("请输入:"); + + while (true) + { + var index = System.Console.ReadLine(); + bool suc = int.TryParse(index, out int num); + if (suc) + { + string code = TaskTypeFactory.GetCodeByIndex(num); + configuration["RunTasks"] = code; + return Task.FromResult(new[] { code }); + } + + logger.LogWarning("输入异常,请输入序号"); + } + } + + private async Task DoTasksAsync(string[] tasks, CancellationToken cancellationToken) + { + using IServiceScope scope = serviceProvider.CreateScope(); + foreach (string task in tasks) + { + var type = TaskTypeFactory.Get(task); + + IAppService appService = (IAppService)scope.ServiceProvider.GetRequiredService(type); + await appService.DoTaskAsync(cancellationToken); + } + } + + private void LogAppInfo() + { + logger.LogInformation(Environment.NewLine + "========================"); + logger.LogInformation( + "v{version} 开源 by {url}", + typeof(Program).Assembly.GetName().Version?.ToString(), + Constants.SourceCodeUrl + Environment.NewLine + ); + //_logger.LogInformation("【当前IP】{ip} ", IpHelper.GetIp()); + } +} diff --git a/src/Ray.BiliBiliTool.Console/Program.cs b/src/Ray.BiliBiliTool.Console/Program.cs new file mode 100644 index 0000000..97949a1 --- /dev/null +++ b/src/Ray.BiliBiliTool.Console/Program.cs @@ -0,0 +1,143 @@ +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Ray.BiliBiliTool.Agent.Extensions; +using Ray.BiliBiliTool.Application.Extensions; +using Ray.BiliBiliTool.Config.Extensions; +using Ray.BiliBiliTool.DomainService.Extensions; +using Ray.BiliBiliTool.Infrastructure; +using Serilog; +using Serilog.Debugging; + +namespace Ray.BiliBiliTool.Console; + +public class Program +{ + public static async Task Main(string[] args) + { + System.Console.CancelKeyPress += (sender, eventArgs) => + { + eventArgs.Cancel = true; + Environment.Exit(0); + }; + + PrintLogo(); + + IHost host = CreateHost(args); + + try + { + await host.RunAsync(); + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly!"); + return 1; + } + finally + { + await Log.CloseAndFlushAsync(); + } + } + + public static IHost CreateHost(string[] args) + { + IHost host = CreateHostBuilder(args).UseConsoleLifetime().Build(); + Global.ServiceProviderRoot = host.Services; + return host; + } + + private static HostBuilder CreateHostBuilder(string[] args) + { + //IHostBuilder hostBuilder = Host.CreateDefaultBuilder(); + var hostBuilder = new HostBuilder(); + + //hostBuilder.UseContentRoot(Directory.GetCurrentDirectory()); + + hostBuilder.ConfigureHostConfiguration(hostConfigurationBuilder => + { + hostConfigurationBuilder.AddEnvironmentVariables(prefix: "DOTNET_"); + + if (args is { Length: > 0 }) + { + hostConfigurationBuilder.AddCommandLine(args); + } + }); + + hostBuilder.ConfigureAppConfiguration( + (hostBuilderContext, configurationBuilder) => + { + IHostEnvironment env = hostBuilderContext.HostingEnvironment; + + //json文件: + string envName = hostBuilderContext.HostingEnvironment.EnvironmentName; + configurationBuilder + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{envName}.json", true, true); + + //用户机密: + if (env.IsDevelopment() && env.ApplicationName?.Length > 0) + { + //var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName)); + var appAssembly = Assembly.GetAssembly(typeof(Program)); + configurationBuilder.AddUserSecrets( + appAssembly!, + optional: true, + reloadOnChange: true + ); + } + + //环境变量: + configurationBuilder.AddEnvironmentVariables("Ray_"); + configurationBuilder.AddEnvironmentVariables(); + + //命令行: + if (args is { Length: > 0 }) + { + configurationBuilder.AddCommandLine( + args, + Config.Constants.CommandLineMappingsDic + ); + } + + //本地cookie存储文件 + configurationBuilder.AddJsonFile("cookies.json", true, true); + } + ); + + SelfLog.Enable(x => System.Console.WriteLine(x ?? "")); + hostBuilder.UseSerilog( + (context, services, configuration) => + configuration.ReadFrom.Configuration(context.Configuration) + ); + + hostBuilder.ConfigureServices( + (hostContext, services) => + { + services.AddHostedService(); + + services.AddBiliBiliConfigs(hostContext.Configuration); + services.AddBiliBiliClientApi(hostContext.Configuration); + services.AddDomainServices(); + services.AddAppServices(); + } + ); + + return hostBuilder; + } + + /// + /// 输出本工具启动logo + /// + private static void PrintLogo() + { + System.Console.WriteLine(@" ____ _ _____ _ "); + System.Console.WriteLine(@" | __ ) _| |_|_ _|__ ___ | | "); + System.Console.WriteLine(@" | _ \(_) (_) | |/ _ \ / _ \| | "); + System.Console.WriteLine(@" | |_) | | | | | | (_) | (_) | | "); + System.Console.WriteLine(@" |____/|_|_|_| |_|\___/ \___/|_| "); + System.Console.WriteLine(); + } +} diff --git a/src/Ray.BiliBiliTool.Console/Properties/launchSettings.json b/src/Ray.BiliBiliTool.Console/Properties/launchSettings.json new file mode 100644 index 0000000..4e3af4d --- /dev/null +++ b/src/Ray.BiliBiliTool.Console/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "Ray.BiliBiliTool.Console": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker" + } + } +} diff --git a/src/Ray.BiliBiliTool.Console/Ray.BiliBiliTool.Console.csproj b/src/Ray.BiliBiliTool.Console/Ray.BiliBiliTool.Console.csproj new file mode 100644 index 0000000..b6c6c5f --- /dev/null +++ b/src/Ray.BiliBiliTool.Console/Ray.BiliBiliTool.Console.csproj @@ -0,0 +1,88 @@ + + + + Exe + net8.0 + enable + enable + 3cc5407e-fe0e-4df6-a127-7385c75abd8a + Linux + ..\.. + + + + + + + + PreserveNewest + true + PreserveNewest + + + PreserveNewest + true + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + diff --git a/src/Ray.BiliBiliTool.Console/appsettings.Development.json b/src/Ray.BiliBiliTool.Console/appsettings.Development.json new file mode 100644 index 0000000..92618dc --- /dev/null +++ b/src/Ray.BiliBiliTool.Console/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "DailyTaskConfig": { + "SupportUpIds": "", + "AutoChargeUpId": "341688380" + }, + "Security": { + "IsSkipDailyTask": false, + "RandomSleepMaxMin": 0, + "IntervalSecondsBetweenRequestApi": 7 + } +} diff --git a/src/Ray.BiliBiliTool.Console/appsettings.Production.json b/src/Ray.BiliBiliTool.Console/appsettings.Production.json new file mode 100644 index 0000000..e335815 --- /dev/null +++ b/src/Ray.BiliBiliTool.Console/appsettings.Production.json @@ -0,0 +1,8 @@ +{ + "DailyTaskConfig": { + "SupportUpIds": "", + "AutoChargeUpId": "341688380" + }, + //用于UT验证 + "IsPrd": true +} diff --git a/src/Ray.BiliBiliTool.Console/appsettings.json b/src/Ray.BiliBiliTool.Console/appsettings.json new file mode 100644 index 0000000..a4694aa --- /dev/null +++ b/src/Ray.BiliBiliTool.Console/appsettings.json @@ -0,0 +1,243 @@ +{ + "RunTasks": "", //要运行的任务名称[Daily,LiveLottery,UnfollowBatched,VipBigPoint,Test],多个使用&分隔,如“Daily&LiveLottery”,建议使用命令行参数指定 + + + "DailyTaskConfig": { + "Cron": "0 0 15 * * ?", + "IsEnable": true, + "IsWatchVideo": true, //是否观看视频 + "IsShareVideo": true, //是否分享视频 + "IsDonateCoinForArticle": false, + "NumberOfCoins": 5, //每日设定的投币数 [0,5] + "NumberOfProtectedCoins": 0, // 要保留的硬币数量 [0,int_max],0 为不保留,int_max 通常取 (2^31)-1 + "SaveCoinsWhenLv6": false, //达到六级后是否开始白嫖[false,true] + "SelectLike": true, //投币时是否同时点赞[false,true] + "SupportUpIds": "", //优先选择支持的up主Id集合,多个以英文逗号分隔,如:"123,456"。配置后会优先从指定的up主下挑选视频进行观看、分享和投币,不配置或配置为-1则表示没有特别支持的up,会从关注和排行耪中随机获取支持视频 + "DevicePlatform": "android", //执行客户端操作时的平台 [ios,android] + }, + + "MangaTaskConfig": { + "Cron": "0 0 14 * * ?", + "IsEnable": true, + "CustomComicId": 27355, //自定义漫画阅读 comic_id,若不清楚含义请勿修改 + "CustomEpId": 381662 //自定义漫画阅读 ep_id,若不清楚含义请勿修改 + }, + + "MangaPrivilegeTaskConfig": { + "Cron": "0 0 15 * * ?", + "IsEnable": true + }, + + "Silver2CoinTaskConfig": { + "Cron": "0 0 8 * * ?", + "IsEnable": true + }, + + "ChargeTaskConfig": { + "Cron": "0 0 12 28 * ?", + "IsEnable": true, + "AutoChargeUpId": "-1", //指定支持的UP主Id,-1表示自己 + "ChargeComment": "" //充电后留言 + }, + + "VipPrivilegeConfig": { + "Cron": "0 0 1 * * ?", + "IsEnable": true + }, + + "VipBigPointConfig": { + "Cron": "0 7 1 * * ?", + "IsEnable": true, + "ViewBangumis": "33378" // 自定义番剧的ssid,若不清楚含义请勿修改(默认为名侦探柯南) + }, + + "LiveLotteryTaskConfig": { + "Cron": "0 0 22 * * ?", + "IsEnable": true, + "ExcludeAwardNames": "舰|船|航海|代金券|自拍|照|写真|图|提督", //根据关键字排除包含这些文字的奖品名称,多个用“|”分隔,如“照|舰|船|航海|代金券|自拍” + "IncludeAwardNames": "", //根据关键字指定奖品名称必须包含的文字,多个用“|”分隔,如“红包|现金|块|元” + "AutoGroupFollowings": true, //抽奖结束后是否自动将关注的主播分组到“天选时刻”分组,值域[true,false] + "DenyUids": "65566781,1277481241,1643654862,603676925" //主播Uid黑名单(一般是中奖后的老赖),多个用英文逗号分隔,配置后不会参加黑名单中的主播的抽奖活动 + }, + + "LiveFansMedalTaskConfig": { + "Cron": "0 5 0 * * ?", + "IsEnable": true, + "DanmakuContent": "OvO", + "HeartBeatNumber": 70, //直播间观看的时长,单位为分钟", + "HeartBeatSendGiveUpThreshold": 5, //当心跳包发送连续失败多少次时放弃 + "IsSkipLevel20Medal": true // 是否跳过粉丝牌等级 >=0 的 + }, + + "UnfollowBatchedTaskConfig": { + "Cron": "0 0 6 1 * ?", + "IsEnable": true, + "GroupName": "天选时刻", //取关的分组名称 + "Count": 20, //本次取关个数(倒序,从后往前取关) + "RetainUids": "" //白名单(保留的UpId),多个用英文都好分隔,配置后,批量取关时不会取关配置的Up + }, + + //安全相关配置 + "Security": { + "IsSkipDailyTask": false, //是否跳过执行任务,用于特殊情况下,通过配置灵活的开启和关闭任务 + "RandomSleepMaxMin": 0, //随机睡眠的最大时长(单位为分钟),用于使每天运行时间在范围内相对随机,值域[0,+];配置为0表示不进行休眠 + "IntervalSecondsBetweenRequestApi": 20, //两次调用api之间的间隔[0,+](单位为秒)。因为有人担心在几秒内连续调用api会被b站安全机制发现,所以为不放心的朋友添加了间隔秒数配置,两次连续调用Api之间会大于该秒数 + "IntervalMethodTypes": "GET,POST", //间隔秒数所针对的HttpMethod,多个用英文逗号隔开,当前有GET和POST两种,可配置如“GET,POST” + "UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0", //请求B站接口时头部传递的User-Agent + "UserAgentApp": "Mozilla/5.0 (Linux; Android 12; SM-S9080 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Mobile Safari/537.36 os/android model/SM-S9080 build/7760700 osVer/12 sdkInt/32 network/2 BiliApp/7760700 mobi_app/android channel/bili innerVer/7760710 c_locale/zh_CN s_locale/zh_CN disable_rcmd/0 7.76.0 os/android model/SM-S9080 mobi_app/android build/7760700 channel/bili innerVer/7760710 osVer/12 network/2", //App请求B站接口时头部传递的User-Agent + "WebProxy": "" //代理,格式为http://host:port,如果有鉴权则为user:password@http://host:port + }, + + "QingLongConfig": { + "ClientId": "", + "ClientSecret": "", + }, + + //日志 + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.Debug", + "Serilog.Sinks.File", + "Ray.Serilog.Sinks.TelegramBatched", + "Ray.Serilog.Sinks.WorkWeiXinBatched", + "Ray.Serilog.Sinks.DingTalkBatched", + "Ray.Serilog.Sinks.ServerChanBatched", + "Ray.Serilog.Sinks.CoolPushBatched", + "Ray.Serilog.Sinks.OtherApiBatched", + "Ray.Serilog.Sinks.PushPlusBatched", + "Ray.Serilog.Sinks.MicrosoftTeamsBatched", + "Ray.Serilog.Sinks.WorkWeiXinAppBatched", + "Ray.Serilog.Sinks.GotifyBatched" + ], + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Warning", + "System": "Warning", + "Microsoft.AspNetCore": "Warning" + } + }, + "WriteTo": [ + //0.Console + { + "Name": "Console", + "Args": { + "restrictedToMinimumLevel": "Information", + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}" + } + }, + //1.Debug + { "Name": "Debug" }, + //2.File + { + "Name": "File", + "Args": { + "path": "Logs/log.txt", + "restrictedToMinimumLevel": "Verbose", + "rollingInterval": 3 + } + }, + + //3.Telegram机器人(https://core.telegram.org/bots/api#available-methods) + { + "Name": "TelegramBatched", + "Args": { + "botToken": "", + "chatId": "", + "restrictedToMinimumLevel": "Information", + "proxy": "", //代理,user:password@host:port + "apiHost": "https://api.telegram.org" //可以替换成自己搭建的反代host(https://hostloc.com/thread-805441-1-1.html) + } + }, + //4.企业微信机器人(https://work.weixin.qq.com/api/doc/90000/90136/91770) + { + "Name": "WorkWeiXinBatched", + "Args": { + "webHookUrl": "", //群机器人生成 + "restrictedToMinimumLevel": "Information" + } + }, + //5.钉钉机器人(https://developers.dingtalk.com/document/app/overview-of-group-robots) + { + "Name": "DingTalkBatched", + "Args": { + "webHookUrl": "", //群机器人生成 + "restrictedToMinimumLevel": "Information" + } + }, + //6.Server酱(http://sc.ftqq.com/9.version) + { + "Name": "ServerChanBatched", + "Args": { + "scKey": "", //已过时,待删除 + "turboScKey": "", //平台生成的ScKey + "restrictedToMinimumLevel": "Information" + } + }, + //7.酷推 + { + "Name": "CoolPushBatched", + "Args": { + "sKey": "", + "restrictedToMinimumLevel": "Information" + } + }, + //8.自定义Api + { + "Name": "OtherApiBatched", + "Args": { + "api": "", + "placeholder": "#msg#", //占位符 + "bodyJsonTemplate": "{\"msgtype\":\"markdown\",\"markdown\":{\"content\":#msg#}}", //json模板,会当作post的body,占位符会被日志内容替换(日志文本为json字符串,已经带有引号,所有模板中占位符不用使用引号包裹,如例子所示为企业微信的标准推送格式) + "restrictedToMinimumLevel": "Information" + } + }, + //9.PushPlus(http://www.pushplus.plus/doc/) + { + "Name": "PushPlusBatched", + "Args": { + "token": "", + "channel": "", //渠道,值域[wechat,webhook,cp,sms,mail],分别对应[微信公众号,指定第三方webhook,企业微信应用,短信,邮件] + "topic": "", //群组编码,用于群发,没有就不填(不填仅发送给自己);channel为webhook时无效 + "webhook": "", //webhook编码(不是地址),仅在channel使用webhook渠道和CP渠道时需要填写 + "restrictedToMinimumLevel": "Information" + } + }, + //10.MicrosoftTeams + { + "Name": "MicrosoftTeamsBatched", + "Args": { + "webhook": "", //webhook完整地址 + "restrictedToMinimumLevel": "Information" + } + }, + //11.企业微信应用推送 + { + "Name": "WorkWeiXinAppBatched", + "Args": { + "corpId": "", //必填 + "agentId": "", //必填 + "secret": "", //必填 + "toUser": "@all", + "toParty": "", + "toTag": "", + "restrictedToMinimumLevel": "Information" + } + }, + //12.gotify推送 + { + "Name": "GotifyBatched", + "Args": { + "host": "", //必填,如https://www.mygotify.com + "token": "", //必填,应用(app)的token + "restrictedToMinimumLevel": "Information" + } + } + ], + "Enrich": [ "FromLogContext" ] + }, + + "PlatformType": "Unknown", + "IsPrd": false +} diff --git a/src/Ray.BiliBiliTool.Domain/BiliLogs.cs b/src/Ray.BiliBiliTool.Domain/BiliLogs.cs new file mode 100644 index 0000000..54e7ebe --- /dev/null +++ b/src/Ray.BiliBiliTool.Domain/BiliLogs.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Ray.BiliBiliTool.Domain; + +[Table("bili_logs")] +public class BiliLogs +{ + [Key] + [Column("id")] + public long Id { get; set; } + + [Column("timeStamp")] + public required DateTime Timestamp { get; set; } + + [Column("level")] + public required string Level { get; set; } + + [Column("exception")] + public string? Exception { get; set; } + + [Column("renderedMessage")] + public string? RenderedMessage { get; set; } + + [Column("properties")] + public string? Properties { get; set; } + + [Column("fireInstanceIdComputed")] + public string? FireInstanceIdComputed { get; set; } + + public string FormattedLogLevel => + Level.ToLower() switch + { + "verbose" => "VERB", + "debug" => "DBG", + "information" => "INFO", + "warning" => "WARN", + "error" => "ERR", + "fatal" => "FATAL", + _ => Level.ToUpper(), + }; +} diff --git a/src/Ray.BiliBiliTool.Domain/ExecutionLog.cs b/src/Ray.BiliBiliTool.Domain/ExecutionLog.cs new file mode 100644 index 0000000..5b16a74 --- /dev/null +++ b/src/Ray.BiliBiliTool.Domain/ExecutionLog.cs @@ -0,0 +1,79 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Ray.BiliBiliTool.Domain; + +[Table("bili_execution_logs")] +public class ExecutionLog +{ + [Key] + public long LogId { get; set; } + + [MaxLength(256)] + public string? RunInstanceId { get; set; } + + [Column(TypeName = "varchar(20)")] + public LogType LogType { get; set; } + + [MaxLength(256)] + public string? JobName { get; set; } + + [MaxLength(256)] + public string? JobGroup { get; set; } + + [MaxLength(256)] + public string? TriggerName { get; set; } + + [MaxLength(256)] + public string? TriggerGroup { get; set; } + + /// + /// Expected time the job should get triggered + /// + public DateTimeOffset? ScheduleFireTimeUtc { get; set; } + + /// + /// Actual time the job got triggered + /// + public DateTimeOffset? FireTimeUtc { get; set; } + + public TimeSpan? JobRunTime { get; set; } + public int? RetryCount { get; set; } + + [MaxLength(8000)] + public string? Result { get; set; } + + [MaxLength(8000)] + public string? ErrorMessage { get; set; } + public bool? IsVetoed { get; set; } + public bool? IsException { get; set; } + + /// + /// Indicate whether the execution is successful or not. + /// If is , it may have value: + /// true - If job does not return IsSuccess or when execution completed successfully + /// false - Execution completed but return code is error or job throw an exception + /// null - Job still running or terminated unexpectedly. + /// If is not value will be null. + /// + public bool? IsSuccess { get; set; } + + /// + /// Return code of execution. + /// Ex. + /// for HTTP call - 200, 404, 500 etc. + /// for command line - 0 = success, -1 = failed + /// + [MaxLength(28)] + public string? ReturnCode { get; set; } + + public DateTimeOffset DateAddedUtc { get; set; } + public ExecutionLogDetail? ExecutionLogDetail { get; set; } + + public ExecutionLog() + { + DateAddedUtc = DateTimeOffset.UtcNow; + } + + public DateTimeOffset? GetFinishTimeUtc() => FireTimeUtc?.Add(JobRunTime ?? TimeSpan.Zero); +} diff --git a/src/Ray.BiliBiliTool.Domain/ExecutionLogDetail.cs b/src/Ray.BiliBiliTool.Domain/ExecutionLogDetail.cs new file mode 100644 index 0000000..9af95bd --- /dev/null +++ b/src/Ray.BiliBiliTool.Domain/ExecutionLogDetail.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace Ray.BiliBiliTool.Domain; + +public class ExecutionLogDetail +{ + public string? ExecutionDetails { get; set; } + public string? ErrorStackTrace { get; set; } + public int? ErrorCode { get; set; } + + [MaxLength(1000)] + public string? ErrorHelpLink { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Domain/LogType.cs b/src/Ray.BiliBiliTool.Domain/LogType.cs new file mode 100644 index 0000000..587b70b --- /dev/null +++ b/src/Ray.BiliBiliTool.Domain/LogType.cs @@ -0,0 +1,8 @@ +namespace Ray.BiliBiliTool.Domain; + +public enum LogType +{ + ScheduleJob, + Trigger, + System, +} diff --git a/src/Ray.BiliBiliTool.Domain/Ray.BiliBiliTool.Domain.csproj b/src/Ray.BiliBiliTool.Domain/Ray.BiliBiliTool.Domain.csproj new file mode 100644 index 0000000..d364a52 --- /dev/null +++ b/src/Ray.BiliBiliTool.Domain/Ray.BiliBiliTool.Domain.csproj @@ -0,0 +1,7 @@ + + + net8.0 + enable + enable + + diff --git a/src/Ray.BiliBiliTool.Domain/User.cs b/src/Ray.BiliBiliTool.Domain/User.cs new file mode 100644 index 0000000..6b1545d --- /dev/null +++ b/src/Ray.BiliBiliTool.Domain/User.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Ray.BiliBiliTool.Domain; + +[Table("bili_user")] +public class User +{ + [Key] + public long Id { get; set; } + public required string Username { get; set; } + public required string PasswordHash { get; set; } + public required string Salt { get; set; } + public List Roles { get; set; } = []; +} diff --git a/src/Ray.BiliBiliTool.DomainService/AccountDomainService.cs b/src/Ray.BiliBiliTool.DomainService/AccountDomainService.cs new file mode 100644 index 0000000..f67ad64 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/AccountDomainService.cs @@ -0,0 +1,258 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; + +namespace Ray.BiliBiliTool.DomainService; + +/// +/// 账户 +/// +public class AccountDomainService( + ILogger logger, + IDailyTaskApi dailyTaskApi, + IUserInfoApi userInfoApi, + IRelationApi relationApi, + IOptionsMonitor unfollowBatchedTaskOptions, + IOptionsMonitor dailyTaskOptions +) : IAccountDomainService +{ + private readonly UnfollowBatchedTaskOptions _unfollowBatchedTaskOptions = + unfollowBatchedTaskOptions.CurrentValue; + private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue; + + /// + /// 登录 + /// + /// + public async Task LoginByCookie(BiliCookie cookie) + { + BiliApiResponse apiResponse = await userInfoApi.LoginByCookie(cookie.ToString()); + + if (apiResponse.Code != 0 || !apiResponse.Data!.IsLogin) + { + throw new Exception("登录失败,请检查Cookie"); + ; + } + + UserInfo useInfo = apiResponse.Data; + + logger.LogInformation("【用户名】{0}", useInfo.GetFuzzyUname()); + logger.LogInformation("【会员类型】{0}", useInfo.VipType.Description()); + logger.LogInformation("【会员状态】{0}", useInfo.VipStatus.Description()); + logger.LogInformation("【硬币余额】{0}", useInfo.Money ?? 0); + + if (useInfo.Level_info?.Current_level < 6) + { + logger.LogInformation( + "【距升级Lv{0}】预计{1}天", + useInfo.Level_info.Current_level + 1, + CalculateUpgradeTime(useInfo) + ); + } + else + { + logger.LogInformation("【当前经验】{0}", useInfo.Level_info?.Current_exp); + logger.LogInformation("您已是 Lv6 的大佬了,无敌是多么寂寞~"); + } + + return useInfo; + } + + /// + /// 获取每日任务完成情况 + /// + /// + public async Task GetDailyTaskStatus(BiliCookie ck) + { + DailyTaskInfo result = new(); + BiliApiResponse apiResponse = await dailyTaskApi.GetDailyTaskRewardInfoAsync( + ck.ToString() + ); + if (apiResponse.Code == 0) + { + logger.LogDebug("请求本日任务完成状态成功"); + result = apiResponse.Data; + } + else + { + logger.LogWarning("获取今日任务完成状态失败:{result}", apiResponse.ToJsonStr()); + result = (await dailyTaskApi.GetDailyTaskRewardInfoAsync(ck.ToString())).Data; + //todo:偶发性请求失败,再请求一次,这么写很丑陋,待用polly再框架层面实现 + } + + return result!; + } + + /// + /// 取关 + /// + /// + /// + public async Task UnfollowBatched(BiliCookie ck) + { + logger.LogInformation("【分组名】{group}", _unfollowBatchedTaskOptions.GroupName); + + //根据分组名称获取tag + TagDto? tag = await GetTag(_unfollowBatchedTaskOptions.GroupName, ck); + var tagId = tag?.Tagid; + int total = tag?.Count ?? 0; + + if (!tagId.HasValue) + { + logger.LogWarning("分组名称不存在"); + return; + } + + if (total == 0) + { + logger.LogWarning("分组下不存在up"); + return; + } + int count = _unfollowBatchedTaskOptions.Count; + if (count == -1) + count = total; + + logger.LogInformation("【分组下共有】{count}人", total); + logger.LogInformation("【目标取关】{count}人" + Environment.NewLine, count); + + //计算共几页 + int totalPage = (int)Math.Ceiling(total / (double)20); + + //从最后一页开始获取 + var req = new GetSpecialFollowingsRequest(long.Parse(ck.UserId), tagId.Value) + { + Pn = totalPage, + }; + List followings = (await relationApi.GetFollowingsByTag(req, ck.ToString())).Data; + followings.Reverse(); + + var targetList = new List(); + + if (count <= followings.Count) + { + targetList = followings.Take(count).ToList(); + } + else + { + int pn = totalPage; + while (targetList.Count < count) + { + targetList.AddRange(followings); + + //获取前一页 + pn -= 1; + if (pn <= 0) + break; + req.Pn = pn; + followings = (await relationApi.GetFollowingsByTag(req, ck.ToString())).Data; + followings.Reverse(); + } + } + + logger.LogInformation("开始取关..." + Environment.NewLine); + int success = 0; + for (int i = 1; i <= targetList.Count && i <= count; i++) + { + UpInfo info = targetList[i - 1]; + + logger.LogInformation("【序号】{num}", i); + logger.LogInformation("【UP】{up}", info.Uname); + + if (_unfollowBatchedTaskOptions.RetainUidList.Contains(info.Mid.ToString())) + { + logger.LogInformation("【取关结果】白名单,跳过" + Environment.NewLine); + continue; + } + + string modifyReferer = string.Format( + RelationApiConstant.ModifyReferer, + ck.UserId, + tagId + ); + var modifyReq = new ModifyRelationRequest(info.Mid, ck.BiliJct); + var re = await relationApi.ModifyRelation(modifyReq, ck.ToString(), modifyReferer); + + if (re.Code == 0) + { + logger.LogInformation("【取关结果】成功" + Environment.NewLine); + success++; + } + else + { + logger.LogInformation("【取关结果】失败"); + logger.LogInformation("【原因】{msg}" + Environment.NewLine, re.Message); + } + } + + logger.LogInformation("【本次共取关】{count}人", success); + + //计算剩余 + tag = await GetTag(_unfollowBatchedTaskOptions.GroupName, ck); + logger.LogInformation("【分组下剩余】{count}人", tag?.Count); + } + + /// + /// 获取分组(标签) + /// + /// + /// + /// + private async Task GetTag(string groupName, BiliCookie ck) + { + string getTagsReferer = string.Format(RelationApiConstant.GetTagsReferer, ck.UserId); + List tagList = (await relationApi.GetTags(ck.ToString(), getTagsReferer)).Data!; + var tag = tagList.FirstOrDefault(x => x.Name == groupName); + return tag; + } + + /// + /// 计算升级时间 + /// + /// + /// 升级时间 + public int CalculateUpgradeTime(UserInfo useInfo) + { + double availableCoins = + decimal.ToDouble(useInfo.Money ?? 0) - _dailyTaskOptions.NumberOfProtectedCoins; + long needExp = + useInfo.Level_info != null + ? useInfo.Level_info.GetNext_expLong() - useInfo.Level_info.Current_exp + : 0; + int needDay; + + if (availableCoins < 0) + needDay = (int)( + (double)needExp / 25 + + _dailyTaskOptions.NumberOfProtectedCoins + - Math.Abs(availableCoins) + ); + + switch (_dailyTaskOptions.NumberOfCoins) + { + case 0: + needDay = (int)(needExp / 15); + break; + case 1: + needDay = (int)(needExp / 25); + break; + default: + int dailyExpAvailable = 15 + _dailyTaskOptions.NumberOfCoins * 10; + double needFrontDay = availableCoins / (_dailyTaskOptions.NumberOfCoins - 1); + + if ((double)needExp / dailyExpAvailable > needFrontDay) + needDay = (int)( + needFrontDay + (needExp - dailyExpAvailable * needFrontDay) / 25 + ); + else + needDay = (int)(needExp / dailyExpAvailable); + break; + } + + return needDay; + } +} diff --git a/src/Ray.BiliBiliTool.DomainService/ArticleDomainService.cs b/src/Ray.BiliBiliTool.DomainService/ArticleDomainService.cs new file mode 100644 index 0000000..0d05c9f --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/ArticleDomainService.cs @@ -0,0 +1,369 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Article; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; + +namespace Ray.BiliBiliTool.DomainService; + +public class ArticleDomainService( + IArticleApi articleApi, + ILogger logger, + IOptionsMonitor dailyTaskOptions, + ICoinDomainService coinDomainService, + IAccountApi accountApi +) : IArticleDomainService +{ + private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue; + + /// + /// up的专栏总数缓存 + /// + private readonly Dictionary _upArticleCountDicCatch = new(); + + /// + /// 已对投币数量缓存 + /// + private readonly Dictionary _alreadyDonatedCoinCountCatch = new(); + + public async Task LikeArticle(long cvid, BiliCookie ck) + { + await articleApi.LikeAsync(cvid, ck.BiliJct, ck.ToString()); + } + + /// + /// 投币专栏任务 + /// + /// + public async Task AddCoinForArticles(BiliCookie ck) + { + var donateCoinsCounts = await CalculateDonateCoinsCounts(ck); + + if (donateCoinsCounts == 0) + { + // 没有可投的币相当于投币任务全部完成 + return true; + } + + int success = 0; + int tryCount = 10; + + for (int i = 0; i <= tryCount && success < donateCoinsCounts; i++) + { + logger.LogDebug("开始尝试第{num}次", i); + + var upId = GetUpFromConfigUps(ck); + if (upId == 0) + { + logger.LogDebug("未能成功选择支持的Up主"); + continue; + } + // 当upId不符合时,会直接报错,需要将两者的判断分隔开 + var cvid = await GetRandomArticleFromUp(upId, ck); + if (cvid == 0) + { + logger.LogDebug("第{num}次尝试,未能成功选择合适的专栏", i); + continue; + } + + if (await AddCoinForArticle(cvid, upId, ck)) + { + // 点赞 + if (_dailyTaskOptions.SelectLike) + { + await LikeArticle(cvid, ck); + logger.LogInformation("专栏点赞成功"); + } + + success++; + } + } + + if (success == donateCoinsCounts) + logger.LogInformation("专栏投币任务完成"); + else + { + logger.LogInformation("投币尝试超过10次,已终止"); + return false; + } + + logger.LogInformation( + "【硬币余额】{coin}", + (await accountApi.GetCoinBalanceAsync(ck.ToString())).Data!.Money ?? 0 + ); + + return true; + } + + /// + /// 给某一篇专栏投币 + /// + /// 文章cvid + /// 文章作者mid + /// 投币是否成功(false 投币失败,true 投币成功) + public async Task AddCoinForArticle(long cvid, long mid, BiliCookie ck) + { + BiliApiResponse result; + try + { + var refer = + $"https://www.bilibili.com/read/cv{cvid}/?from=search&spm_id_from=333.337.0.0"; + result = await articleApi.AddCoinForArticleAsync( + new AddCoinForArticleRequest(cvid, mid, ck.BiliJct), + ck.ToString(), + refer + ); + } + catch (Exception) + { + return false; + } + + if (result.Code == 0) + { + logger.LogInformation("投币成功,经验+10 √"); + return true; + } + else + { + logger.LogError("投币错误 {message}", result.Message); + return false; + } + } + + #region private + + /// + /// 从某个up主中随机挑选一个专栏 + /// + /// + /// 专栏的cvid + private async Task GetRandomArticleFromUp(long mid, BiliCookie ck) + { + if (!_upArticleCountDicCatch.TryGetValue(mid, out int articleCount)) + { + articleCount = await GetArticleCountOfUp(mid, ck); + _upArticleCountDicCatch.Add(mid, articleCount); + } + + // 专栏数为0时 + if (articleCount == 0) + { + return 0; + } + + var req = new SearchArticlesByUpIdDto() + { + mid = mid, + ps = 1, + pn = new Random().Next(1, articleCount + 1), + }; + + BiliApiResponse re = await articleApi.SearchUpArticlesByUpIdAsync( + req + ); + + if (re.Code != 0) + { + throw new Exception(re.Message); + } + + var articleInfo = re.Data.Articles.FirstOrDefault(); + + logger.LogInformation("获取到的专栏{cvid}({title})", articleInfo?.Id, articleInfo?.Title); + + // 检查是否可投 + if (articleInfo == null || !await IsCanDonate(articleInfo.Id)) + { + return 0; + } + + return articleInfo.Id; + } + + // TODO 转变为异步代码 + /// + /// 从支持UP主列表中随机挑选一位 + /// + /// 被挑选up主的mid + private long GetUpFromConfigUps(BiliCookie ck) + { + if ( + _dailyTaskOptions.SupportUpIdList == null + || _dailyTaskOptions.SupportUpIdList.Count == 0 + ) + { + return 0; + } + + try + { + long randomUpId = _dailyTaskOptions.SupportUpIdList[ + new Random().Next(0, _dailyTaskOptions.SupportUpIdList.Count) + ]; + + if (randomUpId is 0 or long.MinValue) + return 0; + + if (randomUpId.ToString() == ck.UserId) + { + logger.LogDebug("不能为自己投币"); + return 0; + } + + logger.LogDebug("挑选出的up主为{UpId}", randomUpId); + return randomUpId; + } + catch (Exception e) + { + logger.LogWarning("异常:{msg}", e); + } + + return 0; + } + + /// + /// 获取Up主专栏总数 + /// + /// up主mid + /// 专栏总数 + /// + private async Task GetArticleCountOfUp(long mid, BiliCookie ck) + { + var req = new SearchArticlesByUpIdDto() { mid = mid }; + + BiliApiResponse re = await articleApi.SearchUpArticlesByUpIdAsync( + req + ); + + if (re.Code != 0) + { + throw new Exception(re.Message); + } + + return re.Data.Count; + } + + /// + /// 计算所需要投的硬币数量 + /// + /// 硬币数量 + private async Task CalculateDonateCoinsCounts(BiliCookie ck) + { + int needCoins = await GetNeedDonateCoinCounts(ck); + + int protectedCoins = _dailyTaskOptions.NumberOfProtectedCoins; + if (needCoins <= 0) + return 0; + + //投币前硬币余额 + decimal coinBalance = await coinDomainService.GetCoinBalance(ck); + logger.LogInformation("【投币前余额】 : {coinBalance}", coinBalance); + _ = int.TryParse( + decimal.Truncate(coinBalance - protectedCoins).ToString(), + out int unprotectedCoins + ); + + if (coinBalance <= 0) + { + logger.LogInformation("因硬币余额不足,今日暂不执行投币任务"); + return 0; + } + + if (coinBalance <= protectedCoins) + { + logger.LogInformation("因硬币余额达到或低于保留值,今日暂不执行投币任务"); + return 0; + } + + //余额小于目标投币数,按余额投 + if (coinBalance < needCoins) + { + _ = int.TryParse(decimal.Truncate(coinBalance).ToString(), out needCoins); + logger.LogInformation("因硬币余额不足,目标投币数调整为: {needCoins}", needCoins); + return needCoins; + } + + //投币后余额小于等于保护值,按保护值允许投 + if (coinBalance - needCoins <= protectedCoins) + { + //排除需投等于保护后可投数量相等时的情况 + if (unprotectedCoins != needCoins) + { + needCoins = unprotectedCoins; + logger.LogInformation( + "因硬币余额投币后将达到或低于保留值,目标投币数调整为: {needCoins}", + needCoins + ); + return needCoins; + } + } + + return needCoins; + } + + private async Task GetNeedDonateCoinCounts(BiliCookie ck) + { + int configCoins = _dailyTaskOptions.NumberOfCoins; + + if (configCoins <= 0) + { + logger.LogInformation("已配置为跳过投币任务"); + return configCoins; + } + + //已投的硬币 + int alreadyCoins = await coinDomainService.GetDonatedCoins(ck); + + int targetCoins = configCoins; + + logger.LogInformation("【今日已投】{already}枚", alreadyCoins); + logger.LogInformation("【目标欲投】{already}枚", targetCoins); + + if (targetCoins > alreadyCoins) + { + int needCoins = targetCoins - alreadyCoins; + logger.LogInformation("【还需再投】{need}枚", needCoins); + return needCoins; + } + + logger.LogInformation("已完成投币任务,不需要再投啦~"); + return 0; + } + + private async Task IsCanDonate(long cvid) + { + try + { + if (_alreadyDonatedCoinCountCatch.Any(x => x.Key == cvid.ToString())) + { + logger.LogDebug("重复专栏,丢弃处理"); + return false; + } + + if (!_alreadyDonatedCoinCountCatch.TryGetValue(cvid.ToString(), out int multiply)) + { + multiply = (await articleApi.SearchArticleInfoAsync(cvid)).Data.Coin; + _alreadyDonatedCoinCountCatch.TryAdd(cvid.ToString(), multiply); + } + + // 在网页端我测试时只能投一枚硬币,暂时设置最多投一枚 + if (multiply >= 1) + { + return false; + } + + return true; + } + catch (Exception e) + { + logger.LogWarning("异常:{mag}", e); + return false; + } + } + + #endregion +} diff --git a/src/Ray.BiliBiliTool.DomainService/ChargeDomainService.cs b/src/Ray.BiliBiliTool.DomainService/ChargeDomainService.cs new file mode 100644 index 0000000..a43b0e8 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/ChargeDomainService.cs @@ -0,0 +1,105 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; + +namespace Ray.BiliBiliTool.DomainService; + +/// +/// 充电 +/// +public class ChargeDomainService( + ILogger logger, + IOptionsMonitor dailyTaskOptions, + IOptionsMonitor chargeTaskOptions, + IDailyTaskApi dailyTaskApi, + IChargeApi chargeApi +) : IChargeDomainService +{ + private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue; + private readonly ChargeTaskOptions _chargeTaskOptions = chargeTaskOptions.CurrentValue; + private readonly IDailyTaskApi _dailyTaskApi = dailyTaskApi; + + /// + /// 月底自动己充电 + /// 仅充会到期的B币券,低于2的时候不会充 + /// + public async Task Charge(UserInfo userInfo, BiliCookie ck) + { + //大会员类型 + VipType vipType = userInfo.GetVipType(); + if (vipType != VipType.Annual) + { + logger.LogInformation("不是年度大会员,跳过"); + return; + } + + logger.LogInformation("【今天】{today}号", DateTime.Today.Day); + + //B币券余额 + decimal couponBalance = userInfo.Wallet?.Coupon_balance ?? 0; + logger.LogInformation("【B币券】{couponBalance}", couponBalance); + if (couponBalance < 2) + { + logger.LogInformation("余额小于2,无法充电"); + return; + } + + //如果没有配置或配了-1,则使用fallback值(B站最新策略已不允许为自己充电) + string targetUpId = + string.IsNullOrWhiteSpace(_chargeTaskOptions.AutoChargeUpId) + || _chargeTaskOptions.AutoChargeUpId == "-1" + ? Config.Constants.FallbackAutoChargeUpId + : _chargeTaskOptions.AutoChargeUpId!; + + logger.LogDebug("【目标Up】{up}", targetUpId); + + var request = new ChargeRequest(couponBalance, long.Parse(targetUpId), ck.BiliJct); + + //BiliApiResponse response = await _chargeApi.Charge(decimal.ToInt32(couponBalance * 10), _dailyTaskOptions.AutoChargeUpId, _cookieOptions.UserId, _cookieOptions.BiliJct); + BiliApiResponse response = await chargeApi.ChargeV2Async( + request, + ck.ToString() + ); + + if (response.Code == 0) + { + if (response.Data?.Status == 4) + { + logger.LogInformation("【充电结果】成功"); + logger.LogInformation("【充值个数】 {num}个B币", couponBalance); + logger.LogInformation("经验+{exp} √", couponBalance); + logger.LogInformation("在过期前使用成功,赠送的B币券没有浪费哦~"); + + //充电留言 + await ChargeComments(response.Data.Order_no, ck); + } + else + { + logger.LogInformation("【充电结果】失败"); + logger.LogError("【原因】{reason}", response.ToJsonStr()); + } + } + else + { + logger.LogInformation("【充电结果】失败"); + logger.LogError("【原因】{reason}", response.Message); + } + } + + /// + /// 充电后留言 + /// + /// + public async Task ChargeComments(string orderNum, BiliCookie ck) + { + var comment = _chargeTaskOptions.ChargeComment ?? ""; + var request = new ChargeCommentRequest(orderNum, comment, ck.BiliJct); + await chargeApi.ChargeCommentAsync(request, ck.ToString()); + + logger.LogInformation("【留言】{comment}", comment); + } +} diff --git a/src/Ray.BiliBiliTool.DomainService/CoinDomainService.cs b/src/Ray.BiliBiliTool.DomainService/CoinDomainService.cs new file mode 100644 index 0000000..e97334e --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/CoinDomainService.cs @@ -0,0 +1,42 @@ +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; +using Ray.BiliBiliTool.DomainService.Interfaces; + +namespace Ray.BiliBiliTool.DomainService; + +/// +/// 硬币 +/// +public class CoinDomainService(IAccountApi accountApi, IDailyTaskApi dailyTaskApi) + : ICoinDomainService +{ + /// + /// 获取账户硬币余额 + /// + /// + public async Task GetCoinBalance(BiliCookie ck) + { + var response = await accountApi.GetCoinBalanceAsync(ck.ToString()); + return response.Data!.Money ?? 0; + } + + /// + /// 获取今日已投币数 + /// + /// + public async Task GetDonatedCoins(BiliCookie ck) + { + return (await GetDonateCoinExp(ck)) / 10; + } + + #region private + /// + /// 获取今日通过投币已获取的经验值 + /// + /// + private async Task GetDonateCoinExp(BiliCookie ck) + { + return (await dailyTaskApi.GetDonateCoinExpAsync(ck.ToString())).Data; + } + #endregion +} diff --git a/src/Ray.BiliBiliTool.DomainService/DonateCoinDomainService.cs b/src/Ray.BiliBiliTool.DomainService/DonateCoinDomainService.cs new file mode 100644 index 0000000..43f8040 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/DonateCoinDomainService.cs @@ -0,0 +1,459 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; + +namespace Ray.BiliBiliTool.DomainService; + +/// +/// 投币 +/// +public class DonateCoinDomainService( + ILogger logger, + IOptionsMonitor dailyTaskOptions, + IAccountApi accountApi, + ICoinDomainService coinDomainService, + IVideoDomainService videoDomainService, + IRelationApi relationApi, + IVideoApi videoApi +) : IDonateCoinDomainService +{ + private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue; + private readonly Dictionary _expDic = Config.Constants.ExpDic; + private readonly Dictionary _donateContinueStatusDic = Config + .Constants + .DonateCoinCanContinueStatusDic; + + /// + /// up的视频稿件总数缓存 + /// + private readonly Dictionary _upVideoCountDicCatch = new(); + + /// + /// 已对视频投币数量缓存 + /// + private readonly Dictionary _alreadyDonatedCoinCountCatch = new(); + + /// + /// 完成投币任务 + /// + public async Task AddCoinsForVideos(BiliCookie ck) + { + int needCoins = await GetNeedDonateCoinNum(ck); + int protectedCoins = _dailyTaskOptions.NumberOfProtectedCoins; + if (needCoins <= 0) + return; + + //投币前硬币余额 + decimal coinBalance = await coinDomainService.GetCoinBalance(ck); + logger.LogInformation("【投币前余额】 : {coinBalance}", coinBalance); + _ = int.TryParse( + decimal.Truncate(coinBalance - protectedCoins).ToString(), + out int unprotectedCoins + ); + + if (coinBalance <= 0) + { + logger.LogInformation("因硬币余额不足,今日暂不执行投币任务"); + return; + } + + if (coinBalance <= protectedCoins) + { + logger.LogInformation("因硬币余额达到或低于保留值,今日暂不执行投币任务"); + return; + } + + //余额小于目标投币数,按余额投 + if (coinBalance < needCoins) + { + _ = int.TryParse(decimal.Truncate(coinBalance).ToString(), out needCoins); + logger.LogInformation("因硬币余额不足,目标投币数调整为: {needCoins}", needCoins); + } + + //投币后余额小于等于保护值,按保护值允许投 + if (coinBalance - needCoins <= protectedCoins) + { + //排除需投等于保护后可投数量相等时的情况 + if (unprotectedCoins != needCoins) + { + needCoins = unprotectedCoins; + logger.LogInformation( + "因硬币余额投币后将达到或低于保留值,目标投币数调整为: {needCoins}", + needCoins + ); + } + } + + int success = 0; + int tryCount = 10; + for (int i = 1; i <= tryCount && success < needCoins; i++) + { + logger.LogDebug("开始尝试第{num}次", i); + + var video = await TryGetCanDonatedVideo(ck); + if (video == null) + continue; + + logger.LogInformation("【视频】{title}", video.Title); + + bool re = await DoAddCoinForVideo(video, _dailyTaskOptions.SelectLike, ck); + if (re) + success++; + } + + if (success == needCoins) + logger.LogInformation("视频投币任务完成"); + else + logger.LogInformation("投币尝试超过10次,已终止"); + + logger.LogInformation( + "【硬币余额】{coin}", + (await accountApi.GetCoinBalanceAsync(ck.ToString())).Data?.Money ?? 0 + ); + } + + /// + /// 尝试获取一个可以投币的视频 + /// + /// + public async Task TryGetCanDonatedVideo(BiliCookie ck) + { + UpVideoInfo? result; + + //从配置的up中随机尝试获取1次 + result = await TryGetCanDonateVideoByConfigUps(1, ck); + if (result != null) + return result; + + //然后从特别关注列表尝试获取1次 + result = await TryGetCanDonateVideoBySpecialUps(1, ck); + if (result != null) + return result; + + //然后从普通关注列表获取1次 + result = await TryGetCanDonateVideoByFollowingUps(1, ck); + if (result != null) + return result; + + //最后从排行榜尝试5次 + result = await TryGetCanDonateVideoByRegion(5, ck); + + return result; + } + + /// + /// 为视频投币 + /// + /// av号 + /// 投币数量 + /// 是否同时点赞 1是0否 + /// 是否投币成功 + public async Task DoAddCoinForVideo(UpVideoInfo video, bool select_like, BiliCookie ck) + { + BiliApiResponse result; + try + { + var request = new AddCoinRequest(video.Aid, ck.BiliJct) + { + Select_like = select_like ? 1 : 0, + }; + var referer = + $"https://www.bilibili.com/video/{video.Bvid}/?spm_id_from=333.1007.tianma.1-1-1.click&vd_source=80c1601a7003934e7a90709c18dfcffd"; + result = await videoApi.AddCoinForVideo(request, ck.ToString(), referer); + } + catch (Exception) + { + return false; + } + + if (result.Code == 0) + { + _expDic.TryGetValue("每日投币", out int exp); + logger.LogInformation("投币成功,经验+{exp} √", exp); + return true; + } + + if (_donateContinueStatusDic.Any(x => x.Key == result.Code.ToString())) + { + logger.LogError("投币失败,原因:{msg}", result.Message); + return false; + } + else + { + string errorMsg = $"投币发生未预计异常:{result.Message}"; + logger.LogError(errorMsg); + throw new Exception(errorMsg); + } + } + + #region private + + /// + /// 获取今日的目标投币数 + /// + /// + private async Task GetNeedDonateCoinNum(BiliCookie ck) + { + //获取自定义配置投币数 + int configCoins = _dailyTaskOptions.NumberOfCoins; + + if (configCoins <= 0) + { + logger.LogInformation("已配置为跳过投币任务"); + return configCoins; + } + + //已投的硬币 + int alreadyCoins = await coinDomainService.GetDonatedCoins(ck); + //目标 + //int targetCoins = configCoins > Constants.MaxNumberOfDonateCoins + // ? Constants.MaxNumberOfDonateCoins + // : configCoins; + int targetCoins = configCoins; + + logger.LogInformation("【今日已投】{already}枚", alreadyCoins); + logger.LogInformation("【目标欲投】{already}枚", targetCoins); + + if (targetCoins > alreadyCoins) + { + int needCoins = targetCoins - alreadyCoins; + logger.LogInformation("【还需再投】{need}枚", needCoins); + return needCoins; + } + + logger.LogInformation("已完成投币任务,不需要再投啦~"); + return 0; + } + + /// + /// 尝试从配置的up主里随机获取一个可以投币的视频 + /// + /// + /// + private async Task TryGetCanDonateVideoByConfigUps(int tryCount, BiliCookie ck) + { + //是否配置了up主 + if (_dailyTaskOptions.SupportUpIdList.Count == 0) + return null; + + return await TryCanDonateVideoByUps(_dailyTaskOptions.SupportUpIdList, tryCount, ck); + ; + } + + /// + /// 尝试从特别关注的Up主中随机获取一个可以投币的视频 + /// + /// + /// + private async Task TryGetCanDonateVideoBySpecialUps(int tryCount, BiliCookie ck) + { + //获取特别关注列表 + var request = new GetSpecialFollowingsRequest(long.Parse(ck.UserId)); + BiliApiResponse> specials = await relationApi.GetFollowingsByTag( + request, + ck.ToString() + ); + if (specials.Data == null || specials.Data.Count == 0) + return null; + + return await TryCanDonateVideoByUps( + specials.Data.Select(x => x.Mid).ToList(), + tryCount, + ck + ); + } + + /// + /// 尝试从普通关注的Up主中随机获取一个可以投币的视频 + /// + /// + /// + private async Task TryGetCanDonateVideoByFollowingUps(int tryCount, BiliCookie ck) + { + //获取特别关注列表 + var request = new GetFollowingsRequest(long.Parse(ck.UserId)); + BiliApiResponse result = await relationApi.GetFollowings( + request, + ck.ToString() + ); + if (result.Data.Total == 0) + return null; + + return await TryCanDonateVideoByUps( + result.Data.List.Select(x => x.Mid).ToList(), + tryCount, + ck + ); + } + + /// + /// 尝试从排行榜中获取一个没有看过的视频 + /// + /// + /// + private async Task TryGetCanDonateVideoByRegion(int tryCount, BiliCookie ck) + { + try + { + for (int i = 0; i < tryCount; i++) + { + RankingInfo video = await videoDomainService.GetRandomVideoOfRanking(); + if (!await IsCanDonate(video.Aid.ToString(), ck)) + continue; + return new UpVideoInfo() + { + Aid = video.Aid, + Bvid = video.Bvid, + Title = video.Title, + Length = "00:15", + }; + } + } + catch (Exception e) + { + //ignore + logger.LogWarning("异常:{msg}", e); + } + return null; + } + + /// + /// 尝试从指定的up主集合中随机获取一个可以尝试投币的视频 + /// + /// + /// + /// + private async Task TryCanDonateVideoByUps( + List upIds, + int tryCount, + BiliCookie ck + ) + { + if (upIds.Count == 0) + return null; + + try + { + //尝试tryCount次 + for (int i = 1; i <= tryCount; i++) + { + //获取随机Up主Id + long randomUpId = upIds[new Random().Next(0, upIds.Count)]; + + if (randomUpId == 0 || randomUpId == long.MinValue) + continue; + + if (randomUpId.ToString() == ck.UserId) + { + logger.LogDebug("不能为自己投币"); + continue; + } + + //该up的视频总数 + if (!_upVideoCountDicCatch.TryGetValue(randomUpId, out int videoCount)) + { + videoCount = await videoDomainService.GetVideoCountOfUp(randomUpId, ck); + _upVideoCountDicCatch.Add(randomUpId, videoCount); + } + if (videoCount == 0) + continue; + + var videoInfo = await videoDomainService.GetRandomVideoOfUp( + randomUpId, + videoCount, + ck + ); + logger.LogDebug("获取到视频{aid}({title})", videoInfo?.Aid, videoInfo?.Title); + + //检查是否可以投 + if (videoInfo == null || !await IsCanDonate(videoInfo.Aid.ToString(), ck)) + continue; + + return videoInfo; + } + } + catch (Exception e) + { + //ignore + logger.LogWarning("异常:{msg}", e); + } + + return null; + } + + /// + /// 已为视频投币个数是否小于最大限制 + /// + /// av号 + /// + private async Task IsDonatedLessThenLimitCoinsForVideo(string aid, BiliCookie ck) + { + try + { + //获取已投币数量 + if (!_alreadyDonatedCoinCountCatch.TryGetValue(aid, out int multiply)) + { + multiply = ( + await videoApi.GetDonatedCoinsForVideo( + new GetAlreadyDonatedCoinsRequest(long.Parse(aid)), + ck.ToString() + ) + ) + .Data + .Multiply; + _alreadyDonatedCoinCountCatch.TryAdd(aid, multiply); + } + + logger.LogDebug("已为Av{aid}投过{num}枚硬币", aid, multiply); + + if (multiply >= 2) + return false; + + //获取该视频可投币数量 + int limitCoinNum = + (await videoDomainService.GetVideoDetail(aid)).Copyright == 1 + ? 2 //原创,最多可投2枚 + : 1; //转载,最多可投1枚 + logger.LogDebug("该视频的最大投币数为{num}", limitCoinNum); + + return multiply < limitCoinNum; + } + catch (Exception e) + { + //ignore + logger.LogWarning("异常:{mag}", e); + return false; + } + } + + /// + /// 检查获取到的视频是否可以投币 + /// + /// + /// + private async Task IsCanDonate(string aid, BiliCookie ck) + { + //本次运行已经尝试投过的,不进行重复投(不管成功还是失败,凡取过尝试过的,不重复尝试) + if (_alreadyDonatedCoinCountCatch.Any(x => x.Key == aid)) + { + logger.LogDebug("重复视频,丢弃处理"); + return false; + } + + //已经投满2个币的,不能再投 + if (!await IsDonatedLessThenLimitCoinsForVideo(aid, ck)) + { + logger.LogDebug("超出单个视频投币数量限制,丢弃处理"); + return false; + } + + return true; + } + + #endregion +} diff --git a/src/Ray.BiliBiliTool.DomainService/Dtos/FansMedalInfoDto.cs b/src/Ray.BiliBiliTool.DomainService/Dtos/FansMedalInfoDto.cs new file mode 100644 index 0000000..42a1b27 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Dtos/FansMedalInfoDto.cs @@ -0,0 +1,26 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +namespace Ray.BiliBiliTool.DomainService.Dtos; + +public class FansMedalInfoDto +{ + public FansMedalInfoDto( + long roomId, + MedalWallDto medalInfo, + GetLiveRoomInfoResponse liveRoomInfo + ) + { + RoomId = roomId; + MedalInfo = medalInfo; + LiveRoomInfo = liveRoomInfo; + } + + // 直播间 id + public long RoomId { get; set; } + + // 粉丝牌信息 + public MedalWallDto MedalInfo { get; set; } + + // 直播间信息 + public GetLiveRoomInfoResponse LiveRoomInfo { get; set; } +} diff --git a/src/Ray.BiliBiliTool.DomainService/Dtos/HeartBeatIterationInfoDto.cs b/src/Ray.BiliBiliTool.DomainService/Dtos/HeartBeatIterationInfoDto.cs new file mode 100644 index 0000000..817919b --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Dtos/HeartBeatIterationInfoDto.cs @@ -0,0 +1,26 @@ +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +namespace Ray.BiliBiliTool.DomainService.Dtos; + +public class HeartBeatIterationInfoDto( + long roomId, + GetLiveRoomInfoResponse roomInfo, + HeartBeatResponse heartBeatInfo, + int heartBeatCount, + long lastBeatTime +) +{ + public long RoomId { get; set; } = roomId; + + public GetLiveRoomInfoResponse RoomInfo { get; set; } = roomInfo; + + public HeartBeatResponse HeartBeatInfo { get; set; } = heartBeatInfo; + + // 成功发送的心跳包个数 + public int HeartBeatCount { get; set; } = heartBeatCount; + + public long LastBeatTime { get; set; } = lastBeatTime; + + // 连续失败的次数 + public int FailedTimes { get; set; } +} diff --git a/src/Ray.BiliBiliTool.DomainService/Dtos/VideoInfoDto.cs b/src/Ray.BiliBiliTool.DomainService/Dtos/VideoInfoDto.cs new file mode 100644 index 0000000..b103e3f --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Dtos/VideoInfoDto.cs @@ -0,0 +1,21 @@ +namespace Ray.BiliBiliTool.DomainService.Dtos; + +public class VideoInfoDto +{ + public required string Aid { get; set; } + + public required string Bvid { get; set; } + + public long Cid { get; set; } + + public required string Title { get; set; } + + public int? Duration { get; set; } + + /// + /// 是否转载 + /// 1:原创 + /// 2:转载 + /// + public int Copyright { get; set; } +} diff --git a/src/Ray.BiliBiliTool.DomainService/Extensions/ServiceCollectionExtensions.cs b/src/Ray.BiliBiliTool.DomainService/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..bcbb6de --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Ray.BiliBiliTool.DomainService.Interfaces; + +namespace Ray.BiliBiliTool.DomainService.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddDomainServices(this IServiceCollection services) + { + services.Scan(scan => + scan.FromAssemblyOf() + .AddClasses(classes => classes.AssignableTo()) + .AsImplementedInterfaces() + .WithTransientLifetime() + ); + + return services; + } +} diff --git a/src/Ray.BiliBiliTool.DomainService/Interfaces/IAccountDomainService.cs b/src/Ray.BiliBiliTool.DomainService/Interfaces/IAccountDomainService.cs new file mode 100644 index 0000000..719ec71 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Interfaces/IAccountDomainService.cs @@ -0,0 +1,34 @@ +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +namespace Ray.BiliBiliTool.DomainService.Interfaces; + +/// +/// 账户 +/// +public interface IAccountDomainService : IDomainService +{ + /// + /// 使用Cookie登录 + /// + /// + Task LoginByCookie(BiliCookie cookie); + + /// + /// 获取每日任务完成情况 + /// + /// + Task GetDailyTaskStatus(BiliCookie ck); + + /// + /// 批量取关 + /// + Task UnfollowBatched(BiliCookie ck); + + /// + /// 计算升级时间 + /// + /// + /// 升级时间 + int CalculateUpgradeTime(UserInfo useInfo); +} diff --git a/src/Ray.BiliBiliTool.DomainService/Interfaces/IArticleDomainService.cs b/src/Ray.BiliBiliTool.DomainService/Interfaces/IArticleDomainService.cs new file mode 100644 index 0000000..460714d --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Interfaces/IArticleDomainService.cs @@ -0,0 +1,12 @@ +using Ray.BiliBiliTool.Agent; + +namespace Ray.BiliBiliTool.DomainService.Interfaces; + +public interface IArticleDomainService : IDomainService +{ + Task AddCoinForArticle(long cvid, long mid, BiliCookie ck); + + Task AddCoinForArticles(BiliCookie ck); + + Task LikeArticle(long cvid, BiliCookie ck); +} diff --git a/src/Ray.BiliBiliTool.DomainService/Interfaces/IChargeDomainService.cs b/src/Ray.BiliBiliTool.DomainService/Interfaces/IChargeDomainService.cs new file mode 100644 index 0000000..672c506 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Interfaces/IChargeDomainService.cs @@ -0,0 +1,22 @@ +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +namespace Ray.BiliBiliTool.DomainService.Interfaces; + +/// +/// 充电 +/// +public interface IChargeDomainService : IDomainService +{ + /// + /// 充电 + /// + /// + Task Charge(UserInfo userInfo, BiliCookie ck); + + /// + /// 充电后留言 + /// + /// + Task ChargeComments(string token, BiliCookie ck); +} diff --git a/src/Ray.BiliBiliTool.DomainService/Interfaces/ICoinDomainService.cs b/src/Ray.BiliBiliTool.DomainService/Interfaces/ICoinDomainService.cs new file mode 100644 index 0000000..5c5a12f --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Interfaces/ICoinDomainService.cs @@ -0,0 +1,21 @@ +using Ray.BiliBiliTool.Agent; + +namespace Ray.BiliBiliTool.DomainService.Interfaces; + +/// +/// B币 +/// +public interface ICoinDomainService : IDomainService +{ + /// + /// 获取账户硬币余额 + /// + /// + Task GetCoinBalance(BiliCookie ck); + + /// + /// 获取今日已投币数量 + /// + /// + Task GetDonatedCoins(BiliCookie ck); +} diff --git a/src/Ray.BiliBiliTool.DomainService/Interfaces/IDomainService.cs b/src/Ray.BiliBiliTool.DomainService/Interfaces/IDomainService.cs new file mode 100644 index 0000000..8e9b960 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Interfaces/IDomainService.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.DomainService.Interfaces; + +/// +/// 定义一个领域服务 +/// +public interface IDomainService { } diff --git a/src/Ray.BiliBiliTool.DomainService/Interfaces/IDonateCoinDomainService.cs b/src/Ray.BiliBiliTool.DomainService/Interfaces/IDonateCoinDomainService.cs new file mode 100644 index 0000000..d95ed2c --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Interfaces/IDonateCoinDomainService.cs @@ -0,0 +1,16 @@ +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +namespace Ray.BiliBiliTool.DomainService.Interfaces; + +/// +/// 投币 +/// +public interface IDonateCoinDomainService : IDomainService +{ + Task AddCoinsForVideos(BiliCookie ck); + + Task TryGetCanDonatedVideo(BiliCookie ck); + + Task DoAddCoinForVideo(UpVideoInfo video, bool select_like, BiliCookie ck); +} diff --git a/src/Ray.BiliBiliTool.DomainService/Interfaces/ILiveDomainService.cs b/src/Ray.BiliBiliTool.DomainService/Interfaces/ILiveDomainService.cs new file mode 100644 index 0000000..5197968 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Interfaces/ILiveDomainService.cs @@ -0,0 +1,45 @@ +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; + +namespace Ray.BiliBiliTool.DomainService.Interfaces; + +/// +/// 直播中心 +/// +public interface ILiveDomainService : IDomainService +{ + /// + /// 签到 + /// + Task LiveSign(BiliCookie ck); + + /// + /// 银瓜子兑换硬币 + /// + /// + Task ExchangeSilver2Coin(BiliCookie ck); + + /// + /// 天选抽奖 + /// + Task TianXuan(BiliCookie ck); + + Task TryJoinTianXuan(ListItemDto target, BiliCookie ck); + + Task GroupFollowing(BiliCookie ck); + + /// + /// 发送弹幕 + /// + Task SendDanmakuToFansMedalLive(BiliCookie ck); + + /// + /// 直播时长挂机 + /// + Task SendHeartBeatToFansMedalLive(BiliCookie ck); + + /// + /// 点赞直播间 + /// + Task LikeFansMedalLive(BiliCookie ck); +} diff --git a/src/Ray.BiliBiliTool.DomainService/Interfaces/ILoginDomainService.cs b/src/Ray.BiliBiliTool.DomainService/Interfaces/ILoginDomainService.cs new file mode 100644 index 0000000..defb95f --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Interfaces/ILoginDomainService.cs @@ -0,0 +1,36 @@ +using Ray.BiliBiliTool.Agent; + +namespace Ray.BiliBiliTool.DomainService.Interfaces; + +/// +/// 账户 +/// +public interface ILoginDomainService : IDomainService +{ + /// + /// 扫描二维码登录 + /// + /// + Task LoginByQrCodeAsync(CancellationToken cancellationToken); + + /// + /// Set Cookie + /// + /// + /// + Task SetCookieAsync(BiliCookie cookie, CancellationToken cancellationToken); + + /// + /// 持久化Cookie到配置文件 + /// + /// + Task SaveCookieToJsonFileAsync(BiliCookie ckInfo, CancellationToken cancellationToken); + + /// + /// 持久化Cookie到青龙环境变量 + /// + /// + /// + /// + Task SaveCookieToQinLongAsync(BiliCookie ckInfo, CancellationToken cancellationToken); +} diff --git a/src/Ray.BiliBiliTool.DomainService/Interfaces/IMangaDomainService.cs b/src/Ray.BiliBiliTool.DomainService/Interfaces/IMangaDomainService.cs new file mode 100644 index 0000000..338d461 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Interfaces/IMangaDomainService.cs @@ -0,0 +1,27 @@ +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +namespace Ray.BiliBiliTool.DomainService.Interfaces; + +/// +/// 漫画 +/// +public interface IMangaDomainService : IDomainService +{ + /// + /// 签到 + /// + Task MangaSign(BiliCookie ck); + + /// + /// 阅读 + /// + Task MangaRead(BiliCookie ck); + + /// + /// 获取大会员权益 + /// + /// + /// + Task ReceiveMangaVipReward(int reason_id, UserInfo userIfo, BiliCookie ck); +} diff --git a/src/Ray.BiliBiliTool.DomainService/Interfaces/IVideoDomainService.cs b/src/Ray.BiliBiliTool.DomainService/Interfaces/IVideoDomainService.cs new file mode 100644 index 0000000..60489d0 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Interfaces/IVideoDomainService.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.DomainService.Dtos; + +namespace Ray.BiliBiliTool.DomainService.Interfaces; + +/// +/// 视频 +/// +public interface IVideoDomainService : IDomainService +{ + /// + /// 获取视频详情 + /// + /// + /// + Task GetVideoDetail(string aid); + + /// + /// 从排行榜获取一个随机视频 + /// + /// + Task GetRandomVideoOfRanking(); + + /// + /// 从某个指定UP下获取随机视频 + /// + /// + /// + /// + Task GetRandomVideoOfUp(long upId, int total, BiliCookie ck); + + Task GetVideoCountOfUp(long upId, BiliCookie ck); + + /// + /// 观看并分享视频 + /// + /// + Task WatchAndShareVideo(DailyTaskInfo dailyTaskStatus, BiliCookie ck); + + /// + /// 观看 + /// + /// + /// + Task WatchVideo(VideoInfoDto videoInfo, BiliCookie ck); + + /// + /// 分享 + /// + /// + /// + Task ShareVideo(VideoInfoDto videoInfo, BiliCookie ck); +} diff --git a/src/Ray.BiliBiliTool.DomainService/Interfaces/IVipBigPointDomainService.cs b/src/Ray.BiliBiliTool.DomainService/Interfaces/IVipBigPointDomainService.cs new file mode 100644 index 0000000..4d1555b --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Interfaces/IVipBigPointDomainService.cs @@ -0,0 +1,32 @@ +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask; + +namespace Ray.BiliBiliTool.DomainService.Interfaces; + +public interface IVipBigPointDomainService : IDomainService +{ + Task GetCombineAsync(BiliCookie ck); + + Task VipExpressAsync(BiliCookie ck); + + Task SignAsync(BiliCookie ck); + + Task ReceiveDailyMissionsAsync(VipBigPointCombine combine, BiliCookie ck); + + Task ReceiveAndCompleteAsync( + VipBigPointCombine info, + string moduleCode, + string taskCode, + BiliCookie ck, + Func> completeFunc + ); + + Task CompleteAsync(string taskCode, BiliCookie ck); + + Task CompleteViewAsync(string taskCode, BiliCookie ck); + + Task CompleteViewVipMallAsync(string taskCode, BiliCookie ck); + + Task CompleteV2Async(string taskCode, BiliCookie ck); +} diff --git a/src/Ray.BiliBiliTool.DomainService/Interfaces/IVipPrivilegeDomainService.cs b/src/Ray.BiliBiliTool.DomainService/Interfaces/IVipPrivilegeDomainService.cs new file mode 100644 index 0000000..f617078 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Interfaces/IVipPrivilegeDomainService.cs @@ -0,0 +1,16 @@ +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; + +namespace Ray.BiliBiliTool.DomainService.Interfaces; + +/// +/// 大会员权益 +/// +public interface IVipPrivilegeDomainService : IDomainService +{ + /// + /// 获取大会员权益 + /// + /// + Task ReceiveVipPrivilege(UserInfo userInfo, BiliCookie ck); +} diff --git a/src/Ray.BiliBiliTool.DomainService/LiveDomainService.cs b/src/Ray.BiliBiliTool.DomainService/LiveDomainService.cs new file mode 100644 index 0000000..bcdcd79 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/LiveDomainService.cs @@ -0,0 +1,746 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Dtos; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Extensions; + +namespace Ray.BiliBiliTool.DomainService; + +/// +/// 直播 +/// +public class LiveDomainService( + ILogger logger, + ILiveApi liveApi, + IRelationApi relationApi, + ILiveTraceApi liveTraceApi, + IOptionsMonitor dailyTaskOptions, + IOptionsMonitor liveLotteryTaskOptions, + IOptionsMonitor liveFansMedalTaskOptions, + IOptionsMonitor securityOptions, + IOptionsMonitor silver2CoinTaskOptions, + IUpInfoApi upInfoApi +) : ILiveDomainService +{ + private readonly LiveLotteryTaskOptions _liveLotteryTaskOptions = + liveLotteryTaskOptions.CurrentValue; + private readonly LiveFansMedalTaskOptions _liveFansMedalTaskOptions = + liveFansMedalTaskOptions.CurrentValue; + private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue; + private readonly SecurityOptions _securityOptions = securityOptions.CurrentValue; + private readonly Silver2CoinTaskOptions _silver2CoinTaskOptions = + silver2CoinTaskOptions.CurrentValue; + + /// + /// 本次通过天选关注的主播 + /// + private List _tianXuanFollowed = new(); + + /// + /// 开始抽奖前最后一个关注的up + /// + private long _lastFollowUpId; + + /// + /// 直播签到 + /// + public async Task LiveSign(BiliCookie ck) + { + var response = await liveApi.Sign(ck.ToString()); + + if (response.Code == 0) + { + logger.LogInformation("【签到结果】成功"); + logger.LogInformation( + "【本次获取】{text},{special}", + response.Data!.Text, + response.Data.SpecialText + ); + } + else + { + logger.LogInformation("【签到结果】失败"); + logger.LogInformation("【原因】{msg}", response.Message); + } + } + + /// + /// 直播中心银瓜子兑换B币 + /// + /// 兑换银瓜子后硬币余额 + public async Task ExchangeSilver2Coin(BiliCookie ck) + { + var result = false; + + if (!_silver2CoinTaskOptions.IsEnable) + { + logger.LogInformation("已配置为关闭,跳过"); + return false; + } + + logger.LogInformation("【今天】{day}号", DateTime.Today.Day); + + BiliApiResponse queryStatus = await liveApi.GetLiveWalletStatus( + ck.ToString() + ); + logger.LogInformation("【银瓜子余额】 {silver}", queryStatus.Data.Silver); + logger.LogInformation("【硬币余额】 {coin}", queryStatus.Data.Coin); + logger.LogInformation("【今日剩余兑换次数】 {left}", queryStatus.Data.Silver_2_coin_left); + + if (queryStatus.Data.Silver_2_coin_left <= 0) + return false; + + logger.LogInformation("开始尝试兑换..."); + Silver2CoinRequest request = new(ck.BiliJct); + var response = await liveApi.Silver2Coin(request, ck.ToString()); + if (response.Code == 0) + { + result = true; + logger.LogInformation("【兑换结果】成功兑换 {coin} 枚硬币", response.Data?.Coin); + logger.LogInformation("【银瓜子余额】 {silver}", response.Data?.Silver); + } + else + { + logger.LogInformation("【兑换结果】失败"); + logger.LogInformation("【原因】{reason}", response.Message); + } + + return result; + } + + #region 天选时刻抽奖 + + /// + /// 天选抽奖 + /// + public async Task TianXuan(BiliCookie ck) + { + _tianXuanFollowed = new List(); + + if (_liveLotteryTaskOptions.AutoGroupFollowings) + { + //获取此时最后一个关注的up,此后再新增的关注,与参与成功的抽奖,取交集,就是本地新增的天选关注 + _lastFollowUpId = await GetLastFollowUpId(ck); + } + + //获取直播的分区 + List areaList = (await liveApi.GetAreaList(ck.ToString())).Data.Data; + + //遍历分区 + int count = 0; + foreach (var area in areaList) + { + logger.LogInformation("【扫描分区】{area}..." + Environment.NewLine, area.Name); + + string defaultSort = ""; + //每个分区下搜索5页 + for (int i = 1; i < 6; i++) + { + var request = new GetListRequest + { + platform = "web", + parent_area_id = area.Id, + area_id = 0, + sort_type = defaultSort, + page = i, + wts = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + }; + var reData = (await liveApi.GetList(request, ck.ToString())).Data; + + foreach (var item in reData.List) + { + if (item.Pendant_info == null || item.Pendant_info.Count == 0) + continue; + var suc = item.Pendant_info.TryGetValue("2", out var pendant); + if (!suc) + continue; + if (pendant?.Pendent_id != 504) + continue; + count++; + + await TryJoinTianXuan(item, ck); + } + + if (reData.Has_more != 1) + break; + defaultSort = reData.New_tags.FirstOrDefault()?.Sort_type ?? ""; + } + + defaultSort = ""; + } + + if (count == 0) + { + logger.LogInformation("未搜索到直播间"); + return; + } + } + + public async Task TryJoinTianXuan(ListItemDto target, BiliCookie ck) + { + logger.LogDebug("【房间】{name}", target.Title); + try + { + //黑名单 + if (_liveLotteryTaskOptions.DenyUidList.Contains(target.Uid.ToString())) + { + logger.LogDebug("黑名单,跳过"); + return; + } + + CheckTianXuanDto check = ( + await liveApi.CheckTianXuan(target.Roomid, ck.ToString()) + ).Data; + + if (check == null) + { + logger.LogDebug("数据异常,跳过"); + return; + } + + if (check.Status != TianXuanStatus.Enable) + { + logger.LogDebug("已开奖,跳过" + Environment.NewLine); + return; + } + + //根据配置过滤 + if ( + !check.AwardNameIsSatisfied( + _liveLotteryTaskOptions.IncludeAwardNameList, + _liveLotteryTaskOptions.ExcludeAwardNameList + ) + ) + { + logger.LogDebug("不满足配置的筛选条件,跳过" + Environment.NewLine); + return; + } + + //是否需要赠礼 + if (check.Gift_price > 0) + { + logger.LogDebug("【赠礼】{gift}", check.GiftDesc); + logger.LogDebug("需赠送礼物,跳过" + Environment.NewLine); + return; + } + + //条件 + if (check.Require_type != RequireType.None && check.Require_type != RequireType.Follow) + { + logger.LogDebug("【条件】{text}", check.Require_text); + logger.LogDebug("要求粉丝勋章,跳过"); + return; + } + + logger.LogInformation("【房间】{name}", target.ShortTitle); + logger.LogInformation("【主播】{name}({id})", target.Uname, target.Uid); + logger.LogInformation( + "【奖品】{name}【条件】{text}", + check.Award_name, + check.Require_text + ); + + var request = new JoinTianXuanRequest + { + Id = check.Id, + Gift_id = check.Gift_id, + Gift_num = check.Gift_num, + Csrf = ck.BiliJct, + }; + var re = await liveApi.Join(request, ck.ToString()); + if (re.Code == 0) + { + logger.LogInformation("【抽奖】成功 √" + Environment.NewLine); + if (check.Require_type == RequireType.Follow) + _tianXuanFollowed.AddIfNotExist(target, x => x.Uid == target.Uid); + return; + } + + logger.LogInformation("【抽奖】失败"); + logger.LogInformation("【原因】{msg}" + Environment.NewLine, re.Message); + } + catch (Exception ex) + { + logger.LogWarning("【异常】{msg},{detail}" + Environment.NewLine, ex.Message, ex); + //ignore + } + } + + /// + /// 将本次抽奖新增的关注统一转移到指定分组中 + /// + public async Task GroupFollowing(BiliCookie ck) + { + if (!_tianXuanFollowed.Any()) + { + logger.LogInformation("未关注主播"); + return; + } + + logger.LogInformation( + "【抽奖的主播】{ups}", + string.Join(",", _tianXuanFollowed.Select(x => x.Uname)) + ); + + //目标分组up集合 + List targetUps = await GetNeedGroup(ck); + logger.LogInformation( + "【将自动分组】{ups}", + string.Join(",", targetUps.Select(x => x.Uname)) + ); + + if (!targetUps.Any()) + { + return; + } + + //目标分组Id + long targetGroupId = await GetOrCreateTianXuanGroupId(ck); + + //执行批量分组 + var referer = string.Format(RelationApiConstant.CopyReferer, ck.UserId); + var req = new CopyUserToGroupRequest( + targetUps.Select(x => x.Uid).ToList(), + targetGroupId.ToString(), + ck.BiliJct + ); + var re = await relationApi.CopyUpsToGroup(req, ck.ToString(), referer); + + if (re.Code == 0) + { + logger.LogInformation("【分组结果】全部成功"); + } + else + { + logger.LogWarning("【分组结果】失败"); + logger.LogWarning("【原因】{msg}", re.Message); + } + } + + /// + /// 获取抽奖前最后一个关注的up + /// + /// + private async Task GetLastFollowUpId(BiliCookie ck) + { + var followings = await relationApi.GetFollowings( + new GetFollowingsRequest(long.Parse(ck.UserId), FollowingsOrderType.TimeDesc), + ck.ToString() + ); + return followings.Data.List.FirstOrDefault()?.Mid ?? 0; + } + + /// + /// 获取本次需要自动分组的主播 + /// + /// + private async Task> GetNeedGroup(BiliCookie ck) + { + List addUpIds = new(); + + //获取最后一个upId之后关注的所有upId + var followings = await relationApi.GetFollowings( + new GetFollowingsRequest(long.Parse(ck.UserId), FollowingsOrderType.TimeDesc), + ck.ToString() + ); + + foreach (UpInfo item in followings.Data.List) + { + if (item.Mid == _lastFollowUpId) + { + break; + } + + addUpIds.Add(item.Mid); + } + + //和成功抽奖的主播取交集 + List target = new(); + foreach (var listItemDto in _tianXuanFollowed) + { + if (addUpIds.Contains(listItemDto.Uid)) + target.Add(listItemDto); + } + + return target; + } + + /// + /// 获取或创建天选时刻分组 + /// + /// + private async Task GetOrCreateTianXuanGroupId(BiliCookie ck) + { + //获取天选分组Id,没有就创建 + long groupId = 0; + string referer = string.Format(RelationApiConstant.GetTagsReferer, ck.UserId); + var groups = await relationApi.GetTags(referer); + var tianXuanGroup = groups.Data!.FirstOrDefault(x => x.Name == "天选时刻"); + if (tianXuanGroup == null) + { + logger.LogInformation("“天选时刻”分组不存在,尝试创建..."); + //创建一个 + var createRe = await relationApi.CreateTag( + new CreateTagRequest { Tag = "天选时刻", Csrf = ck.BiliJct }, + ck.ToString() + ); + groupId = createRe.Data.Tagid; + logger.LogInformation("创建成功"); + } + else + { + logger.LogInformation("“天选时刻”分组已存在"); + groupId = tianXuanGroup.Tagid; + } + + return groupId; + } + + #endregion + + public async Task SendDanmakuToFansMedalLive(BiliCookie ck) + { + if (!await CheckLiveCookie(ck)) + return; + + var infoList = await GetFansMedalInfoList(ck); + + foreach (var info in infoList) + { + var medal = info.MedalInfo; + + logger.LogInformation("【直播间】{liveRoomName}", medal.Target_name); + logger.LogInformation("【粉丝牌】{medalName}", medal.Medal_info.Medal_name); + logger.LogInformation("正在发送弹幕..."); + + // 通过空间主页信息获取直播间 id + var liveHostUserId = medal.Medal_info.Target_id; + var req = new GetSpaceInfoDto() { mid = liveHostUserId }; + + var spaceInfo = await upInfoApi.GetSpaceInfo(req, ck.ToString()); + if (spaceInfo.Code != 0) + { + logger.LogError("【获取直播间信息】失败"); + logger.LogError("【原因】{message}", spaceInfo.Message); + return; + } + + var successCount = 0; + var failedCount = 0; + + // 发送弹幕 + + while ( + successCount < _liveFansMedalTaskOptions.SendDanmakuNumber + && failedCount < _liveFansMedalTaskOptions.SendDanmakugiveUpThreshold + ) + { + var sendResult = await liveApi.SendLiveDanmuku( + new SendLiveDanmukuRequest( + ck.BiliJct, + spaceInfo.Data.Live_room.Roomid, + _liveFansMedalTaskOptions.DanmakuContent + ), + ck.ToString() + ); + + if (sendResult.Code != 0) + { + logger.LogError("【弹幕发送】失败"); + logger.LogError("【原因】{message}", sendResult.Message); + failedCount++; + } + else + successCount++; + + var delay = new Random().Next(2000, 4000); + await Task.Delay(delay); + } + + logger.LogInformation( + "【弹幕发送】发送情况:你向主播 {name} 发送弹幕{success}/{total}", + spaceInfo.Data.Name, + successCount, + successCount + failedCount + ); + } + } + + public async Task SendHeartBeatToFansMedalLive(BiliCookie ck) + { + if (!await CheckLiveCookie(ck)) + return; + + var infoList = new List(); + (await GetFansMedalInfoList(ck)) + .FindAll(info => info.LiveRoomInfo.Live_Status != 0) + .ForEach(medal => infoList.Add(new(medal.RoomId, medal.LiveRoomInfo, new(), 0, 0))); + + if (infoList.Count == 0) + { + logger.LogInformation("【直播观看时长】跳过,未检测到符合条件的主播"); + return; + } + + var Now = () => new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds(); + + while ( + infoList.Min(info => + info.FailedTimes >= _liveFansMedalTaskOptions.HeartBeatSendGiveUpThreshold + ? int.MaxValue + : info.HeartBeatCount + ) < _liveFansMedalTaskOptions.HeartBeatNumber + ) + { + foreach (var info in infoList) + { + // 忽略连续失败超过上限的直播间 + if (info.FailedTimes >= _liveFansMedalTaskOptions.HeartBeatSendGiveUpThreshold) + continue; + + string uuid = Guid.NewGuid().ToString(); + var current = Now(); + if ( + current - info.LastBeatTime + <= (LiveFansMedalTaskOptions.HeartBeatInterval + 5) * 1000 + ) + { + int sleepTime = (int)( + (LiveFansMedalTaskOptions.HeartBeatInterval + 5) * 1000 + - (current - info.LastBeatTime) + ); + logger.LogDebug("【休眠】{time} 毫秒", sleepTime); + Thread.Sleep(sleepTime); + } + + // Heart Beat 接口 + var timestamp = Now(); + BiliApiResponse? heartBeatResult = null; + if (info.HeartBeatCount == 0) + { + heartBeatResult = await liveTraceApi.EnterRoom( + new EnterRoomRequest( + info.RoomId, + info.RoomInfo.Parent_area_id, + info.RoomInfo.Area_id, + info.HeartBeatCount, + timestamp, + _securityOptions.UserAgent, + ck.BiliJct, + info.RoomInfo.Uid, + $"[\"{ck.LiveBuvid}\",\"{uuid}\"]" + ), + ck.ToString() + ); + } + else + { + heartBeatResult = await liveTraceApi.HeartBeat( + new HeartBeatRequest( + info.RoomId, + info.RoomInfo.Parent_area_id, + info.RoomInfo.Area_id, + info.HeartBeatCount, + ck.LiveBuvid, + timestamp, + info.HeartBeatInfo.Timestamp, + _securityOptions.UserAgent, + info.HeartBeatInfo.Secret_rule, + info.HeartBeatInfo.Secret_key!, + ck.BiliJct, + uuid, + $"[\"{ck.LiveBuvid}\",\"{uuid}\"]" + ), + ck.ToString() + ); + } + + info.LastBeatTime = Now(); + + if (heartBeatResult != null && heartBeatResult.Data != null) + { + info.HeartBeatInfo.Secret_key = heartBeatResult.Data.Secret_key; + info.HeartBeatInfo.Secret_rule = heartBeatResult.Data.Secret_rule; + info.HeartBeatInfo.Timestamp = heartBeatResult.Data.Timestamp; + } + + if (heartBeatResult == null || heartBeatResult.Code != 0) + { + logger.LogError("【心跳包】直播间 {room} 发送失败", info.RoomId); + logger.LogError( + "【原因】{message}", + heartBeatResult != null ? heartBeatResult.Message : "" + ); + info.FailedTimes += 1; + continue; + } + + info.HeartBeatCount += 1; + info.FailedTimes = 0; + + logger.LogInformation( + "【直播间】{roomId} 的第 {index} 个心跳包发送成功", + info.RoomId, + info.HeartBeatCount + ); + } + } + + var successCount = infoList.Count(info => + info.HeartBeatCount >= _liveFansMedalTaskOptions.HeartBeatNumber + ); + logger.LogInformation( + "【直播观看时长】完成情况:{success}/{total} ", + successCount, + infoList.Count + ); + } + + /// + /// 点赞直播间 + /// + public async Task LikeFansMedalLive(BiliCookie ck) + { + if (!await CheckLiveCookie(ck)) + return; + + var infoList = await GetFansMedalInfoList(ck); + infoList = infoList.FindAll(info => info.LiveRoomInfo.Live_Status != 0); + logger.LogInformation("当前开播直播间数量:{num}", infoList.Count); + foreach (var info in infoList) + { + // Clike_Time 暂时设置为等于设置的LikeNumber,不清楚是否会被风控,我自己抓包最大值为10 + var request = new LikeLiveRoomRequest( + info.RoomId, + ck.BiliJct, + _liveFansMedalTaskOptions.LikeNumber, + info.LiveRoomInfo.Uid, + ck.UserId + ); + + var result = await liveApi.LikeLiveRoom(request.RawTextBuild(), ck.ToString()); + if (result.Code == 0) + { + logger.LogInformation("【点赞直播间】{roomId} 完成", info.RoomId); + } + else + { + logger.LogError("【点赞直播间】{roomId} 时候出现错误", info.RoomId); + logger.LogError("【原因】{message}", result.Message); + } + + var delay = new Random().Next(5000, 8000); + await Task.Delay(delay); + } + } + + private async Task> GetFansMedalInfoList(BiliCookie ck) + { + logger.LogInformation("【获取直播列表】获取拥有粉丝牌的直播列表"); + var medalWallInfo = await liveApi.GetMedalWall(ck.UserId, ck.ToString()); + + if (medalWallInfo.Code != 0) + { + logger.LogError("【获取直播列表】失败"); + logger.LogError("【原因】{message}", medalWallInfo.Message); + return new List(); + } + + var infoList = new List(); + foreach (var medal in medalWallInfo.Data.List) + { + logger.LogInformation("【主播】{name} ", medal.Target_name); + if (_liveFansMedalTaskOptions.IsSkipLevel20Medal && medal.Medal_info.Level >= 20) + { + logger.LogInformation( + "粉丝牌等级为 {level},观看将不再增长亲密度,跳过", + medal.Medal_info.Level + ); + continue; + } + + // 通过空间主页信息获取直播间 id + var liveHostUserId = medal.Medal_info.Target_id; + var req = new GetSpaceInfoDto() { mid = liveHostUserId }; + + var spaceInfo = await upInfoApi.GetSpaceInfo(req, ck.ToString()); + if (spaceInfo.Code != 0) + { + logger.LogError("【获取空间信息】失败"); + logger.LogError("【原因】{message}", spaceInfo.Message); + continue; + } + + // 用以排除有牌子无直播间的up主 + if (spaceInfo.Data.Live_room is null) + { + logger.LogInformation("【主播】{name} 直播间id获取失败,已跳过", medal.Target_name); + continue; + } + + var roomId = spaceInfo.Data.Live_room.Roomid; + + // 获取直播间详细信息 + var liveRoomInfo = await liveApi.GetLiveRoomInfo(roomId); + if (liveRoomInfo.Code != 0) + { + logger.LogError("【获取直播间信息】失败"); + logger.LogError("【原因】{message}", liveRoomInfo.Message); + continue; + } + + infoList.Add(new FansMedalInfoDto(roomId, medal, liveRoomInfo.Data!)); + } + + return infoList; + } + + /// + /// 自动配置直播相关 Cookie,来兼容较低版本中保存的 Cookie 配置 + /// + /// + /// bool 成功配置 or not + /// + private async Task CheckLiveCookie(BiliCookie ck) + { + // 检测 _biliCookie 是否正确配置 + if (!string.IsNullOrWhiteSpace(ck.LiveBuvid)) + return true; + + try + { + logger.LogInformation("检测到直播 Cookie 未正确配置,尝试自动配置中..."); + + // 请求主播主页来正确配置 cookie + var liveHome = await liveApi.GetLiveHome(ck.ToString()); + var liveHomeContent = JsonConvert.DeserializeObject( + await liveHome.Content.ReadAsStringAsync() + ); + if (liveHomeContent?.Code != 0) + { + throw new Exception(liveHomeContent?.Message); + } + + var setHeader = liveHome.Headers.FirstOrDefault(header => header.Key == "Set-Cookie"); + ck.MergeCurrentCookie(setHeader.Value.ToList()); + + logger.LogDebug("LiveBuvid {value}", ck.LiveBuvid); + logger.LogInformation("直播 Cookie 配置成功!"); + } + catch (Exception exception) + { + logger.LogError("【配置直播Cookie】失败,放弃执行后续任务..."); + logger.LogError("【原因】{message}", exception.Message); + return false; + } + + return true; + } +} diff --git a/src/Ray.BiliBiliTool.DomainService/LoginDomainService.cs b/src/Ray.BiliBiliTool.DomainService/LoginDomainService.cs new file mode 100644 index 0000000..1d7ba24 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/LoginDomainService.cs @@ -0,0 +1,445 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using QRCoder; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Passport; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; +using Ray.BiliBiliTool.Agent.QingLong; +using Ray.BiliBiliTool.Agent.QingLong.Dtos; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; +using Ray.BiliBiliTool.Infrastructure.Cookie; + +namespace Ray.BiliBiliTool.DomainService; + +/// +/// 账户 +/// +public class LoginDomainService( + ILogger logger, + IPassportApi passportApi, + IHostEnvironment hostingEnvironment, + IQingLongApi qingLongApi, + IHomeApi homeApi, + IConfiguration configuration, + IOptions qingLongOptions +) : ILoginDomainService +{ + public async Task LoginByQrCodeAsync(CancellationToken cancellationToken) + { + BiliCookie? cookieInfo = null; + + var re = await passportApi.GenerateQrCode(); + if (re.Code != 0) + { + throw new Exception($"获取二维码失败:{re.ToJsonStr()}"); + } + + var url = re.Data.Url; + GenerateQrCode(url); + + var online = GetOnlinePic(url); + logger.LogInformation(Environment.NewLine + Environment.NewLine); + logger.LogInformation( + "如果上方二维码显示异常,或扫描失败,请使用浏览器访问如下链接,查看高清二维码:" + ); + logger.LogInformation(online + Environment.NewLine + Environment.NewLine); + + var waitTimes = 10; + logger.LogInformation("我数到{num},动作快点", waitTimes); + for (int i = 0; i < waitTimes; i++) + { + logger.LogInformation("[{num}]等待扫描...", i + 1); + + await Task.Delay(5 * 1000, cancellationToken); + + var check = await passportApi.CheckQrCodeHasScaned(re.Data.Qrcode_key); + if (!check.IsSuccessStatusCode) + { + logger.LogWarning("调用检测接口异常"); + continue; + } + + var contentStr = await check.Content.ReadAsStringAsync(cancellationToken); + var content = JsonConvert.DeserializeObject>(contentStr); + if (content?.Code != 0) + { + logger.LogWarning("调用检测接口异常:{msg}", check.ToJsonStr()); + break; + } + + if (content.Data.Code == 86038) //已失效 + { + logger.LogInformation(content.Data.Message); + break; + } + + if (content.Data.Code == 0) + { + logger.LogInformation("扫描成功!"); + IEnumerable cookies = check + .Headers.SingleOrDefault(header => header.Key == "Set-Cookie") + .Value; + + var cookieStr = CookieInfo.ConvertSetCkHeadersToCkStr(cookies); + + cookieInfo = CookieStrFactory.CreateNew(cookieStr); + cookieInfo.Check(); + + break; + } + + logger.LogInformation("{msg}", content.Data.Message + Environment.NewLine); + } + + if (cookieInfo == null) + { + throw new Exception("登录超时"); + } + + return cookieInfo; + } + + public async Task SetCookieAsync( + BiliCookie biliCookie, + CancellationToken cancellationToken + ) + { + try + { + var homePage = await homeApi.GetHomePageAsync(biliCookie.ToString()); + if (homePage.IsSuccessStatusCode) + { + logger.LogInformation("访问主站成功"); + IEnumerable setCookieHeaders = homePage + .Headers.SingleOrDefault(header => header.Key == "Set-Cookie") + .Value; + if (setCookieHeaders != null) + { + biliCookie.MergeCurrentCookieBySetCookieHeaders(setCookieHeaders); + logger.LogInformation("SetCookie成功"); + } + else + { + logger.LogInformation("无需set"); + } + + return biliCookie; + } + logger.LogError("访问主站失败:{msg}", homePage.ToJsonStr()); + } + catch (Exception e) + { + //buvid只影响分享和投币,可以吞掉异常 + logger.LogError(e.ToJsonStr()); + } + + return biliCookie; + } + + public async Task SaveCookieToJsonFileAsync( + BiliCookie ckInfo, + CancellationToken cancellationToken + ) + { + //读取json + var path = hostingEnvironment.ContentRootPath; + var indexOfBin = path.LastIndexOf("bin"); + if (indexOfBin != -1) + { + path = path[..indexOfBin]; + } + if (string.Equals(configuration["PlatformType"], "Web", StringComparison.OrdinalIgnoreCase)) + { + path = Path.Combine(path, "config"); + } + var fileProvider = new PhysicalFileProvider(path); + IFileInfo fileInfo = fileProvider.GetFileInfo("cookies.json"); + logger.LogInformation("目标json地址:{path}", fileInfo.PhysicalPath); + + if (!fileInfo.Exists) + { + await using var stream = File.Create(fileInfo.PhysicalPath!); + await using var sw = new StreamWriter(stream); + await sw.WriteAsync($"{{{Environment.NewLine}}}"); + } + + string json; + await using (var stream = new FileStream(fileInfo.PhysicalPath!, FileMode.Open)) + { + using var reader = new StreamReader(stream); + json = await reader.ReadToEndAsync(); + } + var lines = json.Split(Environment.NewLine).ToList(); + + var indexOfCkConfigKey = lines.FindIndex(x => + x.TrimStart().StartsWith("\"BiliBiliCookies\"") + ); + if (indexOfCkConfigKey == -1) + { + logger.LogInformation("未配置过cookie,初始化并新增"); + + var indexOfInsert = lines.FindIndex(x => x.TrimStart().StartsWith("{")); + lines.InsertRange( + indexOfInsert + 1, + new List() + { + " \"BiliBiliCookies\":[", + $@" ""{ckInfo.CookieStr}"",", + " ],", + } + ); + + await SaveJson(lines, fileInfo); + logger.LogInformation("新增成功!"); + return; + } + + ckInfo.CookieItemDictionary.TryGetValue("DedeUserID", out var userId); + userId ??= ckInfo.CookieStr; + var indexOfCkConfigEnd = lines.FindIndex( + indexOfCkConfigKey, + x => x.TrimStart().StartsWith("]") + ); + var indexOfTargetCk = lines.FindIndex( + indexOfCkConfigKey, + indexOfCkConfigEnd - indexOfCkConfigKey, + x => x.Contains(userId) && !x.TrimStart().StartsWith("//") + ); + + if (indexOfTargetCk == -1) + { + logger.LogInformation("不存在该用户,新增cookie"); + lines.Insert(indexOfCkConfigEnd, $@" ""{ckInfo.CookieStr}"","); + await SaveJson(lines, fileInfo); + logger.LogInformation("新增成功!"); + return; + } + + logger.LogInformation("已存在该用户,更新cookie"); + lines[indexOfTargetCk] = $@" ""{ckInfo.CookieStr}"","; + await SaveJson(lines, fileInfo); + logger.LogInformation("更新成功!"); + } + + public async Task SaveCookieToQinLongAsync( + BiliCookie ckInfo, + CancellationToken cancellationToken + ) + { + try + { + var token = await GetQingLongAuthTokenAsync(); + if (string.IsNullOrEmpty(token)) + { + throw new Exception("获取青龙token失败"); + } + + var qlEnvList = await qingLongApi.GetEnvsAsync("Ray_BiliBiliCookies__", token); + if (qlEnvList.Code != 200) + { + throw new Exception($"查询环境变量失败:{qlEnvList.ToJsonStr()}"); + } + + logger.LogDebug(qlEnvList.Data.ToJsonStr()); + logger.LogDebug(ckInfo.ToString()); + + var list = qlEnvList + .Data.Where(x => x.name.StartsWith("Ray_BiliBiliCookies__")) + .ToList(); + var oldEnv = list.FirstOrDefault(x => x.value.Contains(ckInfo.UserId)); + + if (oldEnv != null) + { + logger.LogInformation("用户已存在,更新cookie"); + logger.LogInformation("Key:{key}", oldEnv.name); + var update = new UpdateQingLongEnv + { + id = oldEnv.id, + name = oldEnv.name, + value = ckInfo.CookieStr, + remarks = string.IsNullOrEmpty(oldEnv.remarks) + ? $"bili-{ckInfo.UserId}" + : oldEnv.remarks, + }; + + var updateRe = await qingLongApi.UpdateEnvsAsync(update, token); + logger.LogInformation(updateRe.Code == 200 ? "更新成功!" : updateRe.ToJsonStr()); + + return true; + } + + logger.LogInformation("用户不存在,新增cookie"); + var maxNum = -1; + if (list.Any()) + { + maxNum = list.Select(x => + { + var num = x.name.Replace("Ray_BiliBiliCookies__", ""); + var parseSuc = int.TryParse(num, out int envNum); + return parseSuc ? envNum : 0; + }) + .Max(); + } + + var name = $"Ray_BiliBiliCookies__{maxNum + 1}"; + logger.LogInformation("Key:{key}", name); + + var add = new AddQingLongEnv + { + name = name, + value = ckInfo.CookieStr, + remarks = $"bili-{ckInfo.UserId}", + }; + var addRe = await qingLongApi.AddEnvsAsync([add], token); + logger.LogInformation(addRe.Code == 200 ? "新增成功!" : addRe.ToJsonStr()); + return true; + } + catch + { + await PrintIfSaveCookieFailAsync(ckInfo, cancellationToken); + return false; + } + } + + #region private + + private void GenerateQrCode(string str) + { + var qrGenerator = new QRCodeGenerator(); + QRCodeData qrCodeData = qrGenerator.CreateQrCode(str, QRCodeGenerator.ECCLevel.L); + + logger.LogInformation("AsciiQRCode:"); + //var qrCode = new AsciiQRCode(qrCodeData); + //var qrCodeStr = qrCode.GetGraphic(1, drawQuietZones: false); + //_logger.LogInformation(Environment.NewLine + qrCodeStr); + + //Console.WriteLine("Console:"); + //Print(qrCodeData); + PrintSmall(qrCodeData); + } + + private void Print(QRCodeData qrCodeData) + { + Console.BackgroundColor = ConsoleColor.White; + for (int i = 0; i < qrCodeData.ModuleMatrix.Count + 2; i++) + Console.Write(" "); //中文全角的空格符 + Console.WriteLine(); + for (int j = 0; j < qrCodeData.ModuleMatrix.Count; j++) + { + for (int i = 0; i < qrCodeData.ModuleMatrix.Count; i++) + { + //char charToPoint = qrCode.Matrix[i, j] ? '█' : ' '; + Console.Write(i == 0 ? " " : ""); //中文全角的空格符 + Console.BackgroundColor = qrCodeData.ModuleMatrix[i][j] + ? ConsoleColor.Black + : ConsoleColor.White; + Console.Write(' '); //中文全角的空格符 + Console.BackgroundColor = ConsoleColor.White; + Console.Write(i == qrCodeData.ModuleMatrix.Count - 1 ? " " : ""); //中文全角的空格符 + } + Console.WriteLine(); + } + for (int i = 0; i < qrCodeData.ModuleMatrix.Count + 2; i++) + Console.Write(" "); //中文全角的空格符 + + Console.WriteLine(); + } + + private void PrintSmall(QRCodeData qrCodeData) + { + //黑黑(" ") + //白白("█") + //黑白("▄") + //白黑("▀") + var dic = new Dictionary() + { + { "11", ' ' }, + { "00", '█' }, + { "10", '▄' }, + { "01", '▀' }, //todo:win平台的cmd会显示?,是已知问题,待想办法解决 + //{"01", '^'},//▼▔ + }; + + var count = qrCodeData.ModuleMatrix.Count; + + var list = new List(); + for (int rowNum = 0; rowNum < count; rowNum++) + { + var rowStr = ""; + for (int colNum = 0; colNum < count; colNum++) + { + var num = qrCodeData.ModuleMatrix[colNum][rowNum] ? "1" : "0"; + var numDown = "0"; + if (rowNum + 1 < count) + numDown = qrCodeData.ModuleMatrix[colNum][rowNum + 1] ? "1" : "0"; + + rowStr += dic[num + numDown]; + } + list.Add(rowStr); + rowNum++; + } + + logger.LogInformation(Environment.NewLine + string.Join(Environment.NewLine, list)); + } + + private string GetOnlinePic(string str) + { + var encode = System.Web.HttpUtility.UrlEncode(str); + return $"https://tool.lu/qrcode/basic.html?text={encode}"; + } + + private async Task SaveJson(List lines, IFileInfo fileInfo) + { + var newJson = string.Join(Environment.NewLine, lines); + + await using var sw = new StreamWriter(fileInfo.PhysicalPath!); + await sw.WriteAsync(newJson); + } + + #region qinglong + + private async Task GetQingLongAuthTokenAsync() + { + logger.LogWarning("使用OpenAPI鉴权"); + if ( + string.IsNullOrWhiteSpace(qingLongOptions.Value.ClientId) + || string.IsNullOrWhiteSpace(qingLongOptions.Value.ClientSecret) + ) + { + logger.LogWarning("未配置青龙的ClientId和ClientSecret,无法自动获取token"); + logger.LogWarning( + "教程:{qingDoc}", + "https://github.com/RayWangQvQ/BiliBiliToolPro/blob/main/qinglong/README.md" + ); + return ""; + } + + var token = await qingLongApi.GetTokenAsync( + qingLongOptions.Value.ClientId!, + qingLongOptions.Value.ClientSecret! + ); + + return $"{token.Data.token_type} {token.Data.token}"; + } + + private Task PrintIfSaveCookieFailAsync(BiliCookie ckInfo, CancellationToken cancellationToken) + { + logger.LogError("持久化失败,青龙版本高于2.18,请手动添加环境变量到青龙"); + logger.LogWarning("变量Key:{key}", "Ray_BiliBiliCookies__0"); + logger.LogWarning("变量值:{value}", ckInfo.CookieStr); + logger.LogWarning( + "如果Key已存在,请自行+1,如Ray_BiliBiliCookies__1,Ray_BiliBiliCookies__2..." + ); + return Task.CompletedTask; + } + + #endregion + + #endregion +} diff --git a/src/Ray.BiliBiliTool.DomainService/MangaDomainService.cs b/src/Ray.BiliBiliTool.DomainService/MangaDomainService.cs new file mode 100644 index 0000000..d0dea1f --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/MangaDomainService.cs @@ -0,0 +1,109 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; + +namespace Ray.BiliBiliTool.DomainService; + +/// +/// 漫画 +/// +public class MangaDomainService( + ILogger logger, + IMangaApi mangaApi, + IOptionsMonitor mangaTaskOptions, + IOptionsMonitor dailyTaskOptions, + IOptionsMonitor vipPrivilegeOptions +) : IMangaDomainService +{ + private readonly MangaTaskOptions _mangaTaskOptions = mangaTaskOptions.CurrentValue; + private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue; + private readonly VipPrivilegeOptions _vipPrivilegeOptions = vipPrivilegeOptions.CurrentValue; + + /// + /// 漫画签到 + /// + public async Task MangaSign(BiliCookie ck) + { + BiliApiResponse response; + try + { + response = await mangaApi.ClockIn(_dailyTaskOptions.DevicePlatform, ck.ToString()); + } + catch (Exception) + { + //ignore + //重复签到会报400异常,这里忽略掉 + logger.LogInformation("【签到结果】失败"); + logger.LogInformation("【原因】今日已签到过,无法重复签到"); + return; + } + + if (response.Code == 0) + { + logger.LogInformation("【签到结果】成功"); + } + else + { + logger.LogInformation("【签到结果】失败"); + logger.LogInformation("【原因】{msg}", response.Message); + } + } + + /// + /// 漫画阅读 + /// + public async Task MangaRead(BiliCookie ck) + { + if (_mangaTaskOptions.CustomComicId <= 0) + return; + BiliApiResponse response = await mangaApi.ReadManga( + _dailyTaskOptions.DevicePlatform, + _mangaTaskOptions.CustomComicId, + _mangaTaskOptions.CustomEpId, + ck.ToString() + ); + + if (response.Code == 0) + { + logger.LogInformation("【漫画阅读】成功"); + } + else + { + logger.LogInformation("【漫画阅读】失败"); + logger.LogInformation("【原因】{msg}", response.Message); + } + } + + /// + /// 获取大会员漫画权益 + /// + /// 权益号,由https://api.bilibili.com/x/vip/privilege/my得到权益号数组,取值范围为数组中的整数 + /// 这里为方便直接取1,为领取漫读劵,暂时不取其他的值 + public async Task ReceiveMangaVipReward(int reason_id, UserInfo userInfo, BiliCookie ck) + { + if (userInfo.GetVipType() == 0) + { + logger.LogInformation("不是会员,跳过"); + return; + } + + int day = DateTime.Today.Day; + logger.LogInformation("【今天】{day}号", day); + + var response = await mangaApi.ReceiveMangaVipReward(reason_id, ck.ToString()); + if (response.Code == 0) + { + logger.LogInformation("【领取结果】成功"); + logger.LogInformation($"【获取】{response.Data.Amount}张漫读劵"); + } + else + { + logger.LogInformation("【领取结果】失败"); + logger.LogInformation("【原因】{msg}", response.Message); + } + } +} diff --git a/src/Ray.BiliBiliTool.DomainService/Ray.BiliBiliTool.DomainService.csproj b/src/Ray.BiliBiliTool.DomainService/Ray.BiliBiliTool.DomainService.csproj new file mode 100644 index 0000000..11b7818 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/Ray.BiliBiliTool.DomainService.csproj @@ -0,0 +1,20 @@ + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/src/Ray.BiliBiliTool.DomainService/VideoDomainService.cs b/src/Ray.BiliBiliTool.DomainService/VideoDomainService.cs new file mode 100644 index 0000000..b5ae19c --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/VideoDomainService.cs @@ -0,0 +1,319 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Video; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Dtos; +using Ray.BiliBiliTool.DomainService.Interfaces; + +namespace Ray.BiliBiliTool.DomainService; + +/// +/// 视频 +/// +public class VideoDomainService( + ILogger logger, + IOptionsMonitor dailyTaskOptions, + IRelationApi relationApi, + IVideoApi videoApi, + IVideoWithoutCookieApi videoWithoutCookieApi +) : IVideoDomainService +{ + private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue; + private readonly Dictionary _expDic = Config.Constants.ExpDic; + + /// + /// 获取视频详情 + /// + /// + /// + public async Task GetVideoDetail(string aid) + { + var re = await videoWithoutCookieApi.GetVideoDetail(aid); + return re.Data!; + } + + /// + /// 从排行榜获取随机视频 + /// + /// + public async Task GetRandomVideoOfRanking() + { + var apiResponse = await videoWithoutCookieApi.GetRegionRankingVideosV2(); + logger.LogDebug("获取排行榜成功"); + var data = apiResponse.Data.List[new Random().Next(apiResponse.Data.List.Count)]; + return data; + } + + public async Task GetRandomVideoOfUp(long upId, int total, BiliCookie ck) + { + if (total <= 0) + return null; + + var req = new SearchVideosByUpIdDto() + { + mid = upId, + ps = 1, + pn = new Random().Next(1, total + 1), + }; + + BiliApiResponse re = await videoApi.SearchVideosByUpId( + req, + ck.ToString() + ); + + if (re.Code != 0) + { + throw new Exception(re.Message); + } + + return re.Data?.List?.Vlist.FirstOrDefault(); + } + + /// + /// 获取UP主的视频总数量 + /// + /// + /// + public async Task GetVideoCountOfUp(long upId, BiliCookie ck) + { + var req = new SearchVideosByUpIdDto() { mid = upId }; + + BiliApiResponse re = await videoApi.SearchVideosByUpId( + req, + ck.ToString() + ); + if (re.Code != 0) + { + throw new Exception(re.Message); + } + + return re.Data!.Page.Count; + } + + public async Task WatchAndShareVideo(DailyTaskInfo dailyTaskStatus, BiliCookie ck) + { + VideoInfoDto? targetVideo = null; + + //至少有一项未完成,获取视频 + if (!dailyTaskStatus.Watch || !dailyTaskStatus.Share) + { + targetVideo = await GetRandomVideoForWatchAndShare(ck); + logger.LogInformation("【随机视频】{title}", targetVideo.Title); + } + + bool watched = false; + //观看 + if (!dailyTaskStatus.Watch && _dailyTaskOptions.IsWatchVideo) + { + await WatchVideo(targetVideo!, ck); + watched = true; + } + else + logger.LogInformation("今天已经观看过了,不需要再看啦"); + + //分享 + if (!dailyTaskStatus.Share && _dailyTaskOptions.IsShareVideo) + { + //如果没有打开观看过,则分享前先打开视频 + if (!watched) + { + try + { + await OpenVideo(targetVideo!, ck); + } + catch (Exception e) + { + //ignore + logger.LogError("打开视频异常:{msg}", e.Message); + } + } + await ShareVideo(targetVideo!, ck); + } + else + logger.LogInformation("今天已经分享过了,不用再分享啦"); + } + + /// + /// 观看视频 + /// + public async Task WatchVideo(VideoInfoDto videoInfo, BiliCookie ck) + { + //开始上报一次 + await OpenVideo(videoInfo, ck); + + //结束上报一次 + videoInfo.Duration = videoInfo.Duration ?? 15; + int max = videoInfo.Duration < 15 ? videoInfo.Duration.Value : 15; + int playedTime = new Random().Next(1, max); + + var request = new UploadVideoHeartbeatRequest + { + Aid = long.Parse(videoInfo.Aid), + Bvid = videoInfo.Bvid, + Cid = videoInfo.Cid, + Mid = long.Parse(ck.UserId), + Csrf = ck.BiliJct, + + Played_time = playedTime, + Realtime = playedTime, + Real_played_time = playedTime, + }; + BiliApiResponse apiResponse = await videoApi.UploadVideoHeartbeat(request, ck.ToString()); + + if (apiResponse.Code == 0) + { + _expDic.TryGetValue("每日观看视频", out int exp); + logger.LogInformation( + "视频播放成功,已观看到第{playedTime}秒,经验+{exp} √", + playedTime, + exp + ); + } + else + { + logger.LogError("视频播放失败,原因:{msg}", apiResponse.Message); + } + } + + /// + /// 分享视频 + /// + /// 视频 + public async Task ShareVideo(VideoInfoDto videoInfo, BiliCookie ck) + { + var request = new ShareVideoRequest(long.Parse(videoInfo.Aid), ck.BiliJct); + BiliApiResponse apiResponse = await videoApi.ShareVideo(request, ck.ToString()); + + if (apiResponse.Code == 0) + { + _expDic.TryGetValue("每日观看视频", out int exp); + logger.LogInformation("视频分享成功,经验+{exp} √", exp); + } + else + { + logger.LogError("视频分享失败,原因: {msg}", apiResponse.Message); + } + } + + /// + /// 模拟打开视频播放(初始上报一次进度) + /// + /// + /// + private async Task OpenVideo(VideoInfoDto videoInfo, BiliCookie ck) + { + var request = new UploadVideoHeartbeatRequest + { + Aid = long.Parse(videoInfo.Aid), + Bvid = videoInfo.Bvid, + Cid = videoInfo.Cid, + + Mid = long.Parse(ck.UserId), + Csrf = ck.BiliJct, + }; + + //开始上报一次 + BiliApiResponse apiResponse = await videoApi.UploadVideoHeartbeat(request, ck.ToString()); + + if (apiResponse.Code == 0) + { + logger.LogDebug("打开视频成功"); + return true; + } + else + { + logger.LogError("视频打开失败,原因:{msg}", apiResponse.Message); + return false; + } + } + + #region private + /// + /// 获取一个视频用来观看并分享 + /// + /// + private async Task GetRandomVideoForWatchAndShare(BiliCookie ck) + { + //先从配置的或关注的up中取 + var video = await GetRandomVideoOfFollowingUps(ck); + if (video != null) + return video; + + //然后从排行榜中取 + var t = await GetRandomVideoOfRanking(); + return new VideoInfoDto + { + Aid = t.Aid.ToString(), + Bvid = t.Bvid, + Cid = t.Cid, + Copyright = t.Copyright, + Duration = t.Duration, + Title = t.Title, + }; + } + + private async Task GetRandomVideoOfFollowingUps(BiliCookie ck) + { + //配置的UpId + int configUpsCount = _dailyTaskOptions.SupportUpIdList.Count; + if (configUpsCount > 0) + { + var video = await GetRandomVideoOfUps(_dailyTaskOptions.SupportUpIdList, ck); + if (video != null) + return video; + } + + //关注列表 + var request = new GetFollowingsRequest(long.Parse(ck.UserId)); + BiliApiResponse result = await relationApi.GetFollowings( + request, + ck.ToString() + ); + if (result.Data.Total > 0) + { + var video = await GetRandomVideoOfUps(result.Data.List.Select(x => x.Mid).ToList(), ck); + if (video != null) + return video; + } + + return null; + } + + /// + /// 从up集合中获取一个随机视频 + /// + /// + /// + private async Task GetRandomVideoOfUps(List upIds, BiliCookie ck) + { + long upId = upIds[new Random().Next(0, upIds.Count)]; + + if (upId == 0 || upId == long.MinValue) + return null; + + int count = await GetVideoCountOfUp(upId, ck); + + if (count > 0) + { + var video = await GetRandomVideoOfUp(upId, count, ck); + if (video == null) + return null; + return new VideoInfoDto + { + Aid = video.Aid.ToString(), + Bvid = video.Bvid, + //Cid=, + //Copyright= + Title = video.Title, + Duration = video.Duration, + }; + } + + return null; + } + #endregion private +} diff --git a/src/Ray.BiliBiliTool.DomainService/VipBigPointDomainService.cs b/src/Ray.BiliBiliTool.DomainService/VipBigPointDomainService.cs new file mode 100644 index 0000000..7246b27 --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/VipBigPointDomainService.cs @@ -0,0 +1,370 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.ViewMall; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask.ThreeDaysSign; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Dtos; +using Ray.BiliBiliTool.DomainService.Interfaces; + +namespace Ray.BiliBiliTool.DomainService; + +public class VipBigPointDomainService( + ILogger logger, + IOptionsMonitor vipBigPointOptions, + IVipBigPointApi vipApi, + IMallApi mallApi, + IVipMallApi vipMallApi, + IVideoApi videoApi, + IAccountDomainService accountDomainService, + IVideoDomainService videoDomainService +) : IVipBigPointDomainService +{ + private readonly VipBigPointOptions _vipBigPointOptions = vipBigPointOptions.CurrentValue; + + public async Task GetCombineAsync(BiliCookie ck) + { + var allTasks = await mallApi.GetCombineAsync( + new GetCombineRequest { csrf = ck.BiliJct, buvid = ck.Buvid }, + ck.ToString() + ); + if (allTasks.Code != 0) + throw new Exception(allTasks.ToJsonStr()); + return allTasks.Data; + } + + /// + /// 领取大会员专属等级加速包 + /// + public async Task VipExpressAsync(BiliCookie ck) + { + var re = await vipApi.GetVouchersInfoAsync(ck.ToString()); + if (re.Code == 0) + { + var state = re.Data.List.Find(x => x.Type == 9)?.State; + + switch (state) + { + case 2: + logger.LogInformation("大会员经验观看任务未完成"); + logger.LogInformation("开始观看视频"); + // 观看视频,暂时没有好办法解决,先这样使着 + DailyTaskInfo dailyTaskInfo = await accountDomainService.GetDailyTaskStatus(ck); + await videoDomainService.WatchAndShareVideo(dailyTaskInfo, ck); + // 跳转到未兑换,执行兑换任务 + goto case 0; + + case 1: + logger.LogInformation("大会员经验已兑换"); + break; + + case 0: + logger.LogInformation("大会员经验未兑换"); + //兑换api + var response = await vipApi.ObtainVipExperienceAsync( + new VipExperienceRequest { csrf = ck.BiliJct }, + ck.ToString() + ); + if (response.Code != 0) + { + logger.LogInformation( + "大会员经验领取失败,错误信息:{message}", + response.Message + ); + break; + } + + logger.LogInformation("领取成功,经验+10 √"); + var combine = await GetCombineAsync(ck); + combine.LogPointInfo(logger); + break; + + default: + logger.LogDebug("大会员经验领取失败,未知错误"); + break; + } + } + } + + /// + /// 签到 + /// + /// + /// + public async Task SignAsync(BiliCookie ck) + { + var signInfo = await vipApi.GetThreeDaySignAsync( + new ThreeDaySignRequest { csrf = ck.BiliJct }, + ck.ToString() + ); + if (signInfo.Data.three_day_sign.signed) + { + logger.LogInformation("已完成,跳过"); + logger.LogInformation(signInfo.Data.ToString()); + return; + } + + BiliApiResponse re = await mallApi.Sign2Async( + new Sign2RequestPath(ck.BiliJct), + new Sign2Request(), + ck.ToString() + ); + if (re.Code != 0) + throw new Exception(re.ToJsonStr()); + + logger.LogInformation("签到成功"); + logger.LogInformation(re.Data.ToString()); + + signInfo = await vipApi.GetThreeDaySignAsync( + new ThreeDaySignRequest { csrf = ck.BiliJct }, + ck.ToString() + ); + signInfo.Data.LogPointInfo(logger); + } + + /// + /// 领取任务 + /// + /// + /// + public async Task ReceiveDailyMissionsAsync(VipBigPointCombine combine, BiliCookie ck) + { + const string moduleCode = "日常任务"; + + var module = combine.Task_info.Modules.FirstOrDefault(x => x.module_title == moduleCode); + var missionsNeedReceive = module?.common_task_item.Where(x => x.state == 0).ToList(); + if (missionsNeedReceive == null || missionsNeedReceive.Count == 0) + { + logger.LogInformation("均已领取,跳过"); + return; + } + + foreach (var targetTask in missionsNeedReceive) + { + logger.LogInformation("开始领取任务:{task}", targetTask.title); + await TryReceive(targetTask.task_code, ck); + } + } + + public async Task ReceiveAndCompleteAsync( + VipBigPointCombine info, + string moduleCode, + string taskCode, + BiliCookie ck, + Func> completeFunc + ) + { + var module = info.Task_info.Modules.FirstOrDefault(x => x.module_title == moduleCode); + var bonusTask = module?.common_task_item.FirstOrDefault(x => x.task_code == taskCode); + + if (bonusTask == null) + { + logger.LogInformation("任务失效"); + return; + } + + if (bonusTask.state == 3) + { + logger.LogInformation("已完成,跳过"); + return; + } + + if (bonusTask.state == 0) + { + logger.LogInformation("开始领取任务"); + await TryReceive(bonusTask.task_code, ck); + } + + logger.LogInformation("开始完成任务"); + var re = await completeFunc(taskCode, ck); + + //确认 + if (re) + { + var combine = await GetCombineAsync(ck); + module = combine.Task_info.Modules.FirstOrDefault(x => x.module_title == moduleCode); + bonusTask = module?.common_task_item.FirstOrDefault(x => x.task_code == taskCode); + var success = bonusTask is { state: 3, complete_times: >= 1 }; + logger.LogInformation("确认:{re}", success ? "成功,经验 +10" : "失败"); + } + } + + public async Task CompleteAsync(string taskCode, BiliCookie ck) + { + var request = new ReceiveOrCompleteTaskRequest(taskCode); + var re = await vipApi.CompleteAsync(request, ck.ToString()); + if (re.Code == 0) + { + logger.LogInformation("已完成"); + return true; + } + + logger.LogInformation("失败:{msg}", re.ToJsonStr()); + return false; + } + + public async Task CompleteViewAsync(string taskCode, BiliCookie ck) + { + var channel = taskCode switch + { + "animatetab" => "jp_channel", + "filmtab" => "tv_channel", + _ => throw new ArgumentOutOfRangeException( + nameof(taskCode), + $"Invalid taskCode: {taskCode}" + ), + }; + + logger.LogInformation("开始浏览"); + await Task.Delay(10 * 1000); + + var request = new ViewRequest(channel); + var re = await vipApi.ViewComplete(request, ck.ToString()); + if (re.Code == 0) + { + logger.LogInformation("浏览完成"); + return true; + } + + logger.LogInformation("浏览失败:{msg}", re.ToJsonStr()); + return false; + } + + public async Task CompleteViewVipMallAsync(string taskCode, BiliCookie ck) + { + var re = await vipMallApi.ViewVipMallAsync( + new ViewVipMallRequest { Csrf = ck.BiliJct }, + ck.ToString() + ); + if (re.Code != 0) + throw new Exception(re.ToJsonStr()); + return true; + } + + public async Task CompleteV2Async(string taskCode, BiliCookie ck) + { + var request = new ReceiveOrCompleteTaskRequest(taskCode); + var re = await vipApi.CompleteV2(request, ck.ToString()); + if (re.Code == 0) + { + logger.LogInformation("已完成"); + return true; + } + + logger.LogInformation("失败:{msg}", re.ToJsonStr()); + return false; + } + + #region private + + /// + /// 领取任务 + /// + private async Task TryReceive(string taskCode, BiliCookie ck) + { + BiliApiResponse? re = null; + try + { + var request = new ReceiveOrCompleteTaskRequest(taskCode); + re = await vipApi.ReceiveV2(request, ck.ToString()); + if (re.Code == 0) + logger.LogInformation("领取任务成功"); + else + logger.LogInformation("领取任务失败:{msg}", re.ToJsonStr()); + } + catch (Exception e) + { + logger.LogError("领取任务异常"); + logger.LogError(e.Message + re?.ToJsonStr()); + } + } + + private async Task WatchBangumi(BiliCookie ck) + { + if (_vipBigPointOptions.ViewBangumiList.Count == 0) + return false; + + long randomSsid = _vipBigPointOptions.ViewBangumiList[ + new Random().Next(0, _vipBigPointOptions.ViewBangumiList.Count) + ]; + + var res = await GetBangumi(randomSsid, ck); + if (res is null) + { + return false; + } + + var videoInfo = res.Value.Item1; + + // 随机播放时间 + int playedTime = new Random().Next(905, 1800); + // 观看该视频 + var request = new UploadVideoHeartbeatRequest() + { + Aid = long.Parse(videoInfo.Aid), + Bvid = videoInfo.Bvid, + Cid = videoInfo.Cid, + Mid = long.Parse(ck.UserId), + Sid = randomSsid, + Epid = res.Value.Item2, + Csrf = ck.BiliJct, + Type = 4, + Sub_type = 1, + Start_ts = DateTime.Now.ToTimeStamp() - playedTime, + Played_time = playedTime, + Realtime = playedTime, + Real_played_time = playedTime, + }; + BiliApiResponse apiResponse = await videoApi.UploadVideoHeartbeat(request, ck.ToString()); + if (apiResponse.Code == 0) + { + return true; + } + + return false; + } + + /// + /// 从自定义的番剧ssid中选择其中的一部中的一集 + /// + /// 番剧ssid + /// + private async Task<(VideoInfoDto, long)?> GetBangumi(long randomSsid, BiliCookie ck) + { + try + { + if (randomSsid is 0 or long.MinValue) + return null; + var bangumiInfo = await videoApi.GetBangumiBySsid(randomSsid, ck.ToString()); + + // 从获取的剧集中随机获得其中的一集 + + var bangumi = bangumiInfo.Result.episodes[ + new Random().Next(0, bangumiInfo.Result.episodes.Count) + ]; + var videoInfo = new VideoInfoDto() + { + Bvid = bangumi.bvid, + Aid = bangumi.aid.ToString(), + Cid = bangumi.cid, + Copyright = 1, + Duration = bangumi.duration, + Title = bangumi.share_copy, + }; + logger.LogInformation("本次播放的正片为:{title}", bangumi.share_copy); + return (videoInfo, bangumi.ep_id); + } + catch (Exception e) + { + logger.LogError(e.Message); + } + + return null; + } + + #endregion +} diff --git a/src/Ray.BiliBiliTool.DomainService/VipPrivilegeDomainService.cs b/src/Ray.BiliBiliTool.DomainService/VipPrivilegeDomainService.cs new file mode 100644 index 0000000..197a29c --- /dev/null +++ b/src/Ray.BiliBiliTool.DomainService/VipPrivilegeDomainService.cs @@ -0,0 +1,119 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ray.BiliBiliTool.Agent; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos; +using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.DomainService.Interfaces; + +namespace Ray.BiliBiliTool.DomainService; + +/// +/// 会员权益 +/// +public class VipPrivilegeDomainService( + ILogger logger, + IDailyTaskApi dailyTaskApi, + IOptionsMonitor receiveVipPrivilegeOptions +) : IVipPrivilegeDomainService +{ + private readonly VipPrivilegeOptions _vipPrivilegeOptions = + receiveVipPrivilegeOptions.CurrentValue; + + /// + /// 每月领取大会员福利(B币券、大会员权益) + /// + /// + /// + public async Task ReceiveVipPrivilege(UserInfo userInfo, BiliCookie ck) + { + if (!_vipPrivilegeOptions.IsEnable) + { + logger.LogInformation("已配置为关闭,跳过"); + return false; + } + + //大会员类型 + VipType vipType = userInfo.GetVipType(); + if (vipType != VipType.Annual) + { + logger.LogInformation("普通会员和月度大会员每月不赠送B币券,不需要领取权益喽"); + return false; + } + + /* + int targetDay = _dailyTaskOptions.DayOfReceiveVipPrivilege == -1 + ? 1 + : _dailyTaskOptions.DayOfReceiveVipPrivilege; + + _logger.LogInformation("【目标领取日期】{targetDay}号", targetDay); + _logger.LogInformation("【今天】{day}号", DateTime.Today.Day); + + if (DateTime.Today.Day != targetDay + && DateTime.Today.Day != DateTime.Today.LastDayOfMonth().Day) + { + _logger.LogInformation("跳过"); + return false; + } + */ + + var suc1 = await ReceiveVipPrivilege(VipPrivilegeType.BCoinCoupon, ck); + var suc2 = await ReceiveVipPrivilege(VipPrivilegeType.MembershipBenefits, ck); + + if (suc1 | suc2) + return true; + return false; + } + + #region private + + /// + /// 领取大会员每月赠送福利 + /// + /// 1.大会员B币券;2.大会员福利 + /// + private async Task ReceiveVipPrivilege(VipPrivilegeType type, BiliCookie ck) + { + var response = await dailyTaskApi.ReceiveVipPrivilegeAsync( + (int)type, + ck.BiliJct, + ck.ToString() + ); + + var name = GetPrivilegeName(type); + logger.LogInformation("【领取】{name}", name); + + if (response.Code == 0) + { + logger.LogInformation("【结果】成功"); + return true; + } + else + { + logger.LogInformation("【结果】失败"); + logger.LogInformation("【原因】 {msg}", response.Message); + return false; + } + } + + /// + /// 获取权益名称 + /// + /// + /// + private string GetPrivilegeName(VipPrivilegeType type) + { + switch (type) + { + case VipPrivilegeType.BCoinCoupon: + return "年度大会员每月赠送的B币券"; + + case VipPrivilegeType.MembershipBenefits: + return "大会员福利/权益"; + } + + return ""; + } + + #endregion private +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/BiliDbContext.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/BiliDbContext.cs new file mode 100644 index 0000000..1e63bd8 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/BiliDbContext.cs @@ -0,0 +1,119 @@ +using AppAny.Quartz.EntityFrameworkCore.Migrations; +using AppAny.Quartz.EntityFrameworkCore.Migrations.SQLite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.Extensions.Configuration; +using Ray.BiliBiliTool.Domain; + +namespace Ray.BiliBiliTool.Infrastructure.EF; + +public class BiliDbContext(IConfiguration config) : DbContext +{ + public DbSet ExecutionLogs { get; set; } + public DbSet BiliLogs { get; set; } + public DbSet Users { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlite(config.GetConnectionString("Sqlite")); + base.OnConfiguring(optionsBuilder); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.AddQuartz(builder => builder.UseSqlite("QRTZ_")); + + AddSqliteDateTimeOffsetSupport(modelBuilder); + + modelBuilder + .Entity() + .OwnsOne( + l => l.ExecutionLogDetail, + e => + { + e.ToTable("bili_execution_log_details"); + e.WithOwner().HasForeignKey("LogId"); + } + ); + + modelBuilder.Entity().HasIndex(l => l.RunInstanceId).IsUnique(); + + // for housekeeping or system log display + modelBuilder.Entity().HasIndex(l => new { l.DateAddedUtc, l.LogType }); + + // joining with job + modelBuilder + .Entity() + .HasIndex(l => new + { + l.TriggerName, + l.TriggerGroup, + l.JobName, + l.JobGroup, + l.DateAddedUtc, + }); + + modelBuilder.Entity().Property(e => e.LogType).HasConversion(); + + modelBuilder.Entity(entity => + { + entity + .Property(x => x.FireInstanceIdComputed) // 定义一个影子属性 + .HasComputedColumnSql( + "json_extract(Properties, '$.FireInstanceId')", + stored: false + ); // stored: true 表示持久化存储,利于索引;false (或省略) 为虚拟列 + + entity + .HasIndex(x => x.FireInstanceIdComputed) // 在计算列上创建索引 + .HasDatabaseName("IX_Logs_FireInstanceIdComputed"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Username).IsRequired().HasMaxLength(50); + entity.Property(e => e.PasswordHash).IsRequired(); + entity.Property(e => e.Salt).IsRequired(); + entity + .Property(e => e.Roles) + .HasConversion( + v => string.Join(',', v), + v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList() + ); + + entity.HasIndex(e => e.Username).IsUnique(); + }); + } + + private void AddSqliteDateTimeOffsetSupport(ModelBuilder builder) + { + if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite") + { + // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations + // here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations + // To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset + // use the DateTimeOffsetToBinaryConverter + // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754 + // This only supports millisecond precision, but should be sufficient for most use cases. + foreach (var entityType in builder.Model.GetEntityTypes()) + { + var properties = entityType + .ClrType.GetProperties() + .Where(p => + p.PropertyType == typeof(DateTimeOffset) + || p.PropertyType == typeof(DateTimeOffset?) + ); + foreach (var property in properties) + { + builder + .Entity(entityType.Name) + .Property(property.Name) + .HasConversion(new DateTimeOffsetToBinaryConverter()); + } + } + } + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/DbInitializer.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/DbInitializer.cs new file mode 100644 index 0000000..61fbea1 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/DbInitializer.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore; +using Ray.BiliBiliTool.Domain; +using Ray.BiliBiliTool.Infrastructure.Helpers; + +namespace Ray.BiliBiliTool.Infrastructure.EF; + +public class DbInitializer(BiliDbContext context) +{ + private const string DefaultUserName = "admin"; + private const string DefaultPassword = "BiliTool@2233"; + + public async Task InitializeAsync() + { + await context.Database.MigrateAsync(); + + await InitUserAsync(); + } + + private async Task InitUserAsync() + { + if (await context.Users.AnyAsync()) + { + return; + } + + var (hash, salt) = PasswordHelper.HashPassword(DefaultPassword); + var adminUser = new User + { + Username = DefaultUserName, + PasswordHash = hash, + Salt = salt, + Roles = ["Administrator"], + }; + + context.Users.Add(adminUser); + await context.SaveChangesAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Extensions/ServiceCollectionExtension.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Extensions/ServiceCollectionExtension.cs new file mode 100644 index 0000000..a60c390 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Extensions/ServiceCollectionExtension.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Ray.BiliBiliTool.Infrastructure.EF.Extensions; + +public static class ServiceCollectionExtension +{ + public static IServiceCollection AddEF(this IServiceCollection services) + { + services.AddDbContextFactory(); + services.AddScoped(); + return services; + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503105406_InitQuartz.Designer.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503105406_InitQuartz.Designer.cs new file mode 100644 index 0000000..2d3cd3e --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503105406_InitQuartz.Designer.cs @@ -0,0 +1,540 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ray.BiliBiliTool.Web; +using Ray.BiliBiliTool.Domain; +using Ray.BiliBiliTool.Infrastructure.EF; + +#nullable disable + +namespace Ray.BiliBiliTool.Web.Migrations +{ + [DbContext(typeof(BiliDbContext))] + [Migration("20250503105406_InitQuartz")] + partial class InitQuartz + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("BLOB_DATA"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_BLOB_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("CALENDAR_NAME"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("CALENDAR"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_CALENDARS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("CRON_EXPRESSION"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("TIME_ZONE_ID"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_CRON_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("ENTRY_ID"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("FIRED_TIME"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("INSTANCE_NAME"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("IS_NONCONCURRENT"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("PRIORITY"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("REQUESTS_RECOVERY"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("SCHED_TIME"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("STATE"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_INST_NAME"); + + b.HasIndex("JobGroup") + .HasDatabaseName("IDX_QRTZ_FT_JOB_GROUP"); + + b.HasIndex("JobName") + .HasDatabaseName("IDX_QRTZ_FT_JOB_NAME"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("IDX_QRTZ_FT_JOB_REQ_RECOVERY"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_GROUP"); + + b.HasIndex("TriggerName") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_NAME"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_NM_GP"); + + b.ToTable("QRTZ_FIRED_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("DESCRIPTION"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("IS_DURABLE"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("IS_NONCONCURRENT"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("IS_UPDATE_DATA"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_CLASS_NAME"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("JOB_DATA"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("REQUESTS_RECOVERY"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("IDX_QRTZ_J_REQ_RECOVERY"); + + b.ToTable("QRTZ_JOB_DETAILS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("LOCK_NAME"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_LOCKS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_PAUSED_TRIGGER_GRPS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("INSTANCE_NAME"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("CHECKIN_INTERVAL"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("LAST_CHECKIN_TIME"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_SCHEDULER_STATE", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("BOOL_PROP_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("BOOL_PROP_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("DEC_PROP_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("DEC_PROP_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("INT_PROP_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("INT_PROP_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("LONG_PROP_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("LONG_PROP_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("STR_PROP_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("STR_PROP_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("STR_PROP_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("TIME_ZONE_ID"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_SIMPROP_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("REPEAT_COUNT"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("REPEAT_INTERVAL"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("TIMES_TRIGGERED"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_SIMPLE_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("CALENDAR_NAME"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("DESCRIPTION"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("END_TIME"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("JOB_DATA"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("MISFIRE_INSTR"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("NEXT_FIRE_TIME"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("PREV_FIRE_TIME"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("PRIORITY"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("START_TIME"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_STATE"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_TYPE"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("IDX_QRTZ_T_NEXT_FIRE_TIME"); + + b.HasIndex("TriggerState") + .HasDatabaseName("IDX_QRTZ_T_STATE"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("IDX_QRTZ_T_NFT_ST"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503105406_InitQuartz.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503105406_InitQuartz.cs new file mode 100644 index 0000000..81008bf --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503105406_InitQuartz.cs @@ -0,0 +1,433 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Ray.BiliBiliTool.Web.Migrations +{ + /// + public partial class InitQuartz : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "QRTZ_CALENDARS", + columns: table => new + { + SCHED_NAME = table.Column(type: "text", nullable: false), + CALENDAR_NAME = table.Column(type: "text", nullable: false), + CALENDAR = table.Column(type: "bytea", nullable: false), + }, + constraints: table => + { + table.PrimaryKey( + "PK_QRTZ_CALENDARS", + x => new { x.SCHED_NAME, x.CALENDAR_NAME } + ); + } + ); + + migrationBuilder.CreateTable( + name: "QRTZ_FIRED_TRIGGERS", + columns: table => new + { + SCHED_NAME = table.Column(type: "text", nullable: false), + ENTRY_ID = table.Column(type: "text", nullable: false), + TRIGGER_NAME = table.Column(type: "text", nullable: false), + TRIGGER_GROUP = table.Column(type: "text", nullable: false), + INSTANCE_NAME = table.Column(type: "text", nullable: false), + FIRED_TIME = table.Column(type: "bigint", nullable: false), + SCHED_TIME = table.Column(type: "bigint", nullable: false), + PRIORITY = table.Column(type: "integer", nullable: false), + STATE = table.Column(type: "text", nullable: false), + JOB_NAME = table.Column(type: "text", nullable: true), + JOB_GROUP = table.Column(type: "text", nullable: true), + IS_NONCONCURRENT = table.Column(type: "bool", nullable: false), + REQUESTS_RECOVERY = table.Column(type: "bool", nullable: true), + }, + constraints: table => + { + table.PrimaryKey( + "PK_QRTZ_FIRED_TRIGGERS", + x => new { x.SCHED_NAME, x.ENTRY_ID } + ); + } + ); + + migrationBuilder.CreateTable( + name: "QRTZ_JOB_DETAILS", + columns: table => new + { + SCHED_NAME = table.Column(type: "text", nullable: false), + JOB_NAME = table.Column(type: "text", nullable: false), + JOB_GROUP = table.Column(type: "text", nullable: false), + DESCRIPTION = table.Column(type: "text", nullable: true), + JOB_CLASS_NAME = table.Column(type: "text", nullable: false), + IS_DURABLE = table.Column(type: "bool", nullable: false), + IS_NONCONCURRENT = table.Column(type: "bool", nullable: false), + IS_UPDATE_DATA = table.Column(type: "bool", nullable: false), + REQUESTS_RECOVERY = table.Column(type: "bool", nullable: false), + JOB_DATA = table.Column(type: "bytea", nullable: true), + }, + constraints: table => + { + table.PrimaryKey( + "PK_QRTZ_JOB_DETAILS", + x => new + { + x.SCHED_NAME, + x.JOB_NAME, + x.JOB_GROUP, + } + ); + } + ); + + migrationBuilder.CreateTable( + name: "QRTZ_LOCKS", + columns: table => new + { + SCHED_NAME = table.Column(type: "text", nullable: false), + LOCK_NAME = table.Column(type: "text", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_LOCKS", x => new { x.SCHED_NAME, x.LOCK_NAME }); + } + ); + + migrationBuilder.CreateTable( + name: "QRTZ_PAUSED_TRIGGER_GRPS", + columns: table => new + { + SCHED_NAME = table.Column(type: "text", nullable: false), + TRIGGER_GROUP = table.Column(type: "text", nullable: false), + }, + constraints: table => + { + table.PrimaryKey( + "PK_QRTZ_PAUSED_TRIGGER_GRPS", + x => new { x.SCHED_NAME, x.TRIGGER_GROUP } + ); + } + ); + + migrationBuilder.CreateTable( + name: "QRTZ_SCHEDULER_STATE", + columns: table => new + { + SCHED_NAME = table.Column(type: "text", nullable: false), + INSTANCE_NAME = table.Column(type: "text", nullable: false), + LAST_CHECKIN_TIME = table.Column(type: "bigint", nullable: false), + CHECKIN_INTERVAL = table.Column(type: "bigint", nullable: false), + }, + constraints: table => + { + table.PrimaryKey( + "PK_QRTZ_SCHEDULER_STATE", + x => new { x.SCHED_NAME, x.INSTANCE_NAME } + ); + } + ); + + migrationBuilder.CreateTable( + name: "QRTZ_TRIGGERS", + columns: table => new + { + SCHED_NAME = table.Column(type: "text", nullable: false), + TRIGGER_NAME = table.Column(type: "text", nullable: false), + TRIGGER_GROUP = table.Column(type: "text", nullable: false), + JOB_NAME = table.Column(type: "text", nullable: false), + JOB_GROUP = table.Column(type: "text", nullable: false), + DESCRIPTION = table.Column(type: "text", nullable: true), + NEXT_FIRE_TIME = table.Column(type: "bigint", nullable: true), + PREV_FIRE_TIME = table.Column(type: "bigint", nullable: true), + PRIORITY = table.Column(type: "integer", nullable: true), + TRIGGER_STATE = table.Column(type: "text", nullable: false), + TRIGGER_TYPE = table.Column(type: "text", nullable: false), + START_TIME = table.Column(type: "bigint", nullable: false), + END_TIME = table.Column(type: "bigint", nullable: true), + CALENDAR_NAME = table.Column(type: "text", nullable: true), + MISFIRE_INSTR = table.Column(type: "smallint", nullable: true), + JOB_DATA = table.Column(type: "bytea", nullable: true), + }, + constraints: table => + { + table.PrimaryKey( + "PK_QRTZ_TRIGGERS", + x => new + { + x.SCHED_NAME, + x.TRIGGER_NAME, + x.TRIGGER_GROUP, + } + ); + table.ForeignKey( + name: "FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS_SCHED_NAME_JOB_NAME_JOB_GROUP", + columns: x => new + { + x.SCHED_NAME, + x.JOB_NAME, + x.JOB_GROUP, + }, + principalTable: "QRTZ_JOB_DETAILS", + principalColumns: new[] { "SCHED_NAME", "JOB_NAME", "JOB_GROUP" }, + onDelete: ReferentialAction.Cascade + ); + } + ); + + migrationBuilder.CreateTable( + name: "QRTZ_BLOB_TRIGGERS", + columns: table => new + { + SCHED_NAME = table.Column(type: "text", nullable: false), + TRIGGER_NAME = table.Column(type: "text", nullable: false), + TRIGGER_GROUP = table.Column(type: "text", nullable: false), + BLOB_DATA = table.Column(type: "bytea", nullable: true), + }, + constraints: table => + { + table.PrimaryKey( + "PK_QRTZ_BLOB_TRIGGERS", + x => new + { + x.SCHED_NAME, + x.TRIGGER_NAME, + x.TRIGGER_GROUP, + } + ); + table.ForeignKey( + name: "FK_QRTZ_BLOB_TRIGGERS_QRTZ_TRIGGERS_SCHED_NAME_TRIGGER_NAME_TRIGGER_GROUP", + columns: x => new + { + x.SCHED_NAME, + x.TRIGGER_NAME, + x.TRIGGER_GROUP, + }, + principalTable: "QRTZ_TRIGGERS", + principalColumns: new[] { "SCHED_NAME", "TRIGGER_NAME", "TRIGGER_GROUP" }, + onDelete: ReferentialAction.Cascade + ); + } + ); + + migrationBuilder.CreateTable( + name: "QRTZ_CRON_TRIGGERS", + columns: table => new + { + SCHED_NAME = table.Column(type: "text", nullable: false), + TRIGGER_NAME = table.Column(type: "text", nullable: false), + TRIGGER_GROUP = table.Column(type: "text", nullable: false), + CRON_EXPRESSION = table.Column(type: "text", nullable: false), + TIME_ZONE_ID = table.Column(type: "text", nullable: true), + }, + constraints: table => + { + table.PrimaryKey( + "PK_QRTZ_CRON_TRIGGERS", + x => new + { + x.SCHED_NAME, + x.TRIGGER_NAME, + x.TRIGGER_GROUP, + } + ); + table.ForeignKey( + name: "FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS_SCHED_NAME_TRIGGER_NAME_TRIGGER_GROUP", + columns: x => new + { + x.SCHED_NAME, + x.TRIGGER_NAME, + x.TRIGGER_GROUP, + }, + principalTable: "QRTZ_TRIGGERS", + principalColumns: new[] { "SCHED_NAME", "TRIGGER_NAME", "TRIGGER_GROUP" }, + onDelete: ReferentialAction.Cascade + ); + } + ); + + migrationBuilder.CreateTable( + name: "QRTZ_SIMPLE_TRIGGERS", + columns: table => new + { + SCHED_NAME = table.Column(type: "text", nullable: false), + TRIGGER_NAME = table.Column(type: "text", nullable: false), + TRIGGER_GROUP = table.Column(type: "text", nullable: false), + REPEAT_COUNT = table.Column(type: "bigint", nullable: false), + REPEAT_INTERVAL = table.Column(type: "bigint", nullable: false), + TIMES_TRIGGERED = table.Column(type: "bigint", nullable: false), + }, + constraints: table => + { + table.PrimaryKey( + "PK_QRTZ_SIMPLE_TRIGGERS", + x => new + { + x.SCHED_NAME, + x.TRIGGER_NAME, + x.TRIGGER_GROUP, + } + ); + table.ForeignKey( + name: "FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS_SCHED_NAME_TRIGGER_NAME_TRIGGER_GROUP", + columns: x => new + { + x.SCHED_NAME, + x.TRIGGER_NAME, + x.TRIGGER_GROUP, + }, + principalTable: "QRTZ_TRIGGERS", + principalColumns: new[] { "SCHED_NAME", "TRIGGER_NAME", "TRIGGER_GROUP" }, + onDelete: ReferentialAction.Cascade + ); + } + ); + + migrationBuilder.CreateTable( + name: "QRTZ_SIMPROP_TRIGGERS", + columns: table => new + { + SCHED_NAME = table.Column(type: "text", nullable: false), + TRIGGER_NAME = table.Column(type: "text", nullable: false), + TRIGGER_GROUP = table.Column(type: "text", nullable: false), + STR_PROP_1 = table.Column(type: "text", nullable: true), + STR_PROP_2 = table.Column(type: "text", nullable: true), + STR_PROP_3 = table.Column(type: "text", nullable: true), + INT_PROP_1 = table.Column(type: "integer", nullable: true), + INT_PROP_2 = table.Column(type: "integer", nullable: true), + LONG_PROP_1 = table.Column(type: "bigint", nullable: true), + LONG_PROP_2 = table.Column(type: "bigint", nullable: true), + DEC_PROP_1 = table.Column(type: "numeric", nullable: true), + DEC_PROP_2 = table.Column(type: "numeric", nullable: true), + BOOL_PROP_1 = table.Column(type: "bool", nullable: true), + BOOL_PROP_2 = table.Column(type: "bool", nullable: true), + TIME_ZONE_ID = table.Column(type: "text", nullable: true), + }, + constraints: table => + { + table.PrimaryKey( + "PK_QRTZ_SIMPROP_TRIGGERS", + x => new + { + x.SCHED_NAME, + x.TRIGGER_NAME, + x.TRIGGER_GROUP, + } + ); + table.ForeignKey( + name: "FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS_SCHED_NAME_TRIGGER_NAME_TRIGGER_GROUP", + columns: x => new + { + x.SCHED_NAME, + x.TRIGGER_NAME, + x.TRIGGER_GROUP, + }, + principalTable: "QRTZ_TRIGGERS", + principalColumns: new[] { "SCHED_NAME", "TRIGGER_NAME", "TRIGGER_GROUP" }, + onDelete: ReferentialAction.Cascade + ); + } + ); + + migrationBuilder.CreateIndex( + name: "IDX_QRTZ_FT_JOB_GROUP", + table: "QRTZ_FIRED_TRIGGERS", + column: "JOB_GROUP" + ); + + migrationBuilder.CreateIndex( + name: "IDX_QRTZ_FT_JOB_NAME", + table: "QRTZ_FIRED_TRIGGERS", + column: "JOB_NAME" + ); + + migrationBuilder.CreateIndex( + name: "IDX_QRTZ_FT_JOB_REQ_RECOVERY", + table: "QRTZ_FIRED_TRIGGERS", + column: "REQUESTS_RECOVERY" + ); + + migrationBuilder.CreateIndex( + name: "IDX_QRTZ_FT_TRIG_GROUP", + table: "QRTZ_FIRED_TRIGGERS", + column: "TRIGGER_GROUP" + ); + + migrationBuilder.CreateIndex( + name: "IDX_QRTZ_FT_TRIG_INST_NAME", + table: "QRTZ_FIRED_TRIGGERS", + column: "INSTANCE_NAME" + ); + + migrationBuilder.CreateIndex( + name: "IDX_QRTZ_FT_TRIG_NAME", + table: "QRTZ_FIRED_TRIGGERS", + column: "TRIGGER_NAME" + ); + + migrationBuilder.CreateIndex( + name: "IDX_QRTZ_FT_TRIG_NM_GP", + table: "QRTZ_FIRED_TRIGGERS", + columns: new[] { "SCHED_NAME", "TRIGGER_NAME", "TRIGGER_GROUP" } + ); + + migrationBuilder.CreateIndex( + name: "IDX_QRTZ_J_REQ_RECOVERY", + table: "QRTZ_JOB_DETAILS", + column: "REQUESTS_RECOVERY" + ); + + migrationBuilder.CreateIndex( + name: "IDX_QRTZ_T_NEXT_FIRE_TIME", + table: "QRTZ_TRIGGERS", + column: "NEXT_FIRE_TIME" + ); + + migrationBuilder.CreateIndex( + name: "IDX_QRTZ_T_NFT_ST", + table: "QRTZ_TRIGGERS", + columns: new[] { "NEXT_FIRE_TIME", "TRIGGER_STATE" } + ); + + migrationBuilder.CreateIndex( + name: "IDX_QRTZ_T_STATE", + table: "QRTZ_TRIGGERS", + column: "TRIGGER_STATE" + ); + + migrationBuilder.CreateIndex( + name: "IX_QRTZ_TRIGGERS_SCHED_NAME_JOB_NAME_JOB_GROUP", + table: "QRTZ_TRIGGERS", + columns: new[] { "SCHED_NAME", "JOB_NAME", "JOB_GROUP" } + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "QRTZ_BLOB_TRIGGERS"); + + migrationBuilder.DropTable(name: "QRTZ_CALENDARS"); + + migrationBuilder.DropTable(name: "QRTZ_CRON_TRIGGERS"); + + migrationBuilder.DropTable(name: "QRTZ_FIRED_TRIGGERS"); + + migrationBuilder.DropTable(name: "QRTZ_LOCKS"); + + migrationBuilder.DropTable(name: "QRTZ_PAUSED_TRIGGER_GRPS"); + + migrationBuilder.DropTable(name: "QRTZ_SCHEDULER_STATE"); + + migrationBuilder.DropTable(name: "QRTZ_SIMPLE_TRIGGERS"); + + migrationBuilder.DropTable(name: "QRTZ_SIMPROP_TRIGGERS"); + + migrationBuilder.DropTable(name: "QRTZ_TRIGGERS"); + + migrationBuilder.DropTable(name: "QRTZ_JOB_DETAILS"); + } + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503164108_Update.Designer.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503164108_Update.Designer.cs new file mode 100644 index 0000000..7215627 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503164108_Update.Designer.cs @@ -0,0 +1,648 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ray.BiliBiliTool.Infrastructure.EF; + +#nullable disable + +namespace Ray.BiliBiliTool.Web.Migrations +{ + [DbContext(typeof(BiliDbContext))] + [Migration("20250503164108_Update")] + partial class Update + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.3"); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("BLOB_DATA"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_BLOB_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("CALENDAR_NAME"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("CALENDAR"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_CALENDARS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("CRON_EXPRESSION"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("TIME_ZONE_ID"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_CRON_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("ENTRY_ID"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("FIRED_TIME"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("INSTANCE_NAME"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("IS_NONCONCURRENT"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("PRIORITY"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("REQUESTS_RECOVERY"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("SCHED_TIME"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("STATE"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_INST_NAME"); + + b.HasIndex("JobGroup") + .HasDatabaseName("IDX_QRTZ_FT_JOB_GROUP"); + + b.HasIndex("JobName") + .HasDatabaseName("IDX_QRTZ_FT_JOB_NAME"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("IDX_QRTZ_FT_JOB_REQ_RECOVERY"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_GROUP"); + + b.HasIndex("TriggerName") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_NAME"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_NM_GP"); + + b.ToTable("QRTZ_FIRED_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("DESCRIPTION"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("IS_DURABLE"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("IS_NONCONCURRENT"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("IS_UPDATE_DATA"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_CLASS_NAME"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("JOB_DATA"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("REQUESTS_RECOVERY"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("IDX_QRTZ_J_REQ_RECOVERY"); + + b.ToTable("QRTZ_JOB_DETAILS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("LOCK_NAME"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_LOCKS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_PAUSED_TRIGGER_GRPS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("INSTANCE_NAME"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("CHECKIN_INTERVAL"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("LAST_CHECKIN_TIME"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_SCHEDULER_STATE", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("BOOL_PROP_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("BOOL_PROP_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("DEC_PROP_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("DEC_PROP_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("INT_PROP_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("INT_PROP_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("LONG_PROP_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("LONG_PROP_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("STR_PROP_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("STR_PROP_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("STR_PROP_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("TIME_ZONE_ID"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_SIMPROP_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("REPEAT_COUNT"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("REPEAT_INTERVAL"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("TIMES_TRIGGERED"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_SIMPLE_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("CALENDAR_NAME"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("DESCRIPTION"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("END_TIME"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("JOB_DATA"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("MISFIRE_INSTR"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("NEXT_FIRE_TIME"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("PREV_FIRE_TIME"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("PRIORITY"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("START_TIME"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_STATE"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_TYPE"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("IDX_QRTZ_T_NEXT_FIRE_TIME"); + + b.HasIndex("TriggerState") + .HasDatabaseName("IDX_QRTZ_T_STATE"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("IDX_QRTZ_T_NFT_ST"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.ExecutionLog", b => + { + b.Property("LogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAddedUtc") + .HasColumnType("INTEGER"); + + b.Property("ErrorMessage") + .HasMaxLength(8000) + .HasColumnType("TEXT"); + + b.Property("FireTimeUtc") + .HasColumnType("INTEGER"); + + b.Property("IsException") + .HasColumnType("INTEGER"); + + b.Property("IsSuccess") + .HasColumnType("INTEGER"); + + b.Property("IsVetoed") + .HasColumnType("INTEGER"); + + b.Property("JobGroup") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("JobName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("JobRunTime") + .HasColumnType("TEXT"); + + b.Property("LogType") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.Property("Result") + .HasMaxLength(8000) + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("ReturnCode") + .HasMaxLength(28) + .HasColumnType("TEXT"); + + b.Property("RunInstanceId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ScheduleFireTimeUtc") + .HasColumnType("INTEGER"); + + b.Property("TriggerGroup") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("TriggerName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("LogId"); + + b.HasIndex("RunInstanceId") + .IsUnique(); + + b.HasIndex("DateAddedUtc", "LogType"); + + b.HasIndex("TriggerName", "TriggerGroup", "JobName", "JobGroup", "DateAddedUtc"); + + b.ToTable("bili_execution_logs"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.ExecutionLog", b => + { + b.OwnsOne("Ray.BiliBiliTool.Domain.ExecutionLogDetail", "ExecutionLogDetail", b1 => + { + b1.Property("LogId") + .HasColumnType("INTEGER"); + + b1.Property("ErrorCode") + .HasColumnType("INTEGER"); + + b1.Property("ErrorHelpLink") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b1.Property("ErrorStackTrace") + .HasColumnType("TEXT"); + + b1.Property("ExecutionDetails") + .HasColumnType("TEXT"); + + b1.HasKey("LogId"); + + b1.ToTable("bili_execution_log_details", (string)null); + + b1.WithOwner() + .HasForeignKey("LogId"); + }); + + b.Navigation("ExecutionLogDetail"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503164108_Update.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503164108_Update.cs new file mode 100644 index 0000000..bbe6cd9 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503164108_Update.cs @@ -0,0 +1,123 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Ray.BiliBiliTool.Web.Migrations +{ + /// + public partial class Update : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "bili_execution_logs", + columns: table => new + { + LogId = table + .Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RunInstanceId = table.Column( + type: "TEXT", + maxLength: 256, + nullable: true + ), + LogType = table.Column(type: "varchar(20)", nullable: false), + JobName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + JobGroup = table.Column(type: "TEXT", maxLength: 256, nullable: true), + TriggerName = table.Column( + type: "TEXT", + maxLength: 256, + nullable: true + ), + TriggerGroup = table.Column( + type: "TEXT", + maxLength: 256, + nullable: true + ), + ScheduleFireTimeUtc = table.Column(type: "INTEGER", nullable: true), + FireTimeUtc = table.Column(type: "INTEGER", nullable: true), + JobRunTime = table.Column(type: "TEXT", nullable: true), + RetryCount = table.Column(type: "INTEGER", nullable: true), + Result = table.Column(type: "TEXT", maxLength: 8000, nullable: true), + ErrorMessage = table.Column( + type: "TEXT", + maxLength: 8000, + nullable: true + ), + IsVetoed = table.Column(type: "INTEGER", nullable: true), + IsException = table.Column(type: "INTEGER", nullable: true), + IsSuccess = table.Column(type: "INTEGER", nullable: true), + ReturnCode = table.Column(type: "TEXT", maxLength: 28, nullable: true), + DateAddedUtc = table.Column(type: "INTEGER", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_bili_execution_logs", x => x.LogId); + } + ); + + migrationBuilder.CreateTable( + name: "bili_execution_log_details", + columns: table => new + { + LogId = table.Column(type: "INTEGER", nullable: false), + ExecutionDetails = table.Column(type: "TEXT", nullable: true), + ErrorStackTrace = table.Column(type: "TEXT", nullable: true), + ErrorCode = table.Column(type: "INTEGER", nullable: true), + ErrorHelpLink = table.Column( + type: "TEXT", + maxLength: 1000, + nullable: true + ), + }, + constraints: table => + { + table.PrimaryKey("PK_bili_execution_log_details", x => x.LogId); + table.ForeignKey( + name: "FK_bili_execution_log_details_bili_execution_logs_LogId", + column: x => x.LogId, + principalTable: "bili_execution_logs", + principalColumn: "LogId", + onDelete: ReferentialAction.Cascade + ); + } + ); + + migrationBuilder.CreateIndex( + name: "IX_bili_execution_logs_DateAddedUtc_LogType", + table: "bili_execution_logs", + columns: new[] { "DateAddedUtc", "LogType" } + ); + + migrationBuilder.CreateIndex( + name: "IX_bili_execution_logs_RunInstanceId", + table: "bili_execution_logs", + column: "RunInstanceId", + unique: true + ); + + migrationBuilder.CreateIndex( + name: "IX_bili_execution_logs_TriggerName_TriggerGroup_JobName_JobGroup_DateAddedUtc", + table: "bili_execution_logs", + columns: new[] + { + "TriggerName", + "TriggerGroup", + "JobName", + "JobGroup", + "DateAddedUtc", + } + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "bili_execution_log_details"); + + migrationBuilder.DropTable(name: "bili_execution_logs"); + } + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250510130427_AddBiliLogs.Designer.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250510130427_AddBiliLogs.Designer.cs new file mode 100644 index 0000000..a62a325 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250510130427_AddBiliLogs.Designer.cs @@ -0,0 +1,690 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ray.BiliBiliTool.Infrastructure.EF; + +#nullable disable + +namespace Ray.BiliBiliTool.Web.Migrations +{ + [DbContext(typeof(BiliDbContext))] + [Migration("20250510130427_AddBiliLogs")] + partial class AddBiliLogs + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.3"); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("BLOB_DATA"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_BLOB_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("CALENDAR_NAME"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("CALENDAR"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_CALENDARS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("CRON_EXPRESSION"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("TIME_ZONE_ID"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_CRON_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("ENTRY_ID"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("FIRED_TIME"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("INSTANCE_NAME"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("IS_NONCONCURRENT"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("PRIORITY"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("REQUESTS_RECOVERY"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("SCHED_TIME"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("STATE"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_INST_NAME"); + + b.HasIndex("JobGroup") + .HasDatabaseName("IDX_QRTZ_FT_JOB_GROUP"); + + b.HasIndex("JobName") + .HasDatabaseName("IDX_QRTZ_FT_JOB_NAME"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("IDX_QRTZ_FT_JOB_REQ_RECOVERY"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_GROUP"); + + b.HasIndex("TriggerName") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_NAME"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_NM_GP"); + + b.ToTable("QRTZ_FIRED_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("DESCRIPTION"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("IS_DURABLE"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("IS_NONCONCURRENT"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("IS_UPDATE_DATA"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_CLASS_NAME"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("JOB_DATA"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("REQUESTS_RECOVERY"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("IDX_QRTZ_J_REQ_RECOVERY"); + + b.ToTable("QRTZ_JOB_DETAILS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("LOCK_NAME"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_LOCKS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_PAUSED_TRIGGER_GRPS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("INSTANCE_NAME"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("CHECKIN_INTERVAL"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("LAST_CHECKIN_TIME"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_SCHEDULER_STATE", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("BOOL_PROP_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("BOOL_PROP_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("DEC_PROP_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("DEC_PROP_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("INT_PROP_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("INT_PROP_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("LONG_PROP_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("LONG_PROP_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("STR_PROP_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("STR_PROP_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("STR_PROP_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("TIME_ZONE_ID"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_SIMPROP_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("REPEAT_COUNT"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("REPEAT_INTERVAL"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("TIMES_TRIGGERED"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_SIMPLE_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("CALENDAR_NAME"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("DESCRIPTION"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("END_TIME"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("JOB_DATA"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("MISFIRE_INSTR"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("NEXT_FIRE_TIME"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("PREV_FIRE_TIME"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("PRIORITY"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("START_TIME"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_STATE"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_TYPE"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("IDX_QRTZ_T_NEXT_FIRE_TIME"); + + b.HasIndex("TriggerState") + .HasDatabaseName("IDX_QRTZ_T_STATE"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("IDX_QRTZ_T_NFT_ST"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.BiliLogs", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Exception") + .HasColumnType("TEXT") + .HasColumnName("exception"); + + b.Property("FireInstanceIdComputed") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasColumnName("fireInstanceIdComputed") + .HasComputedColumnSql("json_extract(Properties, '$.FireInstanceId')", false); + + b.Property("Level") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("level"); + + b.Property("Properties") + .HasColumnType("TEXT") + .HasColumnName("properties"); + + b.Property("RenderedMessage") + .HasColumnType("TEXT") + .HasColumnName("renderedMessage"); + + b.Property("Timestamp") + .HasColumnType("TEXT") + .HasColumnName("timeStamp"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceIdComputed") + .HasDatabaseName("IX_Logs_FireInstanceIdComputed"); + + b.ToTable("bili_logs"); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.ExecutionLog", b => + { + b.Property("LogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAddedUtc") + .HasColumnType("INTEGER"); + + b.Property("ErrorMessage") + .HasMaxLength(8000) + .HasColumnType("TEXT"); + + b.Property("FireTimeUtc") + .HasColumnType("INTEGER"); + + b.Property("IsException") + .HasColumnType("INTEGER"); + + b.Property("IsSuccess") + .HasColumnType("INTEGER"); + + b.Property("IsVetoed") + .HasColumnType("INTEGER"); + + b.Property("JobGroup") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("JobName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("JobRunTime") + .HasColumnType("TEXT"); + + b.Property("LogType") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.Property("Result") + .HasMaxLength(8000) + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("ReturnCode") + .HasMaxLength(28) + .HasColumnType("TEXT"); + + b.Property("RunInstanceId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ScheduleFireTimeUtc") + .HasColumnType("INTEGER"); + + b.Property("TriggerGroup") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("TriggerName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("LogId"); + + b.HasIndex("RunInstanceId") + .IsUnique(); + + b.HasIndex("DateAddedUtc", "LogType"); + + b.HasIndex("TriggerName", "TriggerGroup", "JobName", "JobGroup", "DateAddedUtc"); + + b.ToTable("bili_execution_logs"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.ExecutionLog", b => + { + b.OwnsOne("Ray.BiliBiliTool.Domain.ExecutionLogDetail", "ExecutionLogDetail", b1 => + { + b1.Property("LogId") + .HasColumnType("INTEGER"); + + b1.Property("ErrorCode") + .HasColumnType("INTEGER"); + + b1.Property("ErrorHelpLink") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b1.Property("ErrorStackTrace") + .HasColumnType("TEXT"); + + b1.Property("ExecutionDetails") + .HasColumnType("TEXT"); + + b1.HasKey("LogId"); + + b1.ToTable("bili_execution_log_details", (string)null); + + b1.WithOwner() + .HasForeignKey("LogId"); + }); + + b.Navigation("ExecutionLogDetail"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250510130427_AddBiliLogs.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250510130427_AddBiliLogs.cs new file mode 100644 index 0000000..c865c98 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250510130427_AddBiliLogs.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Ray.BiliBiliTool.Web.Migrations +{ + /// + public partial class AddBiliLogs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + try + { + migrationBuilder.DropTable(name: "bili_logs"); + } + catch + { + // ignored + } + migrationBuilder.CreateTable( + name: "bili_logs", + columns: table => new + { + id = table + .Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + timeStamp = table.Column(type: "TEXT", nullable: false), + level = table.Column(type: "TEXT", nullable: false), + exception = table.Column(type: "TEXT", nullable: true), + renderedMessage = table.Column(type: "TEXT", nullable: true), + properties = table.Column(type: "TEXT", nullable: true), + fireInstanceIdComputed = table.Column( + type: "TEXT", + nullable: true, + computedColumnSql: "json_extract(Properties, '$.FireInstanceId')", + stored: false + ), + }, + constraints: table => + { + table.PrimaryKey("PK_bili_logs", x => x.id); + } + ); + + migrationBuilder.CreateIndex( + name: "IX_Logs_FireInstanceIdComputed", + table: "bili_logs", + column: "fireInstanceIdComputed" + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "bili_logs"); + } + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.Designer.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.Designer.cs new file mode 100644 index 0000000..c818dee --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.Designer.cs @@ -0,0 +1,722 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ray.BiliBiliTool.Infrastructure.EF; + +#nullable disable + +namespace Ray.BiliBiliTool.Web.Migrations +{ + [DbContext(typeof(BiliDbContext))] + [Migration("20250615100041_AddUser")] + partial class AddUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.3"); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("BLOB_DATA"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_BLOB_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("CALENDAR_NAME"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("CALENDAR"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_CALENDARS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("CRON_EXPRESSION"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("TIME_ZONE_ID"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_CRON_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("ENTRY_ID"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("FIRED_TIME"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("INSTANCE_NAME"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("IS_NONCONCURRENT"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("PRIORITY"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("REQUESTS_RECOVERY"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("SCHED_TIME"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("STATE"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_INST_NAME"); + + b.HasIndex("JobGroup") + .HasDatabaseName("IDX_QRTZ_FT_JOB_GROUP"); + + b.HasIndex("JobName") + .HasDatabaseName("IDX_QRTZ_FT_JOB_NAME"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("IDX_QRTZ_FT_JOB_REQ_RECOVERY"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_GROUP"); + + b.HasIndex("TriggerName") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_NAME"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_NM_GP"); + + b.ToTable("QRTZ_FIRED_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("DESCRIPTION"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("IS_DURABLE"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("IS_NONCONCURRENT"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("IS_UPDATE_DATA"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_CLASS_NAME"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("JOB_DATA"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("REQUESTS_RECOVERY"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("IDX_QRTZ_J_REQ_RECOVERY"); + + b.ToTable("QRTZ_JOB_DETAILS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("LOCK_NAME"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_LOCKS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_PAUSED_TRIGGER_GRPS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("INSTANCE_NAME"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("CHECKIN_INTERVAL"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("LAST_CHECKIN_TIME"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_SCHEDULER_STATE", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("BOOL_PROP_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("BOOL_PROP_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("DEC_PROP_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("DEC_PROP_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("INT_PROP_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("INT_PROP_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("LONG_PROP_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("LONG_PROP_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("STR_PROP_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("STR_PROP_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("STR_PROP_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("TIME_ZONE_ID"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_SIMPROP_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("REPEAT_COUNT"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("REPEAT_INTERVAL"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("TIMES_TRIGGERED"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_SIMPLE_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("CALENDAR_NAME"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("DESCRIPTION"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("END_TIME"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("JOB_DATA"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("MISFIRE_INSTR"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("NEXT_FIRE_TIME"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("PREV_FIRE_TIME"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("PRIORITY"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("START_TIME"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_STATE"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_TYPE"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("IDX_QRTZ_T_NEXT_FIRE_TIME"); + + b.HasIndex("TriggerState") + .HasDatabaseName("IDX_QRTZ_T_STATE"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("IDX_QRTZ_T_NFT_ST"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.BiliLogs", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Exception") + .HasColumnType("TEXT") + .HasColumnName("exception"); + + b.Property("FireInstanceIdComputed") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasColumnName("fireInstanceIdComputed") + .HasComputedColumnSql("json_extract(Properties, '$.FireInstanceId')", false); + + b.Property("Level") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("level"); + + b.Property("Properties") + .HasColumnType("TEXT") + .HasColumnName("properties"); + + b.Property("RenderedMessage") + .HasColumnType("TEXT") + .HasColumnName("renderedMessage"); + + b.Property("Timestamp") + .HasColumnType("TEXT") + .HasColumnName("timeStamp"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceIdComputed") + .HasDatabaseName("IX_Logs_FireInstanceIdComputed"); + + b.ToTable("bili_logs"); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.ExecutionLog", b => + { + b.Property("LogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAddedUtc") + .HasColumnType("INTEGER"); + + b.Property("ErrorMessage") + .HasMaxLength(8000) + .HasColumnType("TEXT"); + + b.Property("FireTimeUtc") + .HasColumnType("INTEGER"); + + b.Property("IsException") + .HasColumnType("INTEGER"); + + b.Property("IsSuccess") + .HasColumnType("INTEGER"); + + b.Property("IsVetoed") + .HasColumnType("INTEGER"); + + b.Property("JobGroup") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("JobName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("JobRunTime") + .HasColumnType("TEXT"); + + b.Property("LogType") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.Property("Result") + .HasMaxLength(8000) + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("ReturnCode") + .HasMaxLength(28) + .HasColumnType("TEXT"); + + b.Property("RunInstanceId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ScheduleFireTimeUtc") + .HasColumnType("INTEGER"); + + b.Property("TriggerGroup") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("TriggerName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("LogId"); + + b.HasIndex("RunInstanceId") + .IsUnique(); + + b.HasIndex("DateAddedUtc", "LogType"); + + b.HasIndex("TriggerName", "TriggerGroup", "JobName", "JobGroup", "DateAddedUtc"); + + b.ToTable("bili_execution_logs"); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("bili_User"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.ExecutionLog", b => + { + b.OwnsOne("Ray.BiliBiliTool.Domain.ExecutionLogDetail", "ExecutionLogDetail", b1 => + { + b1.Property("LogId") + .HasColumnType("INTEGER"); + + b1.Property("ErrorCode") + .HasColumnType("INTEGER"); + + b1.Property("ErrorHelpLink") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b1.Property("ErrorStackTrace") + .HasColumnType("TEXT"); + + b1.Property("ExecutionDetails") + .HasColumnType("TEXT"); + + b1.HasKey("LogId"); + + b1.ToTable("bili_execution_log_details", (string)null); + + b1.WithOwner() + .HasForeignKey("LogId"); + }); + + b.Navigation("ExecutionLogDetail"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.cs new file mode 100644 index 0000000..7d26e5e --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Ray.BiliBiliTool.Web.Migrations +{ + /// + public partial class AddUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "bili_user", + columns: table => new + { + Id = table + .Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Username = table.Column(type: "TEXT", maxLength: 50, nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: false), + Salt = table.Column(type: "TEXT", nullable: false), + Roles = table.Column(type: "TEXT", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_bili_user", x => x.Id); + } + ); + + migrationBuilder.CreateIndex( + name: "IX_bili_user_Username", + table: "bili_user", + column: "Username", + unique: true + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "bili_user"); + } + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/BiliDbContextModelSnapshot.cs b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/BiliDbContextModelSnapshot.cs new file mode 100644 index 0000000..ccb478f --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/BiliDbContextModelSnapshot.cs @@ -0,0 +1,719 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ray.BiliBiliTool.Infrastructure.EF; + +#nullable disable + +namespace Ray.BiliBiliTool.Web.Migrations +{ + [DbContext(typeof(BiliDbContext))] + partial class BiliDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.3"); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("BLOB_DATA"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_BLOB_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("CALENDAR_NAME"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("CALENDAR"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_CALENDARS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("CRON_EXPRESSION"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("TIME_ZONE_ID"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_CRON_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("ENTRY_ID"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("FIRED_TIME"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("INSTANCE_NAME"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("IS_NONCONCURRENT"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("PRIORITY"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("REQUESTS_RECOVERY"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("SCHED_TIME"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("STATE"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_INST_NAME"); + + b.HasIndex("JobGroup") + .HasDatabaseName("IDX_QRTZ_FT_JOB_GROUP"); + + b.HasIndex("JobName") + .HasDatabaseName("IDX_QRTZ_FT_JOB_NAME"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("IDX_QRTZ_FT_JOB_REQ_RECOVERY"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_GROUP"); + + b.HasIndex("TriggerName") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_NAME"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("IDX_QRTZ_FT_TRIG_NM_GP"); + + b.ToTable("QRTZ_FIRED_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("DESCRIPTION"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("IS_DURABLE"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("IS_NONCONCURRENT"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("IS_UPDATE_DATA"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_CLASS_NAME"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("JOB_DATA"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("REQUESTS_RECOVERY"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("IDX_QRTZ_J_REQ_RECOVERY"); + + b.ToTable("QRTZ_JOB_DETAILS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("LOCK_NAME"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_LOCKS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_PAUSED_TRIGGER_GRPS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("INSTANCE_NAME"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("CHECKIN_INTERVAL"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("LAST_CHECKIN_TIME"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_SCHEDULER_STATE", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("BOOL_PROP_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("BOOL_PROP_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("DEC_PROP_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("DEC_PROP_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("INT_PROP_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("INT_PROP_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("LONG_PROP_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("LONG_PROP_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("STR_PROP_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("STR_PROP_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("STR_PROP_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("TIME_ZONE_ID"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_SIMPROP_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("REPEAT_COUNT"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("REPEAT_INTERVAL"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("TIMES_TRIGGERED"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_SIMPLE_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("SCHED_NAME"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("TRIGGER_NAME"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("TRIGGER_GROUP"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("CALENDAR_NAME"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("DESCRIPTION"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("END_TIME"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("JOB_DATA"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_GROUP"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("JOB_NAME"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("MISFIRE_INSTR"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("NEXT_FIRE_TIME"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("PREV_FIRE_TIME"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("PRIORITY"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("START_TIME"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_STATE"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("TRIGGER_TYPE"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("IDX_QRTZ_T_NEXT_FIRE_TIME"); + + b.HasIndex("TriggerState") + .HasDatabaseName("IDX_QRTZ_T_STATE"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("IDX_QRTZ_T_NFT_ST"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_TRIGGERS", (string)null); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.BiliLogs", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Exception") + .HasColumnType("TEXT") + .HasColumnName("exception"); + + b.Property("FireInstanceIdComputed") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasColumnName("fireInstanceIdComputed") + .HasComputedColumnSql("json_extract(Properties, '$.FireInstanceId')", false); + + b.Property("Level") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("level"); + + b.Property("Properties") + .HasColumnType("TEXT") + .HasColumnName("properties"); + + b.Property("RenderedMessage") + .HasColumnType("TEXT") + .HasColumnName("renderedMessage"); + + b.Property("Timestamp") + .HasColumnType("TEXT") + .HasColumnName("timeStamp"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceIdComputed") + .HasDatabaseName("IX_Logs_FireInstanceIdComputed"); + + b.ToTable("bili_logs"); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.ExecutionLog", b => + { + b.Property("LogId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAddedUtc") + .HasColumnType("INTEGER"); + + b.Property("ErrorMessage") + .HasMaxLength(8000) + .HasColumnType("TEXT"); + + b.Property("FireTimeUtc") + .HasColumnType("INTEGER"); + + b.Property("IsException") + .HasColumnType("INTEGER"); + + b.Property("IsSuccess") + .HasColumnType("INTEGER"); + + b.Property("IsVetoed") + .HasColumnType("INTEGER"); + + b.Property("JobGroup") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("JobName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("JobRunTime") + .HasColumnType("TEXT"); + + b.Property("LogType") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.Property("Result") + .HasMaxLength(8000) + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("ReturnCode") + .HasMaxLength(28) + .HasColumnType("TEXT"); + + b.Property("RunInstanceId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ScheduleFireTimeUtc") + .HasColumnType("INTEGER"); + + b.Property("TriggerGroup") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("TriggerName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("LogId"); + + b.HasIndex("RunInstanceId") + .IsUnique(); + + b.HasIndex("DateAddedUtc", "LogType"); + + b.HasIndex("TriggerName", "TriggerGroup", "JobName", "JobGroup", "DateAddedUtc"); + + b.ToTable("bili_execution_logs"); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("bili_user"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("Ray.BiliBiliTool.Domain.ExecutionLog", b => + { + b.OwnsOne("Ray.BiliBiliTool.Domain.ExecutionLogDetail", "ExecutionLogDetail", b1 => + { + b1.Property("LogId") + .HasColumnType("INTEGER"); + + b1.Property("ErrorCode") + .HasColumnType("INTEGER"); + + b1.Property("ErrorHelpLink") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b1.Property("ErrorStackTrace") + .HasColumnType("TEXT"); + + b1.Property("ExecutionDetails") + .HasColumnType("TEXT"); + + b1.HasKey("LogId"); + + b1.ToTable("bili_execution_log_details", (string)null); + + b1.WithOwner() + .HasForeignKey("LogId"); + }); + + b.Navigation("ExecutionLogDetail"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/README.md b/src/Ray.BiliBiliTool.Infrastructure.EF/README.md new file mode 100644 index 0000000..505a5a5 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/README.md @@ -0,0 +1,12 @@ +## Add new migration + +```bash +cd ./src/Ray.BiliBiliTool.Web +dotnet ef migrations add AddUser --project ../Ray.BiliBiliTool.Infrastructure.EF +``` + +## Remove migration + +```bash +dotnet ef migrations remove --project ../Ray.BiliBiliTool.Infrastructure.EF +``` diff --git a/src/Ray.BiliBiliTool.Infrastructure.EF/Ray.BiliBiliTool.Infrastructure.EF.csproj b/src/Ray.BiliBiliTool.Infrastructure.EF/Ray.BiliBiliTool.Infrastructure.EF.csproj new file mode 100644 index 0000000..96ab8b4 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure.EF/Ray.BiliBiliTool.Infrastructure.EF.csproj @@ -0,0 +1,19 @@ + + + net8.0 + enable + enable + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/src/Ray.BiliBiliTool.Infrastructure/Cookie/CookieInfo.cs b/src/Ray.BiliBiliTool.Infrastructure/Cookie/CookieInfo.cs new file mode 100644 index 0000000..462a5bd --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Cookie/CookieInfo.cs @@ -0,0 +1,128 @@ +namespace Ray.BiliBiliTool.Infrastructure.Cookie; + +public class CookieInfo(Dictionary cookieDic) +{ + public Dictionary CookieItemDictionary { get; private set; } = cookieDic; + + public string CookieStr => + string.Join( + "; ", + CookieItemDictionary + .Select(item => $"{CkNameBuild(item.Key)}={CkValueBuild(item.Value)}") + .ToList() + ); + + public virtual void Check() + { + if (CookieItemDictionary == null || CookieItemDictionary.Count == 0) + throw new Exception("Cookie字符串为空"); + } + + protected virtual string CkNameBuild(string name) + { + return name; + } + + protected virtual string CkValueBuild(string value) + { + return value; + } + + public override string ToString() + { + var list = CookieItemDictionary.Select(d => + $"{CkNameBuild(d.Key)}={CkValueBuild(d.Value)}" + ); + return string.Join("; ", list); + } + + #region merge + + public void MergeCurrentCookieBySetCookieHeaders(IEnumerable setCookieList) + { + MergeCurrentCookie(ConvertSetCkHeadersToCkItemList(setCookieList)); + } + + public void MergeCurrentCookie(string ckStr) + { + MergeCurrentCookie(ConvertCkStrToCkItemList(ckStr)); + } + + public void MergeCurrentCookie(List ckItemList) + { + MergeCurrentCookie(ConvertCkItemListToCkDic(ckItemList)); + } + + private void MergeCurrentCookie(Dictionary ckDic) + { + foreach (var item in ckDic) + { + CookieItemDictionary[item.Key] = item.Value; + } + } + + #endregion + + #region convert + + /// + /// List of setCkHeader —> list of ckItem + /// + /// + /// + private static List ConvertSetCkHeadersToCkItemList(IEnumerable setCookieList) + { + return setCookieList + .Select(item => item.Split(';').FirstOrDefault()?.Trim() ?? "") + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToList(); + } + + /// + /// List of setCkHeader —> ckStr + /// + /// + /// + public static string ConvertSetCkHeadersToCkStr(IEnumerable setCookieList) + { + var ckItemList = ConvertSetCkHeadersToCkItemList(setCookieList); + return ConvertCkItemListToCkStr(ckItemList); + } + + /// + /// ckStr—>List of ckItem + /// + /// + /// + private static List ConvertCkStrToCkItemList(string ckStr) + { + return ckStr.Split(";", StringSplitOptions.TrimEntries).ToList(); + } + + /// + /// List of ckItem —> ckStr + /// + /// + /// + private static string ConvertCkItemListToCkStr(IEnumerable ckItemList) + { + return string.Join("; ", ckItemList); + } + + /// + /// List of ckItem —> Dictionary + /// + /// + /// + private static Dictionary ConvertCkItemListToCkDic( + IEnumerable ckItemList + ) + { + return ckItemList.ToDictionary( + k => k[..k.IndexOf("=", StringComparison.Ordinal)].Trim(), + v => v[(v.IndexOf("=", StringComparison.Ordinal) + 1)..].Trim().TrimEnd(';') + ); + } + + #endregion +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/Cookie/CookieStrFactory.cs b/src/Ray.BiliBiliTool.Infrastructure/Cookie/CookieStrFactory.cs new file mode 100644 index 0000000..5f9950b --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Cookie/CookieStrFactory.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.Configuration; +using Ray.BiliBiliTool.Infrastructure.Extensions; + +namespace Ray.BiliBiliTool.Infrastructure.Cookie; + +public class CookieStrFactory(IConfiguration configuration) + where TCookieInfo : CookieInfo +{ + private Dictionary> CookieDictionary => GetCookieDictionary(); + + public int Count => CookieDictionary.Count; + + public TCookieInfo GetCookie(int index) + { + var dic = GetCookieDictionary()[index]; + return (TCookieInfo)Activator.CreateInstance(typeof(TCookieInfo), dic)! + ?? throw new InvalidOperationException(); + } + + public static TCookieInfo CreateNew(string cookie) + { + Dictionary dic = CkStrToDictionary(cookie); + return (TCookieInfo)Activator.CreateInstance(typeof(TCookieInfo), dic)! + ?? throw new InvalidOperationException(); + } + + #region private + + private Dictionary> GetCookieDictionary() + { + var list = configuration.GetSection("BiliBiliCookies").Get>() ?? []; + return CookeStrListToCookieDic(list); + } + + private Dictionary> CookeStrListToCookieDic(List ckList) + { + var dic = new Dictionary>(); + ckList ??= []; + + for (int i = 0; i < ckList?.Count; i++) + { + dic.Add(i, CkStrToDictionary(ckList[i])); + } + + return dic; + } + + private static Dictionary CkStrToDictionary(string ckStr) + { + var dic = new Dictionary(); + var ckItemList = ckStr.Split(";", StringSplitOptions.TrimEntries).Distinct(); + foreach (var item in ckItemList) + { + var key = item[..item.IndexOf("=", StringComparison.Ordinal)].Trim(); + var value = item[(item.IndexOf("=", StringComparison.Ordinal) + 1)..].Trim(); + dic.AddIfNotExist(new KeyValuePair(key, value), p => p.Key == key); + } + return dic; + } + + private string DictionaryToCkStr(Dictionary dic) + { + var list = dic.Select(item => $"{item.Key}={item.Value}").ToList(); + return string.Join("; ", list); + } + + #endregion +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/Enums/PlatformType.cs b/src/Ray.BiliBiliTool.Infrastructure/Enums/PlatformType.cs new file mode 100644 index 0000000..d1763f7 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Enums/PlatformType.cs @@ -0,0 +1,10 @@ +namespace Ray.BiliBiliTool.Infrastructure.Enums; + +public enum PlatformType +{ + Unknown, + GitHubActions, + Docker, + QingLong, + Web, +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/Extensions/ICollectionExtensions.cs b/src/Ray.BiliBiliTool.Infrastructure/Extensions/ICollectionExtensions.cs new file mode 100644 index 0000000..a83e921 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Extensions/ICollectionExtensions.cs @@ -0,0 +1,16 @@ +namespace Ray.BiliBiliTool.Infrastructure.Extensions; + +public static class ICollectionExtensions +{ + public static void AddIfNotExist(this ICollection source, T add) + { + if (!source.Any(x => x != null && x.Equals(add))) + source.Add(add); + } + + public static void AddIfNotExist(this ICollection source, T add, Func exist) + { + if (!source.Any(exist)) + source.Add(add); + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/Extensions/KeyValuePairExtensions.cs b/src/Ray.BiliBiliTool.Infrastructure/Extensions/KeyValuePairExtensions.cs new file mode 100644 index 0000000..164dc12 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Extensions/KeyValuePairExtensions.cs @@ -0,0 +1,92 @@ +namespace Ray.BiliBiliTool.Infrastructure.Extensions; + +public static class KeyValuePairExtensions +{ + /// + /// 获得一个新的 + /// + /// 旧Key类型 + /// 旧Value类型 + /// 新Key类型 + /// 新Value类型 + /// + /// 设置新Key的委托 + /// 设置新Value的委托 + /// + public static KeyValuePair New( + this KeyValuePair oldKv, + Func key, + Func value + ) + { + return KeyValuePair.Create(key(oldKv.Key), value(oldKv.Value)); + } + + /// + /// 获得一个新的 + /// + /// 旧Key类型 + /// 旧Value类型 + /// 新Key类型 + /// + /// 设置新Key的委托 + /// + public static KeyValuePair NewKey( + this KeyValuePair oldKv, + Func key + ) + { + return KeyValuePairExtensions.New(oldKv, key, t => t); + } + + /// + /// 获得一个新的 + /// + /// 旧Key类型 + /// 旧Value类型 + /// 新Key类型 + /// + /// 新Key + /// + public static KeyValuePair NewKey( + this KeyValuePair oldKv, + TNewKey key + ) + { + return KeyValuePairExtensions.New(oldKv, t => key, t => t); + } + + /// + /// 获得一个新的 + /// + /// 旧Key类型 + /// 旧Value类型 + /// 新Value类型 + /// + /// 设置新Value的委托 + /// + public static KeyValuePair NewValue( + this KeyValuePair oldKv, + Func value + ) + { + return KeyValuePairExtensions.New(oldKv, t => t, value); + } + + /// + /// 获得一个新的 + /// + /// 旧Key类型 + /// 旧Value类型 + /// 新Value类型 + /// + /// 新Value/param> + /// + public static KeyValuePair NewValue( + this KeyValuePair oldKv, + TNewValue value + ) + { + return KeyValuePairExtensions.New(oldKv, t => t, t => value); + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/Extensions/TypeExtensions.cs b/src/Ray.BiliBiliTool.Infrastructure/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..3c78e39 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Extensions/TypeExtensions.cs @@ -0,0 +1,22 @@ +using System.ComponentModel; + +namespace Ray.BiliBiliTool.Infrastructure.Extensions; + +public static class TypeExtensions +{ + /// + /// 获取属性的Description + /// + /// + /// + /// + public static string GetPropertyDescription(this Type type, string propertyName) + { + var desc = (DescriptionAttribute?) + type.GetProperty(propertyName) + ?.GetCustomAttributes(typeof(DescriptionAttribute), false) + .FirstOrDefault(); + + return desc?.Description ?? ""; + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/Global.cs b/src/Ray.BiliBiliTool.Infrastructure/Global.cs new file mode 100644 index 0000000..f8b2db1 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Global.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Configuration; + +namespace Ray.BiliBiliTool.Infrastructure; + +public class Global +{ + /// + /// 根配置 + /// + public static IConfigurationRoot? ConfigurationRoot { get; set; } + + /// + /// 根容器 + /// + public static IServiceProvider? ServiceProviderRoot { get; set; } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/Helpers/IpHelper.cs b/src/Ray.BiliBiliTool.Infrastructure/Helpers/IpHelper.cs new file mode 100644 index 0000000..2ef9691 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Helpers/IpHelper.cs @@ -0,0 +1,17 @@ +namespace Ray.BiliBiliTool.Infrastructure.Helpers; + +public class IpHelper +{ + public static string? GetIp() + { + try + { + var re = new HttpClient().GetAsync("http://api.ipify.org/").Result; + return re.IsSuccessStatusCode ? re.Content.ReadAsStringAsync().Result : null; + } + catch (Exception) + { + return null; + } + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/Helpers/ObjectHelper.cs b/src/Ray.BiliBiliTool.Infrastructure/Helpers/ObjectHelper.cs new file mode 100644 index 0000000..55636ed --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Helpers/ObjectHelper.cs @@ -0,0 +1,18 @@ +using System.Reflection; + +namespace Ray.BiliBiliTool.Infrastructure.Helpers; + +public static class ObjectHelper +{ + public static Dictionary ObjectToDictionary(object obj) + { + // 获取对象的所有属性 + PropertyInfo[] properties = obj.GetType().GetProperties(); + + // 遍历所有属性并将其添加到字典中 + return properties.ToDictionary( + property => property.Name, + property => property.GetValue(obj) + ); + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/Helpers/PasswordHelper.cs b/src/Ray.BiliBiliTool.Infrastructure/Helpers/PasswordHelper.cs new file mode 100644 index 0000000..c052e37 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Helpers/PasswordHelper.cs @@ -0,0 +1,34 @@ +using System.Security.Cryptography; + +namespace Ray.BiliBiliTool.Infrastructure.Helpers; + +public class PasswordHelper +{ + public static (string hash, string salt) HashPassword(string password) + { + byte[] saltBytes = RandomNumberGenerator.GetBytes(16); + string salt = Convert.ToBase64String(saltBytes); + string hash = ComputeHash(password, salt); + return (hash, salt); + } + + public static bool VerifyPassword(string password, string salt, string hash) + { + string computedHash = ComputeHash(password, salt); + return computedHash == hash; + } + + private static string ComputeHash(string password, string salt) + { + byte[] saltBytes = Convert.FromBase64String(salt); + using var pbkdf2 = new Rfc2898DeriveBytes( + password, + saltBytes, + 100_000, + HashAlgorithmName.SHA256 + ); + byte[] hashBytes = pbkdf2.GetBytes(32); + + return Convert.ToBase64String(hashBytes); + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/Helpers/RandomHelper.cs b/src/Ray.BiliBiliTool.Infrastructure/Helpers/RandomHelper.cs new file mode 100644 index 0000000..0f233ff --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Helpers/RandomHelper.cs @@ -0,0 +1,31 @@ +namespace Ray.BiliBiliTool.Infrastructure.Helpers; + +public class RandomHelper +{ + private int rep = 0; + + public string GenerateCode(int codeCount) + { + string str = string.Empty; + long num2 = DateTime.Now.Ticks + this.rep; + rep++; + Random random = new Random((int)((ulong)num2 & 0xffffffL) | (int)(num2 >> this.rep)); + for (int i = 0; i < codeCount; i++) + { + char ch; + int num = random.Next(); + if ((num % 2) == 0) + { + ch = (char)(0x30 + ((ushort)(num % 10))); + } + else + { + ch = (char)(0x41 + ((ushort)(num % 0x1a))); + } + + str += ch.ToString(); + } + + return str; + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/Helpers/RegexHelper.cs b/src/Ray.BiliBiliTool.Infrastructure/Helpers/RegexHelper.cs new file mode 100644 index 0000000..dd4df46 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Helpers/RegexHelper.cs @@ -0,0 +1,64 @@ +using System.Text.RegularExpressions; + +namespace Ray.BiliBiliTool.Infrastructure.Helpers; + +public class RegexHelper +{ + public static string QuerySingle(string source, string pattern) + { + Regex rg = new Regex(pattern, RegexOptions.Multiline | RegexOptions.Singleline); + return rg.Match(source).Value; + } + + public static List QueryMultiple(string source, string pattern) + { + Regex rg = new Regex(pattern, RegexOptions.Multiline | RegexOptions.Singleline); + //Regex rg = new Regex(pattern, RegexOptions.Singleline); + + MatchCollection matches = rg.Matches(source); + + List resList = new List(); + + foreach (Match item in matches) + resList.Add(item.Value); + + return resList; + } + + /// + /// 截取字符串中开始和结束字符串中间的字符串 + /// + /// 源字符串 + /// 开始字符串 + /// 结束字符串 + /// 中间字符串 + public static string SubstringSingle(string source, string startStr, string endStr) + { + var regexStr = $"(?<=({startStr}))[.\\s\\S]*?(?=({endStr}))"; + Regex rg = new Regex(regexStr, RegexOptions.Multiline | RegexOptions.Singleline); + return rg.Match(source).Value; + } + + /// + /// (批量)截取字符串中开始和结束字符串中间的字符串 + /// + /// 源字符串 + /// 开始字符串 + /// 结束字符串 + /// 中间字符串 + public static List SubstringMultiple(string source, string startStr, string endStr) + { + var regexStr = $"(?<=({startStr}))[.\\s\\S]*?(?=({endStr}))"; + // Regex rg = new Regex(regexStr, RegexOptions.Multiline | RegexOptions.Singleline); + Regex rg = new Regex(regexStr, RegexOptions.Singleline); + + MatchCollection matches = rg.Matches(source); + + List resList = new List(); + + foreach (Match item in matches) + resList.Add(item.Value); + + return resList; + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/Helpers/ZipHelper.cs b/src/Ray.BiliBiliTool.Infrastructure/Helpers/ZipHelper.cs new file mode 100644 index 0000000..e4d80ce --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Helpers/ZipHelper.cs @@ -0,0 +1,37 @@ +using System.IO.Compression; +using System.Text; + +namespace Ray.BiliBiliTool.Infrastructure.Helpers; + +/// +/// 解压缩Helper +/// +public class ZipHelper +{ + /// + /// 将Gzip的byte数组读取为字符串 + /// + /// + /// + /// + public static string ReadGzip(byte[] bytes, string encoding = "UTF-8") + { + string result = string.Empty; + using (MemoryStream ms = new MemoryStream(bytes)) + { + using (GZipStream decompressedStream = new GZipStream(ms, CompressionMode.Decompress)) + { + using ( + StreamReader sr = new StreamReader( + decompressedStream, + Encoding.GetEncoding(encoding) + ) + ) + { + result = sr.ReadToEnd(); + } + } + } + return result; + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/JsonSerializerOptionsBuilder.cs b/src/Ray.BiliBiliTool.Infrastructure/JsonSerializerOptionsBuilder.cs new file mode 100644 index 0000000..16d5256 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/JsonSerializerOptionsBuilder.cs @@ -0,0 +1,43 @@ +using System.Text.Json; + +namespace Ray.BiliBiliTool.Infrastructure; + +/// +/// System.Text.Json的序列化OptionsBuilder +/// +public sealed class JsonSerializerOptionsBuilder +{ + static JsonSerializerOptionsBuilder() + { + DefaultOptions = Create().GetOrBuildDefaultOptions(); + } + + /// + /// 默认配置 + /// + public static readonly JsonSerializerOptions DefaultOptions; + + public List> BuildActionList { get; } + + private JsonSerializerOptionsBuilder() + { + BuildActionList = new List>(); + } + + public static JsonSerializerOptionsBuilder Create() + { + return new JsonSerializerOptionsBuilder(); + } + + public JsonSerializerOptions Build() + { + JsonSerializerOptions options = new(); //这里没有使用 JsonSerializerDefaults.General 避免后续版本更新后设置改变 + + foreach (Action item in BuildActionList) + { + item?.Invoke(options); + } + + return options; + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/JsonSerializerOptionsBuilderExtensions.cs b/src/Ray.BiliBiliTool.Infrastructure/JsonSerializerOptionsBuilderExtensions.cs new file mode 100644 index 0000000..26694b1 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/JsonSerializerOptionsBuilderExtensions.cs @@ -0,0 +1,63 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Unicode; + +namespace Ray.BiliBiliTool.Infrastructure; + +public static class JsonSerializerOptionsBuilderExtensions +{ + private static JsonSerializerOptionsBuilder SetActionBase( + JsonSerializerOptionsBuilder builder, + Action action + ) + { + builder.BuildActionList.Add(action); + return builder; + } + + #region 设置区 + + public static JsonSerializerOptionsBuilder SetCamelCase( + this JsonSerializerOptionsBuilder builder + ) + { + return SetActionBase(builder, t => t.PropertyNamingPolicy = JsonNamingPolicy.CamelCase); + } + + public static JsonSerializerOptionsBuilder SetEncoderToUnicodeRangeAll( + this JsonSerializerOptionsBuilder builder + ) + { + return SetActionBase(builder, t => t.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)); + } + + public static JsonSerializerOptionsBuilder Configure( + this JsonSerializerOptionsBuilder builder, + Action action + ) + { + return SetActionBase(builder, action); + } + + #endregion 设置区 + + private static JsonSerializerOptions? _defaultOptions; + + private static JsonSerializerOptions BuildAndSaveToDefault( + this JsonSerializerOptionsBuilder builder + ) + { + JsonSerializerOptions option = builder.Build(); + _defaultOptions = option; + return option; + } + + public static JsonSerializerOptions GetOrBuildDefaultOptions( + this JsonSerializerOptionsBuilder builder + ) + { + return _defaultOptions == null + ? builder.SetCamelCase().SetEncoderToUnicodeRangeAll().BuildAndSaveToDefault() + : _defaultOptions!; + } +} diff --git a/src/Ray.BiliBiliTool.Infrastructure/Ray.BiliBiliTool.Infrastructure.csproj b/src/Ray.BiliBiliTool.Infrastructure/Ray.BiliBiliTool.Infrastructure.csproj new file mode 100644 index 0000000..a8c7b40 --- /dev/null +++ b/src/Ray.BiliBiliTool.Infrastructure/Ray.BiliBiliTool.Infrastructure.csproj @@ -0,0 +1,12 @@ + + + net8.0 + enable + enable + + + + + + + diff --git a/src/Ray.BiliBiliTool.Web.Client/Pages/Counter.razor b/src/Ray.BiliBiliTool.Web.Client/Pages/Counter.razor new file mode 100644 index 0000000..ce6626a --- /dev/null +++ b/src/Ray.BiliBiliTool.Web.Client/Pages/Counter.razor @@ -0,0 +1,20 @@ +@page "/counter" +@rendermode InteractiveAuto + +Counter + +

Counter

+ +

Current count: @currentCount

+ + + +@code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + +} diff --git a/src/Ray.BiliBiliTool.Web.Client/Program.cs b/src/Ray.BiliBiliTool.Web.Client/Program.cs new file mode 100644 index 0000000..57602e1 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web.Client/Program.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using MudBlazor.Services; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +builder.Services.AddMudServices(); + +await builder.Build().RunAsync(); diff --git a/src/Ray.BiliBiliTool.Web.Client/Ray.BiliBiliTool.Web.Client.csproj b/src/Ray.BiliBiliTool.Web.Client/Ray.BiliBiliTool.Web.Client.csproj new file mode 100644 index 0000000..1c495d4 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web.Client/Ray.BiliBiliTool.Web.Client.csproj @@ -0,0 +1,13 @@ + + + net8.0 + enable + enable + true + Default + + + + + + diff --git a/src/Ray.BiliBiliTool.Web.Client/_Imports.razor b/src/Ray.BiliBiliTool.Web.Client/_Imports.razor new file mode 100644 index 0000000..74f11dc --- /dev/null +++ b/src/Ray.BiliBiliTool.Web.Client/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Ray.BiliBiliTool.Web.Client +@using MudBlazor diff --git a/src/Ray.BiliBiliTool.Web.Client/wwwroot/appsettings.Development.json b/src/Ray.BiliBiliTool.Web.Client/wwwroot/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Ray.BiliBiliTool.Web.Client/wwwroot/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Ray.BiliBiliTool.Web.Client/wwwroot/appsettings.json b/src/Ray.BiliBiliTool.Web.Client/wwwroot/appsettings.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Ray.BiliBiliTool.Web.Client/wwwroot/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Ray.BiliBiliTool.Web/Auth/CustomAuthStateProvider.cs b/src/Ray.BiliBiliTool.Web/Auth/CustomAuthStateProvider.cs new file mode 100644 index 0000000..e3ec068 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Auth/CustomAuthStateProvider.cs @@ -0,0 +1,26 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; + +namespace Ray.BiliBiliTool.Web.Auth; + +public class CustomAuthStateProvider(IHttpContextAccessor httpContextAccessor) + : AuthenticationStateProvider +{ + public override Task GetAuthenticationStateAsync() + { + var identity = new ClaimsIdentity(); + var user = httpContextAccessor.HttpContext?.User; + + if (user?.Identity?.IsAuthenticated == true) + { + identity = new ClaimsIdentity(user.Claims, "Cookies"); + } + + return Task.FromResult(new AuthenticationState(new ClaimsPrincipal(identity))); + } + + public void NotifyAuthenticationStateChanged() + { + NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Components/App.razor b/src/Ray.BiliBiliTool.Web/Components/App.razor new file mode 100644 index 0000000..587a670 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/App.razor @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor new file mode 100644 index 0000000..ac9c2f0 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor @@ -0,0 +1,5 @@ +

BlazingJob

+ +@code { + +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor.cs new file mode 100644 index 0000000..e92e2ba --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor.cs @@ -0,0 +1,148 @@ +using BlazingQuartz.Core.Models; +using BlazingQuartz.Core.Services; +using BlazingQuartz.Jobs.Abstractions; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Ray.BiliBiliTool.Web.Services; + +namespace Ray.BiliBiliTool.Web.Components.Comps; + +public partial class BlazingJob : ComponentBase +{ + [Inject] + private ISchedulerDefinitionService SchedulerDefSvc { get; set; } = null!; + + [Inject] + private ISchedulerService SchedulerSvc { get; set; } = null!; + + [Inject] + private IDialogService DialogSvc { get; set; } = null!; + + [Inject] + private ILogger Logger { get; set; } = null!; + + [Inject] + private IJobUIProvider JobUIProvider { get; set; } = null!; + + [Parameter] + [EditorRequired] + public JobDetailModel JobDetail { get; set; } = new(); + + [Parameter] + public bool IsReadOnly { get; set; } = false; + + [Parameter] + public bool IsValid { get; set; } + + [Parameter] + public EventCallback IsValidChanged { get; set; } + + private Key OriginalJobKey = new(string.Empty, "No Group"); + + private IEnumerable AvailableJobTypes = Enumerable.Empty(); + private IEnumerable? ExistingJobGroups; + private MudForm _form = null!; + private Type? JobUIType = null; + private Dictionary JobUITypeParameters = new(); + private DynamicComponent? _jobUIComponent; + + protected override async Task OnInitializedAsync() + { + var types = SchedulerDefSvc.GetJobTypes(); + var typeList = new HashSet(types); + if (JobDetail.JobClass != null) + { + typeList.Add(JobDetail.JobClass); + await OnJobClassValueChanged(JobDetail.JobClass); + } + AvailableJobTypes = typeList; + + OriginalJobKey = new(JobDetail.Name, JobDetail.Group); + } + + async Task> SearchJobGroup(string value) + { + if (ExistingJobGroups == null) + { + ExistingJobGroups = await SchedulerSvc.GetJobGroups(); + } + + if (string.IsNullOrEmpty(value)) + return ExistingJobGroups; + + var matches = ExistingJobGroups + .Where(x => x.Contains(value, StringComparison.InvariantCultureIgnoreCase)) + .ToList(); + + if (!matches.Any(x => x == value)) + matches.Add(value); + + return matches; + } + + private void OnSetIsValid(bool value) + { + if (IsValid == value) + return; + IsValid = value; + IsValidChanged.InvokeAsync(value); + } + + public async Task Validate() + { + var jobUI = _jobUIComponent?.Instance as IJobUI; + + if (jobUI != null) + { + if (!await jobUI.ApplyChanges()) + { + OnSetIsValid(false); + return; + } + } + + await _form.Validate(); + } + + private async Task ValidateJobName(string name) + { + if (string.IsNullOrEmpty(name)) + return "Job name is required"; + + // accept if same as original + if (OriginalJobKey.Equals(name, JobDetail.Group)) + return null; + + if (IsReadOnly) + { + Logger.LogDebug("Skip checking of job name uniqueness if in readonly mode"); + return null; + } + + var detail = await SchedulerSvc.GetJobDetail(name, JobDetail.Group); + + if (detail != null) + return "Job name already in used. Please choose another name or group."; + + return null; + } + + private async Task OnJobClassValueChanged(Type jobType) + { + JobDetail.JobClass = jobType; + + // clear previous changes + var jobUI = _jobUIComponent?.Instance as IJobUI; + if (jobUI != null) + await jobUI.ClearChanges(); + + var jobUIType = JobUIProvider.GetJobUIType(jobType.FullName); + JobUITypeParameters.Clear(); + JobUITypeParameters[nameof(IsReadOnly)] = IsReadOnly; + if (jobUIType == typeof(DefaultJobUI)) + JobUITypeParameters[nameof(JobDetail)] = JobDetail; + else + JobUITypeParameters[nameof(JobDetail.JobDataMap)] = JobDetail.JobDataMap; + JobUIType = jobUIType; + } +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor.css b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor.css new file mode 100644 index 0000000..e69de29 diff --git a/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor.js b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor.js new file mode 100644 index 0000000..13fcc0c --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor.js @@ -0,0 +1,5 @@ +export class BlazingJob { + +} + +window.BlazingJob = BlazingJob; \ No newline at end of file diff --git a/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor new file mode 100644 index 0000000..c0938b3 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor @@ -0,0 +1,5 @@ +

BlazingTrigger

+ +@code { + +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor.cs new file mode 100644 index 0000000..ef0c3e7 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Components; + +namespace Ray.BiliBiliTool.Web.Components.Comps; + +public partial class BlazingTrigger : ComponentBase +{ + public Task Validate() + { + throw new NotImplementedException(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor.css b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor.css new file mode 100644 index 0000000..e69de29 diff --git a/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor.js b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor.js new file mode 100644 index 0000000..ef16d6e --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor.js @@ -0,0 +1,5 @@ +export class BlazingTrigger { + +} + +window.BlazingTrigger = BlazingTrigger; \ No newline at end of file diff --git a/src/Ray.BiliBiliTool.Web/Components/Comps/DefaultJobUI.razor b/src/Ray.BiliBiliTool.Web/Components/Comps/DefaultJobUI.razor new file mode 100644 index 0000000..9b41999 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Comps/DefaultJobUI.razor @@ -0,0 +1,5 @@ +

DefaultJobUI

+ +@code { + +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Comps/DefaultJobUI.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Comps/DefaultJobUI.razor.cs new file mode 100644 index 0000000..26d0586 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Comps/DefaultJobUI.razor.cs @@ -0,0 +1,5 @@ +using Microsoft.AspNetCore.Components; + +namespace Ray.BiliBiliTool.Web.Components.Comps; + +public partial class DefaultJobUI : ComponentBase { } diff --git a/src/Ray.BiliBiliTool.Web/Components/Comps/ScheduleDialog.razor b/src/Ray.BiliBiliTool.Web/Components/Comps/ScheduleDialog.razor new file mode 100644 index 0000000..11f99c0 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Comps/ScheduleDialog.razor @@ -0,0 +1,138 @@ +@using BlazingQuartz.Core.Models +@using BlazingQuartz.Core.Services +@inject ISchedulerService SchSvc + + + + + Job Details + Trigger Details + + + @if (SelectedTab == ScheduleDialogTab.Job) + { + + } + else + { + + } + + +
+ Cancel +
+ Back + @_nextText +
+
+ + +
+
+@code { + [CascadingParameter] MudDialog MudDialog { get; set; } = null!; + [Parameter] public JobDetailModel JobDetail { get; set; } = new(); + [Parameter] public TriggerDetailModel TriggerDetail { get; set; } = new(); + [Parameter] public bool IsReadOnlyJobDetail { get; set; } = false; + [Parameter] public ScheduleDialogTab SelectedTab { get; set; } = ScheduleDialogTab.Job; + + private bool _jobDetailIsValid; + private bool _triggerDetailIsValid; + + private MudButton _backBtn = null!; + private string _nextText = "Next"; + private string? _nextIcon = Icons.Material.Filled.NavigateNext; + + private BlazingJob _jobPanel = null!; + private BlazingTrigger _triggerPanel = null!; + + protected override void OnInitialized() + { + if (SelectedTab == ScheduleDialogTab.Trigger) + { + _jobDetailIsValid = true; + _nextText = "Save"; + _nextIcon = null; + } + } + + private async Task OnSelectedTabChanged(ScheduleDialogTab tab) + { + if (SelectedTab == tab) + return; + + // validate before change tab + if (SelectedTab == ScheduleDialogTab.Job) + { + await _jobPanel.Validate(); + if (!_jobDetailIsValid) + return; + } + + SelectedTab = tab; + + // update text + if (SelectedTab == ScheduleDialogTab.Job) + { + _nextText = "Next"; + _nextIcon = Icons.Material.Filled.NavigateNext; + } + else if (SelectedTab == ScheduleDialogTab.Trigger) + { + if (string.IsNullOrEmpty(TriggerDetail.Name) && + !string.IsNullOrEmpty(JobDetail.Name)) + { + // use job name as trigger name when trigger name not yet specified + // determine if trigger name can be used + var exists = await SchSvc.ContainsTriggerKey(JobDetail.Name, TriggerDetail.Group); + if (!exists) + TriggerDetail.Name = JobDetail.Name; + } + _nextText = "Save"; + _nextIcon = null; + } + } + + async Task OnBack() + { + await OnSelectedTabChanged(ScheduleDialogTab.Job); + } + + async Task OnSubmit() + { + if (SelectedTab == ScheduleDialogTab.Job) + { + await OnSelectedTabChanged(ScheduleDialogTab.Trigger); + return; + } + + await _triggerPanel.Validate(); + + if (!_jobDetailIsValid || !_triggerDetailIsValid) + { + return; + } + + await MudDialog.CloseAsync(DialogResult.Ok((JobDetail, TriggerDetail))); + } + + void OnCancel() => MudDialog.CloseAsync(); +} + diff --git a/src/Ray.BiliBiliTool.Web/Components/Layout/MainLayout.razor b/src/Ray.BiliBiliTool.Web/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..849bba5 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Layout/MainLayout.razor @@ -0,0 +1,29 @@ +@using Microsoft.AspNetCore.Components.Authorization +@inherits LayoutComponentBase + +
+ + +
+
+ About + + + Log out + + +
+ +
+ @Body +
+
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/src/Ray.BiliBiliTool.Web/Components/Layout/MainLayout.razor.css b/src/Ray.BiliBiliTool.Web/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..038baf1 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Layout/MainLayout.razor.css @@ -0,0 +1,96 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } + + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } + + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/src/Ray.BiliBiliTool.Web/Components/Layout/NavMenu.razor b/src/Ray.BiliBiliTool.Web/Components/Layout/NavMenu.razor new file mode 100644 index 0000000..f42e787 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Layout/NavMenu.razor @@ -0,0 +1,113 @@ +@rendermode InteractiveServer + + + + + + + +@code { + private bool _showConfigSubMenu; +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Layout/NavMenu.razor.css b/src/Ray.BiliBiliTool.Web/Components/Layout/NavMenu.razor.css new file mode 100644 index 0000000..4639a7b --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Layout/NavMenu.razor.css @@ -0,0 +1,186 @@ +.navbar-toggler { + appearance: none; + cursor: pointer; + width: 3.5rem; + height: 2.5rem; + color: white; + position: absolute; + top: 0.5rem; + right: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: 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.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); +} + +.navbar-toggler:checked { + background-color: rgba(255, 255, 255, 0.5); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.navbar-logo { + height: 40px; + margin-right: 10px; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.bi-clock-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-clock' viewBox='0 0 16 16'%3E%3Cpath d='M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z'/%3E%3Cpath d='M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z'/%3E%3C/svg%3E"); +} + +.bi-calendar-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-calendar-event' viewBox='0 0 16 16'%3E%3Cpath d='M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z'/%3E%3Cpath d='M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z'/%3E%3C/svg%3E"); +} + +.bi-person-circle { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-circle' viewBox='0 0 16 16'%3E%3Cpath d='M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0'/%3E%3Cpath fill-rule='evenodd' d='M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1'/%3E%3C/svg%3E"); +} + +.bi-sliders { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-sliders' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M11.5 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM9.05 3a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0V3h9.05zM4.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM2.05 8a2.5 2.5 0 0 1 4.9 0H16v1H6.95a2.5 2.5 0 0 1-4.9 0H0V8h2.05zm9.45 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-2.45 1a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0v-1h9.05z'/%3E%3C/svg%3E"); +} + +.bi-coin { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-coin' viewBox='0 0 16 16'%3E%3Cpath d='M5.5 9.511c.076.954.83 1.697 2.182 1.785V12h.6v-.709c1.4-.098 2.218-.846 2.218-1.932 0-.987-.626-1.496-1.745-1.76l-.473-.112V5.57c.6.068.982.396 1.074.85h1.052c-.076-.919-.864-1.638-2.126-1.716V4h-.6v.719c-1.195.117-2.01.836-2.01 1.853 0 .9.606 1.472 1.613 1.707l.397.098v2.034c-.615-.093-1.022-.43-1.114-.9H5.5zm2.177-2.166c-.59-.137-.91-.416-.91-.836 0-.47.345-.822.915-.925v1.76h-.005zm.692 1.193c.717.166 1.048.435 1.048.91 0 .542-.412.914-1.135.982V8.518l.087.02z'/%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E%3Cpath d='M8 13.5a5.5 5.5 0 1 1 0-11 5.5 5.5 0 0 1 0 11zm0 .5A6 6 0 1 0 8 2a6 6 0 0 0 0 12z'/%3E%3C/svg%3E"); +} + +.bi-book { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-book' viewBox='0 0 16 16'%3E%3Cpath d='M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811V2.828zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z'/%3E%3C/svg%3E"); +} + +.bi-book-half { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-book-half' viewBox='0 0 16 16'%3E%3Cpath d='M8.5 2.687c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z'/%3E%3C/svg%3E"); +} + +.bi-currency-exchange { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-currency-exchange' viewBox='0 0 16 16'%3E%3Cpath d='M0 5a5.002 5.002 0 0 0 4.027 4.905 6.46 6.46 0 0 1 .544-2.073C3.695 7.536 3.132 6.864 3 5.91h-.5v-.426h.466V5.05c0-.046 0-.093.004-.135H2.5v-.427h.511C3.236 3.24 4.213 2.5 5.681 2.5c.316 0 .59.031.819.085v.733a3.46 3.46 0 0 0-.815-.082c-.919 0-1.538.466-1.734 1.252h1.917v.427h-1.98c-.003.046-.003.097-.003.147v.435h1.983v.427H5.93c.118.602.468 1.03 1.005 1.229a6.5 6.5 0 0 1 4.97-3.113A5.002 5.002 0 0 0 0 5zm16 5.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0zm-7.75 1.322c.069.835.746 1.485 1.964 1.562V14h.54v-.62c1.259-.086 1.996-.74 1.996-1.69 0-.865-.563-1.31-1.57-1.54l-.426-.1V8.374c.54.06.884.347.966.745h.948c-.07-.804-.779-1.433-1.914-1.502V7h-.54v.629c-1.076.103-1.808.732-1.808 1.622 0 .787.544 1.288 1.45 1.493l.358.085v1.78c-.554-.08-.92-.376-1.003-.787H8.25zm1.96-1.895c-.532-.12-.82-.364-.82-.732 0-.41.311-.719.824-.809v1.54h-.005zm.622 1.044c.645.145.943.38.943.796 0 .474-.37.8-1.02.86v-1.674l.077.018z'/%3E%3C/svg%3E"); +} + +.bi-lightning-charge { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-lightning-charge' viewBox='0 0 16 16'%3E%3Cpath d='M11.251.068a.5.5 0 0 1 .227.58L9.677 6.5H13a.5.5 0 0 1 .364.843l-8 8.5a.5.5 0 0 1-.842-.49L6.323 9.5H3a.5.5 0 0 1-.364-.843l8-8.5a.5.5 0 0 1 .615-.09zM4.157 8.5H7a.5.5 0 0 1 .478.647L6.11 13.59l5.732-6.09H9a.5.5 0 0 1-.478-.647L9.89 2.41 4.157 8.5z'/%3E%3C/svg%3E"); +} + +.bi-star { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-star' viewBox='0 0 16 16'%3E%3Cpath d='M2.866 14.85c-.078.444.36.791.746.593l4.39-2.256 4.389 2.256c.386.198.824-.149.746-.592l-.83-4.73 3.522-3.356c.33-.314.16-.888-.282-.95l-4.898-.696L8.465.792a.513.513 0 0 0-.927 0L5.354 5.12l-4.898.696c-.441.062-.612.636-.283.95l3.523 3.356-.83 4.73zm4.905-2.767-3.686 1.894.694-3.957a.565.565 0 0 0-.163-.505L1.71 6.745l4.052-.576a.525.525 0 0 0 .393-.288L8 2.223l1.847 3.658a.525.525 0 0 0 .393.288l4.052.575-2.906 2.77a.565.565 0 0 0-.163.506l.694 3.957-3.686-1.894a.503.503 0 0 0-.461 0z'/%3E%3C/svg%3E"); +} + +.bi-award { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-award' viewBox='0 0 16 16'%3E%3Cpath d='M9.669.864 8 0 6.331.864l-1.858.282-.842 1.68-1.337 1.32L2.6 6l-.306 1.854 1.337 1.32.842 1.68 1.858.282L8 12l1.669-.864 1.858-.282.842-1.68 1.337-1.32L13.4 6l.306-1.854-1.337-1.32-.842-1.68L9.669.864zm1.196 1.193.684 1.365 1.086 1.072L12.387 6l.248 1.506-1.086 1.072-.684 1.365-1.51.229L8 10.874l-1.355-.702-1.51-.229-.684-1.365-1.086-1.072L3.614 6l-.25-1.506 1.087-1.072.684-1.365 1.51-.229L8 1.126l1.356.702 1.509.229z'/%3E%3Cpath d='M4 11.794V16l4-1 4 1v-4.206l-2.018.306L8 13.126 6.018 12.1 4 11.794z'/%3E%3C/svg%3E"); +} + +.bi-gift { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-gift' viewBox='0 0 16 16'%3E%3Cpath d='M3 2.5a2.5 2.5 0 0 1 5 0 2.5 2.5 0 0 1 5 0v.006c0 .07 0 .27-.038.494H15a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v1.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 8.5V7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h2.038A2.968 2.968 0 0 1 3 2.506V2.5zm1.068.5H7v-.5a1.5 1.5 0 1 0-3 0c0 .085.002.274.045.43a.522.522 0 0 0 .023.07zM9 3h2.932a.56.56 0 0 0 .023-.07c.043-.156.045-.345.045-.43a1.5 1.5 0 0 0-3 0V3zM1 4v2h6V4H1zm8 0v2h6V4H9zm5 3H9v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5V7H1v1.5A.5.5 0 0 0 1.5 9h2a.5.5 0 0 0 .5-.5V7h6z'/%3E%3C/svg%3E"); +} + +.bi-badge-cc { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-badge-cc' viewBox='0 0 16 16'%3E%3Cpath d='M3.708 7.755c0-1.111.488-1.753 1.319-1.753.681 0 1.138.47 1.186 1.107H7.36V7c-.052-1.186-1.024-2-2.342-2C3.414 5 2.5 6.05 2.5 7.751v.747C2.5 10.052 3.414 11 4.998 11c1.318 0 2.29-.814 2.342-2v-.109H6.213c-.048.638-.505 1.107-1.186 1.107-.83 0-1.319-.642-1.319-1.753V7.755z'/%3E%3Cpath d='M9.708 7.755c0-1.111.488-1.753 1.319-1.753.681 0 1.138.47 1.186 1.107H13.36V7c-.052-1.186-1.024-2-2.342-2C9.414 5 8.5 6.05 8.5 7.751v.747C8.5 10.052 9.414 11 10.998 11c1.318 0 2.29-.814 2.342-2v-.109h-1.127c-.048.638-.505 1.107-1.186 1.107-.83 0-1.319-.642-1.319-1.753V7.755z'/%3E%3Cpath d='M14 3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h12zM2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H2z'/%3E%3C/svg%3E"); +} + +.bi-person-dash { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-dash' viewBox='0 0 16 16'%3E%3Cpath d='M12.5 16a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7ZM11 12h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1Z'/%3E%3Cpath d='M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3Cpath d='M8.256 14a4.474 4.474 0 0 1-.229-1.004H3c.001-.246.154-.986.832-1.664C4.484 10.68 5.711 10 8 10c.26 0 .507.009.74.025.226-.341.496-.65.804-.918C9.077 9.038 8.564 9 8 9c-5 0-6 3-6 4s1 1 1 1h5.256Z'/%3E%3C/svg%3E"); +} + +.bi-chevron-down { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-chevron-down' viewBox='0 0 16 16'%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"); +} + +.bi-chevron-right { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-chevron-right' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep .nav-link { + color: #d7d7d7; + background: none; + border: none; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + width: 100%; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep .nav-link:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +.nav-scrollable { + display: none; +} + +.navbar-toggler:checked ~ .nav-scrollable { + display: block; +} + +.submenu-container { + background-color: rgba(0, 0, 0, 0.1); + margin-bottom: 0.5rem; + border-radius: 0 0 4px 4px; + padding: 0.5rem 0; +} + +.submenu-item { + position: relative; + padding-left: 1rem !important; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .nav-scrollable { + /* Never collapse the sidebar for wide screens */ + display: block; + + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor new file mode 100644 index 0000000..8c57717 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor @@ -0,0 +1,39 @@ +@page "/Admin" +@using Microsoft.AspNetCore.Authorization +@rendermode InteractiveServer +@attribute [Authorize] + +Change Password + +Change Password + + + + @if (!string.IsNullOrEmpty(_errorMessage)) + { + @_errorMessage + } + @if (!string.IsNullOrEmpty(_successMessage)) + { + @_successMessage + } + + + + + + + Submit + + + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor.cs new file mode 100644 index 0000000..ee450e2 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor.cs @@ -0,0 +1,99 @@ +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Ray.BiliBiliTool.Web.Services; + +namespace Ray.BiliBiliTool.Web.Components.Pages; + +public partial class Admin : ComponentBase +{ + [Inject] + private NavigationManager NavigationManager { get; set; } = null!; + + [Inject] + private IAuthService AuthService { get; set; } = null!; + + private string _username = ""; + private string _currentPassword = ""; + private string _newPassword = ""; + private string _confirmPassword = ""; + private string _errorMessage = ""; + private string _successMessage = ""; + + private bool _passwordVisibility; + private InputType _passwordInput = InputType.Password; + private string _passwordInputIcon = Icons.Material.Filled.VisibilityOff; + + private bool _currentPasswordVisibility; + private InputType _currentPasswordInput = InputType.Password; + private string _currentPasswordInputIcon = Icons.Material.Filled.VisibilityOff; + + private void TogglePasswordVisibility() + { + if (_passwordVisibility) + { + _passwordVisibility = false; + _passwordInputIcon = Icons.Material.Filled.VisibilityOff; + _passwordInput = InputType.Password; + } + else + { + _passwordVisibility = true; + _passwordInputIcon = Icons.Material.Filled.Visibility; + _passwordInput = InputType.Text; + } + } + + private void ToggleCurrentPasswordVisibility() + { + if (_currentPasswordVisibility) + { + _currentPasswordVisibility = false; + _currentPasswordInputIcon = Icons.Material.Filled.VisibilityOff; + _currentPasswordInput = InputType.Password; + } + else + { + _currentPasswordVisibility = true; + _currentPasswordInputIcon = Icons.Material.Filled.Visibility; + _currentPasswordInput = InputType.Text; + } + } + + protected override async Task OnInitializedAsync() + { + _username = await AuthService.GetAdminUserNameAsync(); + } + + private async Task ChangePasswordAsync() + { + _errorMessage = ""; + _successMessage = ""; + + if (_newPassword != _confirmPassword) + { + _errorMessage = "The new password and the confirm password do not match"; + return; + } + + if (string.IsNullOrWhiteSpace(_newPassword)) + { + _errorMessage = "Password cannot be empty"; + return; + } + + try + { + await AuthService.ChangePasswordAsync(_username, _currentPassword, _newPassword); + _successMessage = "Update Successful, you will be logged out in 2 seconds"; + await Task.Delay(2000); + _currentPassword = ""; + _newPassword = ""; + _confirmPassword = ""; + NavigationManager.NavigateTo("/auth/logout", true); + } + catch (Exception e) + { + _errorMessage = e.Message; + } + } +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor.css b/src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor.css new file mode 100644 index 0000000..e69de29 diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor.js b/src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor.js new file mode 100644 index 0000000..31c2bfb --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor.js @@ -0,0 +1,5 @@ +export class Admin { + +} + +window.Admin = Admin; \ No newline at end of file diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/BaseConfigComponent.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/BaseConfigComponent.cs new file mode 100644 index 0000000..b6533c4 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/BaseConfigComponent.cs @@ -0,0 +1,187 @@ +using BlazingQuartz.Core.Services; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using Quartz; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.Config.SQLite; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Configs; + +public abstract class BaseConfigComponent : ComponentBase + where T : BaseConfigOptions, new() +{ + [Inject] + protected IConfiguration Configuration { get; set; } = null!; + + [Inject] + protected ISchedulerService? SchedulerService { get; set; } + + [Inject] + protected ISchedulerFactory? SchedulerFactory { get; set; } + + [Inject] + protected ILogger> Logger { get; set; } = null!; + + protected T _config = new(); + protected bool _isLoading = true; + protected MarkupString? _saveMessage; + protected bool _saveSuccess; + + protected abstract IOptionsMonitor OptionsMonitor { get; } + + /// + /// 获取对应的任务JobKey,如果返回null则不控制定时任务 + /// + protected virtual JobKey? GetJobKey() => null; + + /// + /// 获取触发器名称 + /// + protected virtual string GetTriggerName(JobKey jobKey) => $"{jobKey}.Cron.Trigger"; + + protected override async Task OnInitializedAsync() + { + await LoadConfigAsync(); + } + + protected Task LoadConfigAsync() + { + _isLoading = true; + _saveMessage = null; + + try + { + _config = OptionsMonitor.CurrentValue; + } + catch (Exception ex) + { + _saveMessage = new MarkupString($"Failed to load configuration: {ex.Message}"); + _saveSuccess = false; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + + return Task.CompletedTask; + } + + protected virtual async Task HandleValidSubmitAsync() + { + _isLoading = true; + _saveMessage = null; + + try + { + // 保存配置 + var sqliteProvider = GetSqliteConfigurationProvider(); + if (sqliteProvider == null) + { + throw new InvalidOperationException("Unable to get SqliteConfigurationProvider"); + } + + var configValues = _config.ToConfigDictionary(); + sqliteProvider.BatchSet(configValues); + + // 如果有对应的定时任务,同步更新 Quartz 任务状态和 Cron 表达式 + var jobKey = GetJobKey(); + if (jobKey != null && SchedulerService != null) + { + // 更新 Cron 表达式 + await UpdateJobCronAsync(jobKey, _config.Cron); + + // 控制任务启停 + await ControlScheduledJobAsyc(jobKey, _config.IsEnable); + } + + _saveMessage = GetSaveSuccessMessage(); + _saveSuccess = true; + } + catch (Exception ex) + { + _saveMessage = new MarkupString($"Failed to save configuration: {ex.Message}"); + _saveSuccess = false; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private async Task ControlScheduledJobAsyc(JobKey jobKey, bool isEnable) + { + var triggerName = GetTriggerName(jobKey); + var triggerGroup = Constants.BiliJobGroup; + + if (isEnable) + { + // 启用任务:恢复触发器 + await SchedulerService!.ResumeTrigger(triggerName, triggerGroup); + } + else + { + // 禁用任务:暂停触发器 + await SchedulerService!.PauseTrigger(triggerName, triggerGroup); + } + } + + private async Task UpdateJobCronAsync(JobKey jobKey, string? cronExpression) + { + if (string.IsNullOrWhiteSpace(cronExpression) || SchedulerFactory == null) + return; + + var triggerName = GetTriggerName(jobKey); + var triggerKey = new TriggerKey(triggerName, Constants.BiliJobGroup); + + try + { + var scheduler = await SchedulerFactory.GetScheduler(); + + // 创建新的 Cron 触发器 + var newTrigger = TriggerBuilder + .Create() + .WithIdentity(triggerKey) + .ForJob(jobKey) + .WithCronSchedule(cronExpression) + .Build(); + + // 重新调度触发器(替换现有的触发器) + await scheduler.RescheduleJob(triggerKey, newTrigger); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to update cron expression for job {JobKey}", jobKey); + } + } + + private MarkupString GetSaveSuccessMessage() + { + var jobKey = GetJobKey(); + if (jobKey == null) + { + return new MarkupString("Configuration saved successfully!"); + } + + var status = _config.IsEnable ? "enabled" : "disabled"; + return new MarkupString( + $"Configuration saved successfully!
{jobKey} has been {status}." + ); + } + + private SqliteConfigurationProvider? GetSqliteConfigurationProvider() + { + if (Configuration is IConfigurationRoot configRoot) + { + foreach (var provider in configRoot.Providers) + { + if (provider is SqliteConfigurationProvider sqliteProvider) + { + return sqliteProvider; + } + } + } + return null; + } +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/ChargeTaskConfig.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/ChargeTaskConfig.razor new file mode 100644 index 0000000..a89bf35 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/ChargeTaskConfig.razor @@ -0,0 +1,104 @@ +@page "/Configurations/ChargeTaskConfig" +@using Microsoft.AspNetCore.Authorization +@using Ray.BiliBiliTool.Config.Options; +@attribute [Authorize] +@rendermode InteractiveServer +@inherits BaseConfigComponent + +免费B币券充电任务配置 + + + 免费B币券充电任务配置 + + @if (_isLoading) + { + + } + else + { + + + + + + + + 基础配置 + + + + + + + + + + + + + + + + @if (_config.IsEnable) + { + + + + 详细配置 + + + + + + + + + + + @if (_isSpecifyUpToggled) + { +
+
+ + + 支持作者 + +
+
+ } +
+
+
+
+ } + + + + + 保存配置 + 重新加载 + + +
+ + @if (_saveMessage.HasValue) + { + + @_saveMessage + + } + } +
+ + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/ChargeTaskConfig.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/ChargeTaskConfig.razor.cs new file mode 100644 index 0000000..03b98cf --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/ChargeTaskConfig.razor.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using Quartz; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.Web.Jobs; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Configs; + +public partial class ChargeTaskConfig : BaseConfigComponent +{ + [Inject] + private IOptionsMonitor ChargeTaskOptionsMonitor { get; set; } = null!; + + protected override IOptionsMonitor OptionsMonitor => + ChargeTaskOptionsMonitor; + + private bool _isSpecifyUpToggled; + + protected override Task OnInitializedAsync() + { + if (!string.IsNullOrWhiteSpace(OptionsMonitor.CurrentValue.AutoChargeUpId)) + { + _isSpecifyUpToggled = true; + } + return base.OnInitializedAsync(); + } + + private void OnSpecifyUpToggled(bool isSpecified) + { + _isSpecifyUpToggled = !_isSpecifyUpToggled; + StateHasChanged(); + } + + private Task SetSupportAuthor() + { + _config.AutoChargeUpId = "-1"; + return Task.CompletedTask; + } + + protected override JobKey GetJobKey() => ChargeJob.Key; +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor new file mode 100644 index 0000000..007064c --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor @@ -0,0 +1,113 @@ +@page "/Configurations/DailyJobConfig" +@using Microsoft.AspNetCore.Authorization +@using Ray.BiliBiliTool.Config.Options; +@attribute [Authorize] +@rendermode InteractiveServer +@inherits BaseConfigComponent + +每日任务配置 + + + 每日任务配置 + + @if (_isLoading) + { + + } + else + { + + + + + + + + 基础配置 + + + + + + + + + + + + + + + + @if (_config.IsEnable) + { + + + + 详细配置 + + + + + + + + + + + + + + + + + + + + + + + + + Android + iOS + + + + + + } + + + + + 保存配置 + 重新加载 + + + + + @if (_saveMessage.HasValue) + { + + @_saveMessage + + } + } + + + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor.cs new file mode 100644 index 0000000..3da2a13 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using Quartz; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.Web.Jobs; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Configs; + +public partial class DailyJobConfig : BaseConfigComponent +{ + [Inject] + private IOptionsMonitor DailyTaskOptionsMonitor { get; set; } = null!; + + protected override IOptionsMonitor OptionsMonitor => DailyTaskOptionsMonitor; + + protected override JobKey GetJobKey() => DailyJob.Key; +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor.css b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor.css new file mode 100644 index 0000000..e69de29 diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor.js b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor.js new file mode 100644 index 0000000..0f5ffc0 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor.js @@ -0,0 +1,5 @@ +export class DailyJobConfig { + +} + +window.DailyJobConfig = DailyJobConfig; \ No newline at end of file diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveFansMedalTaskConfig.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveFansMedalTaskConfig.razor new file mode 100644 index 0000000..7afb225 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveFansMedalTaskConfig.razor @@ -0,0 +1,105 @@ +@page "/Configurations/LiveFansMedalTaskConfig" +@using Microsoft.AspNetCore.Authorization +@using Ray.BiliBiliTool.Config.Options; +@attribute [Authorize] +@rendermode InteractiveServer +@inherits BaseConfigComponent + +直播粉丝牌任务配置 + + + 直播粉丝牌任务配置 + + @if (_isLoading) + { + + } + else + { + + + + + + + + 基础配置 + + + + + + + + + + + + + + + + @if (_config.IsEnable) + { + + + + 详细配置 + + + + + + + + + + + + + + + + + + + + + + + + + } + + + + + 保存配置 + 重新加载 + + + + + @if (_saveMessage.HasValue) + { + + @_saveMessage + + } + } + + + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveFansMedalTaskConfig.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveFansMedalTaskConfig.razor.cs new file mode 100644 index 0000000..231ef67 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveFansMedalTaskConfig.razor.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using Quartz; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.Web.Jobs; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Configs; + +public partial class LiveFansMedalTaskConfig : BaseConfigComponent +{ + [Inject] + private IOptionsMonitor LiveFansMedalTaskOptionsMonitor { get; set; } = + null!; + + protected override IOptionsMonitor OptionsMonitor => + LiveFansMedalTaskOptionsMonitor; + + protected override JobKey GetJobKey() => LiveFansMedalJob.Key; +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveLotteryTaskConfig.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveLotteryTaskConfig.razor new file mode 100644 index 0000000..ce743c8 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveLotteryTaskConfig.razor @@ -0,0 +1,96 @@ +@page "/Configurations/LiveLotteryTaskConfig" +@using Microsoft.AspNetCore.Authorization +@using Ray.BiliBiliTool.Config.Options; +@attribute [Authorize] +@rendermode InteractiveServer +@inherits BaseConfigComponent + +直播抽奖任务配置 + + + 直播抽奖任务配置 + + @if (_isLoading) + { + + } + else + { + + + + + + + + 基础配置 + + + + + + + + + + + + + + + + @if (_config.IsEnable) + { + + + + 详细配置 + + + + + + + + + + + + + + + + + + + } + + + + + 保存配置 + 重新加载 + + + + + @if (_saveMessage.HasValue) + { + + @_saveMessage + + } + } + + + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveLotteryTaskConfig.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveLotteryTaskConfig.razor.cs new file mode 100644 index 0000000..a0d213b --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveLotteryTaskConfig.razor.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using Quartz; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.Web.Jobs; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Configs; + +public partial class LiveLotteryTaskConfig : BaseConfigComponent +{ + [Inject] + private IOptionsMonitor LiveLotteryTaskOptionsMonitor { get; set; } = + null!; + + protected override IOptionsMonitor OptionsMonitor => + LiveLotteryTaskOptionsMonitor; + + protected override JobKey GetJobKey() => LiveLotteryJob.Key; +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaPrivilegeTaskConfig.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaPrivilegeTaskConfig.razor new file mode 100644 index 0000000..ad79431 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaPrivilegeTaskConfig.razor @@ -0,0 +1,65 @@ +@page "/Configurations/MangaPrivilegeTaskConfig" +@using Microsoft.AspNetCore.Authorization +@using Ray.BiliBiliTool.Config.Options; +@attribute [Authorize] +@rendermode InteractiveServer +@inherits BaseConfigComponent + +漫画特权任务配置 + + + 漫画特权任务配置 + + @if (_isLoading) + { + + } + else + { + + + + + + + + 基础配置 + + + + + + + + + + + + + + + + + + 保存配置 + 重新加载 + + + + + @if (_saveMessage.HasValue) + { + + @_saveMessage + + } + } + + + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaPrivilegeTaskConfig.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaPrivilegeTaskConfig.razor.cs new file mode 100644 index 0000000..f4305f2 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaPrivilegeTaskConfig.razor.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using Quartz; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.Web.Jobs; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Configs; + +public partial class MangaPrivilegeTaskConfig : BaseConfigComponent +{ + [Inject] + private IOptionsMonitor MangaPrivilegeTaskOptionsMonitor { get; set; } = + null!; + + protected override IOptionsMonitor OptionsMonitor => + MangaPrivilegeTaskOptionsMonitor; + + protected override JobKey GetJobKey() => MangaPrivilegeJob.Key; +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaTaskConfig.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaTaskConfig.razor new file mode 100644 index 0000000..47e93c6 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaTaskConfig.razor @@ -0,0 +1,89 @@ +@page "/Configurations/MangaTaskConfig" +@using Microsoft.AspNetCore.Authorization +@using Ray.BiliBiliTool.Config.Options; +@attribute [Authorize] +@rendermode InteractiveServer +@inherits BaseConfigComponent + +漫画任务配置 + + + 漫画任务配置 + + @if (_isLoading) + { + + } + else + { + + + + + + + + 基础配置 + + + + + + + + + + + + + + + + @if (_config.IsEnable) + { + + + + 详细配置 + + + + + + + + + + + + + + } + + + + + 保存配置 + 重新加载 + + + + + @if (_saveMessage.HasValue) + { + + @_saveMessage + + } + } + + + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaTaskConfig.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaTaskConfig.razor.cs new file mode 100644 index 0000000..932eb65 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaTaskConfig.razor.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using Quartz; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.Web.Jobs; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Configs; + +public partial class MangaTaskConfig : BaseConfigComponent +{ + [Inject] + private IOptionsMonitor MangaTaskOptionsMonitor { get; set; } = null!; + + protected override IOptionsMonitor OptionsMonitor => MangaTaskOptionsMonitor; + + protected override JobKey GetJobKey() => MangaJob.Key; +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/Silver2CoinTaskConfig.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/Silver2CoinTaskConfig.razor new file mode 100644 index 0000000..549a4bc --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/Silver2CoinTaskConfig.razor @@ -0,0 +1,65 @@ +@page "/Configurations/Silver2CoinTaskConfig" +@using Microsoft.AspNetCore.Authorization +@using Ray.BiliBiliTool.Config.Options; +@attribute [Authorize] +@rendermode InteractiveServer +@inherits BaseConfigComponent + +银瓜子兑换硬币任务配置 + + + 银瓜子兑换硬币任务配置 + + @if (_isLoading) + { + + } + else + { + + + + + + + + 基础配置 + + + + + + + + + + + + + + + + + + 保存配置 + 重新加载 + + + + + @if (_saveMessage.HasValue) + { + + @_saveMessage + + } + } + + + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/Silver2CoinTaskConfig.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/Silver2CoinTaskConfig.razor.cs new file mode 100644 index 0000000..d9cc563 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/Silver2CoinTaskConfig.razor.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using Quartz; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.Web.Jobs; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Configs; + +public partial class Silver2CoinTaskConfig : BaseConfigComponent +{ + [Inject] + private IOptionsMonitor Silver2CoinTaskOptionsMonitor { get; set; } = + null!; + + protected override IOptionsMonitor OptionsMonitor => + Silver2CoinTaskOptionsMonitor; + + protected override JobKey GetJobKey() => Silver2CoinJob.Key; +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/UnfollowBatchedTaskConfig.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/UnfollowBatchedTaskConfig.razor new file mode 100644 index 0000000..25e43ec --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/UnfollowBatchedTaskConfig.razor @@ -0,0 +1,93 @@ +@page "/Configurations/UnfollowBatchedTaskConfig" +@using Microsoft.AspNetCore.Authorization +@using Ray.BiliBiliTool.Config.Options; +@attribute [Authorize] +@rendermode InteractiveServer +@inherits BaseConfigComponent + +批量取关任务配置 + + + 批量取关任务配置 + + @if (_isLoading) + { + + } + else + { + + + + + + + + 基础配置 + + + + + + + + + + + + + + + + @if (_config.IsEnable) + { + + + + 详细配置 + + + + + + + + + + + + + + + + + } + + + + + 保存配置 + 重新加载 + + + + + @if (_saveMessage.HasValue) + { + + @_saveMessage + + } + } + + + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/UnfollowBatchedTaskConfig.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/UnfollowBatchedTaskConfig.razor.cs new file mode 100644 index 0000000..2d118d0 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/UnfollowBatchedTaskConfig.razor.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using Quartz; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.Web.Jobs; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Configs; + +public partial class UnfollowBatchedTaskConfig : BaseConfigComponent +{ + [Inject] + private IOptionsMonitor UnfollowBatchedTaskOptionsMonitor { get; set; } = + null!; + + protected override IOptionsMonitor OptionsMonitor => + UnfollowBatchedTaskOptionsMonitor; + + protected override JobKey GetJobKey() => UnfollowBatchedJob.Key; + + protected override async Task OnInitializedAsync() + { + // 确保配置对象有默认的GroupName值 + if (string.IsNullOrEmpty(_config.GroupName)) + { + _config.GroupName = "天选时刻"; + } + await base.OnInitializedAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipBigPointConfig.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipBigPointConfig.razor new file mode 100644 index 0000000..9be7f20 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipBigPointConfig.razor @@ -0,0 +1,85 @@ +@page "/Configurations/VipBigPointConfig" +@using Microsoft.AspNetCore.Authorization +@using Ray.BiliBiliTool.Config.Options; +@attribute [Authorize] +@rendermode InteractiveServer +@inherits BaseConfigComponent + +会员大积分配置 + + + 会员大积分配置 + + @if (_isLoading) + { + + } + else + { + + + + + + + + 基础配置 + + + + + + + + + + + + + + + + @if (_config.IsEnable) + { + + + + 详细配置 + + + + + + + + + + + } + + + + + 保存配置 + 重新加载 + + + + + @if (_saveMessage.HasValue) + { + + @_saveMessage + + } + } + + + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipBigPointConfig.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipBigPointConfig.razor.cs new file mode 100644 index 0000000..f3e01db --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipBigPointConfig.razor.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using Quartz; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.Web.Jobs; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Configs; + +public partial class VipBigPointConfig : BaseConfigComponent +{ + [Inject] + private IOptionsMonitor VipBigPointOptionsMonitor { get; set; } = null!; + + protected override IOptionsMonitor OptionsMonitor => + VipBigPointOptionsMonitor; + + protected override JobKey GetJobKey() => VipBigPointJob.Key; +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipPrivilegeConfig.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipPrivilegeConfig.razor new file mode 100644 index 0000000..f425661 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipPrivilegeConfig.razor @@ -0,0 +1,65 @@ +@page "/Configurations/VipPrivilegeConfig" +@using Microsoft.AspNetCore.Authorization +@using Ray.BiliBiliTool.Config.Options; +@attribute [Authorize] +@rendermode InteractiveServer +@inherits BaseConfigComponent + +会员特权配置 + + + 会员特权配置 + + @if (_isLoading) + { + + } + else + { + + + + + + + + 基础配置 + + + + + + + + + + + + + + + + + + 保存配置 + 重新加载 + + + + + @if (_saveMessage.HasValue) + { + + @_saveMessage + + } + } + + + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipPrivilegeConfig.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipPrivilegeConfig.razor.cs new file mode 100644 index 0000000..54fc9ec --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipPrivilegeConfig.razor.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using Quartz; +using Ray.BiliBiliTool.Config.Options; +using Ray.BiliBiliTool.Web.Jobs; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Configs; + +public partial class VipPrivilegeConfig : BaseConfigComponent +{ + [Inject] + private IOptionsMonitor VipPrivilegeOptionsMonitor { get; set; } = null!; + + protected override IOptionsMonitor OptionsMonitor => + VipPrivilegeOptionsMonitor; + + protected override JobKey GetJobKey() => VipPrivilegeJob.Key; +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Error.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Error.razor new file mode 100644 index 0000000..e1369dc --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; + +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Home.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Home.razor new file mode 100644 index 0000000..9001e0b --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Home.razor @@ -0,0 +1,7 @@ +@page "/" + +Home + +

Hello, world!

+ +Welcome to your new app. diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor new file mode 100644 index 0000000..f86102e --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor @@ -0,0 +1,34 @@ +@page "/login" +@rendermode InteractiveServer + +@if (_loginError) +{ + + Incorrect username or password. Please try again. + +} + +
+ + + + + Log in + + + + + + + Log in + + +
+ + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor.cs new file mode 100644 index 0000000..39feb90 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.WebUtilities; +using MudBlazor; + +namespace Ray.BiliBiliTool.Web.Components.Pages; + +public partial class Login : ComponentBase +{ + [Inject] + private NavigationManager NavigationManager { get; set; } = null!; + + private string _username = ""; + private string _password = ""; + + private bool _passwordVisibility; + private InputType _passwordInput = InputType.Password; + private string _passwordInputIcon = Icons.Material.Filled.VisibilityOff; + + private void TogglePasswordVisibility() + { + if (_passwordVisibility) + { + _passwordVisibility = false; + _passwordInputIcon = Icons.Material.Filled.VisibilityOff; + _passwordInput = InputType.Password; + } + else + { + _passwordVisibility = true; + _passwordInputIcon = Icons.Material.Filled.Visibility; + _passwordInput = InputType.Text; + } + } + + private string? returnUrl; + private bool _loginError = false; + + protected override void OnInitialized() + { + var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri); + var query = QueryHelpers.ParseQuery(uri.Query); + if (query.TryGetValue("returnUrl", out var param)) + { + returnUrl = param.First(); + } + if (query.TryGetValue("error", out var errorParam) && bool.TryParse(errorParam.FirstOrDefault(), out var parsed) && parsed) + { + _loginError = true; + } + } +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor.css b/src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor.css new file mode 100644 index 0000000..e69de29 diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor.js b/src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor.js new file mode 100644 index 0000000..00ad349 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor.js @@ -0,0 +1,5 @@ +export class Login { + +} + +window.Login = Login; \ No newline at end of file diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/HistoryDialog.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/HistoryDialog.razor new file mode 100644 index 0000000..a5566e7 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/HistoryDialog.razor @@ -0,0 +1,103 @@ +@rendermode InteractiveServer +@attribute [StreamRendering] +@using Ray.BiliBiliTool.Domain +@using Ray.BiliBiliTool.Web.Extensions + + + +
+ Execution History + +
+
+ + + + @{ + DateTimeOffset? lastDate = null; + + foreach (var log in ExecutionLogs) + { + if (lastDate == null || + lastDate.Value.Date != (log.FireTimeUtc?.Date ?? log.DateAddedUtc.Date)) + { + lastDate = log.FireTimeUtc.HasValue ? log.FireTimeUtc.Value.Date : log.DateAddedUtc.Date; + + + @lastDate.Value.Date.ToLongDateString() + + + } + var finishTimeUtc = log.GetFinishTimeUtc(); + @if (log.IsException ?? false) + { + + + + + + + @GetExecutionTime(log) + @if (log.JobRunTime.HasValue) + { + + (@log.JobRunTime.Value.ToHumanTimeString(4)) + + } + + @log.GetShortExceptionMessage() + Error Details + + + } + else + { + + + @GetExecutionTime(log) + @if(log.JobRunTime.HasValue) + { + + (@log.JobRunTime.Value.ToHumanTimeString(4)) + + } + + + @log.GetShortResultMessage() + + @if (log.ShowExecutionDetailButton()) + { + Execution Details + } + + } + } + } + + @if (HasMore) + { + + Load More + + } + + + + + Close + +
diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/HistoryDialog.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/HistoryDialog.razor.cs new file mode 100644 index 0000000..5a8da91 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/HistoryDialog.razor.cs @@ -0,0 +1,140 @@ +using System.Collections.ObjectModel; +using System.Text; +using BlazingQuartz.Core.Models; +using BlazingQuartz.Core.Services; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Ray.BiliBiliTool.Domain; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Schedules; + +public partial class HistoryDialog : ComponentBase +{ + [CascadingParameter] + IMudDialogInstance MudDialog { get; set; } = null!; + + [Inject] + private IDialogService DialogSvc { get; set; } = null!; + + [Inject] + IExecutionLogService LogSvc { get; set; } = null!; + + [EditorRequired] + [Parameter] + public Key JobKey { get; set; } = null!; + + [EditorRequired] + [Parameter] + public Key? TriggerKey { get; set; } + + ObservableCollection ExecutionLogs { get; set; } = new(); + bool HasMore { get; set; } = false; + + private PageMetadata? _lastPageMeta; + private long _firstLogId; + + void Close() => MudDialog.Cancel(); + + protected override async Task OnInitializedAsync() + { + await OnRefreshHistory(); + } + + private async Task GetMoreLogs() + { + PageMetadata pageMeta; + if (_lastPageMeta == null) + { + pageMeta = new(0, 5); + } + else + { + pageMeta = _lastPageMeta with { Page = _lastPageMeta.Page + 1 }; + } + + var result = await LogSvc.GetLatestExecutionLog( + JobKey.Name, + JobKey.Group ?? BlazingQuartz.Constants.DEFAULT_GROUP, + TriggerKey?.Name, + TriggerKey?.Group, + pageMeta, + _firstLogId + ); + + _lastPageMeta = result.PageMetadata; + if (pageMeta.Page == 0) + { + _firstLogId = result.FirstOrDefault()?.LogId ?? 0; + } + + result.ForEach(l => ExecutionLogs.Add(l)); + + HasMore = result.Count == pageMeta.PageSize; + } + + private async Task OnRefreshHistory() + { + ExecutionLogs.Clear(); + _lastPageMeta = null; + _firstLogId = 0; + HasMore = false; + + await GetMoreLogs(); + } + + private void OnMoreDetails(ExecutionLog log, string title) + { + var options = new DialogOptions + { + CloseOnEscapeKey = true, + FullWidth = true, + MaxWidth = MaxWidth.Medium, + }; + + var parameters = new DialogParameters { ["ExecutionLog"] = log }; + // var dlg = DialogSvc.Show(title, parameters, options); + } + + private string GetExecutionTime(ExecutionLog log) + { + // when fire time is available, display time range + // otherwise just display date added + if (log.FireTimeUtc.HasValue) + { + StringBuilder strBldr = new( + log.FireTimeUtc.Value.LocalDateTime.ToShortDateString() + + " " + + log.FireTimeUtc.Value.LocalDateTime.ToLongTimeString() + ); + + var finishTime = log.GetFinishTimeUtc(); + if (finishTime.HasValue) + { + strBldr.Append(" - "); + if (finishTime.Value.LocalDateTime.Date != log.FireTimeUtc.Value.LocalDateTime.Date) + { + // display ending date + strBldr.Append(finishTime.Value.LocalDateTime.ToShortDateString() + " "); + } + + strBldr.Append(finishTime.Value.LocalDateTime.ToLongTimeString()); + } + return strBldr.ToString(); + } + else + { + return log.DateAddedUtc.LocalDateTime.ToShortDateString() + + " " + + log.DateAddedUtc.LocalDateTime.ToLongTimeString(); + } + } + + private Color GetTimelineDotColor(ExecutionLog log) + { + return log.LogType switch + { + LogType.ScheduleJob => ((log.IsException ?? false) ? Color.Error : Color.Success), + _ => Color.Tertiary, + }; + } +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/LogsDialog.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/LogsDialog.razor new file mode 100644 index 0000000..48c7e3b --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/LogsDialog.razor @@ -0,0 +1,74 @@ +@rendermode InteractiveServer +@attribute [StreamRendering] +@using Ray.BiliBiliTool.Domain +@using Ray.BiliBiliTool.Web.Extensions + + + +
+ Logs + +
+
+ +
+
+
Logs Terminal
+
+
+
+
+
+
+ +
+ @if (_loading && _logs.Count == 0) + { +
+ @DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + SYSTEM + Loading logs from database... +
+ } + else + { + @foreach (var log in _logs) + { +
+ @log.Timestamp.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") + @log.FormattedLogLevel + @((MarkupString)(log.RenderedMessage?.Replace(Environment.NewLine, "
")??""))
+ @if (!string.IsNullOrEmpty(log.Exception)) + { + @log.Exception + } +
+ } + } +
+ @DateTime.Now.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") +
+
+ + +
+
+ + Close + +
diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/LogsDialog.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/LogsDialog.razor.cs new file mode 100644 index 0000000..bec3e13 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/LogsDialog.razor.cs @@ -0,0 +1,122 @@ +using BlazingQuartz.Core.Models; +using BlazingQuartz.Core.Services; +using Microsoft.AspNetCore.Components; +using Microsoft.EntityFrameworkCore; +using MudBlazor; +using Ray.BiliBiliTool.Domain; +using Ray.BiliBiliTool.Infrastructure.EF; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Schedules; + +public partial class LogsDialog : ComponentBase +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + [Inject] + private IDialogService DialogSvc { get; set; } = null!; + + [Inject] + IExecutionLogService LogSvc { get; set; } = null!; + + [Inject] + private IDbContextFactory DbFactory { get; set; } = null!; + + [EditorRequired] + [Parameter] + public Key JobKey { get; set; } = null!; + + [EditorRequired] + [Parameter] + public Key? TriggerKey { get; set; } + + void Close() => MudDialog.Cancel(); + + private List _logs = new(); + private bool _loading = true; + private Timer? _timer; + private CancellationTokenSource _cancellationTokenSource = new(); + private ElementReference _logContainerReference; + private string? _fireInstanceId; + + protected override async Task OnInitializedAsync() + { + await using var context = await DbFactory.CreateDbContextAsync(); + var execution = await context + .ExecutionLogs.Where(x => x.JobName == JobKey.Name && x.TriggerName == TriggerKey!.Name) + .OrderByDescending(x => x.FireTimeUtc) + .FirstOrDefaultAsync(); + _fireInstanceId = execution?.RunInstanceId; + + if (_fireInstanceId == null) + { + return; + } + + await OnRefreshLogs(); + _timer = new Timer( + async _ => + { + await InvokeAsync(async () => + { + await OnRefreshLogs(); + StateHasChanged(); + }); + }, + null, + TimeSpan.Zero, + TimeSpan.FromSeconds(3) + ); + + await base.OnInitializedAsync(); + } + + private async Task OnRefreshLogs() + { + _loading = true; + + try + { + await using var context = await DbFactory.CreateDbContextAsync(); + _logs = await context + .BiliLogs.Where(x => x.FireInstanceIdComputed == _fireInstanceId) + .OrderBy(l => l.Timestamp) + .Take(300) // 限制记录数量,避免加载过多数据 + .ToListAsync(_cancellationTokenSource.Token); + } + catch (Exception ex) + { + // 在生产环境中应该使用日志系统记录异常 + Console.WriteLine($"加载日志失败: {ex.Message}"); + } + finally + { + _loading = false; + StateHasChanged(); + } + } + + private string GetLogLevelClass(string logLevel) + { + return logLevel.ToLower() switch + { + "error" => "log-level-error", + "warning" => "log-level-warning", + "debug" => "log-level-debug", + _ => "log-level-info", + }; + } + + private void ClearDisplay() + { + _logs.Clear(); + StateHasChanged(); + } + + public void Dispose() + { + _timer?.Dispose(); + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/LogsDialog.razor.css b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/LogsDialog.razor.css new file mode 100644 index 0000000..7fc2b94 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/LogsDialog.razor.css @@ -0,0 +1,181 @@ +/* + * 终端风格日志显示的全局样式 + * 放置在wwwroot/css/terminal.css + */ + +.terminal-container { + display: flex; + flex-direction: column; + height: 75vh; + border-radius: 6px; + overflow: hidden; +} + +.terminal-window { + background-color: #0c0c0c; + color: #cccccc; + font-family: 'Consolas', 'Courier New', monospace; + padding: 16px; + overflow-y: auto; + flex: 1; + min-height: 0; +} + +/* 日志条目 */ +.log-entry { + padding: 2px 0; + border-bottom: 1px solid #333333; + animation: fadeIn 0.3s ease-in-out; + display: flex; +} + +/* 时间戳 */ +.log-timestamp { + color: #888888; + margin-right: 8px; + white-space: nowrap; +} + +/* 日志级别通用样式 */ +.log-level { + margin-right: 8px; + padding: 0 6px; + text-align: center; + border-radius: 3px; + white-space: nowrap; + min-width: 50px; +} + +/* 不同日志级别的颜色 */ +.log-level-info { + background-color: #2d5a8a; + color: #a8d7ff; +} + +.log-level-error { + background-color: #8a2d2d; + color: #ffbaba; +} + +.log-level-warning { + background-color: #8a7d2d; + color: #ffe7a8; +} + +.log-level-debug { + background-color: #444444; + color: #cccccc; +} + +/* 日志消息 */ +.log-message { + flex-grow: 1; + word-break: break-word; +} + +/* 日志来源 */ +.log-source { + color: #888888; + margin-left: 8px; + white-space: nowrap; +} + +/* 终端标题栏 */ +.terminal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; + background-color: #333; + border-radius: 6px 6px 0 0; +} + +.terminal-title { + color: white; + font-weight: bold; +} + +/* 终端控制按钮 */ +.terminal-controls { + display: flex; + gap: 8px; +} + +.control-dot { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.dot-red { + background-color: #ff5f56; +} + +.dot-yellow { + background-color: #ffbd2e; +} + +.dot-green { + background-color: #27c93f; +} + +/* 闪烁效果 */ +.blink { + animation: blink-animation 1s steps(5, start) infinite; +} + +/* 终端底部状态栏 */ +.terminal-footer { + background-color: #333; + color: #ccc; + padding: 8px 16px; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 0 0 6px 6px; +} + +/* 光标效果 */ +.cursor::after { + content: "$_"; + animation: blink 1s step-end infinite; +} + +/* 动画定义 */ +@keyframes blink { + from, to { opacity: 1; } + 50% { opacity: 0; } +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-5px); } + to { opacity: 1; transform: translateY(0); } +} + +/* 连接状态样式 */ +.connection-status { + display: flex; + align-items: center; + font-size: 0.8rem; +} + +.status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 6px; +} + +.status-connected { + background-color: #27c93f; +} + +.status-icon { + font-size: 16px; + margin-right: 4px; +} + +/* 按钮悬停效果 */ +.clear-button:hover { + background-color: rgba(255, 255, 255, 0.1); +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor new file mode 100644 index 0000000..9e3a2fe --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor @@ -0,0 +1,274 @@ +@page "/Schedules" +@attribute [Authorize] +@rendermode InteractiveServer +@attribute [StreamRendering] +@using BlazingQuartz +@using BlazingQuartz.Core.Models +@using Microsoft.AspNetCore.Authorization +@using Ray.BiliBiliTool.Web.Extensions + +Schedules + +
+ Schedules + + + + @* checkbox *@ + @* status *@ + @* job name *@ + @* trigger *@ + @* dchedule type *@ + @* job full name *@ + @* next run *@ + @* last type *@ + @* action *@ + + + + + + + + + @switch (context.Item.JobStatus) + { + case JobStatus.Running: + + break; + case JobStatus.Idle: + + break; + case JobStatus.Paused: + + break; + case JobStatus.NoSchedule: + + break; + case JobStatus.Error: + + + + + +
+ @if (!string.IsNullOrEmpty(context.Item.ExceptionMessage)) + { + @("Job has error. " + context.Item.ExceptionMessage) + } + else + { + @("Job has error.") + } +
+
+ +
+ break; + case JobStatus.NoTrigger: + + break; + } +
+
+ + + + @context.Item.JobName + + + + + + @if (context.Item.JobStatus == JobStatus.NoTrigger) + { + -- + } + else + { + + @context.Item.TriggerName + + } + + + + + @if (context.Item.JobStatus == JobStatus.NoTrigger) + { + -- + } + else + { + @if (context.Item.TriggerDetail == null) + { +
+ + @(context.Item.TriggerType == TriggerType.Unknown ? context.Item.TriggerTypeClassName == null ? TriggerType.Unknown.ToString() : context.Item.TriggerTypeClassName : context.Item.TriggerType) +
+ } + else + { + + +
+ + @(context.Item.TriggerType == TriggerType.Unknown ? context.Item.TriggerTypeClassName == null ? TriggerType.Unknown.ToString() : context.Item.TriggerTypeClassName : context.Item.TriggerType) +
+
+ +
@(context.Item.TriggerDetail?.ToSummaryString())
+
+
+ } + } +
+
+ + + + @context.Item.GetJobTypeShortName() + + + + + + @if (context.Item.JobStatus == JobStatus.NoTrigger) + { + -- + } + else + { + @context.Item.NextTriggerTime?.LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss") + } + + + + +
+ @if (!string.IsNullOrEmpty(context.Item.ExceptionMessage) + && context.Item.JobStatus != JobStatus.Error) + { + + + + + +
@context.Item.ExceptionMessage
+
+ +
+ } + + @if (context.Item.JobStatus == JobStatus.NoTrigger && + !context.Item.PreviousTriggerTime.HasValue) + { + -- + } + else + { + @context.Item.PreviousTriggerTime?.LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss") + } +
+
+
+ + + + + + + + + + @if (context.Item.JobStatus is JobStatus.Paused or JobStatus.NoTrigger) + { + +
+ + Enable +
+
+ } + else + { + +
+ + Disable +
+
+ } + +
+ + History +
+
+
+
+
+
+ + + +
+
+ + + + + diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor.cs b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor.cs new file mode 100644 index 0000000..65ff4de --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor.cs @@ -0,0 +1,469 @@ +using System.Collections.ObjectModel; +using BlazingQuartz; +using BlazingQuartz.Core.Events; +using BlazingQuartz.Core.Models; +using BlazingQuartz.Core.Services; +using BlazingQuartz.Jobs.Abstractions; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Quartz; +using Ray.BiliBiliTool.Domain; +using Ray.BiliBiliTool.Web.Extensions; + +namespace Ray.BiliBiliTool.Web.Components.Pages.Schedules; + +public partial class Schedules : ComponentBase, IDisposable +{ + private ScheduleJobFilter _filter = new(); + private readonly Func _groupDefinition = x => x.JobGroup; + private MudDataGrid _scheduleDataGrid = new(); + + [Inject] + private ILogger Logger { get; set; } = null!; + + [Inject] + private ISchedulerService SchedulerSvc { get; set; } = null!; + + [Inject] + private ISchedulerListenerService SchedulerListenerSvc { get; set; } = null!; + + [Inject] + private IExecutionLogService ExecutionLogSvc { get; set; } = null!; + + [Inject] + private IDialogService DialogSvc { get; set; } = null!; + + [Inject] + private ISnackbar Snackbar { get; set; } = null!; + + private ObservableCollection ScheduledJobs { get; } = []; + + private static Func ScheduleRowStyleFunc => + (model, i) => + { + if (model.JobStatus == JobStatus.NoSchedule || model.JobStatus == JobStatus.Error) + return "background-color:var(--mud-palette-background-grey)"; + + return ""; + }; + + protected override async Task OnInitializedAsync() + { + RegisterEventListeners(); + await RefreshJobs(); + } + + public void Dispose() => UnRegisterEventListeners(); + + internal bool IsRunActionDisabled(ScheduleModel model) => + model.JobStatus == JobStatus.NoSchedule || model.JobStatus == JobStatus.NoTrigger; + + internal bool IsPauseActionDisabled(ScheduleModel model) => + model.JobStatus == JobStatus.NoSchedule + || model.JobStatus == JobStatus.Error + || model.JobStatus == JobStatus.NoTrigger; + + internal bool IsTriggerNowActionDisabled(ScheduleModel model) => + model.JobStatus == JobStatus.NoSchedule + || model.JobStatus == JobStatus.Error + || model.JobStatus == JobStatus.Running; + + internal bool IsHistoryActionDisabled(ScheduleModel model) => + model.JobStatus == JobStatus.NoSchedule; + + private void RegisterEventListeners() + { + SchedulerListenerSvc.OnJobToBeExecuted += SchedulerListenerSvc_OnJobToBeExecuted; + SchedulerListenerSvc.OnJobScheduled += SchedulerListenerSvc_OnJobScheduled; + SchedulerListenerSvc.OnJobWasExecuted += SchedulerListenerSvc_OnJobWasExecuted; + SchedulerListenerSvc.OnTriggerFinalized += SchedulerListenerSvc_OnTriggerFinalized; + SchedulerListenerSvc.OnJobDeleted += SchedulerListenerSvc_OnJobDeleted; + SchedulerListenerSvc.OnJobUnscheduled += SchedulerListenerSvc_OnJobUnscheduled; + SchedulerListenerSvc.OnTriggerResumed += SchedulerListenerSvc_OnTriggerResumed; + SchedulerListenerSvc.OnTriggerPaused += SchedulerListenerSvc_OnTriggerPaused; + } + + private async Task RefreshJobs() + { + ScheduledJobs.Clear(); + + IAsyncEnumerable jobs = SchedulerSvc.GetAllJobsAsync(_filter); + await foreach (ScheduleModel job in jobs) + { + ScheduledJobs.Add(job); + } + + if (ScheduledJobs.Any()) + await _scheduleDataGrid.ExpandAllGroupsAsync(); + + await UpdateScheduleModelsLastExecution(); + } + + private void UnRegisterEventListeners() + { + SchedulerListenerSvc.OnJobToBeExecuted -= SchedulerListenerSvc_OnJobToBeExecuted; + SchedulerListenerSvc.OnJobScheduled -= SchedulerListenerSvc_OnJobScheduled; + SchedulerListenerSvc.OnJobWasExecuted -= SchedulerListenerSvc_OnJobWasExecuted; + SchedulerListenerSvc.OnTriggerFinalized -= SchedulerListenerSvc_OnTriggerFinalized; + SchedulerListenerSvc.OnJobDeleted -= SchedulerListenerSvc_OnJobDeleted; + SchedulerListenerSvc.OnJobUnscheduled -= SchedulerListenerSvc_OnJobUnscheduled; + SchedulerListenerSvc.OnTriggerResumed -= SchedulerListenerSvc_OnTriggerResumed; + SchedulerListenerSvc.OnTriggerPaused -= SchedulerListenerSvc_OnTriggerPaused; + } + + private async void SchedulerListenerSvc_OnTriggerPaused(object? sender, EventArgs e) + { + TriggerKey triggerKey = e.Args; + + await InvokeAsync(() => + { + ScheduleModel? model = FindScheduleModelByTrigger(triggerKey).SingleOrDefault(); + if (model != null) + { + model.JobStatus = JobStatus.Paused; + StateHasChanged(); + } + }); + } + + private async void SchedulerListenerSvc_OnTriggerResumed( + object? sender, + EventArgs e + ) + { + TriggerKey triggerKey = e.Args; + + await InvokeAsync(() => + { + ScheduleModel? model = FindScheduleModelByTrigger(triggerKey).SingleOrDefault(); + if (model != null) + { + model.JobStatus = JobStatus.Idle; + StateHasChanged(); + } + }); + } + + private async void SchedulerListenerSvc_OnJobUnscheduled( + object? sender, + EventArgs e + ) + { + Logger.LogInformation("Job trigger {triggerKey} got unscheduled", e.Args); + await OnTriggerRemoved(e.Args); + } + + private async void SchedulerListenerSvc_OnJobDeleted(object? sender, EventArgs e) + { + JobKey jobKey = e.Args; + Logger.LogInformation("Delete all schedule job {jobKey}", jobKey); + + await InvokeAsync(() => + { + List modelList = ScheduledJobs + .Where(s => s.JobName == jobKey.Name && s.JobGroup == jobKey.Group) + .ToList(); + modelList.ForEach(s => ScheduledJobs.Remove(s)); + }); + } + + private async void SchedulerListenerSvc_OnTriggerFinalized( + object? sender, + EventArgs e + ) + { + TriggerKey triggerKey = e.Args.Key; + Logger.LogInformation("Trigger {triggerKey} finalized", triggerKey); + + await OnTriggerRemoved(triggerKey); + } + + private async Task OnTriggerRemoved(TriggerKey triggerKey) => + await InvokeAsync(async () => + { + ScheduleModel? model; + try + { + model = FindScheduleModelByTrigger(triggerKey).SingleOrDefault(); + } + catch (Exception ex) + { + Snackbar.Add( + $"Cannot update trigger status. Found more than one schedule with trigger {triggerKey}", + Severity.Warning + ); + Logger.LogWarning( + ex, + "Cannot update trigger status. Found more than one schedule with trigger {triggerKey}", + triggerKey + ); + return; + } + + if (model is not null) + { + if (model.JobName == null || model.JobStatus == JobStatus.Error) + { + // Just remove if no way to get job details + // if status is error, means get job details will throw exception + ScheduledJobs.Remove(model); + } + else + { + JobDetailModel? jobDetail = await SchedulerSvc.GetJobDetail( + model.JobName, + model.JobGroup + ); + + if (jobDetail != null && jobDetail.IsDurable) + { + // see if similar job name already exists + bool similarJobNameExists = ScheduledJobs.Any(s => + s != model && s.JobName == model.JobName && s.JobGroup == model.JobGroup + ); + if (similarJobNameExists) + { + // delete this duplicate no trigger job + ScheduledJobs.Remove(model); + } + else + { + model.JobStatus = JobStatus.NoTrigger; + model.ClearTrigger(); + } + } + else + { + model.JobStatus = JobStatus.NoSchedule; + } + } + + StateHasChanged(); + } + }); + + private async void SchedulerListenerSvc_OnJobWasExecuted( + object? sender, + JobWasExecutedEventArgs e + ) + { + JobKey jobKey = e.JobExecutionContext.JobDetail.Key; + TriggerKey triggerKey = e.JobExecutionContext.Trigger.Key; + + await InvokeAsync(() => + { + ScheduleModel? model = FindScheduleModel(jobKey, triggerKey).SingleOrDefault(); + if (model is not null) + { + model.PreviousTriggerTime = e.JobExecutionContext.FireTimeUtc; + model.NextTriggerTime = e.JobExecutionContext.NextFireTimeUtc; + model.JobStatus = JobStatus.Idle; + bool? isSuccess = e.JobExecutionContext.GetIsSuccess(); + if (e.JobException != null) + model.ExceptionMessage = e.JobException.Message; + else if (isSuccess.HasValue && !isSuccess.Value) + model.ExceptionMessage = e.JobExecutionContext.GetReturnCodeAndResult(); + + StateHasChanged(); + } + }); + } + + private async void SchedulerListenerSvc_OnJobScheduled(object? sender, EventArgs e) + { + if ( + !_filter.IncludeSystemJobs + && ( + e.Args.JobKey.Group == BlazingQuartz.Constants.SYSTEM_GROUP + || e.Args.Key.Group == BlazingQuartz.Constants.SYSTEM_GROUP + ) + ) + { + // system job is not visible, skip this event + return; + } + + await InvokeAsync(async () => + { + ScheduleModel model = await SchedulerSvc.GetScheduleModelAsync(e.Args); + ScheduledJobs.Add(model); + }); + } + + private async void SchedulerListenerSvc_OnJobToBeExecuted( + object? sender, + EventArgs e + ) + { + JobKey jobKey = e.Args.JobDetail.Key; + TriggerKey triggerKey = e.Args.Trigger.Key; + + await InvokeAsync(() => + { + ScheduleModel? model = FindScheduleModel(jobKey, triggerKey).SingleOrDefault(); + if (model is not null) + { + model.JobStatus = JobStatus.Running; + + StateHasChanged(); + } + }); + } + + private IEnumerable FindScheduleModelByTrigger(TriggerKey triggerKey) => + ScheduledJobs.Where(j => + j.EqualsTriggerKey(triggerKey) + && j.JobStatus != JobStatus.NoSchedule + && j.JobStatus != JobStatus.NoTrigger + ); + + private IEnumerable FindScheduleModel(JobKey jobKey, TriggerKey? triggerKey) => + ScheduledJobs.Where(j => + j.Equals(jobKey, triggerKey) + && ( + (j.JobStatus != JobStatus.NoSchedule && j.JobStatus != JobStatus.NoTrigger) + || (j.JobStatus == JobStatus.Error && j.TriggerName != null) + ) + ); + + private async Task UpdateScheduleModelsLastExecution() + { + var latestResult = new PageMetadata(0, 1); + var scheduleJobType = new HashSet { LogType.ScheduleJob }; + + foreach (ScheduleModel schModel in ScheduledJobs) + { + if (string.IsNullOrEmpty(schModel.JobName)) + continue; + + PagedList latestLogList = await ExecutionLogSvc.GetLatestExecutionLog( + schModel.JobName, + schModel.JobGroup, + schModel.TriggerName, + schModel.TriggerGroup, + latestResult, + logTypes: scheduleJobType + ); + + if (latestLogList != null && latestLogList.Any()) + { + ExecutionLog latestLog = latestLogList.First(); + if (!schModel.PreviousTriggerTime.HasValue) + { + schModel.PreviousTriggerTime = latestLog.FireTimeUtc; + } + + if (latestLog.IsSuccess.HasValue && !latestLog.IsSuccess.Value) + { + schModel.ExceptionMessage = latestLog.GetShortResultMessage(); + } + else if (latestLog.IsException ?? false) + { + schModel.ExceptionMessage = latestLog.GetShortExceptionMessage(); + } + } + } + } + + private async Task OnResumeScheduleJob(ScheduleModel model) + { + if (model.TriggerName == null) + { + Snackbar.Add("Cannot resume schedule. Trigger name is null.", Severity.Error); + return; + } + + await SchedulerSvc.ResumeTrigger(model.TriggerName, model.TriggerGroup); + } + + private async Task OnPauseScheduleJob(ScheduleModel model) + { + if (model.TriggerName == null) + { + Snackbar.Add("Cannot pause schedule. Trigger name is null.", Severity.Error); + return; + } + + await SchedulerSvc.PauseTrigger(model.TriggerName, model.TriggerGroup); + } + + private void OnJobHistory(ScheduleModel model) + { + if (model.JobName == null) + { + // not possible? + return; + } + + var options = new DialogOptions + { + CloseOnEscapeKey = true, + FullWidth = true, + MaxWidth = MaxWidth.Medium, + }; + + var parameters = new DialogParameters + { + ["JobKey"] = new Key(model.JobName, model.JobGroup), + ["TriggerKey"] = + model.TriggerName != null + ? new Key( + model.TriggerName, + model.TriggerGroup ?? BlazingQuartz.Constants.DEFAULT_GROUP + ) + : null, + }; + DialogSvc.ShowAsync("Execution History", parameters, options); + } + + private void OnLogs(ScheduleModel model) + { + if (model.JobName == null) + { + return; + } + + var options = new DialogOptions + { + CloseOnEscapeKey = true, + FullWidth = true, + MaxWidth = MaxWidth.Large, + }; + + var parameters = new DialogParameters + { + ["JobKey"] = new Key(model.JobName, model.JobGroup), + ["TriggerKey"] = + model.TriggerName != null + ? new Key( + model.TriggerName, + model.TriggerGroup ?? BlazingQuartz.Constants.DEFAULT_GROUP + ) + : null, + }; + DialogSvc.ShowAsync("Logs", parameters, options); + } + + private async Task OnTriggerNow(ScheduleModel model) + { + if (model.JobName == null) + { + Snackbar.Add("Cannot add trigger. Check if job still exists.", Severity.Error); + return; + } + + bool? result = await DialogSvc.ShowMessageBox( + title: "Confirm", + markupMessage: (MarkupString)"Do you want to trigger this job now?", + yesText: "Trigger", + cancelText: "Cancel" + ); + + if (result != true) + { + return; + } + + await SchedulerSvc.TriggerJob(model.JobName, model.JobGroup); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor.css b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor.css new file mode 100644 index 0000000..e69de29 diff --git a/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor.js b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor.js new file mode 100644 index 0000000..095b2d5 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor.js @@ -0,0 +1,5 @@ +export class Schedules { + +} + +window.Schedules = Schedules; \ No newline at end of file diff --git a/src/Ray.BiliBiliTool.Web/Components/Routes.razor b/src/Ray.BiliBiliTool.Web/Components/Routes.razor new file mode 100644 index 0000000..12ddaf0 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/Routes.razor @@ -0,0 +1,25 @@ +@using Microsoft.AspNetCore.Components.Authorization + + + + + + + + 您无权访问此页面,请先登录。 + + 前往登录 + + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
+
diff --git a/src/Ray.BiliBiliTool.Web/Components/_Imports.razor b/src/Ray.BiliBiliTool.Web/Components/_Imports.razor new file mode 100644 index 0000000..7f62ee3 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Components/_Imports.razor @@ -0,0 +1,12 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Ray.BiliBiliTool.Web +@using Ray.BiliBiliTool.Web.Client +@using Ray.BiliBiliTool.Web.Components +@using MudBlazor diff --git a/src/Ray.BiliBiliTool.Web/Constants.cs b/src/Ray.BiliBiliTool.Web/Constants.cs new file mode 100644 index 0000000..604d25f --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Constants.cs @@ -0,0 +1,12 @@ +namespace Ray.BiliBiliTool.Web; + +public static class Constants +{ + public const string BiliJobGroup = "BiliJob"; +} + +public enum ScheduleDialogTab +{ + Job, + Trigger, +} diff --git a/src/Ray.BiliBiliTool.Web/Controllers/AuthController.cs b/src/Ray.BiliBiliTool.Web/Controllers/AuthController.cs new file mode 100644 index 0000000..8e3f25d --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Controllers/AuthController.cs @@ -0,0 +1,51 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Mvc; +using Ray.BiliBiliTool.Web.Services; + +namespace Ray.BiliBiliTool.Web.Controllers; + +[ApiController] +[Route("auth")] +public class AuthController(IAuthService authService) : ControllerBase +{ + [HttpPost("login")] + public async Task Login( + [FromForm] string username, + [FromForm] string password, + [FromForm] string? returnUrl + ) + { + var claimsIdentity = await authService.LoginAsync(username, password); + + if (claimsIdentity.IsAuthenticated) + { + var authProperties = new AuthenticationProperties + { + IsPersistent = true, + ExpiresUtc = DateTimeOffset.UtcNow.AddDays(30), + RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/", + }; + + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(claimsIdentity), + authProperties + ); + + returnUrl = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"; + + return Redirect(returnUrl); + } + + return Redirect($"/login?error=true&returnUrl={Uri.EscapeDataString(returnUrl ?? "/")}"); + } + + [HttpGet("logout")] + public async Task Logout() + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return Redirect("/login"); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Controllers/TestController.cs b/src/Ray.BiliBiliTool.Web/Controllers/TestController.cs new file mode 100644 index 0000000..25ac82b --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Controllers/TestController.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Ray.BiliBiliTool.Web.Controllers; + +[ApiController] +[Route("test")] +public class TestController(IConfiguration config) : ControllerBase +{ + [HttpGet("config")] + public async Task Config() + { + await Task.Delay(1); + var testConfig = config["DailyTaskConfig:NumberOfCoins"]; + return Ok(testConfig); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Extensions/ExecutionLogExtensions.cs b/src/Ray.BiliBiliTool.Web/Extensions/ExecutionLogExtensions.cs new file mode 100644 index 0000000..a7fb6c9 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Extensions/ExecutionLogExtensions.cs @@ -0,0 +1,72 @@ +using System.Text; +using Ray.BiliBiliTool.Domain; + +namespace Ray.BiliBiliTool.Web.Extensions; + +public static class ExecutionLogExtensions +{ + private const int RESULT_DISPLAY_LENGTH = 80; + + public static string GetShortResultMessage(this ExecutionLog log) + { + StringBuilder strBldr = new StringBuilder(); + + if (log.ReturnCode != null) + { + strBldr.Append("Return " + log.ReturnCode + ". "); + } + + if (log.Result != null) + { + var shortResult = log.Result.Substring( + 0, + Math.Min(log.Result.Length, RESULT_DISPLAY_LENGTH) + ); + strBldr.Append(shortResult); + } + else if (log.LogType == LogType.ScheduleJob) + { + if (log.IsSuccess is null) + { + strBldr.Append("Executing..."); + } + else if (log.IsSuccess.Value) + { + strBldr.Append("Job executed successfully."); + } + else + { + strBldr.Append("Failed to execute job."); + } + } + + return strBldr.ToString(); + } + + public static string GetShortExceptionMessage(this ExecutionLog log) + { + StringBuilder strBldr = new StringBuilder(); + + if (log.ReturnCode != null) + { + strBldr.Append("Return " + log.ReturnCode + ". "); + } + + if (log.ErrorMessage != null) + { + strBldr.Append( + log.ErrorMessage.Substring( + 0, + Math.Min(log.ErrorMessage.Length, RESULT_DISPLAY_LENGTH) + ) + ); + return strBldr.ToString(); + } + + return string.Empty; + } + + public static bool ShowExecutionDetailButton(this ExecutionLog log) => + log.ExecutionLogDetail?.ExecutionDetails != null + || (log.Result?.Length ?? 0) > RESULT_DISPLAY_LENGTH; +} diff --git a/src/Ray.BiliBiliTool.Web/Extensions/ModelExtensions.cs b/src/Ray.BiliBiliTool.Web/Extensions/ModelExtensions.cs new file mode 100644 index 0000000..500c153 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Extensions/ModelExtensions.cs @@ -0,0 +1,198 @@ +using BlazingQuartz; +using BlazingQuartz.Core.Models; +using MudBlazor; +using Quartz; +using IntervalUnit = BlazingQuartz.IntervalUnit; + +namespace Ray.BiliBiliTool.Web.Extensions; + +public static class ModelExtensions +{ + public static string GetTriggerTypeIcon(this TriggerType triggerType) + { + switch (triggerType) + { + case TriggerType.Cron: + return Icons.Material.Filled.Schedule; + case TriggerType.Daily: + return Icons.Material.Filled.Alarm; + case TriggerType.Simple: + return Icons.Material.Filled.Repeat; + case TriggerType.Calendar: + return Icons.Material.Filled.CalendarMonth; + default: + return Icons.Material.Filled.Settings; + } + } + + public static DataMapType GetDataMapType(this KeyValuePair kv) + { + /* + bool System.Boolean + byte System.Byte + sbyte System.SByte + char System.Char + decimal System.Decimal + double System.Double + float System.Single + int System.Int32 + uint System.UInt32 + nint System.IntPtr + nuint System.UIntPtr + long System.Int64 + ulong System.UInt64 + short System.Int16 + ushort System.UInt16 + */ + switch (kv.Value.GetType().FullName) + { + case "System.String": + return DataMapType.String; + case "System.Int32": + return DataMapType.Integer; + case "System.Int64": + return DataMapType.Long; + case "System.Boolean": + return DataMapType.Bool; + case "System.Single": + return DataMapType.Float; + case "System.Decimal": + return DataMapType.Decimal; + case "System.Double": + return DataMapType.Double; + case "System.Int16": + return DataMapType.Short; + case "System.Char": + return DataMapType.Char; + } + + return DataMapType.Object; + } + + public static string GetDataMapTypeDescription(this KeyValuePair kv) + { + var mapType = kv.GetDataMapType(); + if (mapType == DataMapType.Object) + { + return $"Object ({kv.Value.GetType().FullName})"; + } + + return mapType.ToString(); + } + + /// + /// Converts objects to a simple human-readable string. Examples: 3.1 seconds, 2 minutes, 4.23 hours, etc. + /// + /// The timespan. + /// Significant digits to use for output. + /// + public static string ToHumanTimeString(this TimeSpan span, int significantDigits = 3) + { + var format = "G" + significantDigits; + return span.TotalMilliseconds < 1000 + ? span.TotalMilliseconds.ToString(format) + " ms" + : ( + span.TotalSeconds < 60 + ? span.TotalSeconds.ToString(format) + + (span.TotalSeconds == 1 ? " sec" : " secs") + : ( + span.TotalMinutes < 60 + ? span.TotalMinutes.ToString(format) + + (span.TotalMinutes == 1 ? " min" : " mins") + : ( + span.TotalHours < 24 + ? span.TotalHours.ToString(format) + + (span.TotalHours == 1 ? " hr" : " hrs") + : span.TotalDays.ToString(format) + + (span.TotalDays == 1 ? " day" : " days") + ) + ) + ); + } + + public static bool EqualsTriggerKey(this ScheduleModel model, TriggerKey triggerKey) + { + return model.TriggerName == triggerKey.Name && model.TriggerGroup == triggerKey.Group; + } + + public static bool Equals(this ScheduleModel model, JobKey? jobKey, TriggerKey? triggerKey) + { + if (jobKey != null && triggerKey != null) + return model.JobName == jobKey.Name + && model.JobGroup == jobKey.Group + && model.TriggerName == triggerKey.Name + && model.TriggerGroup == triggerKey.Group; + + if (jobKey != null && triggerKey == null) + return model.JobName == jobKey.Name + && model.JobGroup == jobKey.Group + && model.TriggerName == null + && model.TriggerGroup == null; + + // less possible + if (jobKey == null && triggerKey != null) + return model.TriggerName == triggerKey.Name + && model.TriggerGroup == triggerKey.Group + && model.JobName == null + && model.JobGroup == BlazingQuartz.Constants.DEFAULT_GROUP; + + return model.JobName == null && model.TriggerName == null && model.TriggerGroup == null; + } + + public static TriggerType GetTriggerType(this ITrigger trigger) + { + if (trigger is ICronTrigger) + return TriggerType.Cron; + if (trigger is ISimpleTrigger) + return TriggerType.Simple; + if (trigger is ICalendarIntervalTrigger) + return TriggerType.Calendar; + if (trigger is IDailyTimeIntervalTrigger) + return TriggerType.Daily; + + return TriggerType.Unknown; + } + + public static TimeOfDay ToTimeOfDay(this TimeSpan timeSpan) + { + return new TimeOfDay(timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds); + } + + public static Quartz.IntervalUnit ToQuartzIntervalUnit(this IntervalUnit value) + { + return Enum.Parse(value.ToString()); + } + + public static IntervalUnit ToBlazingQuartzIntervalUnit(this Quartz.IntervalUnit value) + { + return Enum.Parse(value.ToString()); + } + + public static JobKey ToJobKey(this Key key) + { + return key.Group == null ? new JobKey(key.Name) : new JobKey(key.Name, key.Group); + } + + public static TriggerKey ToTriggerKey(this Key key) + { + return key.Group == null ? new TriggerKey(key.Name) : new TriggerKey(key.Name, key.Group); + } + + /// + /// Return closest non null stack trace of exception. + /// Loop until null InnerException to get stack trace. + /// + /// + /// null if inner exceptions does not have stack trace + public static string? NonNullStackTrace(this Exception exception) + { + Exception? currentException = exception; + while (currentException.StackTrace == null) + { + currentException = currentException.InnerException; + if (currentException == null) + break; + } + return currentException?.StackTrace; + } +} diff --git a/src/Ray.BiliBiliTool.Web/Extensions/ServiceCollectionExtension.cs b/src/Ray.BiliBiliTool.Web/Extensions/ServiceCollectionExtension.cs new file mode 100644 index 0000000..312d297 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Extensions/ServiceCollectionExtension.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Components.Authorization; +using Ray.BiliBiliTool.Web.Auth; +using Ray.BiliBiliTool.Web.Services; + +namespace Ray.BiliBiliTool.Web.Extensions; + +public static class ServiceCollectionExtension +{ + public static IServiceCollection AddWebServices(this IServiceCollection services) + { + services.AddScoped(); + + return services; + } + + public static IServiceCollection AddAuthServices(this IServiceCollection services) + { + services.AddAuthenticationCore(); + services.AddAuthorizationCore(); + services + .AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }) + .AddCookie(options => + { + options.Cookie.Name = "BiliToolWebAuth"; + options.LoginPath = "/login"; + options.ExpireTimeSpan = TimeSpan.FromDays(30); + }); + services.AddHttpContextAccessor(); + services.AddScoped(); + + return services; + } +} diff --git a/src/Ray.BiliBiliTool.Web/Extensions/ServiceCollectionQuartzConfiguratorExtensions.cs b/src/Ray.BiliBiliTool.Web/Extensions/ServiceCollectionQuartzConfiguratorExtensions.cs new file mode 100644 index 0000000..59468ea --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Extensions/ServiceCollectionQuartzConfiguratorExtensions.cs @@ -0,0 +1,115 @@ +using Quartz; +using Ray.BiliBiliTool.Web.Jobs; + +namespace Ray.BiliBiliTool.Web.Extensions; + +public static class ServiceCollectionQuartzConfiguratorExtensions +{ + private const string DefaultCron = "0 0 0 1 1 ?"; + + public static IServiceCollectionQuartzConfigurator AddBiliJobs( + this IServiceCollectionQuartzConfigurator quartz, + IConfiguration configuration + ) + { + // Login job + quartz.AddJob(opts => opts.WithIdentity(LoginJob.Key)); + quartz.AddTrigger(opts => + opts.ForJob(LoginJob.Key) + .WithIdentity($"{LoginJob.Key}.Cron.Trigger", Constants.BiliJobGroup) + .WithCronSchedule(DefaultCron) + ); + + // Daily job + quartz.AddJob(opts => opts.WithIdentity(DailyJob.Key)); + quartz.AddTrigger(opts => + opts.ForJob(DailyJob.Key) + .WithIdentity($"{DailyJob.Key}.Cron.Trigger", Constants.BiliJobGroup) + .WithCronSchedule(configuration["DailyTaskConfig:Cron"] ?? DefaultCron) + ); + + // Manga job + quartz.AddJob(opts => opts.WithIdentity(MangaJob.Key)); + quartz.AddTrigger(opts => + opts.ForJob(MangaJob.Key) + .WithIdentity($"{MangaJob.Key}.Cron.Trigger", Constants.BiliJobGroup) + .WithCronSchedule(configuration["MangaTaskConfig:Cron"] ?? DefaultCron) + ); + + // MangaPrivilege job + quartz.AddJob(opts => opts.WithIdentity(MangaPrivilegeJob.Key)); + quartz.AddTrigger(opts => + opts.ForJob(MangaPrivilegeJob.Key) + .WithIdentity($"{MangaPrivilegeJob.Key}.Cron.Trigger", Constants.BiliJobGroup) + .WithCronSchedule(configuration["MangaPrivilegeTaskConfig:Cron"] ?? DefaultCron) + ); + + // ReceiveVipPrivilege job + quartz.AddJob(opts => opts.WithIdentity(VipPrivilegeJob.Key)); + quartz.AddTrigger(opts => + opts.ForJob(VipPrivilegeJob.Key) + .WithIdentity($"{VipPrivilegeJob.Key}.Cron.Trigger", Constants.BiliJobGroup) + .WithCronSchedule( + configuration["VipPrivilegeConfig:Cron"] ?? DefaultCron + ) + ); + + // Silver2Coin job + quartz.AddJob(opts => opts.WithIdentity(Silver2CoinJob.Key)); + quartz.AddTrigger(opts => + opts.ForJob(Silver2CoinJob.Key) + .WithIdentity($"{Silver2CoinJob.Key}.Cron.Trigger", Constants.BiliJobGroup) + .WithCronSchedule(configuration["Silver2CoinTaskConfig:Cron"] ?? DefaultCron) + ); + + // Charge job + quartz.AddJob(opts => opts.WithIdentity(ChargeJob.Key)); + quartz.AddTrigger(opts => + opts.ForJob(ChargeJob.Key) + .WithIdentity($"{ChargeJob.Key}.Cron.Trigger", Constants.BiliJobGroup) + .WithCronSchedule(configuration["ChargeTaskConfig:Cron"] ?? DefaultCron) + ); + + // Vip big point job + quartz.AddJob(opts => opts.WithIdentity(VipBigPointJob.Key)); + quartz.AddTrigger(opts => + opts.ForJob(VipBigPointJob.Key) + .WithIdentity($"{VipBigPointJob.Key}.Cron.Trigger", Constants.BiliJobGroup) + .WithCronSchedule(configuration["VipBigPointConfig:Cron"] ?? DefaultCron) + ); + + // Live lottery job + quartz.AddJob(opts => opts.WithIdentity(LiveLotteryJob.Key)); + quartz.AddTrigger(opts => + opts.ForJob(LiveLotteryJob.Key) + .WithIdentity($"{LiveLotteryJob.Key}.Cron.Trigger", Constants.BiliJobGroup) + .WithCronSchedule(configuration["LiveLotteryTaskConfig:Cron"] ?? DefaultCron) + ); + + // Live fans medal job + quartz.AddJob(opts => opts.WithIdentity(LiveFansMedalJob.Key)); + quartz.AddTrigger(opts => + opts.ForJob(LiveFansMedalJob.Key) + .WithIdentity($"{LiveFansMedalJob.Key}.Cron.Trigger", Constants.BiliJobGroup) + .WithCronSchedule(configuration["LiveFansMedalTaskConfig:Cron"] ?? DefaultCron) + ); + + // Unfollow batched job + quartz.AddJob(opts => opts.WithIdentity(UnfollowBatchedJob.Key)); + quartz.AddTrigger(opts => + opts.ForJob(UnfollowBatchedJob.Key) + .WithIdentity($"{UnfollowBatchedJob.Key}.Cron.Trigger", Constants.BiliJobGroup) + .WithCronSchedule(configuration["UnfollowBatchedTaskConfig:Cron"] ?? DefaultCron) + ); + + // Test bili job + quartz.AddJob(opts => opts.WithIdentity(TestBiliJob.Key)); + quartz.AddTrigger(opts => + opts.ForJob(TestBiliJob.Key) + .WithIdentity($"{TestBiliJob.Key}.Cron.Trigger", Constants.BiliJobGroup) + .WithCronSchedule(DefaultCron) + ); + + return quartz; + } +} diff --git a/src/Ray.BiliBiliTool.Web/Jobs/BaseJob.cs b/src/Ray.BiliBiliTool.Web/Jobs/BaseJob.cs new file mode 100644 index 0000000..638d573 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Jobs/BaseJob.cs @@ -0,0 +1,53 @@ +using System.Reflection; +using Quartz; +using Ray.Serilog.Sinks.Batched; +using Serilog.Context; + +namespace Ray.BiliBiliTool.Web.Jobs; + +public abstract class BaseJob(ILogger logger) : IJob + where TJob : BaseJob +{ + public async Task Execute(IJobExecutionContext context) + { + var fireInstanceId = context.FireInstanceId; + + using (LogContext.PushProperty("FireInstanceId", fireInstanceId)) + using ( + LogContext.PushProperty( + Ray.Serilog.Sinks.Batched.Constants.GroupPropertyKey, + fireInstanceId + ) + ) + { + try + { + await DoExecuteAsync(context); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + } + finally + { + logger.LogInformation("---"); + logger.LogInformation( + "v{version} 开源 by {url}", + typeof(Program).Assembly.GetName().Version?.ToString(), + Config.Constants.SourceCodeUrl + Environment.NewLine + ); + } + } + + try + { + await BatchSinkManager.FlushAsync(fireInstanceId); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Fail to push logs"); + } + } + + protected abstract Task DoExecuteAsync(IJobExecutionContext context); +} diff --git a/src/Ray.BiliBiliTool.Web/Jobs/ChargeJob.cs b/src/Ray.BiliBiliTool.Web/Jobs/ChargeJob.cs new file mode 100644 index 0000000..67e7cac --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Jobs/ChargeJob.cs @@ -0,0 +1,17 @@ +using Quartz; +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Web.Jobs; + +public class ChargeJob(ILogger logger, IChargeTaskAppService appService) + : BaseJob(logger) +{ + private readonly ILogger _logger = logger; + public static readonly JobKey Key = new(nameof(ChargeJob), Constants.BiliJobGroup); + + protected override async Task DoExecuteAsync(IJobExecutionContext context) + { + _logger.LogInformation($"{nameof(ChargeJob)} started."); + await appService.DoTaskAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Jobs/DailyJob.cs b/src/Ray.BiliBiliTool.Web/Jobs/DailyJob.cs new file mode 100644 index 0000000..e1f9055 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Jobs/DailyJob.cs @@ -0,0 +1,17 @@ +using Quartz; +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Web.Jobs; + +public class DailyJob(ILogger logger, IDailyTaskAppService appService) + : BaseJob(logger) +{ + private readonly ILogger _logger = logger; + public static readonly JobKey Key = new(nameof(DailyJob), Constants.BiliJobGroup); + + protected override async Task DoExecuteAsync(IJobExecutionContext context) + { + _logger.LogInformation($"{nameof(DailyJob)} started."); + await appService.DoTaskAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Jobs/LiveFansMedalJob.cs b/src/Ray.BiliBiliTool.Web/Jobs/LiveFansMedalJob.cs new file mode 100644 index 0000000..6c9401a --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Jobs/LiveFansMedalJob.cs @@ -0,0 +1,17 @@ +using Quartz; +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Web.Jobs; + +public class LiveFansMedalJob(ILogger logger, ILiveFansMedalAppService appService) + : BaseJob(logger) +{ + private readonly ILogger _logger = logger; + public static readonly JobKey Key = new(nameof(LiveFansMedalJob), Constants.BiliJobGroup); + + protected override async Task DoExecuteAsync(IJobExecutionContext context) + { + _logger.LogInformation($"{nameof(LiveFansMedalJob)} started."); + await appService.DoTaskAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Jobs/LiveLotteryJob.cs b/src/Ray.BiliBiliTool.Web/Jobs/LiveLotteryJob.cs new file mode 100644 index 0000000..a68e86e --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Jobs/LiveLotteryJob.cs @@ -0,0 +1,17 @@ +using Quartz; +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Web.Jobs; + +public class LiveLotteryJob(ILogger logger, ILiveLotteryTaskAppService appService) + : BaseJob(logger) +{ + private readonly ILogger _logger = logger; + public static readonly JobKey Key = new(nameof(LiveLotteryJob), Constants.BiliJobGroup); + + protected override async Task DoExecuteAsync(IJobExecutionContext context) + { + _logger.LogInformation($"{nameof(LiveLotteryJob)} started."); + await appService.DoTaskAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Jobs/LoginJob.cs b/src/Ray.BiliBiliTool.Web/Jobs/LoginJob.cs new file mode 100644 index 0000000..a2fa48b --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Jobs/LoginJob.cs @@ -0,0 +1,17 @@ +using Quartz; +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Web.Jobs; + +public class LoginJob(ILogger logger, ILoginTaskAppService appService) + : BaseJob(logger) +{ + private readonly ILogger _logger = logger; + public static readonly JobKey Key = new(nameof(LoginJob), Constants.BiliJobGroup); + + protected override async Task DoExecuteAsync(IJobExecutionContext context) + { + _logger.LogInformation($"{nameof(LoginJob)} started."); + await appService.DoTaskAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Jobs/MangaJob.cs b/src/Ray.BiliBiliTool.Web/Jobs/MangaJob.cs new file mode 100644 index 0000000..81cf885 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Jobs/MangaJob.cs @@ -0,0 +1,17 @@ +using Quartz; +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Web.Jobs; + +public class MangaJob(ILogger logger, IMangaTaskAppService appService) + : BaseJob(logger) +{ + private readonly ILogger _logger = logger; + public static readonly JobKey Key = new(nameof(MangaJob), Constants.BiliJobGroup); + + protected override async Task DoExecuteAsync(IJobExecutionContext context) + { + _logger.LogInformation($"{nameof(MangaJob)} started."); + await appService.DoTaskAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Jobs/MangaPrivilegeJob.cs b/src/Ray.BiliBiliTool.Web/Jobs/MangaPrivilegeJob.cs new file mode 100644 index 0000000..297a10f --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Jobs/MangaPrivilegeJob.cs @@ -0,0 +1,19 @@ +using Quartz; +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Web.Jobs; + +public class MangaPrivilegeJob( + ILogger logger, + IMangaPrivilegeTaskAppService appService +) : BaseJob(logger) +{ + private readonly ILogger _logger = logger; + public static readonly JobKey Key = new(nameof(MangaPrivilegeJob), Constants.BiliJobGroup); + + protected override async Task DoExecuteAsync(IJobExecutionContext context) + { + _logger.LogInformation($"{nameof(MangaPrivilegeJob)} started."); + await appService.DoTaskAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Jobs/Silver2CoinJob.cs b/src/Ray.BiliBiliTool.Web/Jobs/Silver2CoinJob.cs new file mode 100644 index 0000000..7fe57ee --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Jobs/Silver2CoinJob.cs @@ -0,0 +1,17 @@ +using Quartz; +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Web.Jobs; + +public class Silver2CoinJob(ILogger logger, ISilver2CoinTaskAppService appService) + : BaseJob(logger) +{ + private readonly ILogger _logger = logger; + public static readonly JobKey Key = new(nameof(Silver2CoinJob), Constants.BiliJobGroup); + + protected override async Task DoExecuteAsync(IJobExecutionContext context) + { + _logger.LogInformation($"{nameof(Silver2CoinJob)} started."); + await appService.DoTaskAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Jobs/TestBiliJob.cs b/src/Ray.BiliBiliTool.Web/Jobs/TestBiliJob.cs new file mode 100644 index 0000000..b0b0911 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Jobs/TestBiliJob.cs @@ -0,0 +1,17 @@ +using Quartz; +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Web.Jobs; + +public class TestBiliJob(ILogger logger, ITestAppService appService) + : BaseJob(logger) +{ + private readonly ILogger _logger = logger; + public static readonly JobKey Key = new(nameof(TestBiliJob), Constants.BiliJobGroup); + + protected override async Task DoExecuteAsync(IJobExecutionContext context) + { + _logger.LogInformation($"{nameof(TestBiliJob)} started."); + await appService.DoTaskAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Jobs/UnfollowBatchedJob.cs b/src/Ray.BiliBiliTool.Web/Jobs/UnfollowBatchedJob.cs new file mode 100644 index 0000000..9331aa3 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Jobs/UnfollowBatchedJob.cs @@ -0,0 +1,19 @@ +using Quartz; +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Web.Jobs; + +public class UnfollowBatchedJob( + ILogger logger, + IUnfollowBatchedTaskAppService appService +) : BaseJob(logger) +{ + private readonly ILogger _logger = logger; + public static readonly JobKey Key = new(nameof(UnfollowBatchedJob), Constants.BiliJobGroup); + + protected override async Task DoExecuteAsync(IJobExecutionContext context) + { + _logger.LogInformation($"{nameof(UnfollowBatchedJob)} started."); + await appService.DoTaskAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Jobs/VipBigPointJob.cs b/src/Ray.BiliBiliTool.Web/Jobs/VipBigPointJob.cs new file mode 100644 index 0000000..41bfa98 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Jobs/VipBigPointJob.cs @@ -0,0 +1,17 @@ +using Quartz; +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Web.Jobs; + +public class VipBigPointJob(ILogger logger, IVipBigPointAppService appService) + : BaseJob(logger) +{ + private readonly ILogger _logger = logger; + public static readonly JobKey Key = new(nameof(VipBigPointJob), Constants.BiliJobGroup); + + protected override async Task DoExecuteAsync(IJobExecutionContext context) + { + _logger.LogInformation($"{nameof(VipBigPointJob)} started."); + await appService.DoTaskAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Jobs/VipPrivilegeJob.cs b/src/Ray.BiliBiliTool.Web/Jobs/VipPrivilegeJob.cs new file mode 100644 index 0000000..6cc1968 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Jobs/VipPrivilegeJob.cs @@ -0,0 +1,19 @@ +using Quartz; +using Ray.BiliBiliTool.Application.Contracts; + +namespace Ray.BiliBiliTool.Web.Jobs; + +public class VipPrivilegeJob( + ILogger logger, + IVipPrivilegeTaskAppService appService +) : BaseJob(logger) +{ + private readonly ILogger _logger = logger; + public static readonly JobKey Key = new(nameof(VipPrivilegeJob), Constants.BiliJobGroup); + + protected override async Task DoExecuteAsync(IJobExecutionContext context) + { + _logger.LogInformation($"{nameof(VipPrivilegeJob)} started."); + await appService.DoTaskAsync(); + } +} diff --git a/src/Ray.BiliBiliTool.Web/Program.cs b/src/Ray.BiliBiliTool.Web/Program.cs new file mode 100644 index 0000000..8db82a4 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Program.cs @@ -0,0 +1,162 @@ +using BlazingQuartz; +using BlazingQuartz.Core; +using Microsoft.OpenApi.Models; +using MudBlazor.Services; +using Quartz; +using Quartz.Impl.AdoJobStore; +using Ray.BiliBiliTool.Agent.Extensions; +using Ray.BiliBiliTool.Application.Extensions; +using Ray.BiliBiliTool.Config.Extensions; +using Ray.BiliBiliTool.Config.SQLite; +using Ray.BiliBiliTool.DomainService.Extensions; +using Ray.BiliBiliTool.Infrastructure; +using Ray.BiliBiliTool.Infrastructure.EF; +using Ray.BiliBiliTool.Infrastructure.EF.Extensions; +using Ray.BiliBiliTool.Web.Components; +using Ray.BiliBiliTool.Web.Extensions; +using Serilog; +using Serilog.Debugging; + +SelfLog.Enable(Console.Error); +Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); + +try +{ + var builder = WebApplication.CreateBuilder(args); + + builder.Configuration.AddJsonFile("config/cookies.json", optional: true, reloadOnChange: true); + var sqliteConnStr = builder.Configuration.GetConnectionString("Sqlite"); + if (!string.IsNullOrEmpty(sqliteConnStr)) + { + builder.Configuration.AddSqlite( + connectionString: sqliteConnStr, + tableName: Ray.BiliBiliTool.Config.Constants.SqliteTableName, + keyColumnName: "Key", + valueColumnName: "Value" + ); + } + + builder + .Services.AddRazorComponents() + .AddInteractiveServerComponents() + .AddInteractiveWebAssemblyComponents(); + builder.Services.AddControllers(); + + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(c => + { + c.SwaggerDoc( + "v1", + new OpenApiInfo + { + Title = "BiliBiliToolPro API", + Version = "v1", + Description = "BiliBiliToolPro的API接口文档", + Contact = new OpenApiContact + { + Name = "BiliBiliToolPro", + Url = new Uri("https://github.com/RayWangQvQ/BiliBiliToolPro"), + }, + } + ); + }); + + builder.Services.AddMudServices(); + + builder.Services.AddEF(); + + builder.Services.AddSerilog( + (services, lc) => + lc + .ReadFrom.Configuration(builder.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.SQLite( + sqliteDbPath: sqliteConnStr?.Split(';')[0].Split('=')[1], + tableName: "bili_logs", + storeTimestampInUtc: true, + batchSize: 7 + ) + ); + + // Add BlazingQuartz + builder.Services.Configure( + builder.Configuration.GetSection("BlazingQuartz") + ); + builder.Services.AddBlazingQuartz(); + builder.Services.AddMudServices(); + + builder.Services.AddQuartz(q => + { + q.UsePersistentStore(storeOptions => + { + storeOptions.UseMicrosoftSQLite(sqlLiteOptions => + { + sqlLiteOptions.UseDriverDelegate(); + sqlLiteOptions.ConnectionString = + sqliteConnStr ?? throw new InvalidOperationException(); + sqlLiteOptions.TablePrefix = "QRTZ_"; + }); + storeOptions.UseSystemTextJsonSerializer(); + }); + + q.AddBiliJobs(builder.Configuration); + }); + builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); + + builder + .Services.AddWebServices() + .AddAuthServices() + .AddAppServices() + .AddDomainServices() + .AddBiliBiliConfigs(builder.Configuration) + .AddBiliBiliClientApi(builder.Configuration); + + var app = builder.Build(); + + Global.ServiceProviderRoot = app.Services; + + using var scope = app.Services.CreateScope(); + var dbInitializer = scope.ServiceProvider.GetRequiredService(); + dbInitializer.InitializeAsync().Wait(); + + if (app.Environment.IsDevelopment()) + { + app.UseWebAssemblyDebugging(); + } + else + { + app.UseExceptionHandler("/Error", createScopeForErrors: true); + app.UseHsts(); + } + + app.UseHttpsRedirection(); + + app.UseStaticFiles(); + app.UseAntiforgery(); + + app.UseSerilogRequestLogging(); + + app.MapControllers(); + app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(Ray.BiliBiliTool.Web.Client._Imports).Assembly); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "BiliBiliToolPro API V1"); + c.RoutePrefix = "swagger"; + }); + + app.Run(); +} +catch (Exception ex) +{ + Log.Fatal(ex, "Application terminated unexpectedly"); +} +finally +{ + Log.CloseAndFlush(); +} diff --git a/src/Ray.BiliBiliTool.Web/Properties/launchSettings.json b/src/Ray.BiliBiliTool.Web/Properties/launchSettings.json new file mode 100644 index 0000000..5149411 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "http_charles": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "HTTP_PROXY": "http://192.168.1.10:8888", + "HTTPS_PROXY": "http://192.168.1.10:8888" + } + } + } + } diff --git a/src/Ray.BiliBiliTool.Web/Ray.BiliBiliTool.Web.csproj b/src/Ray.BiliBiliTool.Web/Ray.BiliBiliTool.Web.csproj new file mode 100644 index 0000000..711e062 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Ray.BiliBiliTool.Web.csproj @@ -0,0 +1,45 @@ + + + + net8.0 + enable + enable + 1970628b-9d9e-45d1-b337-32d4d1e64e95 + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ray.BiliBiliTool.Web/Services/AuthService.cs b/src/Ray.BiliBiliTool.Web/Services/AuthService.cs new file mode 100644 index 0000000..d5b8be7 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Services/AuthService.cs @@ -0,0 +1,68 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.EntityFrameworkCore; +using Ray.BiliBiliTool.Infrastructure.EF; +using Ray.BiliBiliTool.Infrastructure.Helpers; + +namespace Ray.BiliBiliTool.Web.Services; + +public interface IAuthService +{ + Task LoginAsync(string username, string password); + Task ChangePasswordAsync(string username, string currentPassword, string newPassword); + Task GetAdminUserNameAsync(); +} + +public class AuthService(IDbContextFactory dbFactory) : IAuthService +{ + public async Task LoginAsync(string username, string password) + { + await using var context = await dbFactory.CreateDbContextAsync(); + var user = await context.Users.FirstOrDefaultAsync(u => u.Username == username); + + if (user != null && PasswordHelper.VerifyPassword(password, user.Salt, user.PasswordHash)) + { + var claims = new List { new(ClaimTypes.Name, username) }; + claims.AddRange(user.Roles.Select(role => new Claim(ClaimTypes.Role, role))); + + var claimsIdentity = new ClaimsIdentity( + claims, + CookieAuthenticationDefaults.AuthenticationScheme + ); + + return claimsIdentity; + } + + return new ClaimsIdentity(); + } + + public async Task ChangePasswordAsync( + string username, + string currentPassword, + string newPassword + ) + { + await using var context = await dbFactory.CreateDbContextAsync(); + var user = await context.Users.FirstAsync(u => u.Id == 1); + + if (!PasswordHelper.VerifyPassword(currentPassword, user.Salt, user.PasswordHash)) + { + throw new Exception("Current password is incorrect."); + } + + var (hash, salt) = PasswordHelper.HashPassword(newPassword); + + user.Salt = salt; + user.PasswordHash = hash; + user.Username = username; + + await context.SaveChangesAsync(); + } + + public async Task GetAdminUserNameAsync() + { + await using var context = await dbFactory.CreateDbContextAsync(); + var user = await context.Users.FirstAsync(u => u.Id == 1); + return user.Username; + } +} diff --git a/src/Ray.BiliBiliTool.Web/Services/IJobUIProvider.cs b/src/Ray.BiliBiliTool.Web/Services/IJobUIProvider.cs new file mode 100644 index 0000000..2a040f6 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/Services/IJobUIProvider.cs @@ -0,0 +1,6 @@ +namespace Ray.BiliBiliTool.Web.Services; + +public interface IJobUIProvider +{ + Type GetJobUIType(string? jobTypeFullName); +} diff --git a/src/Ray.BiliBiliTool.Web/appsettings.Development.json b/src/Ray.BiliBiliTool.Web/appsettings.Development.json new file mode 100644 index 0000000..b909150 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/appsettings.Development.json @@ -0,0 +1,3 @@ +{ + "DetailedErrors": true +} diff --git a/src/Ray.BiliBiliTool.Web/appsettings.json b/src/Ray.BiliBiliTool.Web/appsettings.json new file mode 100644 index 0000000..98bad6c --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/appsettings.json @@ -0,0 +1,234 @@ +{ + "PlatformType": "Web", + "AllowedHosts": "*", + + "ConnectionStrings": { + "Sqlite": "Data Source=./config/BiliBiliTool.db;Cache=Shared" + }, + + "Security": { + "UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0", //请求B站接口时头部传递的User-Agent + "UserAgentApp": "Mozilla/5.0 (Linux; Android 12; SM-S9080 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Mobile Safari/537.36 os/android model/SM-S9080 build/7760700 osVer/12 sdkInt/32 network/2 BiliApp/7760700 mobi_app/android channel/bili innerVer/7760710 c_locale/zh_CN s_locale/zh_CN disable_rcmd/0 7.76.0 os/android model/SM-S9080 mobi_app/android build/7760700 channel/bili innerVer/7760710 osVer/12 network/2", //App请求B站接口时头部传递的User-Agent + "WebProxy": "" //代理,格式为http://host:port,如果有鉴权则为user:password@http://host:port + }, + + "DailyTaskConfig": { + "Cron": "0 0 15 * * ?", + "IsEnable": true, + "IsWatchVideo": true, //是否观看视频 + "IsShareVideo": true, //是否分享视频 + "IsDonateCoinForArticle": false, + "NumberOfCoins": 5, //每日设定的投币数 [0,5] + "NumberOfProtectedCoins": 0, // 要保留的硬币数量 [0,int_max],0 为不保留,int_max 通常取 (2^31)-1 + "SaveCoinsWhenLv6": false, //达到六级后是否开始白嫖[false,true] + "SelectLike": true, //投币时是否同时点赞[false,true] + "SupportUpIds": "", //优先选择支持的up主Id集合,多个以英文逗号分隔,如:"123,456"。配置后会优先从指定的up主下挑选视频进行观看、分享和投币,不配置或配置为-1则表示没有特别支持的up,会从关注和排行耪中随机获取支持视频 + "DevicePlatform": "android", //执行客户端操作时的平台 [ios,android] + }, + + "MangaTaskConfig": { + "Cron": "0 0 14 * * ?", + "IsEnable": true, + "CustomComicId": 27355, //自定义漫画阅读 comic_id,若不清楚含义请勿修改 + "CustomEpId": 381662 //自定义漫画阅读 ep_id,若不清楚含义请勿修改 + }, + + "MangaPrivilegeTaskConfig": { + "Cron": "0 0 15 * * ?", + "IsEnable": true + }, + + "Silver2CoinTaskConfig": { + "Cron": "0 0 8 * * ?", + "IsEnable": true + }, + + "ChargeTaskConfig": { + "Cron": "0 0 12 28 * ?", + "IsEnable": true, + "AutoChargeUpId": "", //指定支持的UP主Id + "ChargeComment": "" //充电后留言 + }, + + "VipPrivilegeConfig": { + "Cron": "0 0 1 * * ?", + "IsEnable": true + }, + + "VipBigPointConfig": { + "Cron": "0 7 1 * * ?", + "IsEnable": true, + "ViewBangumis": "33378" // 自定义番剧的ssid,若不清楚含义请勿修改(默认为名侦探柯南) + }, + + "LiveLotteryTaskConfig": { + "Cron": "0 0 22 * * ?", + "IsEnable": true, + "ExcludeAwardNames": "舰|船|航海|代金券|自拍|照|写真|图|提督", //根据关键字排除包含这些文字的奖品名称,多个用“|”分隔,如“照|舰|船|航海|代金券|自拍” + "IncludeAwardNames": "", //根据关键字指定奖品名称必须包含的文字,多个用“|”分隔,如“红包|现金|块|元” + "AutoGroupFollowings": true, //抽奖结束后是否自动将关注的主播分组到“天选时刻”分组,值域[true,false] + "DenyUids": "65566781,1277481241,1643654862,603676925" //主播Uid黑名单(一般是中奖后的老赖),多个用英文逗号分隔,配置后不会参加黑名单中的主播的抽奖活动 + }, + + "LiveFansMedalTaskConfig": { + "Cron": "0 5 0 * * ?", + "IsEnable": true, + "DanmakuContent": "OvO", + "HeartBeatNumber": 70, //直播间观看的时长,单位为分钟", + "HeartBeatSendGiveUpThreshold": 5, //当心跳包发送连续失败多少次时放弃 + "IsSkipLevel20Medal": true // 是否跳过粉丝牌等级 >=0 的 + }, + + "UnfollowBatchedTaskConfig": { + "Cron": "0 0 6 1 * ?", + "IsEnable": true, + "GroupName": "天选时刻", //取关的分组名称 + "Count": 20, //本次取关个数(倒序,从后往前取关) + "RetainUids": "" //白名单(保留的UpId),多个用英文都好分隔,配置后,批量取关时不会取关配置的Up + }, + + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.Debug", + "Serilog.Sinks.File", + "Ray.Serilog.Sinks.TelegramBatched", + "Ray.Serilog.Sinks.WorkWeiXinBatched", + "Ray.Serilog.Sinks.DingTalkBatched", + "Ray.Serilog.Sinks.ServerChanBatched", + "Ray.Serilog.Sinks.CoolPushBatched", + "Ray.Serilog.Sinks.OtherApiBatched", + "Ray.Serilog.Sinks.PushPlusBatched", + "Ray.Serilog.Sinks.MicrosoftTeamsBatched", + "Ray.Serilog.Sinks.WorkWeiXinAppBatched", + "Ray.Serilog.Sinks.GotifyBatched" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft.AspNetCore": "Warning", + "Microsoft.AspNetCore.HttpLogging": "Warning", + "System.Net.Http.HttpClient": "Warning", + "Quartz.SQL": "Warning" + } + }, + "WriteTo": [ + //0.Console + { + "Name": "Console", + "Args": { + "restrictedToMinimumLevel": "Information", + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}" + } + }, + //1.Debug + { "Name": "Debug" }, + //2.File + { + "Name": "File", + "Args": { + "path": "Logs/log.txt", + "restrictedToMinimumLevel": "Verbose", + "rollingInterval": 3 + } + }, + + //3.Telegram机器人(https://core.telegram.org/bots/api#available-methods) + { + "Name": "TelegramBatched", + "Args": { + "botToken": "", + "chatId": "", + "restrictedToMinimumLevel": "Information", + "proxy": "", //代理,user:password@host:port + "apiHost": "https://api.telegram.org" //可以替换成自己搭建的反代host(https://hostloc.com/thread-805441-1-1.html) + } + }, + //4.企业微信机器人(https://work.weixin.qq.com/api/doc/90000/90136/91770) + { + "Name": "WorkWeiXinBatched", + "Args": { + "webHookUrl": "", //群机器人生成 + "restrictedToMinimumLevel": "Information" + } + }, + //5.钉钉机器人(https://developers.dingtalk.com/document/app/overview-of-group-robots) + { + "Name": "DingTalkBatched", + "Args": { + "webHookUrl": "", //群机器人生成 + "restrictedToMinimumLevel": "Information" + } + }, + //6.Server酱(http://sc.ftqq.com/9.version) + { + "Name": "ServerChanBatched", + "Args": { + "scKey": "", //已过时,待删除 + "turboScKey": "", //平台生成的ScKey + "restrictedToMinimumLevel": "Information" + } + }, + //7.酷推 + { + "Name": "CoolPushBatched", + "Args": { + "sKey": "", + "restrictedToMinimumLevel": "Information" + } + }, + //8.自定义Api + { + "Name": "OtherApiBatched", + "Args": { + "api": "", + "placeholder": "#msg#", //占位符 + "bodyJsonTemplate": "{\"msgtype\":\"markdown\",\"markdown\":{\"content\":#msg#}}", //json模板,会当作post的body,占位符会被日志内容替换(日志文本为json字符串,已经带有引号,所有模板中占位符不用使用引号包裹,如例子所示为企业微信的标准推送格式) + "restrictedToMinimumLevel": "Information" + } + }, + //9.PushPlus(http://www.pushplus.plus/doc/) + { + "Name": "PushPlusBatched", + "Args": { + "token": "", + "channel": "", //渠道,值域[wechat,webhook,cp,sms,mail],分别对应[微信公众号,指定第三方webhook,企业微信应用,短信,邮件] + "topic": "", //群组编码,用于群发,没有就不填(不填仅发送给自己);channel为webhook时无效 + "webhook": "", //webhook编码(不是地址),仅在channel使用webhook渠道和CP渠道时需要填写 + "restrictedToMinimumLevel": "Information" + } + }, + //10.MicrosoftTeams + { + "Name": "MicrosoftTeamsBatched", + "Args": { + "webhook": "", //webhook完整地址 + "restrictedToMinimumLevel": "Information" + } + }, + //11.企业微信应用推送 + { + "Name": "WorkWeiXinAppBatched", + "Args": { + "corpId": "", //必填 + "agentId": "", //必填 + "secret": "", //必填 + "toUser": "@all", + "toParty": "", + "toTag": "", + "restrictedToMinimumLevel": "Information" + } + }, + //12.gotify推送 + { + "Name": "GotifyBatched", + "Args": { + "host": "", //必填,如https://www.mygotify.com + "token": "", //必填,应用(app)的token + "restrictedToMinimumLevel": "Information" + } + } + ], + "Enrich": [ "FromLogContext" ] + } +} diff --git a/src/Ray.BiliBiliTool.Web/wwwroot/app.css b/src/Ray.BiliBiliTool.Web/wwwroot/app.css new file mode 100644 index 0000000..2bd9b78 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/wwwroot/app.css @@ -0,0 +1,51 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +a, .btn-link { + color: #006bb7; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.darker-border-checkbox.form-check-input { + border-color: #929292; +} diff --git a/src/Ray.BiliBiliTool.Web/wwwroot/bootstrap/bootstrap.min.css b/src/Ray.BiliBiliTool.Web/wwwroot/bootstrap/bootstrap.min.css new file mode 100644 index 0000000..02ae65b --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/wwwroot/bootstrap/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.1.0 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-rgb:33,37,41;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],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:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;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]::-webkit-calendar-picker-indicator{display:none}[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:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}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}::file-selector-button{font:inherit}::-webkit-file-upload-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:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.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:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);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{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.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{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@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{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@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{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@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{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@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{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@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{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.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}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.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(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;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:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;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::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;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}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 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.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;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='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;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:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{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='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%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='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;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='%23fff' 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{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;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='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;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='%2386b7fe'/%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='%23fff'/%3e%3c/svg%3e")}.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:.65}.form-range{width:100%;height:1.5rem;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 #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;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:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;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:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;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{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.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: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)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.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)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){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>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){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}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.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:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.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='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.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:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.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='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;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:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.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,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.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:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.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>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.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:.5625rem;padding-left:.5625rem}.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:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.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{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;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:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.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{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.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:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@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 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-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:.5rem;padding-left:.5rem}.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-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .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:.5rem;padding-left:.5rem}.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-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .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:.5rem;padding-left:.5rem}.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-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .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:.5rem;padding-left:.5rem}.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-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .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:.5rem;padding-left:.5rem}.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-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .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:.5rem;padding-left:.5rem}.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-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image: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.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.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:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@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-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%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");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%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");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.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{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;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:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@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:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.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:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;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:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;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:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;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:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;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:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;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:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;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:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;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='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@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% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-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-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@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-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@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-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@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-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@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-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@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-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.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:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.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 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.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:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.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:2rem;height:2rem;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' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%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}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.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{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.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}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top: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:.25}.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 solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!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-25{height:25%!important}.h-50{height:50%!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}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!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:1.5rem!important}.m-5{margin:3rem!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:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!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:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!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:1.5rem!important}.mt-5{margin-top:3rem!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:1.5rem!important}.me-5{margin-right:3rem!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:1.5rem!important}.mb-5{margin-bottom:3rem!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:1.5rem!important}.ms-5{margin-left:3rem!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:1.5rem!important}.p-5{padding:3rem!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:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!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:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!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:1.5rem!important}.pt-5{padding-top:3rem!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:1.5rem!important}.pe-5{padding-right:3rem!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:1.5rem!important}.pb-5{padding-bottom:3rem!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:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!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:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!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{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-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;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!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}.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:1.5rem!important}.gap-sm-5{gap:3rem!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:1.5rem!important}.m-sm-5{margin:3rem!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:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!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:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!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:1.5rem!important}.mt-sm-5{margin-top:3rem!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:1.5rem!important}.me-sm-5{margin-right:3rem!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:1.5rem!important}.mb-sm-5{margin-bottom:3rem!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:1.5rem!important}.ms-sm-5{margin-left:3rem!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:1.5rem!important}.p-sm-5{padding:3rem!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:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!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:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!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:1.5rem!important}.pt-sm-5{padding-top:3rem!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:1.5rem!important}.pe-sm-5{padding-right:3rem!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:1.5rem!important}.pb-sm-5{padding-bottom:3rem!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:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!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}.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:1.5rem!important}.gap-md-5{gap:3rem!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:1.5rem!important}.m-md-5{margin:3rem!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:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!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:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!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:1.5rem!important}.mt-md-5{margin-top:3rem!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:1.5rem!important}.me-md-5{margin-right:3rem!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:1.5rem!important}.mb-md-5{margin-bottom:3rem!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:1.5rem!important}.ms-md-5{margin-left:3rem!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:1.5rem!important}.p-md-5{padding:3rem!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:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!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:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!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:1.5rem!important}.pt-md-5{padding-top:3rem!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:1.5rem!important}.pe-md-5{padding-right:3rem!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:1.5rem!important}.pb-md-5{padding-bottom:3rem!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:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!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}.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:1.5rem!important}.gap-lg-5{gap:3rem!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:1.5rem!important}.m-lg-5{margin:3rem!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:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!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:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!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:1.5rem!important}.mt-lg-5{margin-top:3rem!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:1.5rem!important}.me-lg-5{margin-right:3rem!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:1.5rem!important}.mb-lg-5{margin-bottom:3rem!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:1.5rem!important}.ms-lg-5{margin-left:3rem!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:1.5rem!important}.p-lg-5{padding:3rem!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:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!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:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!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:1.5rem!important}.pt-lg-5{padding-top:3rem!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:1.5rem!important}.pe-lg-5{padding-right:3rem!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:1.5rem!important}.pb-lg-5{padding-bottom:3rem!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:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!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}.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:1.5rem!important}.gap-xl-5{gap:3rem!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:1.5rem!important}.m-xl-5{margin:3rem!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:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!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:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!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:1.5rem!important}.mt-xl-5{margin-top:3rem!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:1.5rem!important}.me-xl-5{margin-right:3rem!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:1.5rem!important}.mb-xl-5{margin-bottom:3rem!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:1.5rem!important}.ms-xl-5{margin-left:3rem!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:1.5rem!important}.p-xl-5{padding:3rem!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:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!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:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!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:1.5rem!important}.pt-xl-5{padding-top:3rem!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:1.5rem!important}.pe-xl-5{padding-right:3rem!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:1.5rem!important}.pb-xl-5{padding-bottom:3rem!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:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!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}.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:1.5rem!important}.gap-xxl-5{gap:3rem!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:1.5rem!important}.m-xxl-5{margin:3rem!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:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!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:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!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:1.5rem!important}.mt-xxl-5{margin-top:3rem!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:1.5rem!important}.me-xxl-5{margin-right:3rem!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:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!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:1.5rem!important}.ms-xxl-5{margin-left:3rem!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:1.5rem!important}.p-xxl-5{padding:3rem!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:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!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:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!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:1.5rem!important}.pt-xxl-5{padding-top:3rem!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:1.5rem!important}.pe-xxl-5{padding-right:3rem!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:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!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:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!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}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/src/Ray.BiliBiliTool.Web/wwwroot/bootstrap/bootstrap.min.css.map b/src/Ray.BiliBiliTool.Web/wwwroot/bootstrap/bootstrap.min.css.map new file mode 100644 index 0000000..afcd9e3 --- /dev/null +++ b/src/Ray.BiliBiliTool.Web/wwwroot/bootstrap/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Bootstrap v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n// scss-docs-start import-stack\n// Configuration\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"utilities\";\n\n// Layout & components\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"containers\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"accordion\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"alert\";\n@import \"progress\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"offcanvas\";\n@import \"placeholders\";\n\n// Helpers\n@import \"helpers\";\n\n// Utilities\n@import \"utilities/api\";\n// scss-docs-end import-stack\n",":root {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$variable-prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$variable-prefix}#{$color}-rgb: #{$value};\n }\n\n --#{$variable-prefix}white-rgb: #{to-rgb($white)};\n --#{$variable-prefix}black-rgb: #{to-rgb($black)};\n --#{$variable-prefix}body-rgb: #{to-rgb($body-color)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$variable-prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$variable-prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$variable-prefix}gradient: #{$gradient};\n\n // Root and body\n // stylelint-disable custom-property-empty-line-before\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$variable-prefix}root-font-size: #{$font-size-root};\n }\n --#{$variable-prefix}body-font-family: #{$font-family-base};\n --#{$variable-prefix}body-font-size: #{$font-size-base};\n --#{$variable-prefix}body-font-weight: #{$font-weight-base};\n --#{$variable-prefix}body-line-height: #{$line-height-base};\n --#{$variable-prefix}body-color: #{$body-color};\n @if $body-text-align != null {\n --#{$variable-prefix}body-text-align: #{$body-text-align};\n }\n --#{$variable-prefix}body-bg: #{$body-bg};\n // scss-docs-end root-body-variables\n // stylelint-enable custom-property-empty-line-before\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n font-size: var(--#{$variable-prefix}-root-font-size);\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$variable-prefix}body-font-family);\n @include font-size(var(--#{$variable-prefix}body-font-size));\n font-weight: var(--#{$variable-prefix}body-font-weight);\n line-height: var(--#{$variable-prefix}body-line-height);\n color: var(--#{$variable-prefix}body-color);\n text-align: var(--#{$variable-prefix}body-text-align);\n background-color: var(--#{$variable-prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n background-color: currentColor;\n border: 0;\n opacity: $hr-opacity;\n}\n\nhr:not([size]) {\n height: $hr-height; // 2\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`