[
  {
    "path": ".config/dotnet-tools.json",
    "content": "{\n  \"version\": 1,\n  \"isRoot\": true,\n  \"tools\": {\n    \"csharpier\": {\n      \"version\": \"1.0.3\",\n      \"commands\": [\n        \"csharpier\"\n      ]\n    },\n    \"husky\": {\n      \"version\": \"0.7.2\",\n      \"commands\": [\n        \"husky\"\n      ]\n    }\n  }\n}"
  },
  {
    "path": ".dockerignore",
    "content": "# .dockerignore\n\n.git\n.gitattributes\n.gitignore\n.github\n\n.editorconfig\n\nREADME.md\n\nDockerfile\n\n[b|B]in\n[O|o]bj\n"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: https://EditorConfig.org\r\n\r\n# top-most EditorConfig file\r\nroot = true\r\n\r\n# Default settings:\r\n# A newline ending every file\r\n# Use CRLF line break\r\n# Use 4 spaces as indentation\r\n[*]\r\nend_of_line = lf\r\ntrim_trailing_whitespace = true\r\ninsert_final_newline = true\r\nindent_style = space\r\nindent_size = 4\r\n\r\n# Json config files\r\n[*.json]\r\nindent_size = 2\r\n\r\n# Yaml config files\r\n[*.{yml,yaml}]\r\nindent_size = 2\r\n\r\n# Xml project files\r\n[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]\r\nindent_size = 2\r\n\r\n# Xml config files\r\n[*.{props,targets,config,nuspec}]\r\nindent_size = 2\r\n\r\n# c# 文件\r\n[*.cs]\r\n\r\n#### Core EditorConfig 选项 ####\r\n\r\n# 缩进和间距\r\nindent_size = 4\r\nindent_style = space\r\ntab_width = 4\r\n\r\n# 新行首选项\r\nend_of_line = lf\r\ninsert_final_newline = false\r\n\r\n#### .NET 编码约定 ####\r\n\r\n# 组织 Using\r\ndotnet_separate_import_directive_groups = false\r\ndotnet_sort_system_directives_first = false\r\nfile_header_template = unset\r\n\r\n# this. 和 Me. 首选项\r\ndotnet_style_qualification_for_event = false\r\ndotnet_style_qualification_for_field = false\r\ndotnet_style_qualification_for_method = false\r\ndotnet_style_qualification_for_property = false\r\n\r\n# 语言关键字与 bcl 类型首选项\r\ndotnet_style_predefined_type_for_locals_parameters_members = true\r\ndotnet_style_predefined_type_for_member_access = true\r\n\r\n# 括号首选项\r\ndotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity\r\ndotnet_style_parentheses_in_other_binary_operators = always_for_clarity\r\ndotnet_style_parentheses_in_other_operators = never_if_unnecessary\r\ndotnet_style_parentheses_in_relational_binary_operators = always_for_clarity\r\n\r\n# 修饰符首选项\r\ndotnet_style_require_accessibility_modifiers = for_non_interface_members\r\n\r\n# 表达式级首选项\r\ndotnet_style_coalesce_expression = true\r\ndotnet_style_collection_initializer = true\r\ndotnet_style_explicit_tuple_names = true\r\ndotnet_style_namespace_match_folder = true\r\ndotnet_style_null_propagation = true\r\ndotnet_style_object_initializer = true\r\ndotnet_style_operator_placement_when_wrapping = beginning_of_line\r\ndotnet_style_prefer_auto_properties = true\r\ndotnet_style_prefer_collection_expression = when_types_loosely_match\r\ndotnet_style_prefer_compound_assignment = true\r\ndotnet_style_prefer_conditional_expression_over_assignment = true\r\ndotnet_style_prefer_conditional_expression_over_return = true\r\ndotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed\r\ndotnet_style_prefer_inferred_anonymous_type_member_names = true\r\ndotnet_style_prefer_inferred_tuple_names = true\r\ndotnet_style_prefer_is_null_check_over_reference_equality_method = true\r\ndotnet_style_prefer_simplified_boolean_expressions = true\r\ndotnet_style_prefer_simplified_interpolation = true\r\n\r\n# 字段首选项\r\ndotnet_style_readonly_field = true\r\n\r\n# 参数首选项\r\ndotnet_code_quality_unused_parameters = all\r\n\r\n# 禁止显示首选项\r\ndotnet_remove_unnecessary_suppression_exclusions = none\r\n\r\n# 新行首选项\r\ndotnet_style_allow_multiple_blank_lines_experimental = true\r\ndotnet_style_allow_statement_immediately_after_block_experimental = true\r\n\r\n#### c# 编码约定 ####\r\n\r\n# var 首选项\r\ncsharp_style_var_elsewhere = false\r\ncsharp_style_var_for_built_in_types = false\r\ncsharp_style_var_when_type_is_apparent = true\r\n\r\n# Expression-bodied 成员\r\ncsharp_style_expression_bodied_accessors = true\r\ncsharp_style_expression_bodied_constructors = false\r\ncsharp_style_expression_bodied_indexers = true\r\ncsharp_style_expression_bodied_lambdas = true\r\ncsharp_style_expression_bodied_local_functions = false\r\ncsharp_style_expression_bodied_methods = true\r\ncsharp_style_expression_bodied_operators = false\r\ncsharp_style_expression_bodied_properties = true\r\n\r\n# 模式匹配首选项\r\ncsharp_style_pattern_matching_over_as_with_null_check = true\r\ncsharp_style_pattern_matching_over_is_with_cast_check = true\r\ncsharp_style_prefer_extended_property_pattern = true\r\ncsharp_style_prefer_not_pattern = true\r\ncsharp_style_prefer_pattern_matching = true\r\ncsharp_style_prefer_switch_expression = true\r\n\r\n# Null 检查首选项\r\ncsharp_style_conditional_delegate_call = true\r\n\r\n# 修饰符首选项\r\ncsharp_prefer_static_local_function = true\r\ncsharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async\r\ncsharp_style_prefer_readonly_struct = true\r\ncsharp_style_prefer_readonly_struct_member = true\r\n\r\n# 代码块首选项\r\ncsharp_prefer_braces = when_multiline\r\ncsharp_prefer_simple_using_statement = true\r\ncsharp_style_namespace_declarations = file_scoped\r\ncsharp_style_prefer_method_group_conversion = true\r\ncsharp_style_prefer_primary_constructors = true\r\ncsharp_style_prefer_top_level_statements = true\r\n\r\n# 表达式级首选项\r\ncsharp_prefer_simple_default_expression = true\r\ncsharp_style_deconstructed_variable_declaration = true\r\ncsharp_style_implicit_object_creation_when_type_is_apparent = true\r\ncsharp_style_inlined_variable_declaration = true\r\ncsharp_style_prefer_index_operator = true\r\ncsharp_style_prefer_local_over_anonymous_function = true\r\ncsharp_style_prefer_null_check_over_type_check = true\r\ncsharp_style_prefer_range_operator = true\r\ncsharp_style_prefer_tuple_swap = true\r\ncsharp_style_prefer_utf8_string_literals = true\r\ncsharp_style_throw_expression = true\r\ncsharp_style_unused_value_assignment_preference = discard_variable\r\ncsharp_style_unused_value_expression_statement_preference = discard_variable\r\n\r\n# \"using\" 指令首选项\r\ncsharp_using_directive_placement = outside_namespace\r\n\r\n# 新行首选项\r\ncsharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true\r\ncsharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true\r\ncsharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true\r\ncsharp_style_allow_blank_lines_between_consecutive_braces_experimental = true\r\ncsharp_style_allow_embedded_statements_on_same_line_experimental = true\r\n\r\n#### C# 格式规则 ####\r\n\r\n# 新行首选项\r\ncsharp_new_line_before_catch = true\r\ncsharp_new_line_before_else = true\r\ncsharp_new_line_before_finally = true\r\ncsharp_new_line_before_members_in_anonymous_types = true\r\ncsharp_new_line_before_members_in_object_initializers = true\r\ncsharp_new_line_before_open_brace = all\r\ncsharp_new_line_between_query_expression_clauses = true\r\n\r\n# 缩进首选项\r\ncsharp_indent_block_contents = true\r\ncsharp_indent_braces = false\r\ncsharp_indent_case_contents = true\r\ncsharp_indent_case_contents_when_block = true\r\ncsharp_indent_labels = one_less_than_current\r\ncsharp_indent_switch_labels = true\r\n\r\n# 空格键首选项\r\ncsharp_space_after_cast = false\r\ncsharp_space_after_colon_in_inheritance_clause = true\r\ncsharp_space_after_comma = true\r\ncsharp_space_after_dot = false\r\ncsharp_space_after_keywords_in_control_flow_statements = true\r\ncsharp_space_after_semicolon_in_for_statement = true\r\ncsharp_space_around_binary_operators = before_and_after\r\ncsharp_space_around_declaration_statements = false\r\ncsharp_space_before_colon_in_inheritance_clause = true\r\ncsharp_space_before_comma = false\r\ncsharp_space_before_dot = false\r\ncsharp_space_before_open_square_brackets = false\r\ncsharp_space_before_semicolon_in_for_statement = false\r\ncsharp_space_between_empty_square_brackets = false\r\ncsharp_space_between_method_call_empty_parameter_list_parentheses = false\r\ncsharp_space_between_method_call_name_and_opening_parenthesis = false\r\ncsharp_space_between_method_call_parameter_list_parentheses = false\r\ncsharp_space_between_method_declaration_empty_parameter_list_parentheses = false\r\ncsharp_space_between_method_declaration_name_and_open_parenthesis = false\r\ncsharp_space_between_method_declaration_parameter_list_parentheses = false\r\ncsharp_space_between_parentheses = false\r\ncsharp_space_between_square_brackets = false\r\n\r\n# 包装首选项\r\ncsharp_preserve_single_line_blocks = true\r\ncsharp_preserve_single_line_statements = true\r\n\r\n#### 命名样式 ####\r\n\r\n# 命名规则\r\n\r\ndotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion\r\ndotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface\r\ndotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i\r\n\r\ndotnet_naming_rule.types_should_be_pascal_case.severity = suggestion\r\ndotnet_naming_rule.types_should_be_pascal_case.symbols = types\r\ndotnet_naming_rule.types_should_be_pascal_case.style = pascal_case\r\n\r\ndotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion\r\ndotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members\r\ndotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case\r\n\r\n# 符号规范\r\n\r\ndotnet_naming_symbols.interface.applicable_kinds = interface\r\ndotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\r\ndotnet_naming_symbols.interface.required_modifiers = \r\n\r\ndotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum\r\ndotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\r\ndotnet_naming_symbols.types.required_modifiers = \r\n\r\ndotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method\r\ndotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\r\ndotnet_naming_symbols.non_field_members.required_modifiers = \r\n\r\n# 命名样式\r\n\r\ndotnet_naming_style.pascal_case.required_prefix = \r\ndotnet_naming_style.pascal_case.required_suffix = \r\ndotnet_naming_style.pascal_case.word_separator = \r\ndotnet_naming_style.pascal_case.capitalization = pascal_case\r\n\r\ndotnet_naming_style.begins_with_i.required_prefix = I\r\ndotnet_naming_style.begins_with_i.required_suffix = \r\ndotnet_naming_style.begins_with_i.word_separator = \r\ndotnet_naming_style.begins_with_i.capitalization = pascal_case\r\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report----.md",
    "content": "---\nname: Bug report（缺陷）\nabout: 缺陷或异常\ntitle: \"【Bug】<请在标题中清晰地概述你要反馈的异常或缺陷>\"\nlabels: ''\nassignees: ''\n\n---\n\n<!-- 请完整填写如下信息 -->\n<!-- 勾选项可以在提交之后，进行勾选 -->\n\n### 版本\n\nBiliTool版本号：`x.x.x`\n\n### 确认\n\n- [ ] 是的，我已搜索并确认，没有其他相同的议题\n- [ ] 是的，我确认，已尝试升级到最新版，但未解决\n\n### 服务器架构\n\n- [ ] x64\n- [ ] arm64\n- [ ] arm\n- [ ] 其他（请在下面补充）\n\n### 服务器系统\n\n- [ ] Windows\n- [ ] macOS\n- [ ] Linux\n    - [ ] Debian\n    - [ ] Ubuntu\n    - [ ] Windows\n    - [ ] Alpine\n    - [ ] Centos\n    - [ ] 其他（请在下面补充）\n\n### 选择的BiliTool运行模式\n\n- [ ] docker\n- [ ] podman\n- [ ] 下载的Release包\n- [ ] 其他（请在下面补充）\n\n### 问题描述\n\n<!-- 请在下方清晰的描述所您所遇到的问题 -->\n<这里>\n\n### 日志信息\n\n<!-- 请在下方贴出Debug级别的日志信息，以便更高效的确定和解决问题 -->\n\n<details>\n\n```\n<这里>\n```\n\n</details>\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report-qinglong----.md",
    "content": "---\nname: 【QingLong】Bug report（缺陷）\nabout: 缺陷或异常（青龙专属）\ntitle: \"【Bug】【青龙】<请在标题中清晰地概述你要反馈的异常或缺陷>\"\nlabels: ''\nassignees: ''\n\n---\n\n<!-- 请完整填写如下信息 -->\n<!-- 勾选项可以在提交之后，进行勾选 -->\n\n### 版本\n\nBiliTool版本号：`x.x.x`\n\n青龙版本号：`x.x.x`\n\n### 确认\n\n- [ ] 是的，我已搜索并确认，没有其他相同的议题\n- [ ] 是的，我确认，已尝试升级bilitool到最新版，但未解决\n- [ ] 是的，我确认，已尝试升级青龙到最新版，但未解决\n\n### 服务器架构\n\n- [ ] x64\n- [ ] arm64\n- [ ] arm\n- [ ] 其他（请在下面补充）\n\n### 服务器系统\n\n- [ ] Windows\n- [ ] macOS\n- [ ] Linux\n    - [ ] Debian\n    - [ ] Ubuntu\n    - [ ] Windows\n    - [ ] Alpine\n    - [ ] Centos\n    - [ ] 其他（请在下面补充）\n\n### 青龙容器类型\n\n- [ ] Docker\n- [ ] Podman\n- [ ] 其他（请在下面补充）\n\n### 青龙镜像\n\n- [ ] whyour/qinglong:latest（Alpine）\n- [ ] whyour/qinglong:debian（Debian）\n\n### 选择的BiliTool运行模式\n\n- [ ] dotnet\n- [ ] bilitool\n\n### 如果是青龙拉库相关bug，请贴出拉库方式截图\n\n- [ ] 否\n- [ ] 是，截图如下\n\n### 如果是缺失文件相关bug，请贴出容器内文件路径信息\n\n- [ ] 否\n- [ ] 是，信息如下\n\n查看方式参考文档：[提示文件不存在或路径异常怎么排查](https://github.com/RayWangQvQ/BiliBiliToolPro/blob/main/qinglong/README.md#43-提示文件不存在或路径异常怎么排查)\n\nBiliTool仓库文件路径：`<粘贴路径>`\n\n脚本文件路径：`<粘贴路径>`\n\n<这里贴截图>\n\n### 问题描述\n\n<!-- 请在下方清晰的描述所您所遇到的问题 -->\n<这里>\n\n### 日志信息\n\n<!-- 请在下方贴出Debug级别的日志信息，以便更高效的确定和解决问题 -->\n\n<details>\n\n```\n<这里>\n```\n\n</details>\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request----.md",
    "content": "---\nname: Feature request（建议）\nabout: 建议或需求\ntitle: \"【建议】<请在标题中清晰地概述你的建议>\"\nlabels: 建议/enhancement\nassignees: ''\n\n---\n\n<!-- 请完整填写如下信息 -->\n<!-- 勾选项可以在提交之后，进行勾选 -->\n\n### 确认\n\n- [ ] 是的，我已搜索并确认，没有其他相同的议题\n\n### 建议内容\n\n<!-- 请清晰的描述您的需求或建议 -->\n<!-- 如果可以，请描述您认为可行的解决方案 -->\n<这里>\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/other----.md",
    "content": "---\nname: Other（其他）\nabout: 既不是Bug也不是建议的或不能确定的其他议题\ntitle: \"【其他】<请在标题中清晰地概述内容>\"\nlabels: ''\nassignees: ''\n\n---\n\n### 确认\n\n- [ ] 是的，我确认要选Other，因为我的内容既不是Bug，也不是Feature\n- [ ] 是的，我已搜索并确认，没有其他相同的议题\n\n### 描述\n\n<!-- 请在下方清晰的描述您的内容 -->\n<这里>\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!-- 请详细描述你要PR的内容 -->\n\n### 内容\n\n<请描述您将贡献的内容>\n"
  },
  {
    "path": ".github/pull.yml",
    "content": "version: \"1\"\nrules: # Array of rules\n    - base: main # Required. Target branch\n      upstream: RayWangQvQ:main # Required. Must be in the same fork network.\n      mergeMethod: hardreset # Optional, one of [none, merge, squash, rebase, hardreset], Default: hardreset.\n      mergeUnstable: true # Optional, merge pull request even when the mergeable_state is not clean. Default: true\n"
  },
  {
    "path": ".github/workflows/auto-deploy-tencent-scf.yml",
    "content": "# https://github.com/June1991/serverless-express\n\nname: auto-deploy-tencent-scf\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: \"0 2 * * 1,3,5\" # 每周一、三、五的10点\n\nenv:\n  IsAutoDeployTencentScf: ${{ secrets.IS_AUTO_DEPLOY_TENCENT_SCF }} # 是否开启自动部署云函数\n\njobs:\n  pre-check:\n    runs-on: ubuntu-latest\n    outputs:\n      result: ${{ steps.check.outputs.result }} # 不能直接传递secrets的值，否则会被skip，需要转一下\n    steps:\n      - id: check\n        run: |\n          [ ${{ github.event_name }} == 'workflow_dispatch' -o true == \"${{ env.IsAutoDeployTencentScf }}\" ] && echo \"result=开启\" >> $GITHUB_OUTPUT || echo \"result=关闭\" >> $GITHUB_OUTPUT\n\n  deploy:\n    name: deploy serverless\n    runs-on: ubuntu-latest\n    needs: pre-check\n    # if: env.IsAutoDeployTencentScf=='true' # 这里job.if读取不到env或secrets，很坑...但是发现可以读到needs的outputs值\n    if: needs.pre-check.outputs.result=='开启'\n    steps:\n      - name: clone local repository\n        uses: actions/checkout@v3\n      - name: Use Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: 16.x\n      - name: install serverless\n        run: npm i -g serverless-cloud-framework\n      - name: deploy serverless\n        run: |\n          cd ./tencentScf\n          echo \"开始配置云函数：\"\n          echo \"$Tencent_Serverless_Yml\"\n          [ -z \"$Tencent_Serverless_Yml\" ] && echo \"未配置serverless.yml，使用默认值\" || echo \"$Tencent_Serverless_Yml\" > serverless.yml\n          echo \"开始发布项目\"\n          chmod +x publish.sh\n          ./publish.sh\n          echo \"开始部署到云函数\"\n          scf deploy\n        env: # 环境变量\n          STAGE: dev #您的部署环境\n          SERVERLESS_PLATFORM_VENDOR: tencent # serverless海外默认为aws部署，配置为腾讯部署\n          TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }} # 您的腾讯云账号sercret ID\n          TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }} # 您的腾讯云账号sercret key\n          Tencent_Serverless_Yml: ${{ secrets.TENCENT_SERVERLESS_YML }} # 云函数配置（区域、环境变量、触发器等）\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ main, develop, release/* ]\n    paths:\n      - '**/*.js'\n      - '**/*.cs'\n      - '**/*.cshtml'\n      - '**/*.csproj'\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ main, develop ]\n    paths:\n      - '**/*.js'\n      - '**/*.cs'\n      - '**/*.cshtml'\n      - '**/*.csproj'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'csharp' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]\n        # Learn more about CodeQL language support at https://git.io/codeql-language-support\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v3\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v2\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v2\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v2\n"
  },
  {
    "path": ".github/workflows/no-toxic-comments.yml",
    "content": "name: Check Toxic Comments\non: [issue_comment, pull_request_review]\n\njobs:\n  toxic_check:\n    runs-on: ubuntu-latest\n    name: Safe space\n    steps:\n      - uses: actions/checkout@v2\n      - name: Safe space - action step\n        uses: charliegerard/safe-space@master\n        with:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}"
  },
  {
    "path": ".github/workflows/publish-image.yml",
    "content": "name: Publish image\n\non:\n  workflow_dispatch:\n    inputs:\n      autoWithLatestTag:\n        description: 'Auto Add Latest Tag'\n        required: true\n        default: true\n        type: boolean\n  release:\n    types: [created]\n\nenv:\n  DOCKERHUB_USERNAME : ${{ secrets.DOCKERHUB_USERNAME }}\n  DOCKERHUB_PASSWORD : ${{ secrets.DOCKERHUB_PASSWORD }}\n  GHCR_USERNAME: ${{ github.repository_owner }}\n  GHCR_PASSWORD: ${{ secrets.GITHUB_TOKEN }}\n  DOCKER_IMG_NAME: \"zai7lou/bili_tool_web\"\n  GHC_IMG_NAME: \"ghcr.io/raywangqvq/bili_tool_web\"\n\njobs:\n  PublishImage:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: GetTargetVersion\n        id: getTargetVersion\n        run: |\n          TargetVersion=\"\"\n          if [ \"${{ github.event.release.tag_name }}\" ] ; then\n            TargetVersion=${{ github.event.release.tag_name }}\n          else\n            TargetVersion=$(grep -oP '(?<=<Version>).*?(?=<\\/Version>)' ./common.props)\n          fi\n          echo \"TargetVersion: $TargetVersion\"\n          echo \"TargetVersion=$TargetVersion\" >> $GITHUB_OUTPUT\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ env.DOCKERHUB_USERNAME }}\n          password: ${{ env.DOCKERHUB_PASSWORD }}\n\n      - name: Log in to ghcr\n        uses: docker/login-action@v2\n        with:\n          registry: ghcr.io\n          username: ${{ env.GHCR_USERNAME }}\n          password: ${{ env.GHCR_PASSWORD }}\n\n      - name: Generate tags\n        id: tags\n        run: |\n          targetVersion=\"${{ steps.getTargetVersion.outputs.TargetVersion }}\"\n          dockerTagWithVersion=\"${{ env.DOCKER_IMG_NAME }}:$targetVersion\"\n          ghcrTagWithVersion=\"${{ env.GHC_IMG_NAME }}:$targetVersion\"\n          dockerTagWithLatest=\"\"\n          ghcrTagWithLatest=\"\"\n          if [ \"${{ github.event.inputs.autoWithLatestTag }}\" == \"true\" ] || [ ${{ github.event.release.created_at }} ]; then\n            dockerTagWithLatest=\"${{ env.DOCKER_IMG_NAME }}:latest\"\n            ghcrTagWithLatest=\"${{ env.GHC_IMG_NAME }}:latest\"\n          fi\n          echo \"dockerTagWithVersion=$dockerTagWithVersion\" >> $GITHUB_OUTPUT\n          echo \"ghcrTagWithVersion=$ghcrTagWithVersion\" >> $GITHUB_OUTPUT\n          echo \"dockerTagWithLatest=$dockerTagWithLatest\" >> $GITHUB_OUTPUT\n          echo \"ghcrTagWithLatest=$ghcrTagWithLatest\" >> $GITHUB_OUTPUT\n\n      - name: Build and push\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm64\n          push: true\n          tags: |\n            ${{ steps.tags.outputs.dockerTagWithVersion }}\n            ${{ steps.tags.outputs.ghcrTagWithVersion }}\n            ${{ steps.tags.outputs.dockerTagWithLatest }}\n            ${{ steps.tags.outputs.ghcrTagWithLatest }}\n"
  },
  {
    "path": ".github/workflows/publish-release.yml",
    "content": "name: Publish release\n\non:\n  workflow_dispatch:\n\npermissions:\n  contents: write\n  discussions: write\n\njobs:\n  build:\n    name: Publish Release\n    if: ${{ github.repository == 'RayWangQvQ/BiliBiliToolPro' }}\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v3\n\n      - name: Setup .NET Core\n        uses: actions/setup-dotnet@v3\n        with:\n          dotnet-version: '8.0.x'\n\n      - name: Publish and Zip Release\n        run: |\n          cd ./scripts\n          chmod +x ./publish.sh\n          ./publish.sh --runtime all\n\n      - name: Read Version\n        id: version\n        run: echo \"version=$(cat ./src/Ray.BiliBiliTool.Console/bin/Publish/version.txt)\" >> $GITHUB_OUTPUT\n\n      - name: Create Release\n        uses: softprops/action-gh-release@v2\n        with:\n          files: './src/Ray.BiliBiliTool.Console/bin/Publish/*.zip'\n          token: ${{ secrets.GITHUB_TOKEN }}\n          name: \"BiliBiliToolPro-V${{ steps.version.outputs.version }}\"\n          tag_name: ${{ steps.version.outputs.version }}\n          body_path: './src/Ray.BiliBiliTool.Console/bin/Publish/release_notes.md'\n          discussion_category_name: Announcements\n          generate_release_notes: true\n          fail_on_unmatched_files: true\n"
  },
  {
    "path": ".github/workflows/repo-sync.yml",
    "content": "# 自动同步上游仓库\n\nname: repo-sync\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: '0 1 * * 1,3,5'\n    # UTC时区，比我们东八区早8小时，上面示例为：每周一、三、五的9点。\n\njobs:\n  repo-sync:\n    if: ${{ github.repository != 'RayWangQvQ/BiliBiliToolPro' }}\n\n    runs-on: ubuntu-latest\n    steps:  \n      - uses: actions/checkout@v2\n        with:\n          persist-credentials: false\n\n      - name: repo-sync\n        uses: repo-sync/github-sync@v2\n        with:\n          source_repo: \"https://github.com/RayWangQvQ/BiliBiliToolPro.git\"\n          source_branch: \"main\"\n          destination_branch: \"main\"\n          sync_tags: \"true\"\n          github_token: ${{ secrets.PAT }}\n"
  },
  {
    "path": ".github/workflows/stale-issues.yml",
    "content": "name: Close Stale Issues\non:\n  schedule:\n    - cron: \"0 8 * * *\" # 每天的 00:00 运行\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  issues: write\n  pull-requests: write\n\njobs:\n  close_stale_issues:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Close Stale Issues\n        uses: actions/stale@v5\n        with:\n          days-before-stale: 3 # 3 天不活跃后标记Stale\n          days-before-close: 3 # 标记Stale后3天不活跃则关闭问题\n          stale-issue-label: \"Stale\" # 标记为 \"Stale\" 的问题\n          stale-issue-message: \"🕸️ This has been inactive for 3 days, please confirm if it still needs attention~~\" # Comment added\n          close-issue-message: \"🚫 This has been inactive for too long and is now closed, feel free to reopen it if needed!\" # Comment added\n          only-labels: \"needs-more-info\" # 只处理标签为 \"help wanted\" 的问题\n          repo-token: ${{ secrets.GITHUB_TOKEN }}"
  },
  {
    "path": ".github/workflows/tag.yml",
    "content": "name: Tag\n\non:\n  pull_request:\n    types:\n      - closed\n    branches:\n      - main\n\njobs:\n  tag:\n    name: add tag\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v3\n\n      - name: Get current version\n        id: current_version\n        run: |\n          version=$(grep -oP '(?<=<Version>).*?(?=<\\/Version>)' ./common.props)\n          echo \"Curent version: $version\"\n          echo \"current_version=$version\" >> $GITHUB_OUTPUT\n\n      - name: Tag and push\n        run: |\n          new_tag=\"${{ steps.current_version.outputs.current_version }}\"\n          git tag -f \"$new_tag\"\n          git push -f origin \"$new_tag\"\n"
  },
  {
    "path": ".github/workflows/verify-pr.yml",
    "content": "name: VerifyPR\non:\n  pull_request_target:\n    types: [opened, edited]\n\njobs:\n  checkTargetBranch:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: Vankka/pr-target-branch-action@v3\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          target: main\n          change-to: develop\n          exclude: RayWangQvQ/BiliBiliToolPro:develop\n          comment: |\n              Your PR was set to `main`, but PRs should be sent to `develop`\n              The base branch of this PR has been automatically changed to `develop`, please check that there are no merge conflicts.\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n#\n.idea/\ntencentScf/.env\n.obsidian\n\n# vs code\n.vscode\n\n# krew\nkrew/bilipro\nkrew/cmd/kubectl-bilipro\nkrew/kustomization.yaml\nbilipro\nkrew/pkg/utils/fixtures\nkustomization.yaml\n\n# cookie config\n**/Ray.BiliBiliTool.Console/cookies.json\n**/Ray.BiliBiliTool.Web/config/cookies.json\n\n# ut\ncoveragereport\n\n# bruno\nbruno/.env\n\n# db\nsrc/**/BiliBiliTool.db*\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\necho \"This is pre commit\"\ndotnet husky run --group pre-commit"
  },
  {
    "path": ".husky/task-runner.json",
    "content": "{\n   \"$schema\": \"https://alirezanet.github.io/Husky.Net/schema.json\",\n   \"tasks\": [\n      {\n         \"name\": \"welcome-message-example\",\n         \"command\": \"bash\",\n         \"args\": [ \"-c\", \"echo Husky.Net is awesome!\" ],\n         \"windows\": {\n            \"command\": \"cmd\",\n            \"args\": [\"/c\", \"echo Husky.Net is awesome!\" ]\n         }\n      },\n      {\n         \"name\": \"csharpier-install\",\n         \"group\": \"pre-commit\",\n         \"command\": \"dotnet\",\n         \"args\": [ \"tool\", \"install\", \"csharpier\" ]\n      },\n      {\n         \"name\": \"csharpier-fotmat\",\n         \"group\": \"pre-commit\",\n         \"command\": \"dotnet\",\n         \"args\": [ \"csharpier\", \"format\", \"${staged}\" ],\n         \"include\": [ \"**/*.cs\" ]\n      }\n   ]\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## 3.8.2\n- Fix[#1026]: 更新文档\n## 3.8.1\n- Fix[#1005]: 更新文档\n## 3.8.0\n- Feature: 使推送的更版本号信息更简洁\n- Fix[#998]: 修复企业微信 App 推送缺少 access_token 问题\n- Fix[#996]: 修复 Server 酱推送标题不能为空的问题\n- Fix[#669]: 企业微信默认消息类型从 markdown 改为 text\n## 3.7.0\n- Fix[#989]: 修复钉钉推送标题不能为空的问题\n## 3.6.0\n- Feature[#961]: Web 项目新增推送功能\n- Feature[#961]: 升级 Serilog Pkg\n- Feature: 使用中心化包管理\n## 3.5.0\n- Feature[#924]: 新增 Sqlite 配置源\n- Feature[#924]: 新增在线配置页\n- Feature[#924]: 根据任务拆分配置\n- Feature[#924]: 实现开启、关闭任务功能\n- Feature[#924]: 实现修改 Cron 定时时间功能\n- Feature: 定时任务页改为默认50条\n- Feature: 更新配置文档\n## 3.4.0\n- Feature: 优化登录失败时的提示信息\n- Feature: 更新推送的文档说明\n## 3.3.0\n- Feature[#935]: Web 新增登录功能\n- Feature[#935]: Web 新增修改密码功能\n- Feature[#935]: 更新文档\n- Feature: 更新开源协议为 GNU GPLv3\n- Feature: 拆分原本的 Daily 任务\n- Doc: 更新文档\n- Feature: 升级 csharpier\n- Feature: 变更默认数据库文件位置到 /app/config 下\n## 3.2.0\n- Fix: 修复大会员大积分签到任务\n- Fix: 修复大会员大积分的签到和浏览追番频道任务\n- Feature[#901]: 实现大会员大积分的浏览影视频道页任务\n- Feature[#921]: 新增大会员大积分的观看剧集 bruno 信息\n- Feature: 鉴权不再兼容老版本青龙（老版本需要手动添加 bili cookie）\n- Feature: 修复 warnings\n- Feature: 移除无用的using\n- Fix: 修复 VerifyPR CI/CD 流水线\n- Feature: README 添加 Trending 信息\n## 3.1.0\n- Feature[#842]: 对接青龙新的 OpenAPI，实现青龙版 Bili 登录后自动存储 Cookie\n- Feature[#842]: 兼容老版本青龙的文件鉴权方式\n- Feature[#842]: 新增新版青龙添加鉴权的说明文档\n- Feature[#820]: 更新文档解决配置文件边缘场景下的刷新问题\n- Fix[#863]: 修复青龙尝试修复异常任务\n- Fix[#879]: 移除文档内过期的加速器地址\n- Feature: 开启 Nullable 特性，在编译阶段检查潜在的 NullReferenceException 问题\n- Feature: 临时取消 Null warning\n- Fix: Bruno 脚本错误\n- Fix: 尝试修复发布包时 CI/CD 丢失 change log 问题\n## 3.0.0\n- Feature[#884]: 上线 bili_tool_web\n- Fix[#875]: 青龙检测 dotnet 版本只需要大于等于 8.0\n- Fix[#876]: 升级 VipBigPoint 接口，解决风控\n- Fix[#881]: 升级 LiveLottery 接口，解决风控\n## 2.2.2\n- Code refactor\n- Integration Husky.Net and CSharpier\n## 2.2.1\n- Fix[#847]: DefaultRequestHeaders can not be null or empty with dotnet 8\n- Fix[#849]: Temporary disable PublishTrimmed\n## 2.2.0\n- Migrate from dotnet 6.0 to dotnet 8.0\n- Add Bruno to document the APIs\n- Fix[#824]: Log cookie when qinglong save env failed\n- Fix[#648]: Set DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 in qinglong to ignore random \"Couldn't find a valid ICU package\" issue\n## 2.1.3\n- Code refactor\n- Fix[#791]：修复VipBigPoint任务异常导致终止的问题\n## 2.1.2\n- Feature: enhancement CICD scripts\n- Fix[#728]: compatible with qinglong history versions\n## 2.1.1\n- Feature: listen ctrl+c at the very beginning\n- Fix: fix qinglong read cron error\n## 2.1.0\n- Feature[#691]: 重构并优化基于qinglong的部署方式，尝试解决偶发的安装失败的问题\n- Feature[#670]: 新增针对App的AppUserAgent配置项，用于解决大会员大积分异常问题\n- Fix: 修复CICD发布脚本错误\n## 2.0.5\n- Fix[#260]: 再次尝试修复大会员大积分“账号风险”异常\n## 2.0.4\n- Fix: 尝试修复大会员大积分“账号风险”异常\n- Feature：为agent api创建集成测试\n## 2.0.3\n- PR[#641]：实现浏览会员购页面与观看正片内容功能\n- PR[#685]：部分修复大积分功能\n- Fix：更新过于老旧的UserAgent\n- Fix：更新排行榜api\n## 2.0.2\n- PR[#617]：增加专栏投币功能与领取大会员经验的功能\n## 2.0.1\n- PR[#539]：更新文档\n- PR[#557]：修复直播接口权限不足问题\n## 2.0.0\n- Feature[#513]：将基础组件和抽象迁移到nuget包中\n- Fix[#543]：修复部分Api调用报“访问权限不足”\n## 1.0.3\n- Fix #486 : fix release zip error\n## 1.0.2\n- Fix #484 : fix read dic config error\n- Merge PR #472 : add reverse proxy host for telegram notification\n- Merge PR #483 : update login field for entrypoint\n## 1.0.1\n- Fix #463 : do not trust user's ck config\n- Feature #460 : publish single file when release\n- Feature: use new scripts for gh actions's release\n- Feature #473 : let user input when there is no target task in configs\n## 1.0.0\n- Feature: Enable asynchronous\n- Fix #344 : Support `Ctrl + C` to trigger exit event\n- Fix #451 : Rebuild cookie factory pattern and fix bug of donating coin\n- Featur: Replace AOP from MethodBoundaryAspect.Fody to Rougamo.Fody, to fix async exception\n- Merge PR #448 : Fix typo\n- Fix #446 : Change id type from int to long\n## 0.4.6\n- Fix: ck list init empty error\n- Feature #440 : use 'apk add' to install dotnet in qinglong\n## 0.4.5\n- Fix #423 : Change int to string to avoid overflow exception\n## 0.4.4\n- Fix #228 : Try to fix sharing video error\n- Feature: Change default docker image from dockerhub to github\n## 0.4.3\n- Feature #419 : Add a auto shell script for installing with docker\n- Feature #396 : Publish docker image to GitHub pkg\n## 0.4.2\n- Merfe PRs #425 #426 #427 : Enhancement docker things, thx @zclkkk\n## 0.4.1\n- Merge PR #418 : Fix search video api's error, thx @catlair\n## 0.4.0\n- 合并PR（ #381 #383 ），新增直播间挂机功能，感谢@bakapiano\n## 0.3.2\n- Fix( #358 )，获取auth时兼容老版青龙文件路径\n- Fix( #364 )，兼容青龙异形response数据类型\n- Fix( #366 #361 )，修复一些低级bug\n- Feature( #359 )，兼容读取不到`$QL_DIR`的情况\n## 0.3.1\n- Fix( #260 )，在需要的时候encode cookie\n- 更新文档\n## 0.3.0\n- hotfix docker build error\n- 合并PR（#341），新增krew部署，感谢@chenliu1993\n- 合并PR（##348），更新文档，感谢@jexjws\n- 合并PR（#350），修改请求header错误的bug，感谢@catlair\n- 合并PR（#353），新增python扫码登录的feature（仅针对青龙），感谢@AFUL1991\n- Feature（#351）：重构并新增了扫码登录功能，使之适用于各种部署平台\n## 0.2.2\n- 新增`podman`部署教程\n- 合并PR（#264），腾讯云定时任务补充新增的大会员大积分任务，感谢@layui0320\n- 合并PR（#262），更新docker的entry.sh，感谢@syrinka\n- 合并PR（#308 #312），新增Chart部署，感谢@chenliu1993\n- 合并PR（#309）新增lv6后开启白嫖模式的配置（多账号时可以实现不足lv6的继续投币，达到lv6的开始白嫖），感谢@cluom\n- 优化青龙安装dotnet的脚本，改为使用官方`dotnet-install.sh`脚本安装（之前测试网络不通，后发现--no-cdn可以）\n- 优化青龙的执行脚本，提取公共部分，并且在执行前会尝试安装一次dotnet，会清理一次缓存\n## 0.2.1\n- 合并PR（#253、#257），更新文档（@layui0320）\n- 合并PR（#256），重构docker运行是cron构建方式，并优化读取环境变量的方式（@syrinka）\n- Feature(#65)：新增TG推送配置并使用代理功能\n- Feature(#240)：新增gotify推送\n- Feature(#259)：大会员状态改为枚举类型，当非会员时自动跳过大积分任务\n- Feature：更新、优化docker部署文档\n## 0.2.0\n- 新增大会员大积分任务\n## 0.1.2\n- 修复`auto-close-pr.yml`分支错误的bug\n- 【#107】新增自动检测并关闭长时无状态issues的actions：no-response.yml\n- 【#73】【#105】【#108】更新、纠正文档内容\n- HostConfiguration，删除了CommandLine配置源，推荐只使用环境变量，同时更新青龙shell脚本内配置\n- 【#169】领取大会员福利任务更改为每日都尝试执行\n- 青龙拉库兼容大小写问题\n- 【#197】合并PR，新增了阅读漫画功能到每日任务中（@ChanceLuo）\n## 0.1.1\n- 【#54】优化青龙shell脚本读取仓库目录方式，解决青龙新老版本切换导致出现多个repo目录的bug\n- 【#82】【#85】合并外部PR，更新了文档\n- 感谢`JetBrain`提供免费的证书支持\n## 0.1.0\n- 【#62】`codeql-analysis.yml`可以指定检查的文件类型\n- 【#61】`publish-image.yml`手动打镜像时支持指定是否打latest的tag\n- 【#32】新增企业微信的应用推送，实现微信接受推送消息\n- 优化日志格式\n## 0.0.9\n- 【#47】青龙安装`dotnet`环境，支持arm架构服务器\n## 0.0.8\n- 【#55】新增日志推送端：`Microsoft Teams`\n- 【#27】更新README\n## 0.0.7\n- 【#44】兼容青龙最新版本（v2.12.0），修复因青龙调整目录结构导致的bug\n- 更新`publish-image.yml`，只有`release`时才打`latest tag`，手动运行时不打`latest tag`\n## 0.0.6\n- 更新docker镜像的构建\n- 【#12】新增配置`Notification:IsSingleAccountSingleNotify`，支持开启每个账号单独推送消息\n- publish-release.yml新增手动输入tag功能\n## 0.0.5\n- 优化推送日志，在标题中显示运行的任务名称\n- 新增`CodeQL`workflows，用于检测代码\n- 新增`Publish image`workflows，用于发布镜像\n- 新增`no-toxic-comments.yml`，用于检测评论\n- 更新`auto-close-pr.yml`，用于修正PR的目标到`develop`\n## 0.0.4\n- 【#15】修复`Actions`部署到腾讯云函数时的偶发异常\n## 0.0.3\n- 【#16】修复银瓜子兑换硬币bug\n- 【#18】修改[青龙面板](https://github.com/whyour/qinglong)以`Production`环境运行\n- [青龙面板](https://github.com/whyour/qinglong)新增拉取dev先行版功能\n## 0.0.2\n- 更新文档\n- 天选抽奖新增黑名单功能\n- 批量取关新增白名单功能\n## 0.0.1\n- 重启项目\n- 支持[青龙面板](https://github.com/whyour/qinglong)部署\n"
  },
  {
    "path": "Directory.Packages.props",
    "content": "<Project>\n  <PropertyGroup>\n    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>\n  </PropertyGroup>\n  <ItemGroup>\n    <!-- Microsoft Extensions -->\n    <PackageVersion Include=\"Microsoft.Extensions.Configuration\" Version=\"8.0.0\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Configuration.Abstractions\" Version=\"8.0.0\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Configuration.Binder\" Version=\"8.0.2\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Configuration.EnvironmentVariables\" Version=\"8.0.0\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Configuration.UserSecrets\" Version=\"8.0.1\" />\n    <PackageVersion Include=\"Microsoft.Extensions.DependencyInjection\" Version=\"8.0.1\" />\n    <PackageVersion Include=\"Microsoft.Extensions.DependencyInjection.Abstractions\" Version=\"8.0.2\" />\n    <PackageVersion Include=\"Microsoft.Extensions.FileProviders.Physical\" Version=\"8.0.0\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Hosting\" Version=\"8.0.0\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Hosting.Abstractions\" Version=\"8.0.1\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Http\" Version=\"8.0.1\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Http.Polly\" Version=\"8.0.18\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Logging\" Version=\"8.0.1\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Logging.Abstractions\" Version=\"8.0.3\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Logging.Console\" Version=\"8.0.1\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Options\" Version=\"8.0.2\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Options.ConfigurationExtensions\" Version=\"8.0.0\" />\n    <!-- Entity Framework -->\n    <PackageVersion Include=\"Microsoft.EntityFrameworkCore\" Version=\"8.0.18\" />\n    <PackageVersion Include=\"Microsoft.EntityFrameworkCore.Abstractions\" Version=\"8.0.18\" />\n    <PackageVersion Include=\"Microsoft.EntityFrameworkCore.Design\" Version=\"8.0.18\" />\n    <PackageVersion Include=\"Microsoft.EntityFrameworkCore.InMemory\" Version=\"8.0.18\" />\n    <PackageVersion Include=\"Microsoft.EntityFrameworkCore.Relational\" Version=\"8.0.18\" />\n    <PackageVersion Include=\"Microsoft.EntityFrameworkCore.Sqlite\" Version=\"8.0.18\" />\n    <PackageVersion Include=\"Microsoft.Data.Sqlite\" Version=\"8.0.18\" />\n    <PackageVersion Include=\"Npgsql.EntityFrameworkCore.PostgreSQL\" Version=\"8.0.2\" />\n    <PackageVersion Include=\"EFCore.NamingConventions\" Version=\"8.0.3\" />\n    <!-- ASP.NET Core -->\n    <PackageVersion Include=\"Microsoft.AspNetCore.Components.WebAssembly\" Version=\"8.0.18\" />\n    <PackageVersion Include=\"Microsoft.AspNetCore.Components.WebAssembly.Server\" Version=\"8.0.18\" />\n    <!-- Serilog -->\n    <PackageVersion Include=\"Serilog\" Version=\"4.3.0\" />\n    <PackageVersion Include=\"Serilog.AspNetCore\" Version=\"8.0.3\" />\n    <PackageVersion Include=\"Serilog.Extensions.Hosting\" Version=\"8.0.0\" />\n    <PackageVersion Include=\"Serilog.Extensions.Logging\" Version=\"8.0.0\" />\n    <PackageVersion Include=\"Serilog.Settings.Configuration\" Version=\"8.0.4\" />\n    <PackageVersion Include=\"Serilog.Sinks.Console\" Version=\"6.0.0\" />\n    <PackageVersion Include=\"Serilog.Sinks.Debug\" Version=\"3.0.0\" />\n    <PackageVersion Include=\"Serilog.Sinks.File\" Version=\"6.0.0\" />\n    <PackageVersion Include=\"Serilog.Sinks.SQLite.Microsoft\" Version=\"1.0.0\" />\n    <!-- Ray Serilog Sinks -->\n    <PackageVersion Include=\"Ray.Serilog.Sinks.CoolPushBatched\" Version=\"0.1.5\" />\n    <PackageVersion Include=\"Ray.Serilog.Sinks.DingTalkBatched\" Version=\"0.1.5\" />\n    <PackageVersion Include=\"Ray.Serilog.Sinks.GotifyBatched\" Version=\"0.1.5\" />\n    <PackageVersion Include=\"Ray.Serilog.Sinks.MicrosoftTeamsBatched\" Version=\"0.1.5\" />\n    <PackageVersion Include=\"Ray.Serilog.Sinks.OtherApiBatched\" Version=\"0.1.5\" />\n    <PackageVersion Include=\"Ray.Serilog.Sinks.PushPlusBatched\" Version=\"0.1.5\" />\n    <PackageVersion Include=\"Ray.Serilog.Sinks.ServerChanBatched\" Version=\"0.1.5\" />\n    <PackageVersion Include=\"Ray.Serilog.Sinks.TelegramBatched\" Version=\"0.1.5\" />\n    <PackageVersion Include=\"Ray.Serilog.Sinks.WorkWeiXinAppBatched\" Version=\"0.1.5\" />\n    <PackageVersion Include=\"Ray.Serilog.Sinks.WorkWeiXinBatched\" Version=\"0.1.5\" />\n    <!-- Quartz -->\n    <PackageVersion Include=\"Quartz\" Version=\"3.14.0\" />\n    <PackageVersion Include=\"Quartz.AspNetCore\" Version=\"3.14.0\" />\n    <PackageVersion Include=\"Quartz.Serialization.SystemTextJson\" Version=\"3.14.0\" />\n    <PackageVersion Include=\"AppAny.Quartz.EntityFrameworkCore.Migrations.SQLite\" Version=\"0.5.1\" />\n    <PackageVersion Include=\"CronExpressionDescriptor\" Version=\"2.44.0\" />\n    <!-- Other packages - 修复版本兼容性问题 -->\n    <PackageVersion Include=\"Ray.Infrastructure\" Version=\"0.0.26\" />\n    <PackageVersion Include=\"Scrutor\" Version=\"6.1.0\" />\n    <PackageVersion Include=\"WebApiClientCore\" Version=\"2.1.5\" />\n    <PackageVersion Include=\"QRCoder\" Version=\"1.6.0\" />\n    <PackageVersion Include=\"MudBlazor\" Version=\"8.6.0\" />\n    <PackageVersion Include=\"Swashbuckle.AspNetCore\" Version=\"8.1.4\" />\n    <PackageVersion Include=\"Rougamo.Fody\" Version=\"5.0.1\" />\n    <PackageVersion Include=\"Microsoft.VisualStudio.Azure.Containers.Tools.Targets\" Version=\"1.21.0\" />\n    <!-- Test packages -->\n    <PackageVersion Include=\"Microsoft.NET.Test.Sdk\" Version=\"17.12.0\" />\n    <PackageVersion Include=\"xunit\" Version=\"2.9.3\" />\n    <PackageVersion Include=\"xunit.runner.visualstudio\" Version=\"3.0.1\" />\n    <PackageVersion Include=\"coverlet.collector\" Version=\"6.0.4\" />\n    <PackageVersion Include=\"FluentAssertions\" Version=\"8.5.0\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "Dockerfile",
    "content": "#See https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/docker/building-net-docker-images\nFROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base\nWORKDIR /app\nEXPOSE 8080\n\nFROM mcr.microsoft.com/dotnet/sdk:8.0 AS build\nWORKDIR /code\n\nCOPY [\"Directory.Packages.props\", \"./\"]\nCOPY [\"src/Ray.BiliBiliTool.Web/Ray.BiliBiliTool.Web.csproj\", \"src/Ray.BiliBiliTool.Web/\"]\nCOPY [\"src/Ray.BiliBiliTool.Web.Client/Ray.BiliBiliTool.Web.Client.csproj\", \"src/Ray.BiliBiliTool.Web.Client/\"]\nCOPY [\"src/Ray.BiliBiliTool.Application/Ray.BiliBiliTool.Application.csproj\", \"src/Ray.BiliBiliTool.Application/\"]\nCOPY [\"src/Ray.BiliBiliTool.Application.Contracts/Ray.BiliBiliTool.Application.Contracts.csproj\", \"src/Ray.BiliBiliTool.Application.Contracts/\"]\nCOPY [\"src/Ray.BiliBiliTool.Domain/Ray.BiliBiliTool.Domain.csproj\", \"src/Ray.BiliBiliTool.Domain/\"]\nCOPY [\"src/Ray.BiliBiliTool.DomainService/Ray.BiliBiliTool.DomainService.csproj\", \"src/Ray.BiliBiliTool.DomainService/\"]\nCOPY [\"src/Ray.BiliBiliTool.Config/Ray.BiliBiliTool.Config.csproj\", \"src/Ray.BiliBiliTool.Config/\"]\nCOPY [\"src/Ray.BiliBiliTool.Agent/Ray.BiliBiliTool.Agent.csproj\", \"src/Ray.BiliBiliTool.Agent/\"]\nCOPY [\"src/Ray.BiliBiliTool.Infrastructure/Ray.BiliBiliTool.Infrastructure.csproj\", \"src/Ray.BiliBiliTool.Infrastructure/\"]\nCOPY [\"src/Ray.BiliBiliTool.Infrastructure.EF/Ray.BiliBiliTool.Infrastructure.EF.csproj\", \"src/Ray.BiliBiliTool.Infrastructure.EF/\"]\nCOPY [\"src/BlazingQuartz.Core/BlazingQuartz.Core.csproj\", \"src/BlazingQuartz.Core/\"]\nCOPY [\"src/BlazingQuartz.Jobs/BlazingQuartz.Jobs.csproj\", \"src/BlazingQuartz.Jobs/\"]\nCOPY [\"src/BlazingQuartz.Jobs.Abstractions/BlazingQuartz.Jobs.Abstractions.csproj\", \"src/BlazingQuartz.Jobs.Abstractions/\"]\n\nRUN dotnet restore \"src/Ray.BiliBiliTool.Web/Ray.BiliBiliTool.Web.csproj\"\nCOPY . .\nWORKDIR \"/code/src/Ray.BiliBiliTool.Web\"\nRUN dotnet build \"Ray.BiliBiliTool.Web.csproj\" -c Release -o /app/build\n\nFROM build AS publish\nRUN dotnet publish \"Ray.BiliBiliTool.Web.csproj\" -c Release -o /app/publish\n\nFROM base AS final\nWORKDIR /app\nCOPY --from=publish /app/publish .\nCOPY docker/entrypoint.sh /app/entrypoint.sh\nRUN rm -rf /var/lib/apt/lists/* \\\n    && chmod +x /app/entrypoint.sh\nENTRYPOINT [\"/app/entrypoint.sh\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "![2233](docs/imgs/2233.png)\n\n<div align=\"center\">\n\n<h1 align=\"center\">\n\nBiliTool\n\n</h1>\n\n[![GitHub Stars](https://img.shields.io/github/stars/RayWangQvQ/BiliBiliToolPro?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/stargazers)\n[![GitHub Forks](https://img.shields.io/github/forks/RayWangQvQ/BiliBiliToolPro?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/network)\n[![GitHub Issues](https://img.shields.io/github/issues/RayWangQvQ/BiliBiliToolPro?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/issues)\n[![GitHub Contributors](https://img.shields.io/github/contributors/RayWangQvQ/BiliBiliToolPro?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/graphs/contributors)\n[![GitHub All Releases](https://img.shields.io/github/downloads/RayWangQvQ/BiliBiliToolPro/total?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/releases)\n[![GitHub Release (latest SemVer)](https://img.shields.io/github/v/release/RayWangQvQ/BiliBiliToolPro?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/releases)\n[![GitHub License](https://img.shields.io/github/license/RayWangQvQ/BiliBiliToolPro?style=flat-square)](https://github.com/RayWangQvQ/BiliBiliToolPro/blob/main/LICENSE)\n\n<a href=\"https://trendshift.io/repositories/3329\" target=\"_blank\">\n    <img src=\"https://trendshift.io/api/badge/repositories/3329\" alt=\"RayWangQvQ%2FBiliBiliToolPro | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/>\n</a>\n\n</div>\n\n**BiliTool 是一个自动执行任务的工具，当我们忘记做某项任务时，它会像一个贴心小助手，按照我们预先吩咐它的命令，在指定频率、时间范围内帮助我们完成计划的任务。**\n\n**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.**\n\n主要功能如下：\n\n- **扫码登录，自动更新cookie**\n- **每日获取满额升级经验（登录、投币、点赞、分享视频）（支持指定up主）**\n- **直播间挂机**\n- **每天漫画签到**\n- **每天直播签到**\n- **直播中心银瓜子兑换为硬币**\n- **每月领取大会员赠送的 5 张 B 币券和福利（忘记或者不领就浪费了哦）**\n- **每月领取大会员漫画福利**\n- **月底在 B 币券过期前进行充电（支持指定想要支持的up主，如果没有喜欢的up，也可以为自己充个电啊，做个用爱为自己发电的人~）**\n- **直播中心天选时刻自动参与抽奖**\n- **批量取关**\n- **大会员大积分任务**\n- **支持多账号**\n- **理论上支持所有远端的日志推送（默认支持推送到Telegram、企业微信、钉钉、PushPlus、Server酱、酷推，另外也支持自定义推送到任意api）**\n---\n[目录]\n\n<!-- TOC depthFrom:2 -->\n\n- [1. 如何使用](#1-如何使用)\n    - [1.1. 部署 BiliTool](#11-部署-bilitool)\n        - [1.1.1. 方案一：免费在线容器](#111-方案一免费在线容器)\n        - [1.1.2. 方式二：青龙](#112-方式二青龙)\n        - [1.1.3. 方式三：Docker 或 Podman 运行](#113-方式三docker-或-podman-运行)\n        - [1.1.4. 方式四：下载程序包到本地或服务器运行](#114-方式四下载程序包到本地或服务器运行)\n        - [1.1.5. 方式五：Chart部署](#115-方式五chart部署)\n    - [1.2. 消息推送（可选）](#12-消息推送可选)\n- [2. 功能任务说明](#2-功能任务说明)\n- [3. 个性化自定义配置](#3-个性化自定义配置)\n- [4. 多账号支持](#4-多账号支持)\n- [5. 常见问题](#5-常见问题)\n- [6. 版本发布及更新](#6-版本发布及更新)\n- [7. 成为开源贡献成员](#7-成为开源贡献成员)\n    - [7.1. 贡献代码](#71-贡献代码)\n    - [7.2. 贡献文档](#72-贡献文档)\n- [8. 捐赠支持](#8-捐赠支持)\n- [9. 其他](#9-其他)\n\n<!-- /TOC -->\n\n---\n**Github 仓库地址：[RayWangQvQ/BiliBiliToolPro](https://github.com/RayWangQvQ/BiliBiliToolPro)**\n\n**注意：**\n\n- **本应用仅用于学习和测试，作者本人并不对其负责，请于运行测试完成后自行删除，请勿滥用！**\n- **所有代码都是开源且透明的，任何人均可查看，程序不会保存或滥用任何用户的个人信息**\n- **应用内几乎所有功能都开放了配置（如任务开关、日期、id等），详细信息可阅读配置文档**\n\n运行图示：\n\n<p align=\"center\">\n    <img src=\"docs/imgs/web-schedules.png\" alt=\"运行图示\" width=\"800\" />\n    <br/>\n    <img src=\"docs/imgs/web-schedules-log.png\" alt=\"运行日志\" width=\"800\" />\n    <br/>\n    <img src=\"docs/imgs/web-configs.png\" alt=\"运行日志\" width=\"800\" />\n    <br/>\n</p>\n\n## 1. 如何使用\n\nBiliTool 实现自动完成任务的原理，是通过调用一系列开放的api实现的。\n\n**要使用 BiliTool，很简单，按照下面教程部署完成，运行后扫码登录即可。**\n\n### 1.1. 部署 BiliTool\n\n支持多种部署方式，以下选择任一适合自己的方式即可。\n\n#### 1.1.1. 方案一：免费在线容器\n\n有很多平台会提供一定免费额度的在线容器，基于官方镜像，部署 BiliTool 很容易。\n\n以下以 ClawCloud 为例，其他平台操作类似：\n\n[>>ClawCloud 部署教程](docs/claw-cloud.md)\n\n#### 1.1.2. 方式二：青龙\n\n[>>青龙部署教程](qinglong/README.md)\n\n#### 1.1.3. 方式三：Docker 或 Podman 运行\n\n[>>Docker 部署说明](docker/README.md)\n\n[>>Podman 部署说明](podman/README.md)\n\n#### 1.1.4. 方式四：下载程序包到本地或服务器运行\n\n[>>本地部署说明](docs/runInLocal.md)\n\n#### 1.1.5. 方式五：Chart部署\n\n[>>Chart部署说明](helm/README.md)\n\n### 1.2. 消息推送（可选）\n\n如果配置了推送，执行成功后，指定的接收端会收到推送消息，推送效果如下所示：\n\n<p align=\"center\">\n    <img src=\"docs/imgs/push-tg.png\" alt=\"Telegram推送图示\" width=\"300\">\n</p>\n\n目前默认支持**Telegram推送、PushPlus推送、企业微信应用推送、企业微信推送、钉钉推送、Microsoft Teams推送、Server酱推送和酷推QQ推送**（以上顺序即为个人推荐的排序），如果需要推送到其他端，也可以配置为任意的可以接受消息的Api地址，关于如何配置推送请详见下面的**个性化自定义配置**章节。\n\n推送配置见：[confifuration](/docs/configuration.md)\n\n## 2. 功能任务说明\n\n这里的**任务**是指一组功能的集合，是工具每次运行的最小单位。\n\n任务列表如下：\n\n\n|    任务名     |      Code       |                                                功能                                                 |      推荐运行频率      |\n| :--------: | :-------------: | :-----------------------------------------------------------------------------------------------: | :--------------: |\n|    扫码登录    |      Login      |                   使用app扫码登录，用于第一次运行时初始化cookie，或cookie过期时的更新。不同平台会将cookie存储到不同地方                   |       手动         |\n|    每日任务    |      Daily      |                             完成每日任务获取满额65点经验（登录、观看视频、分享视频、投币），快速升级Lv6                              |       每天一次       |\n|   天选时刻抽奖   |   LiveLottery   |                                  直播中心天选时刻抽奖，大部分抽奖都需要关注主播，介意的不要开启                                  |      每天0-4次      |\n|    批量取关    | UnfollowBatched |                                 批量取关指定分组下的所有关注（主要用于清理天选抽奖而产生的关注）                                  |       手动运行       |\n|   大会员大积分   |   VipBigPoint   |                                        大会员大积分任务（签到、浏览、观看）                                         |    每天一次，建议凌晨     |\n|   直播间挂机    |  LiveFansMedal  |                                               直播间挂机                                               |       每天一次       |\n|    漫画任务    |      Manga      |                                              漫画签到、阅读                                              |       每天一次       |\n| 领取大会员漫画权益  | MangaPrivilege  |                                            领取大会员的漫画权益                                             |       每天一次       |\n|  银瓜子兑换硬币   |   Silver2Coin   |                                             使用银瓜子换取硬币                                             |       每天一次       |\n|  免费B币券充电   |     Charge      |                                 大会员每31天可免费领取一张5B币券，可用于给除自己以外的UP充电                                 |       每天一次       |\n|  领取大会员福利   |  VipPrivilege   |                                              领取大会员福利                                              |       每天一次       |\n|  测试Cookie  |      Test       |                                           测试Cookie是否正常                                            |       手动运行       |\n\n\n## 3. 个性化自定义配置\n\n[>>点击查看配置说明文档](docs/configuration.md)\n\n## 4. 多账号支持\n\n部署成功后，直接去运行扫码登录任务，扫码成功后，应用会自动更新或添加cookie。\n\n青龙平台会添加环境变量里，Key 为 `Ray_BiliBiliCookies__0`、`Ray_BiliBiliCookies__1`、`Ray_BiliBiliCookies__2`...\n\n其他平台默认会添加到名为cookies.json的账号配置文件中：\n```\n{\n  \"BiliBiliCookies\": [\n    \"cookie1\",\n    \"cookie2\",\n    \"...\",\n  ],\n}\n\n```\n\n## 5. 常见问题\n\n[>>点击查看常见问题文档](docs/questions.md)\n\n[Issues（议题）](https://github.com/RayWangQvQ/BiliBiliToolPro/issues)板块可以用来提交**Bug**和**建议**；\n\n[Discussions（讨论）](https://github.com/RayWangQvQ/BiliBiliToolPro/discussions)板块可以用来**提问**和**讨论**。\n\n大部分问题其实都可以在文档、议题和讨论中找到答案。\n\n所以如果你有疑问，\n\n* 请先确认是否可以通过升级到最新版本解决\n* 然后搜索文档（特别是配置说明文档和常见问题文档）、议题和讨论，查看是否已有其他人遇到相同问题、是否已有解决方案\n\n如果确认还未解决，可以自己提交 Issue，或发布 Discussions 与大家一起探讨，我会尽快确认并解决。\n\n（关于如何正确的提交Issue，请详见**常见问题文档**）。\n\n## 6. 版本发布及更新\n\n当前正处于稳定的迭代开发中，详细待更新和计划内容可参见 [Projects](https://github.com/RayWangQvQ/BiliBiliToolPro/projects) 和 [Issues](https://github.com/RayWangQvQ/BiliBiliToolPro/issues) 。\n\n想要有重要更新时收到通知的话，可以把仓库右上角的`Star`按钮点亮。\n\n## 7. 成为开源贡献成员\n\n### 7.1. 贡献代码\n\n如果你有好的想法，欢迎向仓库贡献你的代码，贡献步骤：\n\n* 搜索查看 Issue，确定是否已有人提过同类问题\n* 对于不确定的主题，为避免code结束后PR不被接受，可以先新建 Issue，描述问题或建议，讨论清楚后再动手编码\n* 如果确认自己可以解决，请 Fork 仓库后，在**develop 分支**进行编码开发，完成后**提交 PR 到 develop 分支**\n\n我会尽快进行代码审核，测试成功后会合并入 main 主分支，提前感谢您的贡献。\n\n### 7.2. 贡献文档\n\n文档部分由于我个人精力有限（写文档比写代码累多了），所以有些地方写的很简略，甚至有遗漏和错别字，不能贡献代码的朋友也欢迎来一起维护文档，欢迎 PR 来纠正我，一样都算是对开源做贡献了。\n\n## 8. 捐赠支持\n\n个人维护开源不易\n\n如果觉得我写的程序对你小有帮助\n\n或者，就是单纯的想集资给我买瓶霸王增发液\n\n那么下面的赞赏码可以扫一扫啦\n\n（赞赏时记得留下【昵称】和【留言】~ 另外我发现很多留言想要进群或者加好友的，一定一定要记得留下微信号哈，微信赞赏页面是看不到微信号的）\n\n**☟☟☟ 扫码自动赞赏 1 元：☟☟☟**\n\n![赞赏码](docs/imgs/donate.jpg)\n\n> 项目中的优先支持的UP主的配置项，默认是作者的 UpId （只是作为了 JSON 配置文件的默认值，代码是干净的），需要更改的话，直接修改相应配置即可（secrets或环境变量等各种方式都行）。\n当然，不改的话，也算是另一种捐赠支持作者的方式啦。\n\n感谢支持~\n\n## 9. 其他\n\n`API`参考：\n\n- [www.bilibili.com](https://www.bilibili.com/)\n\n- [SocialSisterYi/bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)\n\n- [JunzhouLiu/BILIBILI-HELPER](https://github.com/JunzhouLiu/BILIBILI-HELPER)\n\n❤️Thanks to `JetBrains` for the free certificate support:\n\n<p align=\"center\">\n    <img src=\"https://resources.jetbrains.com/storage/products/company/brand/logos/ReSharper.svg\" alt=\"ReSharper logo\" width=\"200\">\n</p>\n\n❤️Thanks to [YxVM](https://yxvm.com/aff.php?aff=668) & [NodeSeekDev](https://github.com/NodeSeekDev/NodeSupport) for sponsoring the server for testing support:\n\n<p align=\"center\">\n    <a href=\"https://yxvm.com/aff.php?aff=668\">\n        <img src=\"docs/imgs/node-support.png\" alt=\"YxVm logo\" width=\"200\">\n    </a>\n</p>\n\n❤️Thanks to [DartNode](https://dartnode.com?aff=FriskyGopher833) for sponsoring the server for testing support:\n\n[![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com \"Powered by DartNode - Free VPS for Open Source\")\n\n❤️Thank you for your star to this project:\n\n[![Star History Chart](https://api.star-history.com/svg?repos=RayWangQvQ/BiliBiliToolPro&type=Date)](https://www.star-history.com/#RayWangQvQ/BiliBiliToolPro&Date)\n"
  },
  {
    "path": "Ray.BiliBiliTool.sln",
    "content": "﻿\r\nMicrosoft Visual Studio Solution File, Format Version 12.00\r\n# Visual Studio Version 17\r\nVisualStudioVersion = 17.0.32112.339\r\nMinimumVisualStudioVersion = 10.0.40219.1\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"UI\", \"UI\", \"{38736647-2196-417E-8519-C48A012A63D9}\"\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"Application\", \"Application\", \"{98051127-2868-4F5C-9B2C-2150975E05F3}\"\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"Domain\", \"Domain\", \"{120917DC-474C-448B-9EBB-1B3BA3A20B9D}\"\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"src\", \"src\", \"{AF21E067-3307-4E7F-8CE8-C695E6B61876}\"\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"test\", \"test\", \"{E9BDDCBE-A57D-4E3B-8252-708088386ADF}\"\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ray.BiliBiliTool.Console\", \"src\\Ray.BiliBiliTool.Console\\Ray.BiliBiliTool.Console.csproj\", \"{DB227D60-0737-45C2-8CEA-F55FDA711301}\"\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"ConfigTest\", \"test\\ConfigTest\\ConfigTest.csproj\", \"{114D45C8-E4BB-47EE-89AC-BD1DC6FA3BAD}\"\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"LogTest\", \"test\\LogTest\\LogTest.csproj\", \"{2039BF6A-5EC4-439C-8D2E-77313075843A}\"\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"Infrastructure\", \"Infrastructure\", \"{110D3D21-8E9B-45AB-9667-6DA1DB3862AC}\"\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ray.BiliBiliTool.Infrastructure\", \"src\\Ray.BiliBiliTool.Infrastructure\\Ray.BiliBiliTool.Infrastructure.csproj\", \"{7188698C-0A9A-43B2-B3E2-5136B14FDE13}\"\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ray.BiliBiliTool.Application\", \"src\\Ray.BiliBiliTool.Application\\Ray.BiliBiliTool.Application.csproj\", \"{3388A58D-91CC-4875-A29F-3E6FC3B44BF5}\"\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ray.BiliBiliTool.Application.Contracts\", \"src\\Ray.BiliBiliTool.Application.Contracts\\Ray.BiliBiliTool.Application.Contracts.csproj\", \"{B6AEDD60-9C06-4391-9171-65EBD5E9D77A}\"\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ray.BiliBiliTool.Agent\", \"src\\Ray.BiliBiliTool.Agent\\Ray.BiliBiliTool.Agent.csproj\", \"{D5F9FBCE-31BE-4E80-93E2-183CF029431F}\"\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ray.BiliBiliTool.Config\", \"src\\Ray.BiliBiliTool.Config\\Ray.BiliBiliTool.Config.csproj\", \"{191C48BD-5CB5-42CA-B394-7A4A9BFA6275}\"\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ray.BiliBiliTool.DomainService\", \"src\\Ray.BiliBiliTool.DomainService\\Ray.BiliBiliTool.DomainService.csproj\", \"{7105652A-B1C1-4F9E-BA38-8034BC8B26B4}\"\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"Solution Items\", \"Solution Items\", \"{F3DE0D72-426B-4AD9-B3ED-3343CF4223F1}\"\r\n\tProjectSection(SolutionItems) = preProject\r\n\t\t.dockerignore = .dockerignore\r\n\t\t.editorconfig = .editorconfig\r\n\t\t.gitignore = .gitignore\r\n\t\tCHANGELOG.md = CHANGELOG.md\r\n\t\tcommon.props = common.props\r\n\t\tDockerfile = Dockerfile\r\n\t\tLICENSE = LICENSE\r\n\t\tREADME.md = README.md\r\n\tEndProjectSection\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \".github\", \".github\", \"{73DD457B-E06E-45ED-A6BA-7E3C02F8BDF1}\"\r\n\tProjectSection(SolutionItems) = preProject\r\n\t\t.github\\pull.yml = .github\\pull.yml\r\n\t\t.github\\PULL_REQUEST_TEMPLATE.md = .github\\PULL_REQUEST_TEMPLATE.md\r\n\tEndProjectSection\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"workflows\", \"workflows\", \"{61613EF4-3644-42D4-A620-70547291FB38}\"\r\n\tProjectSection(SolutionItems) = preProject\r\n\t\t.github\\workflows\\auto-close-pr.yml = .github\\workflows\\auto-close-pr.yml\r\n\t\t.github\\workflows\\auto-deploy-tencent-scf.yml = .github\\workflows\\auto-deploy-tencent-scf.yml\r\n\t\t.github\\workflows\\codeql-analysis.yml = .github\\workflows\\codeql-analysis.yml\r\n\t\t.github\\workflows\\publish-image.yml = .github\\workflows\\publish-image.yml\r\n\t\t.github\\workflows\\publish-release.yml = .github\\workflows\\publish-release.yml\r\n\t\t.github\\workflows\\repo-sync.yml = .github\\workflows\\repo-sync.yml\r\n\t\t.github\\workflows\\tag.yml = .github\\workflows\\tag.yml\r\n\tEndProjectSection\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"docker\", \"docker\", \"{A93210FD-27B6-40E4-B08D-391F96CA2754}\"\r\n\tProjectSection(SolutionItems) = preProject\r\n\t\tdocker\\README.md = docker\\README.md\r\n\tEndProjectSection\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"sample\", \"sample\", \"{2F1CB892-336C-4672-8A0A-FBAEB4B9EA8A}\"\r\n\tProjectSection(SolutionItems) = preProject\r\n\t\tdocker\\sample\\docker-compose.yml = docker\\sample\\docker-compose.yml\r\n\tEndProjectSection\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"docs\", \"docs\", \"{C0173851-1515-4BE1-A018-84E0B64A6877}\"\r\n\tProjectSection(SolutionItems) = preProject\r\n\t\tdocs\\configuration.md = docs\\configuration.md\r\n\t\tdocs\\donate-list.md = docs\\donate-list.md\r\n\t\tdocs\\questions.md = docs\\questions.md\r\n\tEndProjectSection\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"tencentScf\", \"tencentScf\", \"{DD86F293-AE70-46CF-837C-8870D8F51237}\"\r\n\tProjectSection(SolutionItems) = preProject\r\n\t\ttencentScf\\bootstrap = tencentScf\\bootstrap\r\n\t\ttencentScf\\index.sh = tencentScf\\index.sh\r\n\t\ttencentScf\\publish.bat = tencentScf\\publish.bat\r\n\t\ttencentScf\\publish.sh = tencentScf\\publish.sh\r\n\t\ttencentScf\\README.md = tencentScf\\README.md\r\n\t\ttencentScf\\serverless.yml = tencentScf\\serverless.yml\r\n\tEndProjectSection\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"qinglong\", \"qinglong\", \"{1C6CC38A-A5D5-41EF-9072-70AEEEA211F7}\"\r\n\tProjectSection(SolutionItems) = preProject\r\n\t\tqinglong\\dotnet-install.sh = qinglong\\dotnet-install.sh\r\n\t\tqinglong\\extra.sh = qinglong\\extra.sh\r\n\t\tqinglong\\ray-dotnet-install.sh = qinglong\\ray-dotnet-install.sh\r\n\t\tqinglong\\README.md = qinglong\\README.md\r\n\tEndProjectSection\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"DefaultTasks\", \"DefaultTasks\", \"{DE60A16C-CA3B-45E9-8A9D-0E91ACEBDEE0}\"\r\n\tProjectSection(SolutionItems) = preProject\r\n\t\tqinglong\\DefaultTasks\\bili_dev_task_daily.sh = qinglong\\DefaultTasks\\bili_dev_task_daily.sh\r\n\t\tqinglong\\DefaultTasks\\bili_dev_task_liveLottery.sh = qinglong\\DefaultTasks\\bili_dev_task_liveLottery.sh\r\n\t\tqinglong\\DefaultTasks\\bili_dev_task_test.sh = qinglong\\DefaultTasks\\bili_dev_task_test.sh\r\n\t\tqinglong\\DefaultTasks\\bili_dev_task_unfollowBatched.sh = qinglong\\DefaultTasks\\bili_dev_task_unfollowBatched.sh\r\n\t\tqinglong\\DefaultTasks\\bili_task_daily.sh = qinglong\\DefaultTasks\\bili_task_daily.sh\r\n\t\tqinglong\\DefaultTasks\\bili_task_liveLottery.sh = qinglong\\DefaultTasks\\bili_task_liveLottery.sh\r\n\t\tqinglong\\DefaultTasks\\bili_task_test.sh = qinglong\\DefaultTasks\\bili_task_test.sh\r\n\t\tqinglong\\DefaultTasks\\bili_task_unfollowBatched.sh = qinglong\\DefaultTasks\\bili_task_unfollowBatched.sh\r\n\tEndProjectSection\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"ISSUE_TEMPLATE\", \"ISSUE_TEMPLATE\", \"{830361B7-BCC1-4853-879A-761B0FD86826}\"\r\n\tProjectSection(SolutionItems) = preProject\r\n\t\t.github\\ISSUE_TEMPLATE\\bug-report----.md = .github\\ISSUE_TEMPLATE\\bug-report----.md\r\n\t\t.github\\ISSUE_TEMPLATE\\bug-report-qinglong----.md = .github\\ISSUE_TEMPLATE\\bug-report-qinglong----.md\r\n\t\t.github\\ISSUE_TEMPLATE\\feature-request----.md = .github\\ISSUE_TEMPLATE\\feature-request----.md\r\n\t\t.github\\ISSUE_TEMPLATE\\other----.md = .github\\ISSUE_TEMPLATE\\other----.md\r\n\tEndProjectSection\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"BiliAgentTest\", \"test\\BiliAgentTest\\BiliAgentTest.csproj\", \"{F6B8ED3A-5428-4D26-8172-8B41FBF0C4CF}\"\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"build\", \"build\", \"{75A9CC5C-DF92-4D72-A14C-625AA902855B}\"\r\n\tProjectSection(SolutionItems) = preProject\r\n\t\tdocker\\build\\buildImage.cmd = docker\\build\\buildImage.cmd\r\n\t\tdocker\\build\\buildImage_amd64.cmd = docker\\build\\buildImage_amd64.cmd\r\n\t\tdocker\\build\\buildImage_arm64.cmd = docker\\build\\buildImage_arm64.cmd\r\n\tEndProjectSection\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"AppServiceTest\", \"test\\AppServiceTest\\AppServiceTest.csproj\", \"{1D9DFF34-71EA-44AE-85B0-1F10C9BA0D81}\"\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"DomainServiceTest\", \"test\\DomainServiceTest\\DomainServiceTest.csproj\", \"{26B21C30-7358-4E7B-A73E-2272F10A6CA8}\"\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"InfrastructureTest\", \"test\\InfrastructureTest\\InfrastructureTest.csproj\", \"{90C1DB73-B3DB-4BE5-AD1A-5248FE47860E}\"\r\nEndProject\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Ray.BiliBiliTool.Agent.FunctionalTests\", \"test\\Ray.BiliBiliTool.Agent.FunctionalTests\\Ray.BiliBiliTool.Agent.FunctionalTests.csproj\", \"{16F315CF-056A-4B08-8C3C-A3177EA3CBB9}\"\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"scripts\", \"scripts\", \"{2B5FD099-CC28-4FBC-9F20-F20300C5DFD2}\"\r\n\tProjectSection(SolutionItems) = preProject\r\n\t\tscripts\\clean.cmd = scripts\\clean.cmd\r\n\t\tscripts\\publish.bat = scripts\\publish.bat\r\n\t\tscripts\\publish.ps1 = scripts\\publish.ps1\r\n\t\tscripts\\publish.sh = scripts\\publish.sh\r\n\t\tscripts\\ut.ps1 = scripts\\ut.ps1\r\n\tEndProjectSection\r\nEndProject\r\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ray.BiliBiliTool.Web.Client\", \"src\\Ray.BiliBiliTool.Web.Client\\Ray.BiliBiliTool.Web.Client.csproj\", \"{5772E00F-271F-4B25-8B10-A3C24D8D3E10}\"\r\nEndProject\r\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ray.BiliBiliTool.Web\", \"src\\Ray.BiliBiliTool.Web\\Ray.BiliBiliTool.Web.csproj\", \"{189F1FF4-2BFA-4F91-A6B2-00D00AC2910C}\"\r\nEndProject\r\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"BlazingQuartz.Core\", \"src\\BlazingQuartz.Core\\BlazingQuartz.Core.csproj\", \"{C947CA59-157C-47F0-A842-7912FACDADA7}\"\r\nEndProject\r\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"BlazingQuartz.Jobs.Abstractions\", \"src\\BlazingQuartz.Jobs.Abstractions\\BlazingQuartz.Jobs.Abstractions.csproj\", \"{5792B0A0-A3F5-461D-AD44-8E8778298BE4}\"\r\nEndProject\r\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"BlazingQuartz.Jobs\", \"src\\BlazingQuartz.Jobs\\BlazingQuartz.Jobs.csproj\", \"{1F93A755-60A6-4B14-A522-633A732F3B91}\"\r\nEndProject\r\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ray.BiliBiliTool.Domain\", \"src\\Ray.BiliBiliTool.Domain\\Ray.BiliBiliTool.Domain.csproj\", \"{C1334E67-CCF9-4F5B-9C1F-B9516A8270AB}\"\r\nEndProject\r\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Ray.BiliBiliTool.Infrastructure.EF\", \"src\\Ray.BiliBiliTool.Infrastructure.EF\\Ray.BiliBiliTool.Infrastructure.EF.csproj\", \"{59583288-CE93-42A4-AC9F-67DE347A02A1}\"\r\nEndProject\r\nGlobal\r\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\r\n\t\tDebug|Any CPU = Debug|Any CPU\r\n\t\tRelease|Any CPU = Release|Any CPU\r\n\tEndGlobalSection\r\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\r\n\t\t{DB227D60-0737-45C2-8CEA-F55FDA711301}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{DB227D60-0737-45C2-8CEA-F55FDA711301}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{DB227D60-0737-45C2-8CEA-F55FDA711301}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{DB227D60-0737-45C2-8CEA-F55FDA711301}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{114D45C8-E4BB-47EE-89AC-BD1DC6FA3BAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{114D45C8-E4BB-47EE-89AC-BD1DC6FA3BAD}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{114D45C8-E4BB-47EE-89AC-BD1DC6FA3BAD}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{114D45C8-E4BB-47EE-89AC-BD1DC6FA3BAD}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{2039BF6A-5EC4-439C-8D2E-77313075843A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{2039BF6A-5EC4-439C-8D2E-77313075843A}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{2039BF6A-5EC4-439C-8D2E-77313075843A}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{2039BF6A-5EC4-439C-8D2E-77313075843A}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{7188698C-0A9A-43B2-B3E2-5136B14FDE13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{7188698C-0A9A-43B2-B3E2-5136B14FDE13}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{7188698C-0A9A-43B2-B3E2-5136B14FDE13}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{7188698C-0A9A-43B2-B3E2-5136B14FDE13}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{3388A58D-91CC-4875-A29F-3E6FC3B44BF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{3388A58D-91CC-4875-A29F-3E6FC3B44BF5}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{3388A58D-91CC-4875-A29F-3E6FC3B44BF5}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{3388A58D-91CC-4875-A29F-3E6FC3B44BF5}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{B6AEDD60-9C06-4391-9171-65EBD5E9D77A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{B6AEDD60-9C06-4391-9171-65EBD5E9D77A}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{B6AEDD60-9C06-4391-9171-65EBD5E9D77A}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{B6AEDD60-9C06-4391-9171-65EBD5E9D77A}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{D5F9FBCE-31BE-4E80-93E2-183CF029431F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{D5F9FBCE-31BE-4E80-93E2-183CF029431F}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{D5F9FBCE-31BE-4E80-93E2-183CF029431F}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{D5F9FBCE-31BE-4E80-93E2-183CF029431F}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{191C48BD-5CB5-42CA-B394-7A4A9BFA6275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{191C48BD-5CB5-42CA-B394-7A4A9BFA6275}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{191C48BD-5CB5-42CA-B394-7A4A9BFA6275}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{191C48BD-5CB5-42CA-B394-7A4A9BFA6275}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{7105652A-B1C1-4F9E-BA38-8034BC8B26B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{7105652A-B1C1-4F9E-BA38-8034BC8B26B4}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{7105652A-B1C1-4F9E-BA38-8034BC8B26B4}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{7105652A-B1C1-4F9E-BA38-8034BC8B26B4}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{F6B8ED3A-5428-4D26-8172-8B41FBF0C4CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{F6B8ED3A-5428-4D26-8172-8B41FBF0C4CF}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{F6B8ED3A-5428-4D26-8172-8B41FBF0C4CF}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{F6B8ED3A-5428-4D26-8172-8B41FBF0C4CF}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{1D9DFF34-71EA-44AE-85B0-1F10C9BA0D81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{1D9DFF34-71EA-44AE-85B0-1F10C9BA0D81}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{1D9DFF34-71EA-44AE-85B0-1F10C9BA0D81}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{1D9DFF34-71EA-44AE-85B0-1F10C9BA0D81}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{26B21C30-7358-4E7B-A73E-2272F10A6CA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{26B21C30-7358-4E7B-A73E-2272F10A6CA8}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{26B21C30-7358-4E7B-A73E-2272F10A6CA8}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{26B21C30-7358-4E7B-A73E-2272F10A6CA8}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{90C1DB73-B3DB-4BE5-AD1A-5248FE47860E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{90C1DB73-B3DB-4BE5-AD1A-5248FE47860E}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{90C1DB73-B3DB-4BE5-AD1A-5248FE47860E}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{90C1DB73-B3DB-4BE5-AD1A-5248FE47860E}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{16F315CF-056A-4B08-8C3C-A3177EA3CBB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{16F315CF-056A-4B08-8C3C-A3177EA3CBB9}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{16F315CF-056A-4B08-8C3C-A3177EA3CBB9}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{16F315CF-056A-4B08-8C3C-A3177EA3CBB9}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{5772E00F-271F-4B25-8B10-A3C24D8D3E10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{5772E00F-271F-4B25-8B10-A3C24D8D3E10}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{5772E00F-271F-4B25-8B10-A3C24D8D3E10}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{5772E00F-271F-4B25-8B10-A3C24D8D3E10}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{189F1FF4-2BFA-4F91-A6B2-00D00AC2910C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{189F1FF4-2BFA-4F91-A6B2-00D00AC2910C}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{189F1FF4-2BFA-4F91-A6B2-00D00AC2910C}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{189F1FF4-2BFA-4F91-A6B2-00D00AC2910C}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{C947CA59-157C-47F0-A842-7912FACDADA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{C947CA59-157C-47F0-A842-7912FACDADA7}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{C947CA59-157C-47F0-A842-7912FACDADA7}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{C947CA59-157C-47F0-A842-7912FACDADA7}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{5792B0A0-A3F5-461D-AD44-8E8778298BE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{5792B0A0-A3F5-461D-AD44-8E8778298BE4}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{5792B0A0-A3F5-461D-AD44-8E8778298BE4}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{5792B0A0-A3F5-461D-AD44-8E8778298BE4}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{1F93A755-60A6-4B14-A522-633A732F3B91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{1F93A755-60A6-4B14-A522-633A732F3B91}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{1F93A755-60A6-4B14-A522-633A732F3B91}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{1F93A755-60A6-4B14-A522-633A732F3B91}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{C1334E67-CCF9-4F5B-9C1F-B9516A8270AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{C1334E67-CCF9-4F5B-9C1F-B9516A8270AB}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{C1334E67-CCF9-4F5B-9C1F-B9516A8270AB}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{C1334E67-CCF9-4F5B-9C1F-B9516A8270AB}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{59583288-CE93-42A4-AC9F-67DE347A02A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{59583288-CE93-42A4-AC9F-67DE347A02A1}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{59583288-CE93-42A4-AC9F-67DE347A02A1}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{59583288-CE93-42A4-AC9F-67DE347A02A1}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\tEndGlobalSection\r\n\tGlobalSection(SolutionProperties) = preSolution\r\n\t\tHideSolutionNode = FALSE\r\n\tEndGlobalSection\r\n\tGlobalSection(NestedProjects) = preSolution\r\n\t\t{38736647-2196-417E-8519-C48A012A63D9} = {AF21E067-3307-4E7F-8CE8-C695E6B61876}\r\n\t\t{98051127-2868-4F5C-9B2C-2150975E05F3} = {AF21E067-3307-4E7F-8CE8-C695E6B61876}\r\n\t\t{120917DC-474C-448B-9EBB-1B3BA3A20B9D} = {AF21E067-3307-4E7F-8CE8-C695E6B61876}\r\n\t\t{DB227D60-0737-45C2-8CEA-F55FDA711301} = {38736647-2196-417E-8519-C48A012A63D9}\r\n\t\t{114D45C8-E4BB-47EE-89AC-BD1DC6FA3BAD} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF}\r\n\t\t{2039BF6A-5EC4-439C-8D2E-77313075843A} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF}\r\n\t\t{110D3D21-8E9B-45AB-9667-6DA1DB3862AC} = {AF21E067-3307-4E7F-8CE8-C695E6B61876}\r\n\t\t{7188698C-0A9A-43B2-B3E2-5136B14FDE13} = {110D3D21-8E9B-45AB-9667-6DA1DB3862AC}\r\n\t\t{3388A58D-91CC-4875-A29F-3E6FC3B44BF5} = {98051127-2868-4F5C-9B2C-2150975E05F3}\r\n\t\t{B6AEDD60-9C06-4391-9171-65EBD5E9D77A} = {98051127-2868-4F5C-9B2C-2150975E05F3}\r\n\t\t{D5F9FBCE-31BE-4E80-93E2-183CF029431F} = {120917DC-474C-448B-9EBB-1B3BA3A20B9D}\r\n\t\t{191C48BD-5CB5-42CA-B394-7A4A9BFA6275} = {120917DC-474C-448B-9EBB-1B3BA3A20B9D}\r\n\t\t{7105652A-B1C1-4F9E-BA38-8034BC8B26B4} = {120917DC-474C-448B-9EBB-1B3BA3A20B9D}\r\n\t\t{73DD457B-E06E-45ED-A6BA-7E3C02F8BDF1} = {F3DE0D72-426B-4AD9-B3ED-3343CF4223F1}\r\n\t\t{61613EF4-3644-42D4-A620-70547291FB38} = {73DD457B-E06E-45ED-A6BA-7E3C02F8BDF1}\r\n\t\t{A93210FD-27B6-40E4-B08D-391F96CA2754} = {F3DE0D72-426B-4AD9-B3ED-3343CF4223F1}\r\n\t\t{2F1CB892-336C-4672-8A0A-FBAEB4B9EA8A} = {A93210FD-27B6-40E4-B08D-391F96CA2754}\r\n\t\t{DD86F293-AE70-46CF-837C-8870D8F51237} = {F3DE0D72-426B-4AD9-B3ED-3343CF4223F1}\r\n\t\t{1C6CC38A-A5D5-41EF-9072-70AEEEA211F7} = {F3DE0D72-426B-4AD9-B3ED-3343CF4223F1}\r\n\t\t{DE60A16C-CA3B-45E9-8A9D-0E91ACEBDEE0} = {1C6CC38A-A5D5-41EF-9072-70AEEEA211F7}\r\n\t\t{830361B7-BCC1-4853-879A-761B0FD86826} = {73DD457B-E06E-45ED-A6BA-7E3C02F8BDF1}\r\n\t\t{F6B8ED3A-5428-4D26-8172-8B41FBF0C4CF} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF}\r\n\t\t{75A9CC5C-DF92-4D72-A14C-625AA902855B} = {A93210FD-27B6-40E4-B08D-391F96CA2754}\r\n\t\t{1D9DFF34-71EA-44AE-85B0-1F10C9BA0D81} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF}\r\n\t\t{26B21C30-7358-4E7B-A73E-2272F10A6CA8} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF}\r\n\t\t{90C1DB73-B3DB-4BE5-AD1A-5248FE47860E} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF}\r\n\t\t{16F315CF-056A-4B08-8C3C-A3177EA3CBB9} = {E9BDDCBE-A57D-4E3B-8252-708088386ADF}\r\n\t\t{2B5FD099-CC28-4FBC-9F20-F20300C5DFD2} = {F3DE0D72-426B-4AD9-B3ED-3343CF4223F1}\r\n\t\t{5772E00F-271F-4B25-8B10-A3C24D8D3E10} = {38736647-2196-417E-8519-C48A012A63D9}\r\n\t\t{189F1FF4-2BFA-4F91-A6B2-00D00AC2910C} = {38736647-2196-417E-8519-C48A012A63D9}\r\n\t\t{C947CA59-157C-47F0-A842-7912FACDADA7} = {98051127-2868-4F5C-9B2C-2150975E05F3}\r\n\t\t{5792B0A0-A3F5-461D-AD44-8E8778298BE4} = {98051127-2868-4F5C-9B2C-2150975E05F3}\r\n\t\t{1F93A755-60A6-4B14-A522-633A732F3B91} = {98051127-2868-4F5C-9B2C-2150975E05F3}\r\n\t\t{C1334E67-CCF9-4F5B-9C1F-B9516A8270AB} = {120917DC-474C-448B-9EBB-1B3BA3A20B9D}\r\n\t\t{59583288-CE93-42A4-AC9F-67DE347A02A1} = {110D3D21-8E9B-45AB-9667-6DA1DB3862AC}\r\n\tEndGlobalSection\r\n\tGlobalSection(ExtensibilityGlobals) = postSolution\r\n\t\tSolutionGuid = {197319DA-1148-4A99-847C-8B270B6A29AB}\r\n\tEndGlobalSection\r\nEndGlobal\r\n"
  },
  {
    "path": "bruno/api.bilibili.com/x/space/folder.bru",
    "content": "meta {\n  name: space\n}\n"
  },
  {
    "path": "bruno/api.bilibili.com/x/space/wbi-acc-info.bru",
    "content": "meta {\n  name: wbi-acc-info\n  type: http\n  seq: 1\n}\n\nget {\n  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\n  body: none\n  auth: inherit\n}\n\nparams:query {\n  mid: 23947287\n  token: \n  platform: web\n  web_location: 1550101\n  dm_img_list: []\n  dm_img_str: V2ViR0wgMS4wIChPcGVuR0wgRVMgMi4wIENocm9taXVtKQ\n  dm_cover_img_str: QU5HTEUgKEFNRCwgQU1EIFJhZGVvbihUTSkgR3JhcGhpY3MgKDB4MDAwMDE2MzgpIERpcmVjdDNEMTEgdnNfNV8wIHBzXzVfMCwgRDNEMTEpR29vZ2xlIEluYy4gKEFNRC\n  dm_img_inter: {\"ds\":[],\"wh\":[5563,4171,107],\"of\":[66,132,66]}\n  w_webid: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzcG1faWQiOiIzMzMuMTM4NyIsImJ1dmlkIjoiQ0VGODNDQjAtMzY2NC0zNDU4LTI5RTctRTJFOENCQjY0NzlCNjU2MjFpbmZvYyIsInVzZXJfYWdlbnQiOiJNb3ppbGxhLzUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW42NDsgeDY0KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvMTM3LjAuMC4wIFNhZmFyaS81MzcuMzYgRWRnLzEzNy4wLjAuMCIsImJ1dmlkX2ZwIjoiMDNjNGYyNWQyZTlmMDZkNjU4NzJlMmVhNTdiMjJmMzkiLCJiaWxpX3RpY2tldCI6ImV5SmhiR2NpT2lKSVV6STFOaUlzSW10cFpDSTZJbk13TXlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKbGVIQWlPakUzTkRjM01qYzVORGdzSW1saGRDSTZNVGMwTnpRMk9EWTRPQ3dpY0d4MElqb3RNWDAuQmVkeFFPX3VZMllldFVFb0FvWE5hellBNzFTTkdhc0JtSkNnaFFsVy1iTSIsImNyZWF0ZWRfYXQiOjE3NDc0Njg4NDEsInR0bCI6ODY0MDAsInVybCI6Ii8yMzk0NzI4NyIsInJlc3VsdCI6MCwiaXNzIjoiZ2FpYSIsImlhdCI6MTc0NzQ2ODg0MX0.IhLw1v9Auwys4SFZ7PFaTXgkVqodmz67ZdVPORbnmcw3rxirilDqngVFpr3AYjUQ8hx-gmGTrMgeus12QE4zn9ql-wjTRKzbi9G2WEzDKEwHMNBKVIepaSZqHYIbKBwWsjqdEL9paDeRDpPiZ37YJ4YMKNsFfEP2yC_4ke2_KS2cgfSaaOhaXtXfEZortr_KvKWhY3gtJKC77HqEerZEID5hda8oqCTxZbb7gV7DDuncJ803K9H5ezWn-8a-y3eSdpbBB8Td5-u8At9mhHVgrKcODM7gi-Lhbnb86m7p4yyBoPs_iucDQJSGvs9Lijio57bL60Qm6_mIz0p49rzFWA\n  w_rid: 160dcf0623b0733688e8940d2f6763e3\n  wts: 1747468845\n}\n\nheaders {\n  accept: */*\n  accept-language: en\n  dnt: 1\n  origin: https://space.bilibili.com\n  priority: u=1, i\n  referer: https://space.bilibili.com/23947287\n  sec-ch-ua: \"Microsoft Edge\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\"\n  sec-ch-ua-mobile: ?0\n  sec-ch-ua-platform: \"Windows\"\n  sec-fetch-dest: empty\n  sec-fetch-mode: cors\n  sec-fetch-site: same-site\n  user-agent: {{user-agent}}\n  Cookie: {{cookieStr}}\n}\n"
  },
  {
    "path": "bruno/api.bilibili.com/x/vip/folder.bru",
    "content": "meta {\n  name: vip\n}\n"
  },
  {
    "path": "bruno/api.bilibili.com/x/vip/vip_center/folder.bru",
    "content": "meta {\n  name: vip_center\n}\n"
  },
  {
    "path": "bruno/api.bilibili.com/x/vip/vip_center/sign_in/folder.bru",
    "content": "meta {\n  name: sign_in\n}\n"
  },
  {
    "path": "bruno/api.bilibili.com/x/vip/vip_center/sign_in/three_days_sign.bru",
    "content": "meta {\n  name: three_days_sign\n  type: http\n  seq: 1\n}\n\nget {\n  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\n  body: none\n  auth: inherit\n}\n\nparams:query {\n  access_key: {{access_key}}\n  appkey: {{appKey}}\n  build: 8451100\n  csrf: {{csrf}}\n  device: phone\n  disable_rcmd: 0\n  mobi_app: android\n  platform: android\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"8.45.1\",\"abtest\":\"\"}\n  t: 1748747431084\n  ts: 1748747431\n  web_location: 666.146\n  sign: e95c76f976aed346f84cdc7098f8c35e\n}\n\nheaders {\n  Host: api.bilibili.com\n  Cookie: {{cookieStr}}\n  accept: application/json, text/plain, */*\n  bili-http-engine: ignet\n  buvid: {{buvid}}\n  native_api_from: h5\n  referer: https://big.bilibili.com/mobile/index\n  user-agent: {{user-agent}}\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-metadata-legal-region: CN\n  x-bili-mid: {{mid}}\n  x-bili-net-bin: DQAAgL8gAQ\n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDg3NzU1OTksImlhdCI6MTc0ODc0NjQ5OSwiYnV2aWQiOiJYVUE1NjUxQTlFREY3Mzg3MTUzQTk0NUNERTk2Q0FEQ0I2MDAwIn0.k0x2o3e2Q3W-6Wzc56IhbLgSjDKTaAuUV9om7K213fI\n  x-bili-trace-id: 25ad3d0de58f794c18d1a884fc683bc4:18d1a884fc683bc4:0:0\n}\n\ndocs {\n  Response sample:\n  \n  ```json\n  {\n  \t\"code\": 0,\n  \t\"message\": \"0\",\n  \t\"ttl\": 1,\n  \t\"data\": {\n  \t\t\"big_point\": {\n  \t\t\t\"point\": 405,\n  \t\t\t\"expire_point\": 0,\n  \t\t\t\"expire_time\": 0,\n  \t\t\t\"expire_days\": 0\n  \t\t},\n  \t\t\"three_day_sign\": {\n  \t\t\t\"previous_vip_status\": 0,\n  \t\t\t\"vip_status\": 1,\n  \t\t\t\"day\": 5,\n  \t\t\t\"signed\": false,\n  \t\t\t\"count\": 2,\n  \t\t\t\"has_coupon\": false,\n  \t\t\t\"countdown\": 0,\n  \t\t\t\"icon\": \"\",\n  \t\t\t\"score\": 5,\n  \t\t\t\"vip_score\": 5,\n  \t\t\t\"explain\": \"\",\n  \t\t\t\"exp_value\": 3,\n  \t\t\t\"received_coupon\": false,\n  \t\t\t\"day1_icon\": \"https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day1_icon.png\",\n  \t\t\t\"day2_icon\": \"https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day2_icon.png\",\n  \t\t\t\"day3_icon\": \"https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_icon.png\",\n  \t\t\t\"day3_icon_vip\": \"https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_icon_vip.png\",\n  \t\t\t\"day3_win_img\": \"https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_win_img.png\",\n  \t\t\t\"day3_win_img_vip\": \"https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_win_img_vip.png\",\n  \t\t\t\"day3_icon_received\": \"https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_icon_received.png\",\n  \t\t\t\"day3_icon_vip_received\": \"https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_icon_vip_received.png\",\n  \t\t\t\"duration\": 7\n  \t\t}\n  \t}\n  }\n  ```\n}\n"
  },
  {
    "path": "bruno/api.bilibili.com/x/vip/web/folder.bru",
    "content": "meta {\n  name: web\n}\n"
  },
  {
    "path": "bruno/api.bilibili.com/x/vip/web/vip_center/folder.bru",
    "content": "meta {\n  name: vip_center\n}\n"
  },
  {
    "path": "bruno/api.bilibili.com/x/vip/web/vip_center/modules.bru",
    "content": "meta {\n  name: modules\n  type: http\n  seq: 2\n}\n\nget {\n  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\n  body: none\n  auth: inherit\n}\n\nparams:query {\n  access_key: {{access_key}}\n  act_id: 872\n  appkey: {{appKey}}\n  build: 8451100\n  csrf: {{csrf}}\n  device: phone\n  disable_rcmd: 0\n  is_selected: true\n  mobi_app: android\n  platform: android\n  select_modules: VipExclusive,BigPoint\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"8.45.1\",\"abtest\":\"\"}\n  ts: 1748751549\n  web_location: 666.146\n  sign: 4b1610076d36f3eaf400307591fce0d0\n}\n\nheaders {\n  Host: api.bilibili.com\n  Cookie: {{cookieStr}}\n  accept: application/json, text/plain, */*\n  bili-http-engine: ignet\n  buvid: {{buvid}}\n  native_api_from: h5\n  referer: https://big.bilibili.com/mobile/index\n  user-agent: {{user-agent}}\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-metadata-legal-region: CN\n  x-bili-mid: {{mid}}\n  x-bili-net-bin: DQAAgL8gAQ\n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDg3NzU1OTksImlhdCI6MTc0ODc0NjQ5OSwiYnV2aWQiOiJYVUE1NjUxQTlFREY3Mzg3MTUzQTk0NUNERTk2Q0FEQ0I2MDAwIn0.k0x2o3e2Q3W-6Wzc56IhbLgSjDKTaAuUV9om7K213fI\n  x-bili-trace-id: 32b84b9079d40a0946714475fc683bd4:46714475fc683bd4:0:0\n}\n\ndocs {\n  Response sample:\n  \n  ```json\n  {\n  \t\"code\": 0,\n  \t\"message\": \"0\",\n  \t\"ttl\": 1,\n  \t\"data\": {\n  \t\t\"privileges\": {\n  \t\t\t\"list\": [],\n  \t\t\t\"privileges_covers\": [],\n  \t\t\t\"num\": 0\n  \t\t},\n  \t\t\"banner\": [],\n  \t\t\"union_vip\": {\n  \t\t\t\"union_vips\": []\n  \t\t},\n  \t\t\"other_open_info\": {\n  \t\t\t\"open_infos\": []\n  \t\t},\n  \t\t\"benefits\": [],\n  \t\t\"big_point\": {\n  \t\t\t\"point_info\": {\n  \t\t\t\t\"point\": 405,\n  \t\t\t\t\"expire_point\": 0,\n  \t\t\t\t\"expire_time\": 0,\n  \t\t\t\t\"expire_days\": 0\n  \t\t\t},\n  \t\t\t\"sign_info\": {\n  \t\t\t\t\"sign_remind\": true,\n  \t\t\t\t\"benefit\": 10,\n  \t\t\t\t\"bonus_benefit\": 0,\n  \t\t\t\t\"normal_remind\": true,\n  \t\t\t\t\"muggle_task\": false,\n  \t\t\t\t\"exp_value\": 3\n  \t\t\t},\n  \t\t\t\"sku_info\": {\n  \t\t\t\t\"skus\": [{\n  \t\t\t\t\t\"base\": {\n  \t\t\t\t\t\t\"token\": \"1216669149548369495\",\n  \t\t\t\t\t\t\"title\": \"东航国内机票立减50元优惠券\",\n  \t\t\t\t\t\t\"picture\": \"https://i0.hdslb.com/bfs/activity-plat/58e67cefd0a194b6380d09749ea6a9b6bbd18a84.png\",\n  \t\t\t\t\t\t\"rotation_pictures\": [\"https://i0.hdslb.com/bfs/activity-plat/58e67cefd0a194b6380d09749ea6a9b6bbd18a84.png\"],\n  \t\t\t\t\t\t\"price\": {\n  \t\t\t\t\t\t\t\"origin\": 12999,\n  \t\t\t\t\t\t\t\"promotion\": {\n  \t\t\t\t\t\t\t\t\"price\": 399,\n  \t\t\t\t\t\t\t\t\"type\": 2,\n  \t\t\t\t\t\t\t\t\"discount\": 0,\n  \t\t\t\t\t\t\t\t\"label\": \"秒杀\"\n  \t\t\t\t\t\t\t},\n  \t\t\t\t\t\t\t\"sale\": 12999\n  \t\t\t\t\t\t},\n  \t\t\t\t\t\t\"inventory\": {\n  \t\t\t\t\t\t\t\"available_num\": 2980,\n  \t\t\t\t\t\t\t\"used_num\": 2150,\n  \t\t\t\t\t\t\t\"surplus_num\": 830,\n  \t\t\t\t\t\t\t\"total_num\": 5980,\n  \t\t\t\t\t\t\t\"is_sold_out\": false,\n  \t\t\t\t\t\t\t\"next_sold_time\": 1748836800,\n  \t\t\t\t\t\t\t\"status\": 1\n  \t\t\t\t\t\t},\n  \t\t\t\t\t\t\"user_type\": 2,\n  \t\t\t\t\t\t\"exchange_limit_type\": 2,\n  \t\t\t\t\t\t\"exchange_limit_num\": 1,\n  \t\t\t\t\t\t\"start_time\": 1748593800,\n  \t\t\t\t\t\t\"end_time\": 1750262400,\n  \t\t\t\t\t\t\"state\": 2,\n  \t\t\t\t\t\t\"priority\": 97\n  \t\t\t\t\t}\n  \t\t\t\t}, {\n  \t\t\t\t\t\"base\": {\n  \t\t\t\t\t\t\"token\": \"1214802504886365529\",\n  \t\t\t\t\t\t\"title\": \"BEMOE 初音未来 樱花未来 可爱体UWA系列 毛绒4wa\",\n  \t\t\t\t\t\t\"picture\": \"https://i0.hdslb.com/bfs/activity-plat/b782d7228e8a58d2562d26f33448a50519ce4ec5.png\",\n  \t\t\t\t\t\t\"rotation_pictures\": [\"https://i0.hdslb.com/bfs/activity-plat/b782d7228e8a58d2562d26f33448a50519ce4ec5.png\"],\n  \t\t\t\t\t\t\"price\": {\n  \t\t\t\t\t\t\t\"origin\": 33600,\n  \t\t\t\t\t\t\t\"promotion\": null,\n  \t\t\t\t\t\t\t\"sale\": 33600\n  \t\t\t\t\t\t},\n  \t\t\t\t\t\t\"inventory\": {\n  \t\t\t\t\t\t\t\"available_num\": 1,\n  \t\t\t\t\t\t\t\"used_num\": 0,\n  \t\t\t\t\t\t\t\"surplus_num\": 1,\n  \t\t\t\t\t\t\t\"total_num\": 20,\n  \t\t\t\t\t\t\t\"is_sold_out\": false,\n  \t\t\t\t\t\t\t\"next_sold_time\": -62135596800,\n  \t\t\t\t\t\t\t\"status\": 1\n  \t\t\t\t\t\t},\n  \t\t\t\t\t\t\"user_type\": 2,\n  \t\t\t\t\t\t\"exchange_limit_type\": 2,\n  \t\t\t\t\t\t\"exchange_limit_num\": 1,\n  \t\t\t\t\t\t\"start_time\": 1748404800,\n  \t\t\t\t\t\t\"end_time\": 1751256000,\n  \t\t\t\t\t\t\"state\": 2,\n  \t\t\t\t\t\t\"priority\": 96\n  \t\t\t\t\t}\n  \t\t\t\t}, {\n  \t\t\t\t\t\"base\": {\n  \t\t\t\t\t\t\"token\": \"1214800503834258777\",\n  \t\t\t\t\t\t\"title\": \"BEMOE 初音未来 UWA可爱体系列 亚克力立牌 呜哇满足\",\n  \t\t\t\t\t\t\"picture\": \"https://i0.hdslb.com/bfs/activity-plat/186fa29b28e671422010bf9d8bd0f5e6b46556b9.png\",\n  \t\t\t\t\t\t\"rotation_pictures\": [\"https://i0.hdslb.com/bfs/activity-plat/186fa29b28e671422010bf9d8bd0f5e6b46556b9.png\"],\n  \t\t\t\t\t\t\"price\": {\n  \t\t\t\t\t\t\t\"origin\": 23333,\n  \t\t\t\t\t\t\t\"promotion\": null,\n  \t\t\t\t\t\t\t\"sale\": 23333\n  \t\t\t\t\t\t},\n  \t\t\t\t\t\t\"inventory\": {\n  \t\t\t\t\t\t\t\"available_num\": 3,\n  \t\t\t\t\t\t\t\"used_num\": 2,\n  \t\t\t\t\t\t\t\"surplus_num\": 1,\n  \t\t\t\t\t\t\t\"total_num\": 35,\n  \t\t\t\t\t\t\t\"is_sold_out\": false,\n  \t\t\t\t\t\t\t\"next_sold_time\": -62135596800,\n  \t\t\t\t\t\t\t\"status\": 1\n  \t\t\t\t\t\t},\n  \t\t\t\t\t\t\"user_type\": 2,\n  \t\t\t\t\t\t\"exchange_limit_type\": 2,\n  \t\t\t\t\t\t\"exchange_limit_num\": 1,\n  \t\t\t\t\t\t\"start_time\": 1748404800,\n  \t\t\t\t\t\t\"end_time\": 1751256000,\n  \t\t\t\t\t\t\"state\": 2,\n  \t\t\t\t\t\t\"priority\": 96\n  \t\t\t\t\t}\n  \t\t\t\t}, {\n  \t\t\t\t\t\"base\": {\n  \t\t\t\t\t\t\"token\": \"1214791752167304537\",\n  \t\t\t\t\t\t\"title\": \"暴蒙《蓝色监狱》烫金KV徽章洁世一\",\n  \t\t\t\t\t\t\"picture\": \"https://i0.hdslb.com/bfs/activity-plat/a23b188dcc430c4348b8cccb7ab056bc5527a67d.png\",\n  \t\t\t\t\t\t\"rotation_pictures\": [\"https://i0.hdslb.com/bfs/activity-plat/a23b188dcc430c4348b8cccb7ab056bc5527a67d.png\"],\n  \t\t\t\t\t\t\"price\": {\n  \t\t\t\t\t\t\t\"origin\": 3900,\n  \t\t\t\t\t\t\t\"promotion\": null,\n  \t\t\t\t\t\t\t\"sale\": 3900\n  \t\t\t\t\t\t},\n  \t\t\t\t\t\t\"inventory\": {\n  \t\t\t\t\t\t\t\"available_num\": 5,\n  \t\t\t\t\t\t\t\"used_num\": 4,\n  \t\t\t\t\t\t\t\"surplus_num\": 1,\n  \t\t\t\t\t\t\t\"total_num\": 10,\n  \t\t\t\t\t\t\t\"is_sold_out\": false,\n  \t\t\t\t\t\t\t\"next_sold_time\": -62135596800,\n  \t\t\t\t\t\t\t\"status\": 1\n  \t\t\t\t\t\t},\n  \t\t\t\t\t\t\"user_type\": 2,\n  \t\t\t\t\t\t\"exchange_limit_type\": 2,\n  \t\t\t\t\t\t\"exchange_limit_num\": 1,\n  \t\t\t\t\t\t\"start_time\": 1748404800,\n  \t\t\t\t\t\t\"end_time\": 1751256000,\n  \t\t\t\t\t\t\"state\": 2,\n  \t\t\t\t\t\t\"priority\": 96\n  \t\t\t\t\t}\n  \t\t\t\t}]\n  \t\t\t},\n  \t\t\t\"point_switch_off\": false,\n  \t\t\t\"tips\": [{\n  \t\t\t\t\"content\": \"今日签到10大积分\"\n  \t\t\t}, {\n  \t\t\t\t\"content\": \"今天的任务还没有做完哦\"\n  \t\t\t}],\n  \t\t\t\"button_text\": \"赚大积分\",\n  \t\t\t\"sku_price_hidden\": false\n  \t\t},\n  \t\t\"welfare\": {},\n  \t\t\"experience\": {\n  \t\t\t\"level\": 0,\n  \t\t\t\"cur_exp\": 0,\n  \t\t\t\"next_exp\": 0,\n  \t\t\t\"is_senior_member\": 0,\n  \t\t\t\"is_get_exp\": false,\n  \t\t\t\"is_task_complete\": false,\n  \t\t\t\"state\": 0\n  \t\t},\n  \t\t\"vip_exclusive\": [{\n  \t\t\t\"tap_title\": \"猜你喜欢\",\n  \t\t\t\"tap_type\": 7,\n  \t\t\t\"seasons\": [{\n  \t\t\t\t\"season_id\": 48006,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"逆行人生\",\n  \t\t\t\t\"sub_title\": \"徐峥现实主义喜剧\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/9d2e5c77a7f9a665ca8349d45d283d1bc68d805b.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48006?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39533,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.3\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"007：大破量子危机\",\n  \t\t\t\t\"sub_title\": \"邦德为爱复仇\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/742a56d34b61e5d29ea413a78d0540d32681ac73.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39533?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 43361,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"纪录片\",\n  \t\t\t\t\"title\": \"挤痘大师 第2季\",\n  \t\t\t\t\"sub_title\": \"皮肤疑难杂症的解决\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/1d6fe011a5d474a32eeb1ddb250e17de3b69c459.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss43361\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 44560,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"太极张三丰\",\n  \t\t\t\t\"sub_title\": \"少年练就武林奇功\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/e8e77dc7bbc716a5159358d311cde5c2cdfddba9.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss44560?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 44175,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"纪录片\",\n  \t\t\t\t\"title\": \"挤痘大师 第3季\",\n  \t\t\t\t\"sub_title\": \"皮肤疑难杂症的解决\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/f8df75ad1d7553a4fd26c1b0d4548053c4d3e617.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss44175\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39247,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"纪录片\",\n  \t\t\t\t\"title\": \"人间世 第一季\",\n  \t\t\t\t\"sub_title\": \"生死考验，人间世态\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/32575c2a7feef1a94c3ec1b0d229cfcfd0f01549.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39247\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47463,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.4\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"恶世之子\",\n  \t\t\t\t\"sub_title\": \"美国无差别狙击案件\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/5a18f11ddecbde05d3cba47502d3102dc1eda995.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47463?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48826,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"纪录片\",\n  \t\t\t\t\"title\": \"生命奇观\",\n  \t\t\t\t\"sub_title\": \"无穷小亮自然纪录片\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/0ea69fe30f7f1f9637d42e354a0a4fa96078be4f.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48826\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45789,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.1\",\n  \t\t\t\t\"left_subscript\": \"电视剧\",\n  \t\t\t\t\"title\": \"抓马侦探2\",\n  \t\t\t\t\"sub_title\": \"脑洞反转不停歇！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/4edf685280fba5d957ad57a3b15a5ebc3190fbbb.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45789\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39866,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.3\",\n  \t\t\t\t\"left_subscript\": \"纪录片\",\n  \t\t\t\t\"title\": \"生活如沸2\",\n  \t\t\t\t\"sub_title\": \"煮沸背后的人生百味\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/5f48c27c848a01c1de468fa42bb46f1c011632c8.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39866\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48762,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.1\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"勿言推理 电影版\",\n  \t\t\t\t\"sub_title\": \"菅田将晖硬核破案\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/32004bfaeabca5198803f9497e8b22313336d982.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48762?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 68491,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.5\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"解除好友2：暗网\",\n  \t\t\t\t\"sub_title\": \"捡电脑惹出暗网危机\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/a6f88553ffa9c5f4701e74963e6a280609e49bc7.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss68491?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 87199,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"不说话的爱\",\n  \t\t\t\t\"sub_title\": \"张艺兴演绎无声父爱\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/ff7d6574e4a48e16c2b9a69dc4af7485b57ee5a7.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss87199?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45936,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"电视剧\",\n  \t\t\t\t\"title\": \"片场日记2\",\n  \t\t\t\t\"sub_title\": \"高分爆梗喜剧续作\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/4bda3d5e37b354946c0a866a20e7fffc495ce627.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45936\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33623,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"电视剧\",\n  \t\t\t\t\"title\": \"西游记续集\",\n  \t\t\t\t\"sub_title\": \"师傅被妖怪抓走啦！\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/2a4782407b25733fbef641595214aa004247ae19.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33623\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48014,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.2\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"野孩子\",\n  \t\t\t\t\"sub_title\": \"流浪兄弟相互救赎！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/94be52c8ad4ed1a4ec74c118bf2260d0fc1df03f.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48014?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47703,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"宝贝老板\",\n  \t\t\t\t\"sub_title\": \"宝贝总裁来袭！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/5a79d2e49059f4e4f3f3977994ae334ea7fad32a.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47703?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45582,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"蜡笔小新：新婚旅行飓风之遗失的野原广志\",\n  \t\t\t\t\"sub_title\": \"广志争夺战开启\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/ebebac20774747787cb01521e1165e97e47d4b6b.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45582?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45579,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"蜡笔小新：我的搬家物语 仙人掌大袭击\",\n  \t\t\t\t\"sub_title\": \"小新洒泪告别风间\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/d772a242b74b9ccc18b622a61a13e4f0005791e4.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45579?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 46350,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"纪录片\",\n  \t\t\t\t\"title\": \"何以中国\",\n  \t\t\t\t\"sub_title\": \"百年中国考古\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/81910f6dc47e499929d2e4c234531e1b46285cb0.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss46350\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39358,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"8.4\",\n  \t\t\t\t\"left_subscript\": \"电视剧\",\n  \t\t\t\t\"title\": \"六神无主\",\n  \t\t\t\t\"sub_title\": \"一个身体五个“魂”\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/d4e3e965ce95d3ea21d73fe8d84594f75770f46a.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39358\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 46178,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"纪录片\",\n  \t\t\t\t\"title\": \"地球脉动 第三季\",\n  \t\t\t\t\"sub_title\": \"口碑系列重磅回归\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/571c98fe1d9b532c98cb5aa23b9b875ced8dcae0.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss46178\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 37677,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"笑破铁幕\",\n  \t\t\t\t\"sub_title\": \"无厘头搞笑电影先驱\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/6d8ea4a7cc81eb72e51fc68bef346f1f41904778.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss37677?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47750,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.2\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"名侦探柯南：百万美元的五棱星\",\n  \t\t\t\t\"sub_title\": \"柯南与基德的对决\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/aa71c6cc991b3c60d8a7b5557e025de5ca422799.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47750?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33628,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"纪录片\",\n  \t\t\t\t\"title\": \"体内怪物 第八季\",\n  \t\t\t\t\"sub_title\": \"致命寄生虫\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/0afbcdf68628459419b095911e6cc2b5531063b2.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33628\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47860,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"综艺\",\n  \t\t\t\t\"title\": \"此生要去的100个地方\",\n  \t\t\t\t\"sub_title\": \"共赴祖国大好河山！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/a9f0c60f74a21b9bd3f1451f61912d4df5dd403c.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47860\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 31800,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"综艺\",\n  \t\t\t\t\"title\": \"非正式会谈 第6季\",\n  \t\t\t\t\"sub_title\": \"发现新世界的入口\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/83fd62f43b83f2e3916c5c112aa853e6133318f0.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss31800\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45660,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"电视剧\",\n  \t\t\t\t\"title\": \"西部世界 第一季\",\n  \t\t\t\t\"sub_title\": \"HBO科幻神剧\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/6befebb8e6390b1f496938c08ac43c0a7cf4fe3f.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45660\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 35874,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"综艺\",\n  \t\t\t\t\"title\": \"2020最美的夜 bilibili晚会\",\n  \t\t\t\t\"sub_title\": \"来B站一起跨年\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/72cb12d4fd380865529ed16603c1001436977589.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss35874\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 41716,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"电视剧\",\n  \t\t\t\t\"title\": \"笑傲江湖\",\n  \t\t\t\t\"sub_title\": \"快意江湖，情仇难断\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/e82f485783df97ba0907576f29b9e6219769f1e4.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss41716\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 90926,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"5.1\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"老狗\",\n  \t\t\t\t\"sub_title\": \"戏骨上演全员恶人\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/9d7495085282c939491652ddd23d7fef3e116742.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss90926?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47684,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"4.4\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"海关战线\",\n  \t\t\t\t\"sub_title\": \"激燃海战与人性深渊\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/1d2585639d6b4ff47e2f18706e075f7663752089.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47684?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47918,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.8\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"加菲猫家族\",\n  \t\t\t\t\"sub_title\": \"经典重启 笑中带泪\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/f4dbf78f0819771d12422bb61fd8874ec2b68b22.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47918?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 95645,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"纪录片\",\n  \t\t\t\t\"title\": \"舌尖上的中国4\",\n  \t\t\t\t\"sub_title\": \"博大精深的中国饮食\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/d9be392c25addb449de9760cae48233ac1a1daf7.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss95645\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48311,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.5\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"重生\",\n  \t\t\t\t\"sub_title\": \"害必除 仇必报\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/ff7257c083d6684e4e5d178d8e553c89f6d79a42.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48311?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48603,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"电影\",\n  \t\t\t\t\"title\": \"乔妍的心事\",\n  \t\t\t\t\"sub_title\": \"姐妹花双陷身份疑云\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/0d9dc6e9855b16260305cdc323fd7e7b94ad2484.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48603?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}]\n  \t\t}, {\n  \t\t\t\"tap_title\": \"电影\",\n  \t\t\t\"tap_type\": 2,\n  \t\t\t\"seasons\": [{\n  \t\t\t\t\"season_id\": 48415,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.9\",\n  \t\t\t\t\"left_subscript\": \"喜剧\",\n  \t\t\t\t\"title\": \"唐探1900\",\n  \t\t\t\t\"sub_title\": \"神探CP携手破悬案\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/07574ea9959f75d89b3c7f0f8e125188b7fadb0a.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48415?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 99873,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.3\",\n  \t\t\t\t\"left_subscript\": \"喜剧\",\n  \t\t\t\t\"title\": \"黄沙漫天\",\n  \t\t\t\t\"sub_title\": \"小沈阳伪装骗巨款\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/16f93b5398f4473fb1c202a60519d8bb91035af7.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss99873?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 90102,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"向阳·花\",\n  \t\t\t\t\"sub_title\": \"诠释女性绝境逆袭\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/c3d8516d035853f0cb3193e0b04c53e3ac0512ae.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss90102?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 32419,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"动作\",\n  \t\t\t\t\"title\": \"碟中谍4\",\n  \t\t\t\t\"sub_title\": \"阿汤哥飞跃迪拜塔？\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/9f823b8936176c2de9f8a09094198013abe7eb0d.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss32419?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48413,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"误杀3\",\n  \t\t\t\t\"sub_title\": \"肖央再演绝望父亲\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/2d16c9853e9845b7aef9a9129e53a63fefb551ba.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48413?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48548,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/a41bd3a088b9d869806406acb28ac7f2a1150a88.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"动作\",\n  \t\t\t\t\"title\": \"毒液：最后一舞\",\n  \t\t\t\t\"sub_title\": \"“毒液”系列终章\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/22d871c521f6bfae45a990628c61fb4f87f4ee06.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48548?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48894,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"冒险\",\n  \t\t\t\t\"title\": \"根本停不下来\",\n  \t\t\t\t\"sub_title\": \"刹车失灵全家狂飙！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/30e0887dba817b3b701a2bbe3f4f0cab1f0c6155.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48894?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 87199,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"不说话的爱\",\n  \t\t\t\t\"sub_title\": \"张艺兴演绎无声父爱\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/ff7d6574e4a48e16c2b9a69dc4af7485b57ee5a7.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss87199?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 68561,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.3\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"胜券在握\",\n  \t\t\t\t\"sub_title\": \"邓超智斗黑心公司\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/2d325939328078ade57877955602ba508807d70b.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss68561?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48234,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/a41bd3a088b9d869806406acb28ac7f2a1150a88.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"科幻\",\n  \t\t\t\t\"title\": \"美国队长4\",\n  \t\t\t\t\"sub_title\": \"美队大战红浩克\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/1c4d71a0f5084098d124b9b6ea8c6e6bb80293cb.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48234?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 32436,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"动作\",\n  \t\t\t\t\"title\": \"环太平洋\",\n  \t\t\t\t\"sub_title\": \"巨型机甲对抗巨兽\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/5d2547fccd75717eab7426cb94b021ec71473b76.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss32436?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 73302,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"破·地狱\",\n  \t\t\t\t\"sub_title\": \"破心魔，解生死！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/d8309a399890ee5ab1245d45352ab19ebb9e996f.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss73302?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 28281,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"哈利·波特与魔法石\",\n  \t\t\t\t\"sub_title\": \"霍格沃茨开学了！\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/dd9a113e724e3c60dc2da31562e45d57e3c2d707.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss28281?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 76849,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"科幻\",\n  \t\t\t\t\"title\": \"穿越时空的少女\",\n  \t\t\t\t\"sub_title\": \"细田守巅峰之作！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/dd195431e5d3985ff8391683b79ddeaf400c10de.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss76849?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47468,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"喜剧\",\n  \t\t\t\t\"title\": \"间谍过家家 代号：白\",\n  \t\t\t\t\"sub_title\": \"全家拯救阿尼亚\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/295c0441df86fb8ce6627db92edbc9e7bb453de0.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47468?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33354,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"喜剧\",\n  \t\t\t\t\"title\": \"夏洛特烦恼\",\n  \t\t\t\t\"sub_title\": \"马冬梅的排列组合\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/136d1616456e60732d3c84e40e0f925e5e119003.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33354?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 46054,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/a41bd3a088b9d869806406acb28ac7f2a1150a88.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"喜剧\",\n  \t\t\t\t\"title\": \"疯狂动物城\",\n  \t\t\t\t\"sub_title\": \"成人世界乌托邦\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/d250b5bde3c6556894684d4b0818841190df5743.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss46054?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 32314,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"动作\",\n  \t\t\t\t\"title\": \"碟中谍\",\n  \t\t\t\t\"sub_title\": \"阿汤哥经典特工系列\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/dbe606a3b5b6f7ff50fc8637e23ff71b6693eb1b.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss32314?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48590,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"奇幻\",\n  \t\t\t\t\"title\": \"哈尔的移动城堡\",\n  \t\t\t\t\"sub_title\": \"魔法之下的爱与勇气\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/156dfe963b71dc62dbf4315f3985a070844d0593.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48590?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 96607,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"瘦身大作战\",\n  \t\t\t\t\"sub_title\": \"失恋女孩的蜕变计划\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/b097f0584d5424892145e62518609af3215c09fd.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss96607?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 44847,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"科幻\",\n  \t\t\t\t\"title\": \"流浪地球2\",\n  \t\t\t\t\"sub_title\": \"国产科幻巨制\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/4ee5e1cea45fb742c77d17c1b2d115f09508a878.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss44847?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 42715,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"喜剧\",\n  \t\t\t\t\"title\": \"坏蛋联盟\",\n  \t\t\t\t\"sub_title\": \"天生“坏蛋”想从良\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/7d63615d0749d8fe2ce161d155ac1c8198869591.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss42715?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 41179,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"喜剧\",\n  \t\t\t\t\"title\": \"史密斯夫妇\",\n  \t\t\t\t\"sub_title\": \"杀手夫妻甜蜜交锋\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/bc434e10efbfa7ab0efb3741b0be80fbf2bdef3b.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss41179?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 42384,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"动作\",\n  \t\t\t\t\"title\": \"头号玩家\",\n  \t\t\t\t\"sub_title\": \"斯皮尔伯格科幻大片\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/d1baef273cc7862431978b2e63fc04ccdd353556.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss42384?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 32523,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"喜剧\",\n  \t\t\t\t\"title\": \"功夫\",\n  \t\t\t\t\"sub_title\": \"周星驰经典动作喜剧\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/0c65b23a92ea20115147df841cdfaa3e4b1170df.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss32523?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45140,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"沙丘2\",\n  \t\t\t\t\"sub_title\": \"沙虫来袭 圣战将至\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/fb57cbefc33ed173e40299e243ef89fda151c9f4.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45140?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47065,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"7.4\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"坚如磐石\",\n  \t\t\t\t\"sub_title\": \"权钱交叠破解杀机\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/e00f39c55ac9a89318144d1a5dd23d33b536cf9e.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47065?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 68514,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"动作\",\n  \t\t\t\t\"title\": \"因果报应\",\n  \t\t\t\t\"sub_title\": \"极限反转，恶有恶报\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/c570d4131545158f69623a0f0f6103e8994e1874.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss68514?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 28585,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"科幻\",\n  \t\t\t\t\"title\": \"星际穿越\",\n  \t\t\t\t\"sub_title\": \"不要温和地走进良夜\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/3b8acd7a69aa3462297b0b1f206e7093e4961914.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss28585?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 38552,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"动作\",\n  \t\t\t\t\"title\": \"正义联盟：扎克·施奈德版\",\n  \t\t\t\t\"sub_title\": \"最全DC宇宙阵容集结！\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/32a08ede8cdcf941b0a2d2ceddd6c1b991c64699.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss38552?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47750,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.2\",\n  \t\t\t\t\"left_subscript\": \"犯罪\",\n  \t\t\t\t\"title\": \"名侦探柯南：百万美元的五棱星\",\n  \t\t\t\t\"sub_title\": \"柯南与基德的对决\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/aa71c6cc991b3c60d8a7b5557e025de5ca422799.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47750?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48013,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.2\",\n  \t\t\t\t\"left_subscript\": \"喜剧\",\n  \t\t\t\t\"title\": \"抓娃娃\",\n  \t\t\t\t\"sub_title\": \"沈马组合爆笑养娃\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/57f8d498302dddf7b197e3d3c9b0bccde9b80406.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48013?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 68640,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"奇幻\",\n  \t\t\t\t\"title\": \"火影忍者剧场版：忍者之路\",\n  \t\t\t\t\"sub_title\": \"鸣人，欢迎回家！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/5fbf4380fd163b3817b9d6d595eb8875253b4e79.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss68640?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 46319,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"科幻\",\n  \t\t\t\t\"title\": \"后天\",\n  \t\t\t\t\"sub_title\": \"人类重回冰川时代\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/839683656c9eaabe48c5d84a06b47e52a613f422.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss46319?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33615,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"绿皮书\",\n  \t\t\t\t\"sub_title\": \"黑白配公路片\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/99e62fe75cda9d99450df0460307a73d65cf1578.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33615?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39558,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"动作\",\n  \t\t\t\t\"title\": \"碟中谍6：全面瓦解\",\n  \t\t\t\t\"sub_title\": \"阿汤哥招牌系列新作\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/4bfe927ed1254de55d32dabc6237630b10d8e1d9.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39558?theme=movie\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}]\n  \t\t}, {\n  \t\t\t\"tap_title\": \"电视剧\",\n  \t\t\t\"tap_type\": 5,\n  \t\t\t\"seasons\": [{\n  \t\t\t\t\"season_id\": 33626,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"战争\",\n  \t\t\t\t\"title\": \"三国演义\",\n  \t\t\t\t\"sub_title\": \"正所谓乱世出英雄\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/1d2eb8d863e3d0aa9d6557ae5b32764159953d8b.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33626\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 21020,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.2\",\n  \t\t\t\t\"left_subscript\": \"历史\",\n  \t\t\t\t\"title\": \"康熙王朝\",\n  \t\t\t\t\"sub_title\": \"康熙帝传奇的一生\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/4f6c371428b8679b3e84406437fde071aae3927b.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss21020\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 44480,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"雍正王朝\",\n  \t\t\t\t\"sub_title\": \"古装历史剧巅峰之作\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/308cb2bb99bbbbf507e2898460918689a514ac26.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss44480\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 96905,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"喜剧\",\n  \t\t\t\t\"title\": \"我爱我家\",\n  \t\t\t\t\"sub_title\": \"中国首部情景喜剧\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/a600d1778e570fae87bbedb62ed724cfd04626d5.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss96905\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 24053,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"悬疑\",\n  \t\t\t\t\"title\": \"非自然死亡\",\n  \t\t\t\t\"sub_title\": \"不自然死因研究所\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/72f01ec71e4e8cb1a765f875b7a26fb78b9ae6eb.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss24053\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33622,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"奇幻\",\n  \t\t\t\t\"title\": \"西游记\",\n  \t\t\t\t\"sub_title\": \"俺老孙来也！\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/f9b7d89acb62d2ed385381ebcf5ac80b65fadd73.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33622\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 68632,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"影后\",\n  \t\t\t\t\"sub_title\": \"娱乐圈抓马大戏\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/d8943e9a8682b7ec3a9c744e2d8aca40643cfba1.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss68632\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 20117,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"搞笑\",\n  \t\t\t\t\"title\": \"家有儿女\",\n  \t\t\t\t\"sub_title\": \"童年回忆杀\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/ffd860e68feeccd2e14531ac5c7d12c683e0924c.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss20117\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 96519,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.5\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"姜颂\",\n  \t\t\t\t\"sub_title\": \"高能反转虐恋爽剧\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/9871644abfdefeb026d2833312eedfdba3721d3b.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss96519\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33625,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9\",\n  \t\t\t\t\"left_subscript\": \"古装\",\n  \t\t\t\t\"title\": \"水浒传\",\n  \t\t\t\t\"sub_title\": \"大郎，该吃药了\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/78241bf67ee92b92ca4e8d1b8006b939f602aad6.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33625\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33624,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"古装\",\n  \t\t\t\t\"title\": \"红楼梦\",\n  \t\t\t\t\"sub_title\": \"这个妹妹我见过\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/7e52b373f9522884d4c878c042d46dd532774057.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33624\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 96622,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"搞笑\",\n  \t\t\t\t\"title\": \"粉红女郎\",\n  \t\t\t\t\"sub_title\": \"千禧回忆杀！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/47b9e608859127d57728911c0dd30517e74850da.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss96622\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 38729,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"搞笑\",\n  \t\t\t\t\"title\": \"老友记 第一季\",\n  \t\t\t\t\"sub_title\": \"经典美剧六人行\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/28f4634e43c3b8def94218101622b9ae69a20d56.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss38729\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 42962,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.3\",\n  \t\t\t\t\"left_subscript\": \"青春\",\n  \t\t\t\t\"title\": \"三悦有了新工作\",\n  \t\t\t\t\"sub_title\": \"废柴95后打工日记\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/a4070f48d2f2e2d482523a4df1f64ded626e010c.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss42962\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 41349,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"机智的医生生活 第二季\",\n  \t\t\t\t\"sub_title\": \"还是医院五人组\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/367b0470947cab63c0ca8a5227c72688d6ea2be3.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss41349\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47841,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"8.8\",\n  \t\t\t\t\"left_subscript\": \"奇幻\",\n  \t\t\t\t\"title\": \"时光代理人\",\n  \t\t\t\t\"sub_title\": \"新的穿越委托请查收\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/f833064b3ba7777bd33c8ca75fe1499f38ae5b22.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47841\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 43891,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"机智的医生生活\",\n  \t\t\t\t\"sub_title\": \"医院五人组\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/5cc912b5ecb8dbb82f8478e3db055c604675c8e3.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss43891\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 85532,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"神探夏洛克 第一季\",\n  \t\t\t\t\"sub_title\": \"天才侦探的推理奇旅\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/41c268584969ed952ac3c771855d705c912b0eee.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss85532\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33981,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"8.1\",\n  \t\t\t\t\"left_subscript\": \"青春\",\n  \t\t\t\t\"title\": \"风犬少年的天空\",\n  \t\t\t\t\"sub_title\": \"彭昱畅喊你来追剧\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/874d8d77ffd1e08c6706cd9a8358eaba27d5e36e.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33981\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47710,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"铠甲勇士刑天\",\n  \t\t\t\t\"sub_title\": \"难以超越的童年经典\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/ef0b9764b79c769f9ca698f40e1a7fffdb0d2a89.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47710\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 73355,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"喜剧\",\n  \t\t\t\t\"title\": \"生活大爆炸 第一季\",\n  \t\t\t\t\"sub_title\": \"怪咖初聚开启新奇缘\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/c516741a0eda05c8fc33b7fad0c3ad8d473bb409.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss73355\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45658,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"白莲花\",\n  \t\t\t\t\"sub_title\": \"全员自私，人性博弈\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/42b40e255eeb2df7c562f15ce48d1840f03bf58e.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45658\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45419,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"奇幻\",\n  \t\t\t\t\"title\": \"古相思曲\",\n  \t\t\t\t\"sub_title\": \"逆向时空，甜虐来袭\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/6dc69dcab662e0d35e36c8429ecba40ff8938507.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45419\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 46356,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"搞笑\",\n  \t\t\t\t\"title\": \"重启人生\",\n  \t\t\t\t\"sub_title\": \"高口碑治愈系日剧\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/905806209c3d1b1d69e83866b53ea0a16a344586.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss46356\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 43551,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.3\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"学校2015\",\n  \t\t\t\t\"sub_title\": \"双胞胎姐妹命运错置\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/47ab568509ccee7794b257c53cb2bfe2bd56a189.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss43551\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 46681,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"奇幻\",\n  \t\t\t\t\"title\": \"权力的游戏 第一季\",\n  \t\t\t\t\"sub_title\": \"经典史诗奇幻神剧\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/13e892349ff97970340eef24c20e163fe7aad842.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss46681\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 41453,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"8.1\",\n  \t\t\t\t\"left_subscript\": \"古装\",\n  \t\t\t\t\"title\": \"珍馐记\",\n  \t\t\t\t\"sub_title\": \"暖胃珍馐，吃笑人间\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/736891730e6160b0d26897905b3b3f797df211cf.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss41453\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33623,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"奇幻\",\n  \t\t\t\t\"title\": \"西游记续集\",\n  \t\t\t\t\"sub_title\": \"师傅被妖怪抓走啦！\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/2a4782407b25733fbef641595214aa004247ae19.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33623\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 41716,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"武侠\",\n  \t\t\t\t\"title\": \"笑傲江湖\",\n  \t\t\t\t\"sub_title\": \"快意江湖，情仇难断\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/e82f485783df97ba0907576f29b9e6219769f1e4.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss41716\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 46138,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"搞笑\",\n  \t\t\t\t\"title\": \"法律至上1\",\n  \t\t\t\t\"sub_title\": \"日本幽默律政片\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/b68bd5e65a9f8e140ae912a8941871aff436a25a.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss46138\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 43894,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"海岸村恰恰恰\",\n  \t\t\t\t\"sub_title\": \"社畜与咸鱼温情碰撞\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/46b2d1a1088214b314956b07a4ccd2aa05b0b154.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss43894\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 38629,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"白色巨塔\",\n  \t\t\t\t\"sub_title\": \"超经典医疗剧\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/b70a6a12cdab2bcb9f81677446ebf52f367ca7bd.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss38629\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 68652,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"搞笑\",\n  \t\t\t\t\"title\": \"孤独的美食家 番外篇\",\n  \t\t\t\t\"sub_title\": \"电子榨菜 下饭必看\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/5f4e236634b4d8caacc37b6cc3d161fe6020d1aa.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss68652\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 20115,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"搞笑\",\n  \t\t\t\t\"title\": \"家有儿女2\",\n  \t\t\t\t\"sub_title\": \"经典大型情景喜剧\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/dabe19fe246c0422cb419237d78891fe4b2afe28.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss20115\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 21288,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"家庭\",\n  \t\t\t\t\"title\": \"外来媳妇本地郎\",\n  \t\t\t\t\"sub_title\": \"最长寿的电视剧之一\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/25cb9e09f1a7aa2f3575bacef6dadd1df3208881.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss21288\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 85911,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"剧情\",\n  \t\t\t\t\"title\": \"悠长假期\",\n  \t\t\t\t\"sub_title\": \"与木村拓哉的恋爱假\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/70577dcf598beb0663e028745835ae0f9b93ea0c.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss85911\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}]\n  \t\t}, {\n  \t\t\t\"tap_title\": \"番剧\",\n  \t\t\t\"tap_type\": 1,\n  \t\t\t\"seasons\": [{\n  \t\t\t\t\"season_id\": 42176,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.2\",\n  \t\t\t\t\"left_subscript\": \"日常\",\n  \t\t\t\t\"title\": \"莉可丽丝\",\n  \t\t\t\t\"sub_title\": \"虚假的咖啡厅日常\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/53e6a08505b5a889feb14d2f5ee8a7f22b961847.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss42176\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 91776,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"摇滚乃是淑女的爱好\",\n  \t\t\t\t\"sub_title\": \"硬核摇滚，就好这个\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/9dc89cedb3d03174de8bfd24cceab74ce6b4ce0b.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss91776\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 99688,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"丁丁历险记 第一季\",\n  \t\t\t\t\"sub_title\": \"一人一狗闯世界\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/1e0db7c64755ced8ad7a9615f692500cc2db9898.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss99688\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 91812,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"赛马娘 芦毛灰姑娘\",\n  \t\t\t\t\"sub_title\": \"一马当先，万马无光\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/c72d6ee8b3cba39355af14bebd4857935b4fb083.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss91812\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 90684,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"原创\",\n  \t\t\t\t\"title\": \"末日后酒店\",\n  \t\t\t\t\"sub_title\": \"让AI顺应你的心\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/2f5946880c07914d1cccd112702884f232b647e0.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss90684\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 96969,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"九龙大众浪漫\",\n  \t\t\t\t\"sub_title\": \"金鱼游弋的谎言盛宴\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/bf7a2acd78fffc9d0eb8887f92de681924afb73d.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss96969\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 99644,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"小城日常\",\n  \t\t\t\t\"sub_title\": \"《日常》精神续作\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/6694a18852af6eeea804e604376bc014d1a84441.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss99644\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 91513,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"8.2\",\n  \t\t\t\t\"left_subscript\": \"小说改\",\n  \t\t\t\t\"title\": \"天才治疗师退队作为无照治疗师快乐过活\",\n  \t\t\t\t\"sub_title\": \"真乃妙手回春啊大夫\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/75d7c9d18faa0b792d8bfab181f1608184c32b8b.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss91513\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 91498,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"小说改\",\n  \t\t\t\t\"title\": \"打了300年的史莱姆，不知不觉就练到了满级 第二季\",\n  \t\t\t\t\"sub_title\": \"满级魔女只想养老\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/d9727717ee6b507225b26ae367a43a444a6157ca.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss91498\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33415,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"智斗\",\n  \t\t\t\t\"title\": \"名侦探柯南（中配）\",\n  \t\t\t\t\"sub_title\": \"小学生的侦探生活\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/38e2a273f528fd01c34f1fc4df0f69c64487efad.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33415\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 91894,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"5.8\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"最强王者的第二人生\",\n  \t\t\t\t\"sub_title\": \"三岁开始当卷王\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/ff55e211d6ab08f063ef2df0bcb064854d08f221.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss91894\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 99693,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"丁丁历险记 第三季\",\n  \t\t\t\t\"sub_title\": \"一人一狗闯世界\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/5a314ae3c28f1e7819c77672bbeeb99f7cda346b.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss99693\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 98687,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"原创\",\n  \t\t\t\t\"title\": \"少年骇客 第一季\",\n  \t\t\t\t\"sub_title\": \"少年获得神奇手表\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/7cf7938af567ceb370a72ac57433f346d6bd9b87.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss98687\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48810,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"热血\",\n  \t\t\t\t\"title\": \"假面骑士加布\",\n  \t\t\t\t\"sub_title\": \"令和第六作假面骑士\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/8f3bf6b461ad4dfae2a221e5b194415d5f7ca697.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48810\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 91577,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"8.9\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"炎炎消防队 叁之章\",\n  \t\t\t\t\"sub_title\": \"焰人谜团步入最终章\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/efb0bfbcd060f1b2280bad5f40c8b75db64de442.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss91577\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 91305,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"测不准的阿波连同学 第二季\",\n  \t\t\t\t\"sub_title\": \"猜不中的来堂同学\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/f56b2c9938b981bc6e029ce4d7b2bb0bd29948c4.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss91305\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33378,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"智斗\",\n  \t\t\t\t\"title\": \"名侦探柯南\",\n  \t\t\t\t\"sub_title\": \"真相永远只有一个\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/38e2a273f528fd01c34f1fc4df0f69c64487efad.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33378\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 98696,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"原创\",\n  \t\t\t\t\"title\": \"少年骇客 第二季 中文配音\",\n  \t\t\t\t\"sub_title\": \"少年获得神奇手表\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/3ae6f63e48d295abb3f637ba71c007ca2c61f153.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss98696\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 91466,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"魔女与使魔\",\n  \t\t\t\t\"sub_title\": \"魔女的魔法有副作用\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/d7337f0b197da961d57b4321011048db6b518bb8.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss91466\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 100274,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"小说改\",\n  \t\t\t\t\"title\": \"肥宅勇者\",\n  \t\t\t\t\"sub_title\": \"牺牲颜值的强者之路\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/1e90562eb0a76cb48f1558ee73e2560104057ff0.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss100274\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 92458,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.9\",\n  \t\t\t\t\"left_subscript\": \"原创\",\n  \t\t\t\t\"title\": \"宝可梦 地平线（中配）\",\n  \t\t\t\t\"sub_title\": \"与全新角色开始冒险\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/c82293ce7fadd7188be20c0487869a187f2ba0eb.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss92458\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48297,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"日常\",\n  \t\t\t\t\"title\": \"小猪佩奇 第十季 中文配音\",\n  \t\t\t\t\"sub_title\": \"你好！我是佩奇！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/0ea83a1ab2ed156fd4b6ce2a29e0f209d0d43927.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48297\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 91755,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"哆啦A梦 第五季\",\n  \t\t\t\t\"sub_title\": \"哆啦A梦帮帮我！！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/5b05e7f5892b9cd1d8b41610114583e124f3c85a.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss91755\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 98694,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"原创\",\n  \t\t\t\t\"title\": \"少年骇客 第一季 中文配音\",\n  \t\t\t\t\"sub_title\": \"少年获得神奇手表\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/f72fac0118e212e577ee4a3a0b0b0e3021c90e58.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss98694\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48034,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"6.4\",\n  \t\t\t\t\"left_subscript\": \"小说改\",\n  \t\t\t\t\"title\": \"你与我最后的战场，亦或是世界起始的圣战 第二季\",\n  \t\t\t\t\"sub_title\": \"夫妻同心 其利断金\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/d4974b6f095f9362493f1ae79b36da8c96552f66.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48034\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 91777,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.4\",\n  \t\t\t\t\"left_subscript\": \"小说改\",\n  \t\t\t\t\"title\": \"直至魔女消逝\",\n  \t\t\t\t\"sub_title\": \"见习魔女生命倒计时\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/588cc1e9f43b57b1e8ec4fcb0f16bfc3f743de00.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss91777\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 98698,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"原创\",\n  \t\t\t\t\"title\": \"少年骇客 第三季 中文配音\",\n  \t\t\t\t\"sub_title\": \"少年获得神奇手表\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/0ec2ec57520cff4ae813f51b633988fd37d7c00d.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss98698\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 96971,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"4.3\",\n  \t\t\t\t\"left_subscript\": \"原创\",\n  \t\t\t\t\"title\": \"假面骑士利维斯\",\n  \t\t\t\t\"sub_title\": \"英雄与恶魔搭档最强\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/ffdea4aaa6324fc47f8cb89d540cebed25ee28fa.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss96971\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 6262,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"搞笑\",\n  \t\t\t\t\"title\": \"蜡笔小新 第二季（中文）\",\n  \t\t\t\t\"sub_title\": \"不想来笑一下吗？\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/6d8bd12e0e1ab2d4d5e8567bdba18240e75d7a1b.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss6262\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 5398,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"热血\",\n  \t\t\t\t\"title\": \"JOJO的奇妙冒险 不灭钻石\",\n  \t\t\t\t\"sub_title\": \"不一样的热血动画\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/6a04c87e990ab74cd8d555ef45a863de0993b161.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss5398\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 91510,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"8\",\n  \t\t\t\t\"left_subscript\": \"小说改\",\n  \t\t\t\t\"title\": \"乡下大叔成为剑圣\",\n  \t\t\t\t\"sub_title\": \"剑术老师桃李满天下\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/0f5be9db05e5bc60c1ba6cee80debe02c5f73f56.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss91510\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48922,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.4\",\n  \t\t\t\t\"left_subscript\": \"原创\",\n  \t\t\t\t\"title\": \"假面骑士加布（中配）\",\n  \t\t\t\t\"sub_title\": \"令和第六作假面骑士\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/8f3bf6b461ad4dfae2a221e5b194415d5f7ca697.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48922\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 91814,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"战队大失格 第二季\",\n  \t\t\t\t\"sub_title\": \"五个人都是战队英雄\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/17cc74e662b3fb3c8fd35d0983c046f6b7e38700.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss91814\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 25681,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"热血\",\n  \t\t\t\t\"title\": \"JOJO的奇妙冒险 黄金之风\",\n  \t\t\t\t\"sub_title\": \"这就是黄金体验\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/f34ff3975c39913af936c133ae60a5891babba08.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss25681\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 38157,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"热血\",\n  \t\t\t\t\"title\": \"银魂\",\n  \t\t\t\t\"sub_title\": \"最无厘头的热血动画\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/087b862b772ee4e644478a36c757a26db476193d.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss38157\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 92889,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"胆大党 中配版\",\n  \t\t\t\t\"sub_title\": \"妖怪vs外星人\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/0a681472cb6ccfcd7f18094433ea9b750d9c0849.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss92889\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}]\n  \t\t}, {\n  \t\t\t\"tap_title\": \"纪录片\",\n  \t\t\t\"tap_type\": 3,\n  \t\t\t\"seasons\": [{\n  \t\t\t\t\"season_id\": 96695,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"人文\",\n  \t\t\t\t\"title\": \"人生海海\",\n  \t\t\t\t\"sub_title\": \"火锅江湖，人生沉浮\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/d3faa4f109b93f3c03a4ba6e8ec9cea02252fb31.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss96695\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 95645,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"美食\",\n  \t\t\t\t\"title\": \"舌尖上的中国4\",\n  \t\t\t\t\"sub_title\": \"博大精深的中国饮食\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/d9be392c25addb449de9760cae48233ac1a1daf7.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss95645\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 88835,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"社会\",\n  \t\t\t\t\"title\": \"你好，12315\",\n  \t\t\t\t\"sub_title\": \"12315打假实录\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/3c1a01ed28c60ac04b0f6edf2f0707ae1afa100f.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss88835\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48055,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"社会\",\n  \t\t\t\t\"title\": \"守护解放西5\",\n  \t\t\t\t\"sub_title\": \"守护新篇章\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/dc62303faa9882065fe0d1ce3e7226f3eaad17fa.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48055\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48056,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"社会\",\n  \t\t\t\t\"title\": \"闪闪的儿科医生2\",\n  \t\t\t\t\"sub_title\": \"爆款医疗节目回归！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/8d5ffd3bacd836778474fa719a321f9f2a4948b6.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48056\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48826,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"自然\",\n  \t\t\t\t\"title\": \"生命奇观\",\n  \t\t\t\t\"sub_title\": \"无穷小亮自然纪录片\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/0ea69fe30f7f1f9637d42e354a0a4fa96078be4f.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48826\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48800,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"探险\",\n  \t\t\t\t\"title\": \"单挑荒野：水之章\",\n  \t\t\t\t\"sub_title\": \"德爷开启单挑新副本\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/29f33feb30d440aae93ac060377e29b61742c07f.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48800\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 44473,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"社会\",\n  \t\t\t\t\"title\": \"守护解放西4\",\n  \t\t\t\t\"sub_title\": \"全新的守护者联盟\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/6dd3cd3b6105071f0c2e22032c3196175117333f.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss44473\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33585,\n  \t\t\t\t\"right_superscript\": \"https://i0.hdslb.com/bfs/bangumi/image/c264551547b8d7bf55d047002c27635bc180821e.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"人文\",\n  \t\t\t\t\"title\": \"中国通史\",\n  \t\t\t\t\"sub_title\": \"浩瀚历史图景\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/48fdbee6cd5ea2bef11863efcaa8b65498991fb7.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33585\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45209,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"探险\",\n  \t\t\t\t\"title\": \"野境求真\",\n  \t\t\t\t\"sub_title\": \"UP主野外调查\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/6a9fd3c1b0d79339dd4e6a552f59c4e862c27dee.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45209\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 26421,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"社会\",\n  \t\t\t\t\"title\": \"这就是中国\",\n  \t\t\t\t\"sub_title\": \"张维为解读中国\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/c886d6dfa0fab0c80d2a005b50ae3fafa9394134.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss26421\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39188,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"人文\",\n  \t\t\t\t\"title\": \"人生一串 第三季\",\n  \t\t\t\t\"sub_title\": \"七荤八素的口腹之欲\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/6c05210675336fc19583ef76c38c893ac6ce46b0.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39188\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 28277,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"社会\",\n  \t\t\t\t\"title\": \"守护解放西\",\n  \t\t\t\t\"sub_title\": \"警动星城\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/13059001c9284c9392c7fb046cdb64dff159effa.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss28277\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 44170,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"社会\",\n  \t\t\t\t\"title\": \"闪闪的儿科医生\",\n  \t\t\t\t\"sub_title\": \"治愈系医疗纪实节目\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/c72a32feef5f3f0c9a9b6be4de529e483a639fa5.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss44170\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 95803,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"科技\",\n  \t\t\t\t\"title\": \"流言终结者 第一季\",\n  \t\t\t\t\"sub_title\": \"“梦开始的地方！”\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/b81cfdedefb20ba3d0334ae3aece16c690ef50cf.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss95803\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 40509,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"社会\",\n  \t\t\t\t\"title\": \"守护解放西3\",\n  \t\t\t\t\"sub_title\": \"星城守卫者\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/3a5e05f8277b4e6b0c5190d1e649a2ba856b2197.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss40509\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 46178,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"自然\",\n  \t\t\t\t\"title\": \"地球脉动 第三季\",\n  \t\t\t\t\"sub_title\": \"口碑系列重磅回归\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/571c98fe1d9b532c98cb5aa23b9b875ced8dcae0.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss46178\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 43161,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"人文\",\n  \t\t\t\t\"title\": \"不止考古·我与三星堆\",\n  \t\t\t\t\"sub_title\": \"真实考古三星堆\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/cea6473bb07655ac3d76954709d7e4da756daf41.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss43161\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 34733,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"社会\",\n  \t\t\t\t\"title\": \"守护解放西2\",\n  \t\t\t\t\"sub_title\": \"星城卫士\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/ba37f849fcc927fc532e9c790601608f1d8ae267.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss34733\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45211,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"人文\",\n  \t\t\t\t\"title\": \"国宝迷踪\",\n  \t\t\t\t\"sub_title\": \"千山万水，带你回家\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/42a88d17b68fa9f12724f6c88d5b49e3d50d3f52.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45211\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 76317,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"动物\",\n  \t\t\t\t\"title\": \"宝贝星球\",\n  \t\t\t\t\"sub_title\": \"心有灵犀，爱无界限\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/c21d2b6c1258cca8ac81cc3f77f71b1c9881bdb4.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss76317\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39868,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9\",\n  \t\t\t\t\"left_subscript\": \"美食\",\n  \t\t\t\t\"title\": \"奇食记2\",\n  \t\t\t\t\"sub_title\": \"怪异的“美食”\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/4570f21b0d436879f458c96eeb83612cc1b810f4.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39868\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45196,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"社会\",\n  \t\t\t\t\"title\": \"是坏情绪啊，没关系\",\n  \t\t\t\t\"sub_title\": \"正视自己的情绪\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/db2f2d89d7b65e46fb6c46e8a77920b6485858ad.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45196\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45187,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"社会\",\n  \t\t\t\t\"title\": \"中国救护\",\n  \t\t\t\t\"sub_title\": \"聚焦院前急救现场\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/41a48436a32a4ff1783daf239d2538a0247de805.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45187\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 98337,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"历史\",\n  \t\t\t\t\"title\": \"庞贝古城：人民的故事\",\n  \t\t\t\t\"sub_title\": \"探索古罗马生活遗址\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/7294bad232ac788142a797e1344b5d143ff15507.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss98337\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 41403,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.8\",\n  \t\t\t\t\"left_subscript\": \"社会\",\n  \t\t\t\t\"title\": \"案件聚焦\",\n  \t\t\t\t\"sub_title\": \"人民关心的案件\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/a4aabad2347d26350172b0411abd60775c3a8877.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss41403\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39865,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"人文\",\n  \t\t\t\t\"title\": \"众神之地\",\n  \t\t\t\t\"sub_title\": \"B站出品自然巨制\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/49249fd8bfcc1a461a1529debe14c7c4e176e4e9.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39865\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 44165,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"人文\",\n  \t\t\t\t\"title\": \"可以跟着去你家吗（精选版）\",\n  \t\t\t\t\"sub_title\": \"访问陌生人的家\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/b993eaae907e933f7e00d8c74dc4fdf0d61b5ba4.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss44165\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 85805,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"美食\",\n  \t\t\t\t\"title\": \" 老广的味道 第十季\",\n  \t\t\t\t\"sub_title\": \"发掘地道广州味\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/c56a8210afd75e1b162f76276023fea90196d798.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss85805\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 94795,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"人文\",\n  \t\t\t\t\"title\": \"监狱纪实\",\n  \t\t\t\t\"sub_title\": \"引人深思的纪录片\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/ae5181a102705346ef84c51be79fe21806596f3f.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss94795\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 89322,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"人文\",\n  \t\t\t\t\"title\": \"无穷之路4：一带一路\",\n  \t\t\t\t\"sub_title\": \"跨越七国记录时代\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/7dfaaedc9bfd7cbead819949de49e229b37a6061.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss89322\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 27763,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"探险\",\n  \t\t\t\t\"title\": \"荒野求生 第四季\",\n  \t\t\t\t\"sub_title\": \"贝爷的生存大挑战\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/086415cdafc5d5111fe847a1298f9cdcc8d9faf3.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss27763\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 99554,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"社会\",\n  \t\t\t\t\"title\": \"生命缘 第15季\",\n  \t\t\t\t\"sub_title\": \"执炬者行，向心燎原\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/f2635bff0d9762aa3667c0fa61f8a0c3b72eef71.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss99554\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 24439,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"美食\",\n  \t\t\t\t\"title\": \"人生一串\",\n  \t\t\t\t\"sub_title\": \"撸上烤串，快意江湖\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/7a790c64ff70f12c11888be0532b6981a923afd5.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss24439\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 72882,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"人文\",\n  \t\t\t\t\"title\": \"养猫的人\",\n  \t\t\t\t\"sub_title\": \"聚焦人与猫的故事\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/9fc65038b8b74f3f2895c2e8036b35ef0daf9e40.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss72882\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 34917,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"自然\",\n  \t\t\t\t\"title\": \"绿色星球\",\n  \t\t\t\t\"sub_title\": \"4K沉浸式聚焦植物\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/eebafc7c374701cdf27e5068b5e885c6337d0567.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss34917\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}]\n  \t\t}, {\n  \t\t\t\"tap_title\": \"国创\",\n  \t\t\t\"tap_type\": 4,\n  \t\t\t\"seasons\": [{\n  \t\t\t\t\"season_id\": 28747,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"小说改\",\n  \t\t\t\t\"title\": \"凡人修仙传\",\n  \t\t\t\t\"sub_title\": \"韩立驰骋外海震乾坤\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/ee347bb7ec67696bce514d51b4cef55a5e192284.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss28747\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 46585,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"原创\",\n  \t\t\t\t\"title\": \"灵笼 第二季\",\n  \t\t\t\t\"sub_title\": \"末世如何才能生存\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/08bf0c1e24e454de51b58d1e26c0a9aecbe9b0c1.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss46585\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 22088,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"灵笼\",\n  \t\t\t\t\"sub_title\": \"末世如何才能生存\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/cfab7e0fbdb4786ff4e885d050b7cf37f8829486.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss22088\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45969,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"小说改\",\n  \t\t\t\t\"title\": \"牧神记\",\n  \t\t\t\t\"sub_title\": \"放牛少年，放牧诸神\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/53241feb96db2fbe353840b13a3c9412465a1cee.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45969\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 43554,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"原创\",\n  \t\t\t\t\"title\": \"凸变英雄X\",\n  \t\t\t\t\"sub_title\": \"信赖造就英雄\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/82568ec2918b7e266ec82ce485ea14018a2d4e71.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss43554\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48518,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"小说改\",\n  \t\t\t\t\"title\": \"宗门里除了我都是卧底\",\n  \t\t\t\t\"sub_title\": \"宗门游戏，坐等开玩\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/7a72973b174b473850556d4b6402a13db679f7b8.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48518\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48578,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"镇魂街 第四季\",\n  \t\t\t\t\"sub_title\": \"天武风雷，群英大战\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/22c53b77405270b079b95ad6400d6d6bfd1de377.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48578\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 2543,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"狐妖小红娘\",\n  \t\t\t\t\"sub_title\": \"可爱狐妖为你牵红线\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/cbb20ee03e97a9f3ad2e1506a10fd1271f1c584a.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss2543\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45965,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.2\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"鲲吞天下之掌门归来\",\n  \t\t\t\t\"sub_title\": \"修真全靠吞\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/fcd3e1f1c3eb2245a9c2b4da5c175fb2b8f628e7.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45965\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 28763,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"8.9\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"天官赐福\",\n  \t\t\t\t\"sub_title\": \"天官赐福，百无禁忌\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/8d1be9e8c77696f34886b8f471d935f504a014d3.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss28763\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 46188,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"6.7\",\n  \t\t\t\t\"left_subscript\": \"小说改\",\n  \t\t\t\t\"title\": \"少年歌行 血染天启篇\",\n  \t\t\t\t\"sub_title\": \"踏破天启城\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/a50f4e5b7de83c2040a104c6d8bbaaa2bd9fac80.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss46188\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 35220,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"时光代理人\",\n  \t\t\t\t\"sub_title\": \"无论过去，不问将来\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/7dac8d193a93409777ce027dba35b068efaca718.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss35220\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 46593,\n  \t\t\t\t\"right_superscript\": \"https://i0.hdslb.com/bfs/bangumi/image/c264551547b8d7bf55d047002c27635bc180821e.png\",\n  \t\t\t\t\"right_subscript\": \"8.5\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"月魁传 动态漫画\",\n  \t\t\t\t\"sub_title\": \"白月魁旧世界往事\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/1b836f93b1da90ab4d257d9bc58ab6f120e20683.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss46593\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 90883,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"小说改\",\n  \t\t\t\t\"title\": \"我的师兄太强了\",\n  \t\t\t\t\"sub_title\": \"正邪双魂逆袭爽！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/13713d7120e60cc5dedbef6118057357e9a9c6e5.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss90883\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 24298,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"非人哉\",\n  \t\t\t\t\"sub_title\": \"神话人物的日常生活\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/8f96b3cb7a5018f7fb077549248568e2fcac3b2a.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss24298\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39947,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"有兽焉\",\n  \t\t\t\t\"sub_title\": \"神兽下凡，福瑞相伴\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/8aa38d11d0d174e554903aa6ca24f98fc1c5def7.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39947\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48143,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.3\",\n  \t\t\t\t\"left_subscript\": \"原创\",\n  \t\t\t\t\"title\": \"时光代理人 英都篇\",\n  \t\t\t\t\"sub_title\": \"无论过去，不问将来\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/c3566191a329529014cbcc1cb141ba24b2baf366.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48143\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 26257,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"8.2\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"三体\",\n  \t\t\t\t\"sub_title\": \"面壁计划开启\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/9870f898b8a39bbb8048f34317f8d78a02cc1770.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss26257\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 5626,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"7.4\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"镇魂街 第二季\",\n  \t\t\t\t\"sub_title\": \"镇魂将之间的激斗\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/6a762ae614e567fc5c322c8cb240bcd4d1e06969.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss5626\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33323,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"雾山五行\",\n  \t\t\t\t\"sub_title\": \"热血水墨国风动画\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/c6d4a6d2f601aceb3b18124352e3cebd4c6e2e02.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33323\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39666,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.1\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"时光代理人 第二季\",\n  \t\t\t\t\"sub_title\": \"无论过去，不问将来\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/88f464de635f87a3a8cec8fbb2b106221efd84e6.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39666\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48315,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"7.7\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"伍六七之记忆碎片\",\n  \t\t\t\t\"sub_title\": \"伍六七身世之谜揭开\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/4ca65598089129eb1cb5916f97dbb2c5f032ae88.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48315\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 73961,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"小说改\",\n  \t\t\t\t\"title\": \"君有云 第二季\",\n  \t\t\t\t\"sub_title\": \"终其一生，唯君有云\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/a36c6e90cf612d58a99488c54af53c965b6cc863.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss73961\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 35213,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"8.6\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"永生\",\n  \t\t\t\t\"sub_title\": \"仙魔双修，唯我独尊\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/739a202bc7dd0efc2d9c7d2a130dd705324f928f.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss35213\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 43547,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"漫画改\",\n  \t\t\t\t\"title\": \"镇魂街 第三季\",\n  \t\t\t\t\"sub_title\": \"镇魂将至，故人归来\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/dbf13f1b3a55d9adc9fe5dcfc095b9835aabdef2.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss43547\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47853,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"喜羊羊与灰太狼\",\n  \t\t\t\t\"sub_title\": \"别看我只是一只羊~\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/4cd5d75b3ac57d30a114bbe21a9dacd4f7c2fa81.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47853\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 1733,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"罗小黑战记\",\n  \t\t\t\t\"sub_title\": \"有生之年\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/b00ab23001f8b73e7ed09fe55c1af14781f27d14.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss1733\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 28758,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"猫之茗\",\n  \t\t\t\t\"sub_title\": \"穿越成猫耳魔法师\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/5d8d1f7613f9b5ffc3cec898a7daf032e67ae052.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss28758\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47144,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"熊出没\",\n  \t\t\t\t\"sub_title\": \"熊大熊二保护森林\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/fb6ca695ef8e93ea8cd8fde26ecddd2278cb1d8b.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47144\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39659,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"第一序列\",\n  \t\t\t\t\"sub_title\": \"人类文明依然屹立\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/5188f5b8a775b2d72581a6f83d1228db79d2d63e.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39659\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 97550,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"搞笑\",\n  \t\t\t\t\"title\": \"虫小绿历史为什么之穿越篇\",\n  \t\t\t\t\"sub_title\": \"与虫小绿一穿越古代\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/51092965ade79fd4c9277219e88637e8e46ace86.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss97550\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 91575,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.3\",\n  \t\t\t\t\"left_subscript\": \"原创\",\n  \t\t\t\t\"title\": \"凸变英雄X 日语版\",\n  \t\t\t\t\"sub_title\": \"人人皆可为英雄\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/9580fba03884c8e0298683cb2302793324c787e1.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss91575\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 26196,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"我家大师兄是个反派\",\n  \t\t\t\t\"sub_title\": \"你是正道？还是魔！\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/80b37298d6aa9c5d687c0446b5f63f6407a3ee2a.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss26196\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 26194,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"4.7\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"仙王的日常生活\",\n  \t\t\t\t\"sub_title\": \"因为太强而苦恼\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/54bb0aa41490093244c33422883729dc36efe146.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss26194\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 44176,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.2\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"伍六七之暗影宿命\",\n  \t\t\t\t\"sub_title\": \"打破暗影刺客的宿命\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/6e5c5848ff8943fd6366e943a4f75b98598c50c6.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss44176\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39696,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"7.8\",\n  \t\t\t\t\"left_subscript\": \"\",\n  \t\t\t\t\"title\": \"仙王的日常生活 第三季\",\n  \t\t\t\t\"sub_title\": \"地表最强男人又来了\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/96f86387f98d5dfcb864925f419431eb00577651.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39696\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}]\n  \t\t}, {\n  \t\t\t\"tap_title\": \"综艺\",\n  \t\t\t\"tap_type\": 6,\n  \t\t\t\"seasons\": [{\n  \t\t\t\t\"season_id\": 95313,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.6\",\n  \t\t\t\t\"left_subscript\": \"音乐\",\n  \t\t\t\t\"title\": \"天赐的声音 第6季\",\n  \t\t\t\t\"sub_title\": \"推荐金曲争夺战\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/6dead58cd29bf75a46db57161f055ceaebefae48.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss95313\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39256,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.2\",\n  \t\t\t\t\"left_subscript\": \"音乐\",\n  \t\t\t\t\"title\": \"我的音乐你听吗\",\n  \t\t\t\t\"sub_title\": \"B站年度原创音综\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/58fc33109292a14e9861b50a24a189bd24e02807.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39256\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33039,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"极限挑战 第1季\",\n  \t\t\t\t\"sub_title\": \"跟男人帮挑战极限\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/4818cb0854bab948631d0a23673fb0d973b80c54.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33039\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33043,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"极限挑战 第3季\",\n  \t\t\t\t\"sub_title\": \"笑点和泪点出其不意\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/e96f282a38df98e3cef790b68ad355c6f9353291.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33043\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33930,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.3\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"奔跑吧兄弟 第1季\",\n  \t\t\t\t\"sub_title\": \"下饭综艺再来亿遍\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/a301c006f7326c5445ed2ff2998aa3e1dcf3dacf.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33930\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33041,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"极限挑战 第2季\",\n  \t\t\t\t\"sub_title\": \"极挑男人帮这就是命\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/4745a2a3fc99ce4066e60f6ebcee4877143c5dde.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33041\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 48928,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"音乐\",\n  \t\t\t\t\"title\": \"2024最美的夜 bilibili跨年晚会\",\n  \t\t\t\t\"sub_title\": \"跨年音乐现场狂欢\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/c086af8102fbec8a19c35ca0ae4f6ee7e01b2d96.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss48928\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33795,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"7\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"奔跑吧兄弟 第4季\",\n  \t\t\t\t\"sub_title\": \"经典IP再来亿遍\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/3ff61ef89717e8261333d74dc89da507079852e6.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33795\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 34053,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"音乐\",\n  \t\t\t\t\"title\": \"说唱新世代\",\n  \t\t\t\t\"sub_title\": \"万物皆可说唱\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/e797e6d180d9f198a0cdc76fd75e50f29c745f25.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss34053\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47737,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.7\",\n  \t\t\t\t\"left_subscript\": \"音乐\",\n  \t\t\t\t\"title\": \"天赐的声音 第5季\",\n  \t\t\t\t\"sub_title\": \"推荐金曲争夺战\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/bd3a1bc83a6cd2fd3da63568c0e3394c2484c61b.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47737\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33999,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"7.1\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"奔跑吧 第1季\",\n  \t\t\t\t\"sub_title\": \"奔跑吧伐木累\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/263dbd735331f4abd08199a4bbb223cf8e405125.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33999\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 92082,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"职场\",\n  \t\t\t\t\"title\": \"今晚不加班\",\n  \t\t\t\t\"sub_title\": \"职场人专属下班综艺\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/9793264f50220040c72eafee094e8728e150cc14.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss92082\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33914,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"7.7\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"奔跑吧兄弟 第3季\",\n  \t\t\t\t\"sub_title\": \"干大事！兄弟一起上\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/eb2bff1c40dbef4e62cebb292f366d0db7b4b7d8.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33914\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33929,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"7.7\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"奔跑吧兄弟 第2季\",\n  \t\t\t\t\"sub_title\": \"好兄弟就奔跑吧\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/55071eefcd545309de16db999aa405be9a5e0f01.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33929\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45833,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"梦想改造家 第3季\",\n  \t\t\t\t\"sub_title\": \"挑战13套房屋改造\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/932fe3f3cfe6e87c903e0056cf3d12e677103de7.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45833\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 96546,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"音乐\",\n  \t\t\t\t\"title\": \"永远22！2025bilibili毕业歌会\",\n  \t\t\t\t\"sub_title\": \"万人心跳 同频共振\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/1a9780a1a5566127ae53e35bc76f2dd8cade5855.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss96546\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33044,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.7\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"极限挑战 第4季\",\n  \t\t\t\t\"sub_title\": \"向美好生活出发\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/899d2909e71970e9cf153d872279705528329483.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33044\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 37761,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"超级变变变\",\n  \t\t\t\t\"sub_title\": \"创意竞赛！干掉无聊\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/e3b53f0b1b3db8b6e524a2e5e791bf51643c9ffa.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss37761\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 34038,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"6.7\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"奔跑吧 第2季\",\n  \t\t\t\t\"sub_title\": \"保持奔跑！\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/0e6b00fbcf0be62e82a1c6ecc306cb0db1e1d9fc.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss34038\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 38318,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"8.9\",\n  \t\t\t\t\"left_subscript\": \"脱口秀\",\n  \t\t\t\t\"title\": \"康熙来了 2014\",\n  \t\t\t\t\"sub_title\": \"下饭综艺回来啦\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/efb99941602b0fc22a241ec5dad0c406bcf8eea6.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss38318\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 35905,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.2\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"我是特优声\",\n  \t\t\t\t\"sub_title\": \"声音怪物集结\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/c2a9dce9e54e96964c5c31905a9208258152d5d9.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss35905\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39117,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"8.6\",\n  \t\t\t\t\"left_subscript\": \"访谈\",\n  \t\t\t\t\"title\": \"康熙来了 2011\",\n  \t\t\t\t\"sub_title\": \"无脑欢乐多笑成猪叫\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/bf75625315da514581912aaaaba8ef3b9e72a902.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39117\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 33821,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.1\",\n  \t\t\t\t\"left_subscript\": \"访谈\",\n  \t\t\t\t\"title\": \"康熙来了 2015\",\n  \t\t\t\t\"sub_title\": \"爆笑神综\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/8845ba3b409fe31c70a2b1c2272f3b60e06885fa.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss33821\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 41442,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"访谈\",\n  \t\t\t\t\"title\": \"非正式会谈 第7季\",\n  \t\t\t\t\"sub_title\": \"外国人用中文搞事情\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/3dcca94624af6a020a35b0d17a03179a34bc08c1.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss41442\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 79829,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"晚会\",\n  \t\t\t\t\"title\": \"2025年中央广播电视总台春节联欢晚会\",\n  \t\t\t\t\"sub_title\": \"巳巳如意，生生不息\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/26c2b1106131d39241fd248d8f953cc8d9f23109.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss79829\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 45202,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.3\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"奇迹焕新家\",\n  \t\t\t\t\"sub_title\": \"专注“妈见打”装修\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/109f74bd48a385475ce2138cd4c8f7af20245531.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss45202\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 46137,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.6\",\n  \t\t\t\t\"left_subscript\": \"访谈\",\n  \t\t\t\t\"title\": \"非正式会谈 第8季\",\n  \t\t\t\t\"sub_title\": \"分享各国趣闻\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/f784a15cea5d11a9f1ae29e5e6b57a1daf806f6e.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss46137\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 34686,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9\",\n  \t\t\t\t\"left_subscript\": \"脱口秀\",\n  \t\t\t\t\"title\": \"康熙来了 2013\",\n  \t\t\t\t\"sub_title\": \"下饭综艺回来啦\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/35c7ae3df0225bcf5597384fb2249ef7bdeef6d1.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss34686\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47706,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/403e4e8492f9c275490bf11fcf0f0ca68bbe0bbf.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"音乐\",\n  \t\t\t\t\"title\": \"永远22！2024bilibili毕业歌会\",\n  \t\t\t\t\"sub_title\": \"万人心跳 同频共振\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/c61dea3cbc2ac597207b8541e955f13a53fd9016.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47706\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39213,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.9\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"你好生活 第3季\",\n  \t\t\t\t\"sub_title\": \"再度被节目治愈\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/e7d53a2309da297a76dc19d285239e7841745c6d.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39213\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 32363,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.5\",\n  \t\t\t\t\"left_subscript\": \"脱口秀\",\n  \t\t\t\t\"title\": \"今夜百乐门 \",\n  \t\t\t\t\"sub_title\": \"青岛大姨名场面回顾\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/b62416c4704599a4d9a0cce9c3257cb9674d6c9d.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss32363\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 34109,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"8.7\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"王牌对王牌 第5季\",\n  \t\t\t\t\"sub_title\": \"王牌家族等你来玩\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/ea449420304765fffc651c395ffa7a51b940fe59.jpg\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss34109\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 39047,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.4\",\n  \t\t\t\t\"left_subscript\": \"访谈\",\n  \t\t\t\t\"title\": \"康熙来了 2010\",\n  \t\t\t\t\"sub_title\": \"下饭神综回来了\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/861c2990681d33f90ce914e670133e1273f27ea5.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss39047\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 38316,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"9.4\",\n  \t\t\t\t\"left_subscript\": \"访谈\",\n  \t\t\t\t\"title\": \"康熙来了 2004\",\n  \t\t\t\t\"sub_title\": \"考古康熙好笑到炸裂\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/8cc29702578aa07348a6d769588c6f6805b20c3e.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss38316\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 47860,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/vip/7a099c1d698eeb9036a4f1c4716c25d3cc1fb3ec.png\",\n  \t\t\t\t\"right_subscript\": \"9.8\",\n  \t\t\t\t\"left_subscript\": \"真人秀\",\n  \t\t\t\t\"title\": \"此生要去的100个地方\",\n  \t\t\t\t\"sub_title\": \"共赴祖国大好河山！\",\n  \t\t\t\t\"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/a9f0c60f74a21b9bd3f1451f61912d4df5dd403c.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss47860\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}, {\n  \t\t\t\t\"season_id\": 38838,\n  \t\t\t\t\"right_superscript\": \"http://i0.hdslb.com/bfs/bangumi/image/e31027bd41fb1cec4cbd1477f09799d0749bdf88.png\",\n  \t\t\t\t\"right_subscript\": \"\",\n  \t\t\t\t\"left_subscript\": \"访谈\",\n  \t\t\t\t\"title\": \"康熙来了 2012\",\n  \t\t\t\t\"sub_title\": \"爆笑神综\",\n  \t\t\t\t\"cover\": \"http://i0.hdslb.com/bfs/bangumi/image/f9bcd6e6b8820b1717e675249cdc18e114b3a22c.png\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ss38838\",\n  \t\t\t\t\"track_params\": null\n  \t\t\t}]\n  \t\t}],\n  \t\t\"draw_cards_welfare\": [],\n  \t\t\"free_welfare\": [],\n  \t\t\"buoy\": {\n  \t\t\t\"id\": 0,\n  \t\t\t\"img\": \"\",\n  \t\t\t\"link\": \"\",\n  \t\t\t\"track_params\": null\n  \t\t},\n  \t\t\"extra_params\": {\n  \t\t\t\"switch_on\": false,\n  \t\t\t\"birthday_sku_switch_on\": false,\n  \t\t\t\"is_buy_birthday_sku\": false,\n  \t\t\t\"is_birthday_off\": false,\n  \t\t\t\"phone_bind_state\": 0,\n  \t\t\t\"app_times\": 0,\n  \t\t\t\"na_ab\": 0,\n  \t\t\t\"na_ab_group_id\": \"\",\n  \t\t\t\"is_same_to_last_session\": false,\n  \t\t\t\"is_white_list\": false,\n  \t\t\t\"cloud_ab\": 0,\n  \t\t\t\"cloud_ab_group_id\": \"\",\n  \t\t\t\"banner_ab\": 0,\n  \t\t\t\"banner_ab_group_id\": \"\",\n  \t\t\t\"now_time\": 0,\n  \t\t\t\"free_welfare_ab\": 0,\n  \t\t\t\"device_limit_ab\": 0,\n  \t\t\t\"offline_ab\": 0,\n  \t\t\t\"welfare_module_height_ab\": 0\n  \t\t}\n  \t}\n  }\n  ```\n}\n"
  },
  {
    "path": "bruno/api.bilibili.com/x/vip/web/vip_center/v2.bru",
    "content": "meta {\n  name: v2\n  type: http\n  seq: 1\n}\n\nget {\n  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\n  body: none\n  auth: inherit\n}\n\nparams:query {\n  access_key: {{access_key}}\n  act_id: 872\n  appkey: {{appKey}}\n  build: 8451100\n  csrf: {{csrf}}\n  device: phone\n  disable_rcmd: 0\n  is_selected: false\n  mobi_app: android\n  platform: android\n  select_modules: VipExclusive,BigPoint\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"8.45.1\",\"abtest\":\"\"}\n  ts: 1748751549\n  web_location: 666.146\n  sign: 6e336f0cc44606c32560ee09b6ae23fa\n}\n\nheaders {\n  Host: api.bilibili.com\n  Cookie: {{cookieStr}}\n  accept: application/json, text/plain, */*\n  bili-http-engine: ignet\n  buvid: {{buvid}}\n  native_api_from: h5\n  referer: https://big.bilibili.com/mobile/index\n  user-agent: {{user-agent}}\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-metadata-legal-region: CN\n  x-bili-mid: {{mid}}\n  x-bili-net-bin: DQAAgL8gAQ\n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDg3NzU1OTksImlhdCI6MTc0ODc0NjQ5OSwiYnV2aWQiOiJYVUE1NjUxQTlFREY3Mzg3MTUzQTk0NUNERTk2Q0FEQ0I2MDAwIn0.k0x2o3e2Q3W-6Wzc56IhbLgSjDKTaAuUV9om7K213fI\n  x-bili-trace-id: 14fbd3682d7fc79650e739ba8d683bd4:50e739ba8d683bd4:0:0\n}\n\ndocs {\n  Response sample:\n  \n  ```json\n  {\n  \t\"code\": 0,\n  \t\"message\": \"0\",\n  \t\"ttl\": 1,\n  \t\"data\": {\n  \t\t\"panel_info\": {\n  \t\t\t\"face\": {\n  \t\t\t\t\"container_size\": {\n  \t\t\t\t\t\"width\": 1,\n  \t\t\t\t\t\"height\": 1\n  \t\t\t\t},\n  \t\t\t\t\"fallback_layers\": {\n  \t\t\t\t\t\"layers\": [{\n  \t\t\t\t\t\t\"visible\": true,\n  \t\t\t\t\t\t\"general_spec\": {\n  \t\t\t\t\t\t\t\"pos_spec\": {\n  \t\t\t\t\t\t\t\t\"coordinate_pos\": 2,\n  \t\t\t\t\t\t\t\t\"axis_x\": 0.5,\n  \t\t\t\t\t\t\t\t\"axis_y\": 0.5\n  \t\t\t\t\t\t\t},\n  \t\t\t\t\t\t\t\"size_spec\": {\n  \t\t\t\t\t\t\t\t\"width\": 1.0555555555555556,\n  \t\t\t\t\t\t\t\t\"height\": 1.0555555555555556\n  \t\t\t\t\t\t\t},\n  \t\t\t\t\t\t\t\"render_spec\": {\n  \t\t\t\t\t\t\t\t\"opacity\": 1\n  \t\t\t\t\t\t\t}\n  \t\t\t\t\t\t},\n  \t\t\t\t\t\t\"layer_config\": {\n  \t\t\t\t\t\t\t\"tags\": {\n  \t\t\t\t\t\t\t\t\"AVATAR_LAYER\": {},\n  \t\t\t\t\t\t\t\t\"GENERAL_CFG\": {\n  \t\t\t\t\t\t\t\t\t\"config_type\": 1,\n  \t\t\t\t\t\t\t\t\t\"general_config\": {\n  \t\t\t\t\t\t\t\t\t\t\"web_css_style\": {\n  \t\t\t\t\t\t\t\t\t\t\t\"background-color\": \"rgb(255,255,255)\",\n  \t\t\t\t\t\t\t\t\t\t\t\"border\": \"1px solid rgba(255,255,255,1)\",\n  \t\t\t\t\t\t\t\t\t\t\t\"borderRadius\": \"50%\",\n  \t\t\t\t\t\t\t\t\t\t\t\"boxSizing\": \"border-box\"\n  \t\t\t\t\t\t\t\t\t\t}\n  \t\t\t\t\t\t\t\t\t}\n  \t\t\t\t\t\t\t\t}\n  \t\t\t\t\t\t\t},\n  \t\t\t\t\t\t\t\"is_critical\": true,\n  \t\t\t\t\t\t\t\"allow_over_paint\": true\n  \t\t\t\t\t\t},\n  \t\t\t\t\t\t\"resource\": {\n  \t\t\t\t\t\t\t\"res_type\": 3,\n  \t\t\t\t\t\t\t\"res_image\": {\n  \t\t\t\t\t\t\t\t\"image_src\": {\n  \t\t\t\t\t\t\t\t\t\"src_type\": 1,\n  \t\t\t\t\t\t\t\t\t\"placeholder\": 6,\n  \t\t\t\t\t\t\t\t\t\"remote\": {\n  \t\t\t\t\t\t\t\t\t\t\"url\": \"https://i2.hdslb.com/bfs/face/0fed8598ff2ae289bbb7efe62fba30914f906da9.jpg\",\n  \t\t\t\t\t\t\t\t\t\t\"bfs_style\": \"widget-layer-avatar\"\n  \t\t\t\t\t\t\t\t\t}\n  \t\t\t\t\t\t\t\t}\n  \t\t\t\t\t\t\t}\n  \t\t\t\t\t\t}\n  \t\t\t\t\t}, {\n  \t\t\t\t\t\t\"visible\": true,\n  \t\t\t\t\t\t\"general_spec\": {\n  \t\t\t\t\t\t\t\"pos_spec\": {\n  \t\t\t\t\t\t\t\t\"coordinate_pos\": 1,\n  \t\t\t\t\t\t\t\t\"axis_x\": 0.638888888888889,\n  \t\t\t\t\t\t\t\t\"axis_y\": 0.638888888888889\n  \t\t\t\t\t\t\t},\n  \t\t\t\t\t\t\t\"size_spec\": {\n  \t\t\t\t\t\t\t\t\"width\": 0.38888888888888884,\n  \t\t\t\t\t\t\t\t\"height\": 0.38888888888888884\n  \t\t\t\t\t\t\t},\n  \t\t\t\t\t\t\t\"render_spec\": {\n  \t\t\t\t\t\t\t\t\"opacity\": 1\n  \t\t\t\t\t\t\t}\n  \t\t\t\t\t\t},\n  \t\t\t\t\t\t\"layer_config\": {\n  \t\t\t\t\t\t\t\"tags\": {\n  \t\t\t\t\t\t\t\t\"GENERAL_CFG\": {\n  \t\t\t\t\t\t\t\t\t\"config_type\": 1,\n  \t\t\t\t\t\t\t\t\t\"general_config\": {\n  \t\t\t\t\t\t\t\t\t\t\"web_css_style\": {\n  \t\t\t\t\t\t\t\t\t\t\t\"background-color\": \"rgb(255,255,255)\",\n  \t\t\t\t\t\t\t\t\t\t\t\"border\": \"1px solid rgba(255,255,255,1)\",\n  \t\t\t\t\t\t\t\t\t\t\t\"borderRadius\": \"50%\",\n  \t\t\t\t\t\t\t\t\t\t\t\"boxSizing\": \"border-box\"\n  \t\t\t\t\t\t\t\t\t\t}\n  \t\t\t\t\t\t\t\t\t}\n  \t\t\t\t\t\t\t\t},\n  \t\t\t\t\t\t\t\t\"ICON_LAYER\": {}\n  \t\t\t\t\t\t\t},\n  \t\t\t\t\t\t\t\"allow_over_paint\": true\n  \t\t\t\t\t\t},\n  \t\t\t\t\t\t\"resource\": {\n  \t\t\t\t\t\t\t\"res_type\": 3,\n  \t\t\t\t\t\t\t\"res_image\": {\n  \t\t\t\t\t\t\t\t\"image_src\": {\n  \t\t\t\t\t\t\t\t\t\"src_type\": 1,\n  \t\t\t\t\t\t\t\t\t\"placeholder\": 1,\n  \t\t\t\t\t\t\t\t\t\"remote\": {\n  \t\t\t\t\t\t\t\t\t\t\"url\": \"https://i0.hdslb.com/bfs/bangumi/kt/aba51485c0d02940c89aeefcf6680510d9858472.png\",\n  \t\t\t\t\t\t\t\t\t\t\"bfs_style\": \"widget-layer-avatar\"\n  \t\t\t\t\t\t\t\t\t}\n  \t\t\t\t\t\t\t\t}\n  \t\t\t\t\t\t\t}\n  \t\t\t\t\t\t}\n  \t\t\t\t\t}],\n  \t\t\t\t\t\"is_critical_group\": true\n  \t\t\t\t},\n  \t\t\t\t\"mid\": \"341688380\"\n  \t\t\t},\n  \t\t\t\"label\": {\n  \t\t\t\t\"text\": \"年度大会员\",\n  \t\t\t\t\"label_theme\": \"annual_vip\",\n  \t\t\t\t\"text_color\": \"#FFFFFF\",\n  \t\t\t\t\"bg_style\": 1,\n  \t\t\t\t\"bg_color\": \"#FB7299\",\n  \t\t\t\t\"border_color\": \"\",\n  \t\t\t\t\"use_img_label\": true,\n  \t\t\t\t\"img_label_uri_hans\": \"\",\n  \t\t\t\t\"img_label_uri_hant\": \"\",\n  \t\t\t\t\"img_label_uri_hans_static\": \"https://i0.hdslb.com/bfs/bangumi/kt/629e28d4426f1b44af1131ade99d27741cc61d4b.png\",\n  \t\t\t\t\"img_label_uri_hant_static\": \"https://i0.hdslb.com/bfs/activity-plat/static/20220614/e369244d0b14644f5e1a06431e22a4d5/VEW8fCC0hg.png\"\n  \t\t\t},\n  \t\t\t\"panel_title\": \"TM说的\",\n  \t\t\t\"panel_sub_title\": \"大会员已连续陪伴17天\",\n  \t\t\t\"panel_sub_title2\": \"已连续陪伴17天\",\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"立即续费\",\n  \t\t\t\t\"action_type\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t},\n  \t\t\t\"vip_overdue_explain\": \"大会员 2026年05月18日到期\",\n  \t\t\t\"tv_overdue_explain\": \"超级大会员 尚未开通\",\n  \t\t\t\"num_of_assets\": 13,\n  \t\t\t\"UserFace\": \"https://i2.hdslb.com/bfs/face/0fed8598ff2ae289bbb7efe62fba30914f906da9.jpg\",\n  \t\t\t\"offline_activity_link\": \"https://www.bilibili.com/blackboard/activity-vlCEesbdFB.html\"\n  \t\t},\n  \t\t\"notice\": {\n  \t\t\t\"text\": \"\",\n  \t\t\t\"tv_text\": \"\",\n  \t\t\t\"type\": 0,\n  \t\t\t\"can_close\": false,\n  \t\t\t\"surplus_seconds\": 30282051,\n  \t\t\t\"tv_surplus_seconds\": -1748751549,\n  \t\t\t\"account_exception_text\": \"\",\n  \t\t\t\"link\": \"\",\n  \t\t\t\"unique_key\": \"\"\n  \t\t},\n  \t\t\"vip_info\": {\n  \t\t\t\"mid\": 341688380,\n  \t\t\t\"vip_type\": 2,\n  \t\t\t\"vip_status\": 1,\n  \t\t\t\"vip_due_date\": 1779033600,\n  \t\t\t\"vip_pay_type\": 0,\n  \t\t\t\"vip_is_new_user\": false,\n  \t\t\t\"vip_is_annual\": true,\n  \t\t\t\"vip_is_month\": false,\n  \t\t\t\"is_auto_renew\": false,\n  \t\t\t\"vip_is_valid\": true,\n  \t\t\t\"vip_is_overdue\": false,\n  \t\t\t\"vip_keep_time\": 1747398345,\n  \t\t\t\"vip_expire_days\": 351,\n  \t\t\t\"vip_remain_days\": 17,\n  \t\t\t\"tv_vip_type\": 0,\n  \t\t\t\"tv_vip_pay_type\": 0,\n  \t\t\t\"tv_status\": 0,\n  \t\t\t\"tv_due_date\": 0,\n  \t\t\t\"tv_is_sign_surplus\": false,\n  \t\t\t\"tv_is_auto_renew\": false,\n  \t\t\t\"nickname_color\": \"#FB7299\",\n  \t\t\t\"label\": {\n  \t\t\t\t\"text\": \"年度大会员\",\n  \t\t\t\t\"label_theme\": \"annual_vip\",\n  \t\t\t\t\"text_color\": \"#FFFFFF\",\n  \t\t\t\t\"bg_style\": 1,\n  \t\t\t\t\"bg_color\": \"#FB7299\",\n  \t\t\t\t\"border_color\": \"\",\n  \t\t\t\t\"use_img_label\": true,\n  \t\t\t\t\"img_label_uri_hans\": \"\",\n  \t\t\t\t\"img_label_uri_hant\": \"\",\n  \t\t\t\t\"img_label_uri_hans_static\": \"https://i0.hdslb.com/bfs/bangumi/kt/629e28d4426f1b44af1131ade99d27741cc61d4b.png\",\n  \t\t\t\t\"img_label_uri_hant_static\": \"https://i0.hdslb.com/bfs/activity-plat/static/20220614/e369244d0b14644f5e1a06431e22a4d5/VEW8fCC0hg.png\"\n  \t\t\t},\n  \t\t\t\"vip_role\": 3\n  \t\t},\n  \t\t\"privileges\": {\n  \t\t\t\"list\": [{\n  \t\t\t\t\"id\": 3,\n  \t\t\t\t\"name\": \"大视听\",\n  \t\t\t\t\"child_privileges\": [{\n  \t\t\t\t\t\"first_id\": 3,\n  \t\t\t\t\t\"report_id\": \"\",\n  \t\t\t\t\t\"name\": \"点映会\",\n  \t\t\t\t\t\"desc\": \"线下免费抢先\",\n  \t\t\t\t\t\"explain\": \"大会员可免费参与专属的线下点映活动，十年及以上大会员有机会带1位好友共同参与，最终根据参与人数随机抽取。福利活动不定期开展，具体城市及内容以实际通知为准。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/a38a61d90e871bec04f4bb39f425fb3890256275.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E7%82%B9%E6%98%A0%E4%BC%9A\\u0026closable=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/7e30545c23658da326434449429192365ebe311c.jpg\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 89\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 3,\n  \t\t\t\t\t\"report_id\": \"freewatch\",\n  \t\t\t\t\t\"name\": \"免费看\",\n  \t\t\t\t\t\"desc\": \"会员用户免费看\",\n  \t\t\t\t\t\"explain\": \"需要付费才能观看的影视内容，大会员可以免费观看（播放页面提示“大会员半价”的除外，部分视频仅限在中国大陆观看）。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/55644358e506de5c55ae2447d51d1a59ad923eba.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"http://i0.hdslb.com/bfs/vip/b8ea804c872fb2b096715f52b87deb0e6cdfd476.png\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E5%85%8D%E8%B4%B9%E7%9C%8B\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/22c3735f9db313b7be35d87c1b5dd6da81cea48e.jpg\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 1,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 1\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 3,\n  \t\t\t\t\t\"report_id\": \"clearwatch\",\n  \t\t\t\t\t\"name\": \"超清看\",\n  \t\t\t\t\t\"desc\": \"会员用户超清晰观看\",\n  \t\t\t\t\t\"explain\": \"大会员可专享高帧率、高码率画质（最高可达超清4k），觉醒超凡视觉体验。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/f6714a06c34ac32b54cae4022a6f6b07b5c731aa.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"http://i0.hdslb.com/bfs/vip/21d79540e10618ee9bbaf8874ae711442d10edf0.png\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E8%B6%85%E6%B8%85%E7%9C%8B\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/fbe4cf2288571d7b0a509c7014d5182789ffdd74.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 1,\n  \t\t\t\t\t\"id\": 3\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 3,\n  \t\t\t\t\t\"report_id\": \"firstwatch\",\n  \t\t\t\t\t\"name\": \"抢先看\",\n  \t\t\t\t\t\"desc\": \"会员用户可以快人一步抢先观看\",\n  \t\t\t\t\t\"explain\": \"连载内容中需要付费抢先看的内容，大会员可以直接观看，不限次数。（部分视频仅限在中国大陆观看）\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/49762e82b09b6cfc572d8d8160f4dd72199c1403.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"http://i0.hdslb.com/bfs/vip/20b40771e4bf180a606ddc021dfdfe6a7e56b713.png\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E6%8A%A2%E5%85%88%E7%9C%8B\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/21c0f30302944b694a12f12cbf4ee02733e1e580.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 2\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 3,\n  \t\t\t\t\t\"report_id\": \"halfprice\",\n  \t\t\t\t\t\"name\": \"半价点播\",\n  \t\t\t\t\t\"desc\": \"付费内容半价即享\",\n  \t\t\t\t\t\"explain\": \"部分付费点播内容，大会员可享受半价购买。购买成功后，48小时内不限次数观看该影片（部分内容仅限在中国大陆观看）。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/53f0df779fcf3b394a3f01bf9d837a061ce3de49.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"http://i0.hdslb.com/bfs/vip/3a613880463e01c8f9496f3b571e198a111191e5.png\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E5%8D%8A%E4%BB%B7%E7%82%B9%E6%92%AD\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/07f8b89c6d044723ece8a42f558d1e84041ff991.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 1,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 55\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 3,\n  \t\t\t\t\t\"report_id\": \"\",\n  \t\t\t\t\t\"name\": \"边下边播\",\n  \t\t\t\t\t\"desc\": \"追番看剧拒绝卡顿\",\n  \t\t\t\t\t\"explain\": \"大会员下载剧集时，已下载部分可以播放，不用等下载完成即可观看（仅限手机端使用）。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/6a37b055361b2f3a77e8a9ccfbd84c79d9efadcb.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E8%BE%B9%E4%B8%8B%E8%BE%B9%E6%92%AD\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/e3ae66f7c72056e95d252a33ebceac70d32a27cc.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 1,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 53\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 3,\n  \t\t\t\t\t\"report_id\": \"\",\n  \t\t\t\t\t\"name\": \"专属缓存\",\n  \t\t\t\t\t\"desc\": \"随时随地想看就看\",\n  \t\t\t\t\t\"explain\": \"海量番剧、国创、电影大片，大会员独享专属缓存特权。（仅限手机端使用，部分内容受版权或地区限制无法缓存）。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/74f9924fdc32fa68043521f9df7699a5ae80f835.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E4%B8%93%E5%B1%9E%E7%BC%93%E5%AD%98\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/6cf08314a94aeb5579e956aa40a2f37ab68baa2d.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 51\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 3,\n  \t\t\t\t\t\"report_id\": \"dolby\",\n  \t\t\t\t\t\"name\": \"杜比音效\",\n  \t\t\t\t\t\"desc\": \"更优质的听觉盛宴\",\n  \t\t\t\t\t\"explain\": \"大会员专享杜比音效（立体声、环绕声）以及杜比全景声，采用全新的音效技术，为你带来身临其中的听觉盛宴。(该权益仅可在移动端上部分内容支持使用)\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/2fe4fb620d7926df86d2819cb045b32596194560.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E6%9D%9C%E6%AF%94%E9%9F%B3%E6%95%88\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/08377c70a185359242cf5c83f1cd5ed5c8b3c057.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 59\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 3,\n  \t\t\t\t\t\"report_id\": \"hdr\",\n  \t\t\t\t\t\"name\": \"真彩HDR\",\n  \t\t\t\t\t\"desc\": \"更真实的视觉体验\",\n  \t\t\t\t\t\"explain\": \"哔哩哔哩提供基于HDR10技术的“真彩HDR”观影模式。HDR能够呈现更多的动态范围，细致优化画面中的明暗对比及色彩显示，更好的反映出真实环境中的视觉效果。使您可以享受到色彩细腻鲜艳，明暗层次丰富的高品质观影体验。\\r\\n注意事项： \\r\\n移动端请更新APP至6.9及以上版本；安卓机型需7及以上系统，iOS机型需13及以上系统，PC端仅部分浏览器支持。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/adea8d637e9dd4e2c9001357cd2cd4a36a743b68.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E7%9C%9F%E5%BD%A9HDR\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/240c332b35355cfbfd982aa7b3bc8e48b31672f0.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 1,\n  \t\t\t\t\t\"id\": 57\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 3,\n  \t\t\t\t\t\"report_id\": \"\",\n  \t\t\t\t\t\"name\": \"并行下载\",\n  \t\t\t\t\t\"desc\": \"3集一起下才够快\",\n  \t\t\t\t\t\"explain\": \"大会员下载视频时，至多可支持2-3个视频同时缓存（仅限手机端使用）。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/ababe7062afbb1e6240068662624f4b29c3a119a.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E5%B9%B6%E8%A1%8C%E4%B8%8B%E8%BD%BD\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/188e6a0905a3729905a3a053ffa7dad324705ca6.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 1,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 54\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 3,\n  \t\t\t\t\t\"report_id\": \"\",\n  \t\t\t\t\t\"name\": \"预约缓存\",\n  \t\t\t\t\t\"desc\": \"后台运行即更即存\",\n  \t\t\t\t\t\"explain\": \"连载内容尚未播出的剧集可提前预约缓存，新剧集上线后，第一时间在wifi环境下自动缓存下载到本地，省时省力追番更轻松（仅限手机端使用）。\\r\\n使用说明：\\r\\n1.此权益需要将哔哩哔哩APP设置后台自动运行状态；\\r\\n2.具体以可预约下载剧集的播出安排为准。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/035ebd10109133c9d1e63bd72aeef571fe7584cf.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E9%A2%84%E7%BA%A6%E7%BC%93%E5%AD%98\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/8e01e169551909bcaae6ed8b40759c4e1bae95cf.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 52\n  \t\t\t\t}]\n  \t\t\t}, {\n  \t\t\t\t\"id\": 4,\n  \t\t\t\t\"name\": \"大排面\",\n  \t\t\t\t\"child_privileges\": [{\n  \t\t\t\t\t\"first_id\": 4,\n  \t\t\t\t\t\"report_id\": \"\",\n  \t\t\t\t\t\"name\": \"漫展休息区\",\n  \t\t\t\t\t\"desc\": \"漫展享受免费休息区及系列专属活动\",\n  \t\t\t\t\t\"explain\": \"部分头部漫展设有大会员休息区，凭会员身份可入内休息充电，领取专属赠礼、与coser互动合影，实际体验以每场漫展为准。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/53dc3ad60da74161fcd03541c5faaba094239058.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=漫展休息区\\u0026closable=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/175fe0a0d628887407abfb2bd4a8f61976cc32df.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 93\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 4,\n  \t\t\t\t\t\"report_id\": \"\",\n  \t\t\t\t\t\"name\": \"生日礼\",\n  \t\t\t\t\t\"desc\": \"专属生日福利\",\n  \t\t\t\t\t\"explain\": \"大会员用户可以在生日周期内领取生日权益，生日周期为用户生日当天及生日后7天；在一个自然年内，仅可领取一次生日权益。生日权益以当前活动页展示内容为准。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/efa3c31ac28374c2feb41515d1eb583d6d54bdd5.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=生日礼\\u0026closable=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/4d992169ab45b5b58ee5c57dacbc859a330d4ce2.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 91\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 4,\n  \t\t\t\t\t\"report_id\": \"update\",\n  \t\t\t\t\t\"name\": \"身份升级\",\n  \t\t\t\t\t\"desc\": \"连续购买享受更高级权益\",\n  \t\t\t\t\t\"explain\": \"购买大会员连续累计时长超过366天，即可免费升级为年度大会员身份，升级后可立即享受粉色昵称、游戏礼包、B币券等年度大会员专享权益。\\r\\n注意：中断续费的话，年度大会员身份会收回哦~\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/bd245efc500d86d1433402b225278362a0db6d54.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E8%BA%AB%E4%BB%BD%E5%8D%87%E7%BA%A7\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/75e8a25686e7556877be4074f002c426afe8d4a6.jpg\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 56\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 4,\n  \t\t\t\t\t\"report_id\": \"\",\n  \t\t\t\t\t\"name\": \"等级加速\",\n  \t\t\t\t\t\"desc\": \"加速升级Lv6\",\n  \t\t\t\t\t\"explain\": \"Lv1-Lv6的在期大会员每日观看视频1分钟，可在会员中心额外获得10经验值用于社区等级提升。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/afdf447d23112dbd313da104ae875f34a7f1d464.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E7%AD%89%E7%BA%A7%E5%8A%A0%E9%80%9F\\u0026closable=1\",\n  \t\t\t\t\t\"image_url\": \"\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 88\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 4,\n  \t\t\t\t\t\"report_id\": \"boot_screen\",\n  \t\t\t\t\t\"name\": \"专属闪屏图片\",\n  \t\t\t\t\t\"desc\": \"解锁精美闪屏\",\n  \t\t\t\t\t\"explain\": \"大会员可选择会员专享的闪屏图片/视频，解锁精美开屏。具体路径为：我的-设置-开屏画面设置-自选模式-会员专享。注意事项：此功能仅在哔哩哔哩移动端7.32及以上版本可用。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/2cf33f1eda3df0250597e117ebd41404ef862f50.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"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\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/4d099bb587392d6445d25b36dff0ec2b071d30bc.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 1,\n  \t\t\t\t\t\"id\": 87\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 4,\n  \t\t\t\t\t\"report_id\": \"barrage_colour\",\n  \t\t\t\t\t\"name\": \"彩色弹幕\",\n  \t\t\t\t\t\"desc\": \"炫酷粉蓝走起\",\n  \t\t\t\t\t\"explain\": \"大会员用户在发布弹幕时，可在弹幕框中点击A进入颜色选择，选中大会员专属的粉蓝渐变色， 发送独特炫酷弹幕。注意事项：此功能仅在哔哩哔哩移动端7.32及以上版本、web端可用。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/3982a5110b5615caa15c892d990ea374e61ec583.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E5%BD%A9%E8%89%B2%E5%BC%B9%E5%B9%95\\u0026closable=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/4ca5f2d7b03c501f3c99174c0cb812a647ea9804.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 1,\n  \t\t\t\t\t\"id\": 86\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 4,\n  \t\t\t\t\t\"report_id\": \"gifcomment\",\n  \t\t\t\t\t\"name\": \"专属评论图片\",\n  \t\t\t\t\t\"desc\": \"评论发图不仅更大了、还能动了\",\n  \t\t\t\t\t\"explain\": \"大会员在视频播放页评论区发表的图片在预览时将被放大1.5倍。并且，大会员还能在视频播放页评论区发表动图。\\r\\n注意事项：\\r\\n1.此功能仅在哔哩哔哩手机端7.29.0及以上版本可用\\r\\n2.仅支持发表GIF格式、大小不超过14MB的动图\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/8a9ee4af815b7368737ed02a40f116f9f5c3ac88.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"http://i0.hdslb.com/bfs/vip/5df897b08912e60fc893cddaa0554d7564e34ae4.png\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"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\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/bf8159c3c6de16ac23b4f6f613af74119fa6afe0.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 1,\n  \t\t\t\t\t\"id\": 85\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 4,\n  \t\t\t\t\t\"report_id\": \"nickname\",\n  \t\t\t\t\t\"name\": \"粉色昵称\",\n  \t\t\t\t\t\"desc\": \"尊享闪亮粉色昵称\",\n  \t\t\t\t\t\"explain\": \"大会员的昵称将以粉色高亮显示。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/2aca4c6dbc50caaae08949d4ff7380700fdb6c07.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"http://i0.hdslb.com/bfs/vip/b67288e008a9fdfec0b13a27a527bb8db701c0d4.png\",\n  \t\t\t\t\t\"background_image_url\": \"http://i0.hdslb.com/bfs/vip/fa946f3c4011c28fe780d0cdd4da279fb996903f.png\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E7%B2%89%E8%89%B2%E6%98%B5%E7%A7%B0\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/b609d85d3e30450586653b245ac9772740ec184c.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 18\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 4,\n  \t\t\t\t\t\"report_id\": \"card\",\n  \t\t\t\t\t\"name\": \"动态卡片装扮\",\n  \t\t\t\t\t\"desc\": \"动态卡片装扮\",\n  \t\t\t\t\t\"explain\": \"大会员可以免费使用大会员专属动态卡片装扮，用于装扮自己的动态卡片，彰显不一样的自己！\\r\\n有效期内随意装扮，有效期结束后动态卡片装扮自动卸下~（当前仅限客户端）\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/fd97209767a0d713c62b9a9d4c58a69777f75a53.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"http://i0.hdslb.com/bfs/vip/8775308a302014e3bfbfa0dfc69faa2e8faeaa3a.png\",\n  \t\t\t\t\t\"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\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/21880645864fbdace3d4d0b52eb895551ecd536d.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 46\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 4,\n  \t\t\t\t\t\"report_id\": \"pendant\",\n  \t\t\t\t\t\"name\": \"专属挂件\",\n  \t\t\t\t\t\"desc\": \"专属挂件免费换\",\n  \t\t\t\t\t\"explain\": \"大会员可免费领取专属挂件，用于装扮自己的头像，展示在评论区、个人空间等等位置。有钱也买不到哦！\\r\\n有效期内可以随便领，有效期结束后挂件自动卸下~\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/dfdda1c6ee68d306061129aa63e8d889e80c8f8a.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"http://i0.hdslb.com/bfs/vip/ae16ed0dcf8246a28e45403243bc65eea0e7b4c7.png\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E4%B8%93%E5%B1%9E%E6%8C%82%E4%BB%B6\\u0026closable=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/1e6799197b0749c263dd8a28067c0e2b6327cab5.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 23\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 4,\n  \t\t\t\t\t\"report_id\": \"spacepicture\",\n  \t\t\t\t\t\"name\": \"空间自主头图\",\n  \t\t\t\t\t\"desc\": \"空间自主头图\",\n  \t\t\t\t\t\"explain\": \"大会员可上传个性化图片来装扮个人空间头图，让自己的空间独具魅力。\\r\\nweb端进入个人空间后，点击头图右上角更换头图时，可以上传自定义头图。\\r\\n手机客户端进入个人空间后，即可通过点击头图上的“小衣服”按钮更换头图。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/d406573095427320835eba3d20819a95c2a1de78.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"http://i0.hdslb.com/bfs/vip/0ebb039d9fefd37e094f0f181d7cfac9efd019be.png\",\n  \t\t\t\t\t\"background_image_url\": \"http://i0.hdslb.com/bfs/vip/6c32fe89bb56096fc963ed35118092744cb463b6.png\",\n  \t\t\t\t\t\"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\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/f0b521b39a941f0f7198fbe7884aa41af0817ffe.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 1,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 24\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 4,\n  \t\t\t\t\t\"report_id\": \"emoticon\",\n  \t\t\t\t\t\"name\": \"评论表情\",\n  \t\t\t\t\t\"desc\": \"评论有表情\",\n  \t\t\t\t\t\"explain\": \"会员可在评论中发送图片表情，表情多多，表情包常常更新哦。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/0a2199caf544263440dbc3e60722881f1a4b6381.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"http://i0.hdslb.com/bfs/vip/f882182fcbca520194d9047ca4903dc2c1e42372.png\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E8%AF%84%E8%AE%BA%E8%A1%A8%E6%83%85\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/bbb74fa3264ef9cc0ae1de15e4989b9473a0d6d3.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 1,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 22\n  \t\t\t\t}]\n  \t\t\t}, {\n  \t\t\t\t\"id\": 2,\n  \t\t\t\t\"name\": \"大福利\",\n  \t\t\t\t\"child_privileges\": [{\n  \t\t\t\t\t\"first_id\": 2,\n  \t\t\t\t\t\"report_id\": \"\",\n  \t\t\t\t\t\"name\": \"游戏代金券\",\n  \t\t\t\t\t\"desc\": \"多款游戏满10-3代金券\",\n  \t\t\t\t\t\"explain\": \"大会员在会员有效期内，每31天可领取1张适用于多款游戏的满10-3代金券。\\r\\n代金券适用于哔哩哔哩游戏中心下载的多款游戏，适用游戏名单每月随券更新；\\r\\n代金券有效期为每月1日至次月16日，具体适用游戏范围和有效期详见代金券说明；用户领取后可前往App“游戏中心--我的-右上角“代金券”查看游戏代金券，并在游戏提交内购订单时，选择代金券进行使用。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/1cd8ec97f28d030180671dd05026de692fc5fbac.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=游戏代金券\\u0026closable=1\",\n  \t\t\t\t\t\"image_url\": \"\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 95\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 2,\n  \t\t\t\t\t\"report_id\": \"\",\n  \t\t\t\t\t\"name\": \"漫画商城券\",\n  \t\t\t\t\t\"desc\": \"每月可领\",\n  \t\t\t\t\t\"explain\": \"年度大会员在会员有效期内，在哔哩哔哩APP\\\"我的”-“我的资产”及哔哩哔哩漫画APP“我的”-“卡券包\\\"-\\\"大会员特权”，每31天可领取1张漫画商城福利券。福利券仅可在哩哩漫画App中使用。\\r\\n(可在\\\"漫画商城\\\"全品类中使用)\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/317f64b8ff192bb37762499520aa985cb8d95492.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=漫画商城券\\u0026closable=1\",\n  \t\t\t\t\t\"image_url\": \"\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 94\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 2,\n  \t\t\t\t\t\"report_id\": \"\",\n  \t\t\t\t\t\"name\": \"演出观影特权\",\n  \t\t\t\t\t\"desc\": \"提前抢演唱会、优惠看电影\",\n  \t\t\t\t\t\"explain\": \"开通时长大于等于31天的大会员，在会员有效期内，每30天可领取1次以下权益：30天淘麦VIP体验会员身份，同时赠送300淘麦会员积分。淘麦VIP会员的使用需遵循《淘麦VIP会员服务协议》。 30天淘麦VIP体验会员有效期为自用户领取本权益之日起算(含领取当日)30天。大会员用户可以基于淘麦身份及积分兑换，享受演出提前抢及电影优惠。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/ea23fe0fcbf309eeded2f64f3e510140e2e075c1.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=演出观影特权\\u0026closable=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/395cb31f72505d3649d77e923283460b60602124.jpg\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 92\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 2,\n  \t\t\t\t\t\"report_id\": \"lesson\",\n  \t\t\t\t\t\"name\": \"课程折扣\",\n  \t\t\t\t\t\"desc\": \"课程5折优惠券\",\n  \t\t\t\t\t\"explain\": \"开通时长大于等于31天的大会员，在会员有效期内，每31天可领取1张课程5折优惠券，优惠券领取后15天内有效，具体适用范围及更多详情可前往：“哔哩哔哩App-我的-我的课程-券包”。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/0378963a07b8e866e8303892c1d21e358e3b4143.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E8%AF%BE%E7%A8%8B%E6%8A%98%E6%89%A3\\u0026closable=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/06f0cc1095c402595648db8deb5e7ba67cdec1df.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 1,\n  \t\t\t\t\t\"new_type\": 1,\n  \t\t\t\t\t\"id\": 84\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 2,\n  \t\t\t\t\t\"report_id\": \"discountcos\",\n  \t\t\t\t\t\"name\": \"装扮折扣\",\n  \t\t\t\t\t\"desc\": \"全场装扮8折起\",\n  \t\t\t\t\t\"explain\": \"大会员用户可在会员有效期内，可享受专属折扣购买个性装扮套装（除部分版权受限装扮以外），适用装扮和折扣详情可前往“哔哩哔哩App-我的-个性装扮”查看。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/320bf3cc05173e59c9af4d65e282ea23016b8431.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://www.bilibili.com/h5/mall/home?navhide=1\\u0026f_source=vip\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/eddd3cf6508e4304e47a6e1c93a78e24aa1d1149.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 1,\n  \t\t\t\t\t\"new_type\": 1,\n  \t\t\t\t\t\"id\": 83\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 2,\n  \t\t\t\t\t\"report_id\": \"freecos\",\n  \t\t\t\t\t\"name\": \"装扮体验\",\n  \t\t\t\t\t\"desc\": \"全场装扮免费体验\",\n  \t\t\t\t\t\"explain\": \"大会员用户可在会员有效期内，每31天可领取3张装扮体验卡，每张体验卡可体验穿戴任一装扮套装3天（除部分版权受限装扮以外）。体验卡领取后30天内有效，详情可前往“哔哩哔哩App-我的-个性装扮-我的装扮-体验卡”查看。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/569dc5289dc12b2cf8e8694c00862603437c6e30.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"\",\n  \t\t\t\t\t\"link\": \"https://www.bilibili.com/h5/mall/home?navhide=1\\u0026f_source=vip\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/801f7799d14786281d33adffd8cd998b3d7402f6.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 1,\n  \t\t\t\t\t\"new_type\": 1,\n  \t\t\t\t\t\"id\": 82\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 2,\n  \t\t\t\t\t\"report_id\": \"bcoupon\",\n  \t\t\t\t\t\"name\": \"B币券/观影券\",\n  \t\t\t\t\t\"desc\": \"特色权益二选一\",\n  \t\t\t\t\t\"explain\": \"开通大会员时长大于等于31天的年度大会员，在会员有效期内，每31天可领取1张5B币券或1张观影券。当月开通或升级的年度大会员，也可以立即领取。该权益属于附赠权益。\\r\\n观影券：领取后30天内有效，可用于兑换“电影”分区内需单片付费的1部的电影（实际应支付金额在6元及以内）的观看资格，兑换后观看有效期为48小时，请在规定有效期内兑换并观看，到期未使用不予补偿。\\r\\nB币券：领取后30天内有效，可用于版权内容点播、哔哩哔哩漫画、装扮等场景中的支付抵扣。赠送的B币券在使用时不再赠送会员积分，到期未使用不予补偿。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/2f5e098267b070ef42ba9ee58520b858e5bc00b7.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"http://i0.hdslb.com/bfs/vip/c3dcef2bfea737a1342dacea1027a3b299d3cf71.png\",\n  \t\t\t\t\t\"background_image_url\": \"http://i0.hdslb.com/bfs/vip/20de7f4e81775c4cec1a9653131e5b10c8c8f41d.png\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=B%E5%B8%81%E5%88%B8\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/11b77dd4ac7aabe42616031a7fe4b2a17f9e632d.png\",\n  \t\t\t\t\t\"type\": 1,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 19\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 2,\n  \t\t\t\t\t\"report_id\": \"vipmall\",\n  \t\t\t\t\t\"name\": \"会员购\",\n  \t\t\t\t\t\"desc\": \"会员购优惠券\",\n  \t\t\t\t\t\"explain\": \"开通时长大于等于31天的大会员，在会员有效期内，每31天可领取1张会员购10元包邮券；开通时长大于等于31天的年度大会员，在会员有效期内，每31天可领取1张会员购10元包邮券、1张会员购满50-10元优惠券。当月开通或升级的年度大会员，也可以立即领取；\\r\\n优惠券及包邮券有效期至领取后15天，具体有效期及使用范围详见优惠券说明;\\r\\n年度大会员可前往App“分区--会员购--右上角“优惠券”查看优惠券及包邮券，并前往App“分区--会员购”，在提交订单时选择优惠券及包邮券进行使用；该大会员特权需将哔哩哔哩APP升级至6.65版本及以上领取和使用。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/7384adee91e7afa1e2d2a97f0c2aaaab71f0e415.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"http://i0.hdslb.com/bfs/vip/972283284cfb7f3b3063b1b391aeeb4cbed3249d.png\",\n  \t\t\t\t\t\"background_image_url\": \"http://i0.hdslb.com/bfs/vip/21d79540e10618ee9bbaf8874ae711442d10edf0.png\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E4%BC%9A%E5%91%98%E8%B4%AD\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/9af168f768312da3c31df23c056f33b3dcaefe8a.jpg\",\n  \t\t\t\t\t\"type\": 1,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 1,\n  \t\t\t\t\t\"id\": 20\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 2,\n  \t\t\t\t\t\"report_id\": \"giftbag\",\n  \t\t\t\t\t\"name\": \"年度游戏礼包\",\n  \t\t\t\t\t\"desc\": \"不定期更新\",\n  \t\t\t\t\t\"explain\": \"年度大会员可以在游戏礼包中心领取不同游戏的多款超值礼包，礼包数量和内容常常更新。\\r\\n\\r\\n具体使用方法请参照各个礼包的使用详情。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/6af11bdd8bb30d187daa6215bc1f4098498caa9c.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"http://i0.hdslb.com/bfs/vip/c1c810d0ad13b325da6f3dbde1adb5f351adc55c.png\",\n  \t\t\t\t\t\"background_image_url\": \"http://i0.hdslb.com/bfs/vip/013b5c7b3ba45c7a0f4b7a4967cf55aca3c92e40.png\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E6%B8%B8%E6%88%8F%E7%A4%BC%E5%8C%85\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/80a5e1a19192ae65f2c267f1a672c3aaeb582447.png\",\n  \t\t\t\t\t\"type\": 1,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 21\n  \t\t\t\t}, {\n  \t\t\t\t\t\"first_id\": 2,\n  \t\t\t\t\t\"report_id\": \"comic\",\n  \t\t\t\t\t\"name\": \"漫读券\",\n  \t\t\t\t\t\"desc\": \"每月赠送漫画阅读券\",\n  \t\t\t\t\t\"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日。\",\n  \t\t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/3c542377eaa697620a5ff856ff994db6198cb8a6.png\",\n  \t\t\t\t\t\"icon_gray_url\": \"\",\n  \t\t\t\t\t\"background_image_url\": \"http://i0.hdslb.com/bfs/vip/ba0c9df7c41d6c23c3c2470b5dbbbd5cf4d3d9c2.png\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/explainDetails?name=%E6%BC%AB%E8%AF%BB%E5%88%B8\\u0026closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"image_url\": \"http://i0.hdslb.com/bfs/vip/8a0d392d0d509c4bdff76aa98ccc007cd22b65a9.png\",\n  \t\t\t\t\t\"type\": 0,\n  \t\t\t\t\t\"hot_type\": 0,\n  \t\t\t\t\t\"new_type\": 0,\n  \t\t\t\t\t\"id\": 47\n  \t\t\t\t}]\n  \t\t\t}],\n  \t\t\t\"privileges_covers\": [{\n  \t\t\t\t\"text\": \"免费看\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/wbpZMLYRH2.png\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}, {\n  \t\t\t\t\"text\": \"超清看\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/N32Mx1ZAPE.png\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}, {\n  \t\t\t\t\"text\": \"粉色昵称\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/7ODRSwp27r.png\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}, {\n  \t\t\t\t\"text\": \"等级加速\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/0p5VT8WFDb.png\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}, {\n  \t\t\t\t\"text\": \"彩色弹幕\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/25iQqgExtx.png\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}, {\n  \t\t\t\t\"text\": \"抢先看\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/8Ul33HFTHD.png\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}, {\n  \t\t\t\t\"text\": \"半价点播\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/E4ivS8ARC5.png\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}, {\n  \t\t\t\t\"text\": \"杜比音效\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/saSBoy57sz.png\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}, {\n  \t\t\t\t\"text\": \"装扮体验\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/uhqJANCq1u.png\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}, {\n  \t\t\t\t\"text\": \"点映会\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20231226/b6cfae4893bc9c708716e1bd73a161d4/SPNOKtD7oT.png\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}],\n  \t\t\t\"num\": 33\n  \t\t},\n  \t\t\"banner\": [{\n  \t\t\t\"id\": 115071,\n  \t\t\t\"index\": 1,\n  \t\t\t\"image\": \"\",\n  \t\t\t\"bigger_image\": \"https://i1.hdslb.com/bfs/vip/44acd5ca4e98b646d5edc2fd268138ec36c76be0.png\",\n  \t\t\t\"title\": \"ads_title_1\",\n  \t\t\t\"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\",\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"13\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"115071\",\n  \t\t\t\t\"tips_repeat_key\": \"115071:13:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"31819\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 115637,\n  \t\t\t\"index\": 2,\n  \t\t\t\"image\": \"\",\n  \t\t\t\"bigger_image\": \"https://i0.hdslb.com/bfs/vip/5ad9c5740658d9148a83b8af21d18bf4a2a8eaa1.png\",\n  \t\t\t\"title\": \"ads_title_2\",\n  \t\t\t\"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\",\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"13\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"115637\",\n  \t\t\t\t\"tips_repeat_key\": \"115637:13:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"31970\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 114722,\n  \t\t\t\"index\": 3,\n  \t\t\t\"image\": \"\",\n  \t\t\t\"bigger_image\": \"https://i0.hdslb.com/bfs/vip/0b0d1ce706df42537d05fbc1c783c3875aeec7e8.jpg\",\n  \t\t\t\"title\": \"ads_title_3\",\n  \t\t\t\"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\",\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"13\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"114722\",\n  \t\t\t\t\"tips_repeat_key\": \"114722:13:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"31640\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 115552,\n  \t\t\t\"index\": 4,\n  \t\t\t\"image\": \"\",\n  \t\t\t\"bigger_image\": \"https://i0.hdslb.com/bfs/vip/40cc4380429c1fdd638978c3aefd71331bd2da09.jpg\",\n  \t\t\t\"title\": \"ads_title_4\",\n  \t\t\t\"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\",\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"13\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"115552\",\n  \t\t\t\t\"tips_repeat_key\": \"115552:13:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"31949\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 115356,\n  \t\t\t\"index\": 5,\n  \t\t\t\"image\": \"\",\n  \t\t\t\"bigger_image\": \"https://i0.hdslb.com/bfs/vip/705588e4db9342ea9c5dd0645d5ac13476a79008.jpg\",\n  \t\t\t\"title\": \"ads_title_5\",\n  \t\t\t\"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\",\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"13\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"115356\",\n  \t\t\t\t\"tips_repeat_key\": \"115356:13:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"31893\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 112569,\n  \t\t\t\"index\": 7,\n  \t\t\t\"image\": \"\",\n  \t\t\t\"bigger_image\": \"https://i0.hdslb.com/bfs/vip/d7f15a9feffa8f11ff39f152e96db3c58735e294.jpg\",\n  \t\t\t\"title\": \"ads_title_7\",\n  \t\t\t\"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\",\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"13\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"112569\",\n  \t\t\t\t\"tips_repeat_key\": \"112569:13:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"30705\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}],\n  \t\t\"union_vip\": {\n  \t\t\t\"union_vips\": [],\n  \t\t\t\"sort\": 1,\n  \t\t\t\"title\": \"联合会员\",\n  \t\t\t\"jump_link\": \"https://www.bilibili.com/blackboard/activity-yIBnONpv1d.html?msource=juheye\\u0026app_id=125\\u0026source_from=666.146.selfDef.coWorkBannerClick\"\n  \t\t},\n  \t\t\"other_open_info\": {\n  \t\t\t\"open_infos\": [{\n  \t\t\t\t\"title\": \"赠送好友\",\n  \t\t\t\t\"url\": \"https://m.bilibili.com/doria/vip-gift.html?navhide=1\\u0026app_id=125\\u0026source_from=666.146.moreAddons.Click\",\n  \t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/vip/8468cab8be6ad16ac42e55c2c62191b65ca251cc.png\",\n  \t\t\t\t\"desc\": \"与好友一起干杯\",\n  \t\t\t\t\"sort\": 1,\n  \t\t\t\t\"id\": 15\n  \t\t\t}, {\n  \t\t\t\t\"title\": \"激活码开通\",\n  \t\t\t\t\"url\": \"https://big.bilibili.com/mobile/activation?closable=1\\u0026navhide=1\",\n  \t\t\t\t\"icon_url\": \"http://i0.hdslb.com/bfs/archive/78b8e391b06a55f70b20362d8245682f491c0a77.png\",\n  \t\t\t\t\"desc\": \"免费开通大会员\",\n  \t\t\t\t\"sort\": 2,\n  \t\t\t\t\"id\": 16\n  \t\t\t}],\n  \t\t\t\"sort\": 2\n  \t\t},\n  \t\t\"benefits\": [{\n  \t\t\t\"id\": 114391,\n  \t\t\t\"index\": 0,\n  \t\t\t\"type\": 5,\n  \t\t\t\"sub_type\": null,\n  \t\t\t\"title\": \"大会员专属演出观影特权\",\n  \t\t\t\"sub_title\": \"300积分+8元电影优惠券\",\n  \t\t\t\"sub_title_type\": 0,\n  \t\t\t\"dress_id\": 0,\n  \t\t\t\"collection_id\": 0,\n  \t\t\t\"collection_activity_id\": 0,\n  \t\t\t\"color\": \"\",\n  \t\t\t\"img\": {\n  \t\t\t\t\"text\": \"\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/vip/f214387b86ce6826c7edc7a3dacde9c215c46bf0.jpg\",\n  \t\t\t\t\"image_small\": \"https://i0.hdslb.com/bfs/vip/8003bf0608f943709a792efbeea89dcca5b58608.jpg\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t},\n  \t\t\t\"garb_imgs\": [],\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"每月领\",\n  \t\t\t\t\"action_type\": \"\",\n  \t\t\t\t\"link\": \"https://m.bilibili.com/doria/v2/union-benefits.html?msource=getbenefits\"\n  \t\t\t},\n  \t\t\t\"state\": 0,\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"outer_type\": \"taomai\",\n  \t\t\t\t\"position_id\": \"62\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"taomai_period_end_unix\": \"0\",\n  \t\t\t\t\"taomai_timestamp\": \"1748751549\",\n  \t\t\t\t\"tips_id\": \"114391\",\n  \t\t\t\t\"tips_repeat_key\": \"114391:62:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"31440\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 113041,\n  \t\t\t\"index\": 0,\n  \t\t\t\"type\": 1,\n  \t\t\t\"sub_type\": [6],\n  \t\t\t\"title\": \"良辰美景·不问天\",\n  \t\t\t\"sub_title\": \"3张免费体验卡可领取\",\n  \t\t\t\"sub_title_type\": 2,\n  \t\t\t\"dress_id\": 4019,\n  \t\t\t\"collection_id\": 0,\n  \t\t\t\"collection_activity_id\": 0,\n  \t\t\t\"color\": \"#e7e1f1\",\n  \t\t\t\"img\": {\n  \t\t\t\t\"text\": \"主题\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/garb/item/3dacdb35435c9474fe89bb69174af61b38befa8d.jpg\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t},\n  \t\t\t\"garb_imgs\": [{\n  \t\t\t\t\"text\": \"空间背景\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/garb/item/2961ae11f3f77f9086fd9702e2cb5b30e6f4d39f.jpg\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}, {\n  \t\t\t\t\"text\": \"表情包\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/emote/1c455847f5f1521345b108bdbc1761715850a15e.png\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}, {\n  \t\t\t\t\"text\": \"动态卡片\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/garb/item/ef9c5ac04356c52907dbad8e54d630338045476e.png\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}, {\n  \t\t\t\t\"text\": \"评论背景\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/garb/item/8626432939ec2a06f482cd7d87dfe66aa44036b2.png\",\n  \t\t\t\t\"image_small\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t}],\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"立即领取\",\n  \t\t\t\t\"action_type\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t},\n  \t\t\t\"state\": 0,\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"62\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"113041\",\n  \t\t\t\t\"tips_repeat_key\": \"113041:62:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"14545\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 115499,\n  \t\t\t\"index\": 0,\n  \t\t\t\"type\": 4,\n  \t\t\t\"sub_type\": null,\n  \t\t\t\"title\": \"《三国：谋定天下》福利\",\n  \t\t\t\"sub_title\": \"领超萌小电视定制表情\",\n  \t\t\t\"sub_title_type\": 0,\n  \t\t\t\"dress_id\": 0,\n  \t\t\t\"collection_id\": 0,\n  \t\t\t\"collection_activity_id\": 0,\n  \t\t\t\"color\": \"\",\n  \t\t\t\"img\": {\n  \t\t\t\t\"text\": \"\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/vip/b724cea88d0e4003a1fc4d62d59ca61f4b150df0.jpg\",\n  \t\t\t\t\"image_small\": \"https://i0.hdslb.com/bfs/vip/a532ee4b1061a8424e5e8c57378e5d269d03a32f.jpg\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t},\n  \t\t\t\"garb_imgs\": [],\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"立刻领取\",\n  \t\t\t\t\"action_type\": \"\",\n  \t\t\t\t\"link\": \"https://game.bilibili.com/nslg/dhyhdSE/?sourceFrom=1000680022\"\n  \t\t\t},\n  \t\t\t\"state\": 0,\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"62\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"115499\",\n  \t\t\t\t\"tips_repeat_key\": \"115499:62:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"31908\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 114536,\n  \t\t\t\"index\": 0,\n  \t\t\t\"type\": 4,\n  \t\t\t\"sub_type\": null,\n  \t\t\t\"title\": \"大会员专享游戏权益\",\n  \t\t\t\"sub_title\": \"游戏折扣随心领\",\n  \t\t\t\"sub_title_type\": 0,\n  \t\t\t\"dress_id\": 0,\n  \t\t\t\"collection_id\": 0,\n  \t\t\t\"collection_activity_id\": 0,\n  \t\t\t\"color\": \"\",\n  \t\t\t\"img\": {\n  \t\t\t\t\"text\": \"\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/vip/03643637fb6720e88063f5483eaca4f1b7875154.jpg\",\n  \t\t\t\t\"image_small\": \"https://i0.hdslb.com/bfs/vip/ea774ed25b98e5668c30753c816ff21c1775d1ed.png\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t},\n  \t\t\t\"garb_imgs\": [],\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"去领取\",\n  \t\t\t\t\"action_type\": \"\",\n  \t\t\t\t\"link\": \"https://big.bilibili.com/mobile/cardBag?closable=1\\u0026navhide=1\\u0026tab=welfare\\u0026skuType=118\"\n  \t\t\t},\n  \t\t\t\"state\": 0,\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"62\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"114536\",\n  \t\t\t\t\"tips_repeat_key\": \"114536:62:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"31546\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 45028,\n  \t\t\t\"index\": 0,\n  \t\t\t\"type\": 3,\n  \t\t\t\"sub_type\": [2, 4],\n  \t\t\t\"title\": \"专属会员购福利\",\n  \t\t\t\"sub_title\": \"精品周边超值购\",\n  \t\t\t\"sub_title_type\": 0,\n  \t\t\t\"dress_id\": 0,\n  \t\t\t\"collection_id\": 0,\n  \t\t\t\"collection_activity_id\": 0,\n  \t\t\t\"color\": \"\",\n  \t\t\t\"img\": {\n  \t\t\t\t\"text\": \"\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20241105/4cefecc6742f8995a6bd22402a6d0b8b/jqJzlNE45x.png\",\n  \t\t\t\t\"image_small\": \"https://i0.hdslb.com/bfs/vip/c019881f9e0c4a20a99a3cc3604ca2f3cdbaf858.jpg\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t},\n  \t\t\t\"garb_imgs\": [],\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"去使用\",\n  \t\t\t\t\"action_type\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t},\n  \t\t\t\"state\": 0,\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"62\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"45028\",\n  \t\t\t\t\"tips_repeat_key\": \"45028:62:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"14542\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 45032,\n  \t\t\t\"index\": 0,\n  \t\t\t\"type\": 3,\n  \t\t\t\"sub_type\": [3],\n  \t\t\t\"title\": \"专享漫画礼包\",\n  \t\t\t\"sub_title\": \"海量漫画随心看\",\n  \t\t\t\"sub_title_type\": 0,\n  \t\t\t\"dress_id\": 0,\n  \t\t\t\"collection_id\": 0,\n  \t\t\t\"collection_activity_id\": 0,\n  \t\t\t\"color\": \"\",\n  \t\t\t\"img\": {\n  \t\t\t\t\"text\": \"\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20241105/4cefecc6742f8995a6bd22402a6d0b8b/06GTgEunNO.png\",\n  \t\t\t\t\"image_small\": \"https://i0.hdslb.com/bfs/vip/54adfdfeb811917a4d41ce618a82b9966f4e0e49.jpg\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t},\n  \t\t\t\"garb_imgs\": [],\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"去使用\",\n  \t\t\t\t\"action_type\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t},\n  \t\t\t\"state\": 0,\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"62\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"45032\",\n  \t\t\t\t\"tips_repeat_key\": \"45032:62:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"14546\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 45990,\n  \t\t\t\"index\": 0,\n  \t\t\t\"type\": 3,\n  \t\t\t\"sub_type\": [7],\n  \t\t\t\"title\": \"会员特享课程折扣\",\n  \t\t\t\"sub_title\": \"品质课程5折享\",\n  \t\t\t\"sub_title_type\": 0,\n  \t\t\t\"dress_id\": 0,\n  \t\t\t\"collection_id\": 0,\n  \t\t\t\"collection_activity_id\": 0,\n  \t\t\t\"color\": \"\",\n  \t\t\t\"img\": {\n  \t\t\t\t\"text\": \"\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20241105/4cefecc6742f8995a6bd22402a6d0b8b/55JxkQCJpd.png\",\n  \t\t\t\t\"image_small\": \"https://i0.hdslb.com/bfs/vip/080f4fb4f7789f6bf7e3a71907690f0f1906d66c.jpg\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t},\n  \t\t\t\"garb_imgs\": [],\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"去使用\",\n  \t\t\t\t\"action_type\": \"\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t},\n  \t\t\t\"state\": 0,\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"62\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"45990\",\n  \t\t\t\t\"tips_repeat_key\": \"45990:62:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"14853\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 46005,\n  \t\t\t\"index\": 0,\n  \t\t\t\"type\": 4,\n  \t\t\t\"sub_type\": null,\n  \t\t\t\"title\": \"王者荣耀每日礼包\",\n  \t\t\t\"sub_title\": \"概率得皮肤心意金\",\n  \t\t\t\"sub_title_type\": 0,\n  \t\t\t\"dress_id\": 0,\n  \t\t\t\"collection_id\": 0,\n  \t\t\t\"collection_activity_id\": 0,\n  \t\t\t\"color\": \"\",\n  \t\t\t\"img\": {\n  \t\t\t\t\"text\": \"\",\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/ba11dab14bce2372c9e7f13ee617e4e9f8bc1701.jpg\",\n  \t\t\t\t\"image_small\": \"https://i0.hdslb.com/bfs/vip/2f6f16ca854b0a1e55aa726f3353cb7c5d11e068.jpg\",\n  \t\t\t\t\"link\": \"\"\n  \t\t\t},\n  \t\t\t\"garb_imgs\": [],\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"点我领取\",\n  \t\t\t\t\"action_type\": \"\",\n  \t\t\t\t\"link\": \"https://igame.qq.com/tip/ingame-page/igame-regift-box/index.html?brandid=b1683628293\\u0026multiCfgId=b16836282931693884106\"\n  \t\t\t},\n  \t\t\t\"state\": 0,\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"62\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"46005\",\n  \t\t\t\t\"tips_repeat_key\": \"46005:62:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"14865\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}],\n  \t\t\"big_point\": {\n  \t\t\t\"point_info\": null,\n  \t\t\t\"sign_info\": null,\n  \t\t\t\"sku_info\": null,\n  \t\t\t\"point_switch_off\": false,\n  \t\t\t\"tips\": null,\n  \t\t\t\"button_text\": \"\",\n  \t\t\t\"sku_price_hidden\": false\n  \t\t},\n  \t\t\"welfare\": {\n  \t\t\t\"count\": 2,\n  \t\t\t\"list\": [{\n  \t\t\t\t\"id\": 122,\n  \t\t\t\t\"name\": \"移动免费领2G流量\",\n  \t\t\t\t\"homepage_uri\": \"https://i1.hdslb.com/bfs/vip/14a31d6e1ee260db2114b80e26469432880375ae.png\",\n  \t\t\t\t\"backdrop_uri\": \"https://i1.hdslb.com/bfs/vip/9343d88045e915e76e1f6eb40887810a8da00997.png\",\n  \t\t\t\t\"tid\": 0,\n  \t\t\t\t\"rank\": 1,\n  \t\t\t\t\"receive_uri\": \"https://wx.10086.cn/qwhdhub/leadin/1025012314?A_C_CODE=tApqjxVygk\\u0026channelId=P00000112259\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 80,\n  \t\t\t\t\"name\": \"联通首月1分钱\",\n  \t\t\t\t\"homepage_uri\": \"https://i0.hdslb.com/bfs/vip/30f34965287b07e2fd752c114a90aa275951940a.png\",\n  \t\t\t\t\"backdrop_uri\": \"https://i0.hdslb.com/bfs/vip/73897d68b84e2caa47fc5f84953ae5b94df28208.png\",\n  \t\t\t\t\"tid\": 0,\n  \t\t\t\t\"rank\": 3,\n  \t\t\t\t\"receive_uri\": \"https://operation.bol.wo.cn/a/#/e86e506acf4f\"\n  \t\t\t}]\n  \t\t},\n  \t\t\"experience\": {\n  \t\t\t\"level\": 6,\n  \t\t\t\"cur_exp\": 49792,\n  \t\t\t\"next_exp\": -1,\n  \t\t\t\"is_senior_member\": 0,\n  \t\t\t\"is_get_exp\": false,\n  \t\t\t\"is_task_complete\": true,\n  \t\t\t\"state\": 0\n  \t\t},\n  \t\t\"vip_exclusive\": [],\n  \t\t\"draw_cards_welfare\": [{\n  \t\t\t\"id\": 105056,\n  \t\t\t\"title\": \"少女乐队的呐喊\",\n  \t\t\t\"sub_title\": \"会员首抽福利，低至4.9元\",\n  \t\t\t\"img\": {},\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"去抽取\"\n  \t\t\t},\n  \t\t\t\"state\": 0,\n  \t\t\t\"garb_imgs\": [{\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/d1ec5f815df64fe371d3a9150f19f466fee5ea2e.jpg\",\n  \t\t\t\t\"text\": \"13张卡片\",\n  \t\t\t\t\"color\": \"#4e3738\"\n  \t\t\t}, {\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/1b4501496bce918624c4d8054c072e3eacb7a5ae.png\",\n  \t\t\t\t\"text\": \"评论背景\"\n  \t\t\t}, {\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/1ad674dc624758e8e7d7875a00d2895316f78a77.png\",\n  \t\t\t\t\"text\": \"头像框\"\n  \t\t\t}],\n  \t\t\t\"collection_activity_id\": 104978,\n  \t\t\t\"exp_params\": null,\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"65\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"104928\",\n  \t\t\t\t\"tips_repeat_key\": \"104928:65:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"27796\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 105607,\n  \t\t\t\"title\": \"蔚蓝档案\",\n  \t\t\t\"sub_title\": \"会员首抽福利，低至4.9元\",\n  \t\t\t\"img\": {},\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"去抽取\"\n  \t\t\t},\n  \t\t\t\"state\": 0,\n  \t\t\t\"garb_imgs\": [{\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/8e5c8a257b8cb75ad1921cde40e1f3834b4b0649.png\",\n  \t\t\t\t\"text\": \"16张卡片\",\n  \t\t\t\t\"color\": \"#dfc0d9\"\n  \t\t\t}, {\n  \t\t\t\t\"img_url\": \"http://i0.hdslb.com/bfs/archive/3d9af842999732755c5181497a1bf6e17574a2b1.png\",\n  \t\t\t\t\"text\": \"评论背景\"\n  \t\t\t}, {\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/40c5554bec046c8c58c7f18f8d845a2c8f51e6a0.png\",\n  \t\t\t\t\"text\": \"头像框\"\n  \t\t\t}],\n  \t\t\t\"collection_activity_id\": 105606,\n  \t\t\t\"exp_params\": null,\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"65\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"104928\",\n  \t\t\t\t\"tips_repeat_key\": \"104928:65:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"27796\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 102858,\n  \t\t\t\"title\": \"良辰共此曲\",\n  \t\t\t\"sub_title\": \"会员首抽福利，低至4.9元\",\n  \t\t\t\"img\": {},\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"去抽取\"\n  \t\t\t},\n  \t\t\t\"state\": 0,\n  \t\t\t\"garb_imgs\": [{\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/05f5737c93982f6ac76fe43e101ed7aa015977f8.png\",\n  \t\t\t\t\"text\": \"25张卡片\",\n  \t\t\t\t\"color\": \"#3b312e\"\n  \t\t\t}, {\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/c97b571ae4e93ed7aff312e324992e0d08c16a14.png\",\n  \t\t\t\t\"text\": \"评论背景\"\n  \t\t\t}, {\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/0e8854c5f5572cd282e3dd1749ed8f312f25e663.png\",\n  \t\t\t\t\"text\": \"头像框\"\n  \t\t\t}],\n  \t\t\t\"collection_activity_id\": 102857,\n  \t\t\t\"exp_params\": null,\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"65\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"104928\",\n  \t\t\t\t\"tips_repeat_key\": \"104928:65:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"27796\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 101580,\n  \t\t\t\"title\": \"EVA：序破\",\n  \t\t\t\"sub_title\": \"会员首抽福利，低至4.9元\",\n  \t\t\t\"img\": {},\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"去抽取\"\n  \t\t\t},\n  \t\t\t\"state\": 0,\n  \t\t\t\"garb_imgs\": [{\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/4c043be753fcbf2f827553de9f01a5ba4231ddc6.png\",\n  \t\t\t\t\"text\": \"19张卡片\",\n  \t\t\t\t\"color\": \"#5f1a0d\"\n  \t\t\t}, {\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/80920496892f96ba14b2869676fc449969cdfda8.png\",\n  \t\t\t\t\"text\": \"评论背景\"\n  \t\t\t}, {\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/6fe67f4fd45ae75e31a706bb7b89c19d0d09c719.png\",\n  \t\t\t\t\"text\": \"头像框\"\n  \t\t\t}],\n  \t\t\t\"collection_activity_id\": 101579,\n  \t\t\t\"exp_params\": null,\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"65\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"104928\",\n  \t\t\t\t\"tips_repeat_key\": \"104928:65:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"27796\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}, {\n  \t\t\t\"id\": 106111,\n  \t\t\t\"title\": \"魔女之夜\",\n  \t\t\t\"sub_title\": \"会员首抽福利，低至4.9元\",\n  \t\t\t\"img\": {},\n  \t\t\t\"button\": {\n  \t\t\t\t\"text\": \"去抽取\"\n  \t\t\t},\n  \t\t\t\"state\": 0,\n  \t\t\t\"garb_imgs\": [{\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/23ef7df4711a1418e9d5d56f9b48937abd4b8564.png\",\n  \t\t\t\t\"text\": \"16张卡片\",\n  \t\t\t\t\"color\": \"#2b2e20\"\n  \t\t\t}, {\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/867e6db4dd50523ca22cfef98eea069cd8949fc6.png\",\n  \t\t\t\t\"text\": \"评论背景\"\n  \t\t\t}, {\n  \t\t\t\t\"img_url\": \"https://i0.hdslb.com/bfs/garb/open/b9a055a3e4340f3291e4c934999b886562ef64c2.png\",\n  \t\t\t\t\"text\": \"头像框\"\n  \t\t\t}],\n  \t\t\t\"collection_activity_id\": 102161,\n  \t\t\t\"exp_params\": null,\n  \t\t\t\"track_params\": {\n  \t\t\t\t\"exp_group_tag\": \"def\",\n  \t\t\t\t\"exp_tag\": \"def\",\n  \t\t\t\t\"material_type\": \"3\",\n  \t\t\t\t\"position_id\": \"65\",\n  \t\t\t\t\"request_id\": \"29d91790874e23e60e8e2224ac683bd4\",\n  \t\t\t\t\"tips_id\": \"104928\",\n  \t\t\t\t\"tips_repeat_key\": \"104928:65:1748751549:341688380\",\n  \t\t\t\t\"unit_id\": \"27796\",\n  \t\t\t\t\"vip_status\": \"1\",\n  \t\t\t\t\"vip_type\": \"2\"\n  \t\t\t}\n  \t\t}],\n  \t\t\"free_welfare\": [{\n  \t\t\t\"id\": 4650011,\n  \t\t\t\"icon\": \"https://i0.hdslb.com/bfs/bangumi/image/d6b4efd6fc925dcb98aae009432101f892795f84.jpg\",\n  \t\t\t\"title\": \"电信·星级服务｜专属礼遇\",\n  \t\t\t\"titleColor\": \"\",\n  \t\t\t\"subtitle\": \"大会员任意兑\",\n  \t\t\t\"subtitleColor\": \"#9499A0\",\n  \t\t\t\"subtitle2\": \"2500积分起\",\n  \t\t\t\"subtitle2Color\": \"#FF6699\",\n  \t\t\t\"task_state\": 1,\n  \t\t\t\"link\": \"https://www.bilibili.com/blackboard/activity-r4Qm9nZabK.html?taskId=4650011\\u0026channel=outer_h5\",\n  \t\t\t\"channel\": \"outer_h5\"\n  \t\t}, {\n  \t\t\t\"id\": 4200111,\n  \t\t\t\"icon\": \"https://i0.hdslb.com/bfs/bangumi/image/d2b22160e79c43399c34f40ff1b8ccaadd0d95d5.png\",\n  \t\t\t\"title\": \"开通江苏电信X连续会员包\",\n  \t\t\t\"titleColor\": \"\",\n  \t\t\t\"subtitle\": \"首月1分钱\",\n  \t\t\t\"subtitleColor\": \"#9499A0\",\n  \t\t\t\"subtitle2\": \"每月享大会员\",\n  \t\t\t\"subtitle2Color\": \"#FF6699\",\n  \t\t\t\"task_state\": 1,\n  \t\t\t\"link\": \"https://www.bilibili.com/blackboard/activity-8mit5o2IL9.html?taskId=4200111\\u0026channel=outer_h5\",\n  \t\t\t\"channel\": \"outer_h5\"\n  \t\t}, {\n  \t\t\t\"id\": 4620003,\n  \t\t\t\"icon\": \"https://i0.hdslb.com/bfs/bangumi/image/6fd085fca6f1ad5c35bcba5f64a8959801eba563.jpg\",\n  \t\t\t\"title\": \"农行信用卡客户专享\",\n  \t\t\t\"titleColor\": \"\",\n  \t\t\t\"subtitle\": \"积分兑大会员\",\n  \t\t\t\"subtitleColor\": \"#9499A0\",\n  \t\t\t\"subtitle2\": \"月卡0元得\",\n  \t\t\t\"subtitle2Color\": \"#FF6699\",\n  \t\t\t\"task_state\": 1,\n  \t\t\t\"link\": \"https://www.bilibili.com/blackboard/activity-EfBpx3pZAx.html?taskId=4620003\\u0026channel=outer_h5\",\n  \t\t\t\"channel\": \"outer_h5\"\n  \t\t}, {\n  \t\t\t\"id\": 4800017,\n  \t\t\t\"icon\": \"https://i0.hdslb.com/bfs/bangumi/image/9a6849d7dceff553f88e7b49beb810ac82900bc2.png\",\n  \t\t\t\"title\": \"开通电信XB站会员流量包\",\n  \t\t\t\"titleColor\": \"\",\n  \t\t\t\"subtitle\": \"大会员月月发\",\n  \t\t\t\"subtitleColor\": \"#9499A0\",\n  \t\t\t\"subtitle2\": \"+2G流量\",\n  \t\t\t\"subtitle2Color\": \"#FF6699\",\n  \t\t\t\"task_state\": 1,\n  \t\t\t\"link\": \"https://b23.tv/p7JnLcH?taskId=4800017\\u0026channel=outer_h5\",\n  \t\t\t\"channel\": \"outer_h5\"\n  \t\t}, {\n  \t\t\t\"id\": 4230055,\n  \t\t\t\"icon\": \"https://i0.hdslb.com/bfs/bangumi/image/68729e950e732218de8e5deb803a8822190f6607.png\",\n  \t\t\t\"title\": \"招行信用卡积分兑会员\",\n  \t\t\t\"titleColor\": \"\",\n  \t\t\t\"subtitle\": \"大会员任意兑\",\n  \t\t\t\"subtitleColor\": \"#9499A0\",\n  \t\t\t\"subtitle2\": \"899积分起\",\n  \t\t\t\"subtitle2Color\": \"#FF6699\",\n  \t\t\t\"task_state\": 1,\n  \t\t\t\"link\": \"https://www.bilibili.com/blackboard/activity-NWCapLCOgS.html?taskId=4230055\\u0026channel=outer_h5\",\n  \t\t\t\"channel\": \"outer_h5\"\n  \t\t}, {\n  \t\t\t\"id\": 4290081,\n  \t\t\t\"icon\": \"https://i0.hdslb.com/bfs/bangumi/image/5a0027a21d5298fe62266968a0b65f1ab84efa72.png\",\n  \t\t\t\"title\": \"开通联通王卡\",\n  \t\t\t\"titleColor\": \"#000000\",\n  \t\t\t\"subtitle\": \"每年享960G流量\",\n  \t\t\t\"subtitleColor\": \"#9499A0\",\n  \t\t\t\"subtitle2\": \"领24个月大会员\",\n  \t\t\t\"subtitle2Color\": \"#FF6699\",\n  \t\t\t\"task_state\": 1,\n  \t\t\t\"link\": \"https://www.bilibili.com/blackboard/activity-amrPj9oRBu.html?taskId=4290081\\u0026channel=cucc_king\",\n  \t\t\t\"channel\": \"cucc_king\"\n  \t\t}, {\n  \t\t\t\"id\": 4260105,\n  \t\t\t\"icon\": \"https://i0.hdslb.com/bfs/bangumi/image/19a8a2c8437e36560885adaf18ca382f024dffd8.png\",\n  \t\t\t\"title\": \"南航里程兑大会员\",\n  \t\t\t\"titleColor\": \"\",\n  \t\t\t\"subtitle\": \"南航用户专享\",\n  \t\t\t\"subtitleColor\": \"#9499A0\",\n  \t\t\t\"subtitle2\": \"最高领366天大会员\",\n  \t\t\t\"subtitle2Color\": \"#FF6699\",\n  \t\t\t\"task_state\": 1,\n  \t\t\t\"link\": \"https://www.bilibili.com/blackboard/activity-U0jNxo0ARy.html?taskId=4260105\\u0026channel=outer_h5\",\n  \t\t\t\"channel\": \"outer_h5\"\n  \t\t}],\n  \t\t\"model_order\": [{\n  \t\t\t\"is_hidden\": false,\n  \t\t\t\"name\": \"Property,\",\n  \t\t\t\"order\": 0,\n  \t\t\t\"fixed\": false,\n  \t\t\t\"is_show\": false,\n  \t\t\t\"style\": 0\n  \t\t}, {\n  \t\t\t\"is_hidden\": false,\n  \t\t\t\"name\": \"Notice\",\n  \t\t\t\"order\": 0,\n  \t\t\t\"fixed\": false,\n  \t\t\t\"is_show\": false,\n  \t\t\t\"style\": 0\n  \t\t}, {\n  \t\t\t\"is_hidden\": false,\n  \t\t\t\"name\": \"Privilige\",\n  \t\t\t\"order\": 0,\n  \t\t\t\"fixed\": false,\n  \t\t\t\"is_show\": false,\n  \t\t\t\"style\": 0\n  \t\t}, {\n  \t\t\t\"is_hidden\": false,\n  \t\t\t\"name\": \"Banner\",\n  \t\t\t\"order\": 0,\n  \t\t\t\"fixed\": false,\n  \t\t\t\"is_show\": false,\n  \t\t\t\"style\": 0\n  \t\t}, {\n  \t\t\t\"is_hidden\": false,\n  \t\t\t\"name\": \"UnionVip\",\n  \t\t\t\"order\": 0,\n  \t\t\t\"fixed\": false,\n  \t\t\t\"is_show\": false,\n  \t\t\t\"style\": 0\n  \t\t}, {\n  \t\t\t\"is_hidden\": false,\n  \t\t\t\"name\": \"VipExclusive\",\n  \t\t\t\"order\": 0,\n  \t\t\t\"fixed\": false,\n  \t\t\t\"is_show\": false,\n  \t\t\t\"style\": 0\n  \t\t}, {\n  \t\t\t\"is_hidden\": false,\n  \t\t\t\"name\": \"Experience\",\n  \t\t\t\"order\": 0,\n  \t\t\t\"fixed\": false,\n  \t\t\t\"is_show\": false,\n  \t\t\t\"style\": 0\n  \t\t}, {\n  \t\t\t\"is_hidden\": false,\n  \t\t\t\"name\": \"Benefit\",\n  \t\t\t\"order\": 0,\n  \t\t\t\"fixed\": false,\n  \t\t\t\"is_show\": false,\n  \t\t\t\"style\": 0\n  \t\t}, {\n  \t\t\t\"is_hidden\": false,\n  \t\t\t\"name\": \"GarbCard\",\n  \t\t\t\"order\": 0,\n  \t\t\t\"fixed\": false,\n  \t\t\t\"is_show\": false,\n  \t\t\t\"style\": 0\n  \t\t}, {\n  \t\t\t\"is_hidden\": false,\n  \t\t\t\"name\": \"BigPoint\",\n  \t\t\t\"order\": 0,\n  \t\t\t\"fixed\": false,\n  \t\t\t\"is_show\": false,\n  \t\t\t\"style\": 0\n  \t\t}, {\n  \t\t\t\"is_hidden\": false,\n  \t\t\t\"name\": \"FreeGet\",\n  \t\t\t\"order\": 0,\n  \t\t\t\"fixed\": false,\n  \t\t\t\"is_show\": false,\n  \t\t\t\"style\": 0\n  \t\t}, {\n  \t\t\t\"is_hidden\": false,\n  \t\t\t\"name\": \"Welfare\",\n  \t\t\t\"order\": 0,\n  \t\t\t\"fixed\": false,\n  \t\t\t\"is_show\": false,\n  \t\t\t\"style\": 0\n  \t\t}, {\n  \t\t\t\"is_hidden\": false,\n  \t\t\t\"name\": \"OtherOpen\",\n  \t\t\t\"order\": 0,\n  \t\t\t\"fixed\": false,\n  \t\t\t\"is_show\": false,\n  \t\t\t\"style\": 0\n  \t\t}],\n  \t\t\"buoy\": {\n  \t\t\t\"id\": 0,\n  \t\t\t\"img\": \"\",\n  \t\t\t\"link\": \"\",\n  \t\t\t\"track_params\": null\n  \t\t},\n  \t\t\"offline_activity\": {\n  \t\t\t\"link\": \"https://b23.tv/gjVVysQ\",\n  \t\t\t\"title\": \"大会员专享点映会\",\n  \t\t\t\"items\": [{\n  \t\t\t\t\"id\": 4511,\n  \t\t\t\t\"index\": 0,\n  \t\t\t\t\"title\": \"免费看《攻壳机动队》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"石家庄、青岛、厦门、宁波等10城\",\n  \t\t\t\t\"color\": \"#4e2b1e\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/45cadd4555494ce0403b310d38ea65e16f9f0092.png\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg\",\n  \t\t\t\t\"time_range\": \"2025-05-10 15:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/opus/1066777107187630083?spm_id_from=333.1387.0.0\",\n  \t\t\t\t\"start_time\": 1745906400,\n  \t\t\t\t\"end_time\": 1746374399,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"aswkxyiezx\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-JVSbo5S3PJ.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 4499,\n  \t\t\t\t\"index\": 1,\n  \t\t\t\t\"title\": \"《孤独摇滚》系列首映礼\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"上海\",\n  \t\t\t\t\"color\": \"#4c4039\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/f3f96dd77a5a75988b00b8ddc69515c991b2b17e.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/ONJBt3XdwS.jpg\",\n  \t\t\t\t\"time_range\": \"2025-05-01 14:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/opus/1062394468895817744?spm_id_from=333.1387.0.0\",\n  \t\t\t\t\"start_time\": 1745557200,\n  \t\t\t\t\"end_time\": 1745726400,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"dhtlvygfvo\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-bxcF1ZxuCO.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 4466,\n  \t\t\t\t\"index\": 2,\n  \t\t\t\t\"title\": \"《雷霆特攻队》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"成都、武汉、苏州等全国10城\",\n  \t\t\t\t\"color\": \"#272116\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/9da3eacfa8b8a42e186dd5f438c2818244ce14e6.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg\",\n  \t\t\t\t\"time_range\": \"2025-04-30 19:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/opus/1064828910022164505?spm_id_from=333.1387.0.0\",\n  \t\t\t\t\"start_time\": 1745208000,\n  \t\t\t\t\"end_time\": 1745640000,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"rwoskyltbn\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-LWhbslNy4m.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 4432,\n  \t\t\t\t\"index\": 3,\n  \t\t\t\t\"title\": \"大会员免费看《鲲吞天下》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"广州\",\n  \t\t\t\t\"color\": \"#6a707b\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/9cb6ab34f3e71bb20dcdd2c4331166deea21ddfa.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/ONJBt3XdwS.jpg\",\n  \t\t\t\t\"time_range\": \"2025-04-26 18:30:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ep1644561?spm_id_from=333.337.search-card.all.click\",\n  \t\t\t\t\"start_time\": 1744603200,\n  \t\t\t\t\"end_time\": 1745164799,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"tcpbpyvpxf\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-wGA7tabyt7.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 4454,\n  \t\t\t\t\"index\": 4,\n  \t\t\t\t\"title\": \"大会员免费看《孤独摇滚（上）》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"青岛 上海 温州 西安 成都\",\n  \t\t\t\t\"color\": \"#3e2b23\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/d5ea0f60f075e1f5a7cdf3c7273a0df8e7ec0f39.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg\",\n  \t\t\t\t\"time_range\": \"2025-04-25 19:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/opus/1060789761724121112?spm_id_from=333.1387.0.0\",\n  \t\t\t\t\"start_time\": 1744783200,\n  \t\t\t\t\"end_time\": 1745208000,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"gatbfzmsan\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-oe5xpiydmA.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 4355,\n  \t\t\t\t\"index\": 5,\n  \t\t\t\t\"title\": \"大会员免费看《火之鸟 伊甸之花》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"上海 北京 广州 武汉 天津 合肥 福州 昆明 无锡 青岛\",\n  \t\t\t\t\"color\": \"#413d4e\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/ac8a32709afaf984be1d7d689f89639f85f88907.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg\",\n  \t\t\t\t\"time_range\": \"2025-04-18 19:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/opus/1059637821452582949?spm_id_from=333.1387.0.0\",\n  \t\t\t\t\"start_time\": 1744192800,\n  \t\t\t\t\"end_time\": 1744603200,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"efgfnwffut\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-MzADOE0NrP.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 4230,\n  \t\t\t\t\"index\": 6,\n  \t\t\t\t\"title\": \"《我的世界大电影》点映会\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"上海、北京、郑州、苏州、西安、成都\",\n  \t\t\t\t\"color\": \"#3b5553\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/4f9e349909a9b518c6be170ecd7e56b7128eaf6b.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg\",\n  \t\t\t\t\"time_range\": \"2025-04-04 14:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/opus/1054102270026711044?spm_id_from=333.1387.0.0\",\n  \t\t\t\t\"start_time\": 1742788800,\n  \t\t\t\t\"end_time\": 1743307200,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"qbmniosrdn\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-Sgflj5mZCQ.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 4232,\n  \t\t\t\t\"index\": 7,\n  \t\t\t\t\"title\": \"《机动战士高达：跨时之战》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"上海 北京 杭州 重庆 武汉 南京 长沙 宁波 济南 广州\",\n  \t\t\t\t\"color\": \"#344c65\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/bd9ef6967e89e3aec959d03e6a5113ee1efbd10b.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/ONJBt3XdwS.jpg\",\n  \t\t\t\t\"time_range\": \"2025-04-03 19:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/opus/1053819132876685329?spm_id_from=333.1387.0.0\",\n  \t\t\t\t\"start_time\": 1743064200,\n  \t\t\t\t\"end_time\": 1743332400,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"uanckmbinj\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-5hqPBpKE8c.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 4212,\n  \t\t\t\t\"index\": 8,\n  \t\t\t\t\"title\": \"《凸变英雄X》点映会(大陆地区）\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \" 北京、上海、天津、重庆、兰州、西安、福州等34城\",\n  \t\t\t\t\"color\": \"#65515c\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/67873b32cbf4f2ccb220f0ede452aad82ba52335.png\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg\",\n  \t\t\t\t\"time_range\": \"2025-03-29 14:00:00\",\n  \t\t\t\t\"link\": \"https://t.bilibili.com/1051195363130605570?spm_id_from=333.1387.0.0\",\n  \t\t\t\t\"start_time\": 1742004000,\n  \t\t\t\t\"end_time\": 1742745599,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"vygcgwztne\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-0zTwwAW1P2.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 4098,\n  \t\t\t\t\"index\": 9,\n  \t\t\t\t\"title\": \"《猫猫的奇幻漂流》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"上海、广州、深圳、重庆、武汉、天津、福州、昆明、济南、厦门\",\n  \t\t\t\t\"color\": \"#37292f\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/413453de3d830c3fd3fcb74b88cd783beddce160.png\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg\",\n  \t\t\t\t\"time_range\": \"2025-02-28 19:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/opus/1040061725606412297?spm_id_from=333.1365.0.0\",\n  \t\t\t\t\"start_time\": 1739851200,\n  \t\t\t\t\"end_time\": 1740326399,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"akinevpeff\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-5YZwSRTkr3.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 4074,\n  \t\t\t\t\"index\": 10,\n  \t\t\t\t\"title\": \"《你的颜色》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"上海、北京、成都、深圳、杭州、武汉、南京、西安、长沙、合肥\",\n  \t\t\t\t\"color\": \"#4f3d44\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/c8c608e1ff80e662d9e8f07518349b20d92533be.png\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg\",\n  \t\t\t\t\"time_range\": \"2025-02-21 19:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/opus/1037454345412542464?spm_id_from=333.1365.0.0\",\n  \t\t\t\t\"start_time\": 1739412000,\n  \t\t\t\t\"end_time\": 1739764799,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"afamrmcijv\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-wrTmFmjqYg.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 3946,\n  \t\t\t\t\"index\": 11,\n  \t\t\t\t\"title\": \"《哪吒之魔童闹海》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"上海、北京、广州、成都、西安、重庆、郑州、青岛、东莞、哈尔滨\",\n  \t\t\t\t\"color\": \"#322126\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/02cabab77c79e3f8da6c1b1e61d64881c273d144.png\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/sCLQXxhiFZ.jpg\",\n  \t\t\t\t\"time_range\": \"2025-02-02 14:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/opus/1030756202187849733?spm_id_from=333.1365.0.0\",\n  \t\t\t\t\"start_time\": 1737432000,\n  \t\t\t\t\"end_time\": 1737993599,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"dzwazoqncj\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-g97AHtCUsb.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 3719,\n  \t\t\t\t\"index\": 12,\n  \t\t\t\t\"title\": \"《坂本日常》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"上海· SFC上影影城（八佰伴店）\",\n  \t\t\t\t\"color\": \"#534042\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/96698374759e02e963140f79572a49beac29b84a.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/ONJBt3XdwS.jpg\",\n  \t\t\t\t\"time_range\": \"2025-01-05 15:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ep1360452?spm_id_from=333.999.list.card_archive.click\",\n  \t\t\t\t\"start_time\": 1734946200,\n  \t\t\t\t\"end_time\": 1735660799,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"jctqvnokfb\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-wDvDvbiDXs.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 3697,\n  \t\t\t\t\"index\": 13,\n  \t\t\t\t\"title\": \"《名侦探柯南：迷宫的十字路口》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"北京、成都、广州、南京、上海、深圳、沈阳、天津、武汉、西安\",\n  \t\t\t\t\"color\": \"#5c4748\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/dd3ca2860c87352086020cf9312ccb56c0f1e0ba.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg\",\n  \t\t\t\t\"time_range\": \"2024-12-27 19:30:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/video/BV1KD67YPEKL/?spm_id_from=333.999.list.card_archive.click\\u0026vd_source=a8d547984b84c6842dee0d5de84e3413\",\n  \t\t\t\t\"start_time\": 1734503400,\n  \t\t\t\t\"end_time\": 1734926400,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"phkoulvyhq\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-i5WfTjIads.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 3274,\n  \t\t\t\t\"index\": 14,\n  \t\t\t\t\"title\": \"《蓦然回首》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"北京、上海、杭州、天津、成都、武汉、厦门、福州、昆明、青岛\",\n  \t\t\t\t\"color\": \"#2f1c1f\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/d4105070e4cc5a99a0ae3fd06eb3f8935f3b7a9b.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg\",\n  \t\t\t\t\"time_range\": \"2024-10-25 19:10:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/video/BV1hYStYZEwj/?spm_id_from=333.999.0.0\\u0026vd_source=a8d547984b84c6842dee0d5de84e3413\",\n  \t\t\t\t\"start_time\": 1729245600,\n  \t\t\t\t\"end_time\": 1729526399,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"zgftsxqegy\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-G9X0bwaTNd.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 3308,\n  \t\t\t\t\"index\": 15,\n  \t\t\t\t\"title\": \"《镇魂街》天武风雷篇\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"上海、广州、成都\",\n  \t\t\t\t\"color\": \"#382220\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/32dc66c169654154fd55a9b3f13d6dceb13262ba.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/B5hEufyuXo.jpg\",\n  \t\t\t\t\"time_range\": \"2024-11-16 16:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/bangumi/play/ep1114855?spm_id_from=333.999.0.0\",\n  \t\t\t\t\"start_time\": 1730718000,\n  \t\t\t\t\"end_time\": 1731340799,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"hinjqiumuj\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-48TAYrtJr0.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 3286,\n  \t\t\t\t\"index\": 16,\n  \t\t\t\t\"title\": \"《火影忍者：忍者之路 》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"北京、上海、广州、成都、深圳、南京、重庆、长沙、苏州、郑州\",\n  \t\t\t\t\"color\": \"#422b27\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/9f29c05877092f6dc0010af578efe6e83cfd824a.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg\",\n  \t\t\t\t\"time_range\": \"2024-11-02 16:55:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/video/BV19BDbYTEb6/?spm_id_from=333.999.0.0\\u0026vd_source=a8d547984b84c6842dee0d5de84e3413\",\n  \t\t\t\t\"start_time\": 1729742400,\n  \t\t\t\t\"end_time\": 1730131199,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"hahrideehl\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-BmThoecvQn.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 3220,\n  \t\t\t\t\"index\": 17,\n  \t\t\t\t\"title\": \"《毒液》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"北京、上海、杭州、哈尔滨、深圳、广州、成都、西安、合肥、重庆\",\n  \t\t\t\t\"color\": \"#291f22\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/58723e95c544349bcc197bd410cd1b8857edcf57.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg\",\n  \t\t\t\t\"time_range\": \"2024-10-23 19:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/video/BV1y81LYqEJc/?spm_id_from=333.999.0.0\\u0026vd_source=a8d547984b84c6842dee0d5de84e3413\",\n  \t\t\t\t\"start_time\": 1728705600,\n  \t\t\t\t\"end_time\": 1729353599,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"wcazjqsfzh\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-dJielz3qj4.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 3098,\n  \t\t\t\t\"index\": 18,\n  \t\t\t\t\"title\": \"《荒野机器人》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"上海、北京、广州、杭州、深圳、武汉、长沙、重庆、成都、天津\",\n  \t\t\t\t\"color\": \"#23170b\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/ba65216b160c8237eaa323043160e99c97278f07.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/ONJBt3XdwS.jpg\",\n  \t\t\t\t\"time_range\": \"2024-09-07 19:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/video/BV18o48ehEsg/?spm_id_from=333.999.0.0\\u0026vd_source=a8d547984b84c6842dee0d5de84e3413\",\n  \t\t\t\t\"start_time\": 1725004800,\n  \t\t\t\t\"end_time\": 1725379199,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"hulrbpulhm\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-Iy6p0W79lO.html\"\n  \t\t\t}, {\n  \t\t\t\t\"id\": 3156,\n  \t\t\t\t\"index\": 19,\n  \t\t\t\t\"title\": \"《变形金刚：起源》\",\n  \t\t\t\t\"type\": 3,\n  \t\t\t\t\"city\": \"上海、南京、西安、成都、广州、北京、武汉、沈阳、太原、青岛\",\n  \t\t\t\t\"color\": \"#4e3841\",\n  \t\t\t\t\"state\": 4,\n  \t\t\t\t\"image\": \"https://i0.hdslb.com/bfs/bangumi/kt/f6f4605aea0153e8a5f961c04a030bf35b3330bf.jpg\",\n  \t\t\t\t\"sub_image\": \"https://i0.hdslb.com/bfs/activity-plat/static/20240808/ac42a64944b270ce34d21d3ee72bfd10/3jTOS7UyWc.jpg\",\n  \t\t\t\t\"time_range\": \"2024-09-28 15:00:00\",\n  \t\t\t\t\"link\": \"https://www.bilibili.com/video/BV13d2cY7Exn/?spm_id_from=333.999.0.0\\u0026vd_source=a8d547984b84c6842dee0d5de84e3413\",\n  \t\t\t\t\"start_time\": 1726113600,\n  \t\t\t\t\"end_time\": 1727107199,\n  \t\t\t\t\"movie_reservation_state\": false,\n  \t\t\t\t\"activity_code\": \"krefykcmys\",\n  \t\t\t\t\"register_info_share_link\": \"https://www.bilibili.com/blackboard/activity-U3JZmvoM3m.html\"\n  \t\t\t}]\n  \t\t},\n  \t\t\"extra_params\": {\n  \t\t\t\"switch_on\": false,\n  \t\t\t\"birthday_sku_switch_on\": false,\n  \t\t\t\"is_buy_birthday_sku\": false,\n  \t\t\t\"is_birthday_off\": false,\n  \t\t\t\"phone_bind_state\": 0,\n  \t\t\t\"app_times\": 1,\n  \t\t\t\"na_ab\": 2,\n  \t\t\t\"na_ab_group_id\": \"59534,56618,56049,35348\",\n  \t\t\t\"is_same_to_last_session\": true,\n  \t\t\t\"is_white_list\": false,\n  \t\t\t\"cloud_ab\": 2,\n  \t\t\t\"cloud_ab_group_id\": \"59958,59534,56618,56049,35348\",\n  \t\t\t\"banner_ab\": 2,\n  \t\t\t\"banner_ab_group_id\": \"59534,56049\",\n  \t\t\t\"now_time\": 0,\n  \t\t\t\"free_welfare_ab\": 3,\n  \t\t\t\"device_limit_ab\": 0,\n  \t\t\t\"offline_ab\": 2,\n  \t\t\t\"welfare_module_height_ab\": 2\n  \t\t},\n  \t\t\"hit_ab\": false,\n  \t\t\"has_privilege_coupon\": false,\n  \t\t\"is_new_header\": 0,\n  \t\t\"header_bubble\": null\n  \t}\n  }\n  ```\n}\n"
  },
  {
    "path": "bruno/app.bilibili.com/folder.bru",
    "content": "meta {\n  name: app.bilibili.com\n}\n\nscript:pre-request {\n  const CryptoJS = require('crypto-js');\n  \n  console.log(\"start\");\n  \n  const md5 = (str) => CryptoJS.MD5(str).toString(CryptoJS.enc.Hex);\n  \n  const replacePlaceholders = (body) => {\n    for (const key in body) {\n      if (typeof body[key] === 'string') {\n        // Check if value contains {{}} placeholders\n        const matches = body[key].match(/{{(.*?)}}/g);\n        if (matches) {\n          matches.forEach(match => {\n            const placeholder = match.slice(2, -2); // Remove the {{ and }}\n            const value = bru.getEnvVar(placeholder);\n            body[key] = body[key].replace(match, value);\n          });\n        }\n      }\n    }\n  };\n  \n  function appSign(params, appkey, appsec) {\n    params.appkey = appkey;\n    delete body.sign;\n    const sortedKeys = Object.keys(params).sort();\n    const sortedParams = sortedKeys.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');\n    console.log(sortedParams);\n    return md5(sortedParams + appsec);\n  }\n  \n  const body = req.getBody();\n  \n  if (body && body.hasOwnProperty('sign')) {\n    replacePlaceholders(body);\n    const sign = appSign(body, bru.getEnvVar(\"appKey\"), bru.getEnvVar(\"appSec\"));\n    console.log(\"calculate sign:\" + sign);\n  \n    body.sign = sign;\n  }\n  \n  req.setBody(body);\n}\n"
  },
  {
    "path": "bruno/app.bilibili.com/pgc/activity/deliver/material/receive.bru",
    "content": "meta {\n  name: receive\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://app.bilibili.com/pgc/activity/deliver/material/receive\n  body: formUrlEncoded\n  auth: none\n}\n\nheaders {\n  Host: api.bilibili.com\n  buvid: {{buvid}}\n  fp_local: {{device_id}}\n  fp_remote: {{device_id}}\n  session_id: e04d2e05\n  env: prod\n  app-key: android64\n  user-agent: {{user-agent}}\n  x-bili-trace-id: 0564afa825e0e1ec59164fe59367755a:59164fe59367755a:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NzI2NDcsImlhdCI6MTczNTc0MzU0NywiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.eafhpooLoe2q6cA45_Xrgq1VO-y490pxP5gwJ4qm_ik\n  bili-http-engine: cronet\n  Cookie: {{cookieStr}}\n}\n\nbody:form-urlencoded {\n  activity_code: \n  appkey: {{appKey}}\n  build: {{build}}\n  c_locale: zh_CN\n  channel: bili\n  disable_rcmd: 0\n  ep_id: 328482\n  from_spmid: activity.h5.0.0\n  mobi_app: android\n  platform: android\n  s_locale: zh_CN\n  season_id: 12548\n  spmid: united.player-video-detail.0.0\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"7.72.0\",\"abtest\":\"\"}\n  ts: 1736179521\n  sign: 132d2532467ef649a925aece247cdb4b\n  access_key: {{access_key}}\n}\n\ndocs {\n  终端：APP\n  \n  作用：开始大会员赚大积分任务-观看剧集内容\n  \n  入口：\n    - 我的->会员中心->赚大积分->查看8项任务，点击“观看剧集内容”，选择视频后触发\n    \n  传入剧集的id，会返回task_id和token，用于标识该次观看任务\n  \n  该sample的视频为《让子弹飞》\n  \n  完整的观看剧集内容任务调用接口如下：\n  \n  - 领取：app.bilibili.com/pgc/activity/score/task/receive/v2\n  - 开始：app.bilibili.com/pgc/activity/deliver/material/receive\n  - 上报完成：app.bilibili.com/pgc/activity/deliver/task/complete\n  \n  Response Sample:\n  \n  ```json\n  {\n    \"code\": 0,\n    \"data\": {\n      \"closeType\": \"close_win\",\n      \"container\": [],\n      \"showTime\": \"\",\n      \"watch_count_down_cfg\": {\n        \"action\": \"url\",\n        \"closeType\": \"close_win\",\n        \"complete_status_desc\": \"大积分已到账\",\n        \"complete_status_jump_url\": \"https://big.bilibili.com/mobile/bigPoint?navhide=1&closable=1\",\n        \"count_down_status_desc\": \"看${time}获大积分\",\n        \"login\": true,\n        \"milliseconds\": 600000,\n        \"pause_status_desc\": \"计时暂停\",\n        \"showTime\": \"ENTER\",\n        \"task_id\": \"4320003\",\n        \"token\": \"67ba5888e7\"\n      }\n    },\n    \"message\": \"success\"\n  }\n  ```\n}\n"
  },
  {
    "path": "bruno/app.bilibili.com/pgc/activity/deliver/task/complete-ogv.bru",
    "content": "meta {\n  name: complete-ogv\n  type: http\n  seq: 2\n}\n\npost {\n  url: https://app.bilibili.com/pgc/activity/deliver/task/complete\n  body: formUrlEncoded\n  auth: none\n}\n\nheaders {\n  Host: api.bilibili.com\n  buvid: {{buvid}}\n  fp_local: {{device_id}}\n  fp_remote: {{device_id}}\n  session_id: e04d2e05\n  env: prod\n  app-key: android64\n  user-agent: {{user-agent}}\n  x-bili-trace-id: a301946d9621645a707b40973f67755c:707b40973f67755c:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NzI2NDcsImlhdCI6MTczNTc0MzU0NywiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.eafhpooLoe2q6cA45_Xrgq1VO-y490pxP5gwJ4qm_ik\n  bili-http-engine: cronet\n  Cookie: {{cookieStr}}\n}\n\nbody:form-urlencoded {\n  build: {{build}}\n  c_locale: zh_CN\n  channel: bili\n  disable_rcmd: 0\n  mobi_app: android\n  platform: android\n  s_locale: zh_CN\n  statistics: {{statistics}}\n  access_key: {{access_key}}\n  ts: 1735744760\n  sign: 2292d647d9b3f6dbd2f99b5a90cbddaf\n  appkey: {{appKey}}\n  task_id: 4320003\n  task_sign: 95cbef871100151e526fa5580534a364\n  timestamp: 1748884714621\n  token: 67ba5888e7\n}\n\ndocs {\n  终端：APP\n  \n  作用：上报完成大会员赚大积分任务-观看剧集内容\n  \n  入口：\n    - 我的->会员中心->赚大积分->查看8项任务，点击“观看剧集内容”，挑选视频，观看10分钟后，自动触发\n  \n  传入剧集的id，会返回task_id和token，用于标识该次观看任务\n  \n  task_sign必传，与sign的生成方式相同。即，先排除掉task_sign和sign，生成签名后赋值给task_sign，然后在签名一次得到sign\n  \n  且只能调用成功一次，第二次及之后会返回400\n  \n  完整的观看剧集内容任务调用接口如下：\n  \n  - 领取：app.bilibili.com/pgc/activity/score/task/receive/v2\n  - 开始：app.bilibili.com/pgc/activity/deliver/material/receive\n  - 上报完成：app.bilibili.com/pgc/activity/deliver/task/complete\n}\n"
  },
  {
    "path": "bruno/app.bilibili.com/pgc/activity/deliver/task/complete.bru",
    "content": "meta {\n  name: complete\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://app.bilibili.com/pgc/activity/deliver/task/complete\n  body: formUrlEncoded\n  auth: none\n}\n\nheaders {\n  Host: api.bilibili.com\n  buvid: {{buvid}}\n  fp_local: {{device_id}}\n  fp_remote: {{device_id}}\n  session_id: e04d2e05\n  env: prod\n  app-key: android64\n  user-agent: {{user-agent}}\n  x-bili-trace-id: a301946d9621645a707b40973f67755c:707b40973f67755c:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NzI2NDcsImlhdCI6MTczNTc0MzU0NywiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.eafhpooLoe2q6cA45_Xrgq1VO-y490pxP5gwJ4qm_ik\n  bili-http-engine: cronet\n  Cookie: {{cookieStr}}\n}\n\nbody:form-urlencoded {\n  build: {{build}}\n  c_locale: zh_CN\n  channel: bili\n  disable_rcmd: 0\n  mobi_app: android\n  platform: android\n  s_locale: zh_CN\n  statistics: {{statistics}}\n  access_key: {{access_key}}\n  ts: 1735744760\n  sign: 2292d647d9b3f6dbd2f99b5a90cbddaf\n  appkey: {{appKey}}\n  position: tv_channel\n  win_id: bigscore-filmtab\n}\n\ndocs {\n  终端：APP\n  \n  作用：上报完成大会员赚大积分任务-浏览追番频道页、浏览影视频道页（观看剧集内容接口相同，但入参不同，在另一个接口）\n  \n  入口：\n    - 我的->会员中心->赚大积分->查看8项任务，点击跳转后，自动触发\n  \n  完整的观看剧集内容任务调用接口如下：\n  \n  - 领取：app.bilibili.com/pgc/activity/score/task/receive/v2\n  - 上报完成：app.bilibili.com/pgc/activity/deliver/task/complete\n  \n  入参position:\n  \n  - animatetab: jp_channel\n  - filmtab: tv_channel\n}\n"
  },
  {
    "path": "bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/dressbuyamount.bru",
    "content": "meta {\n  name: dressbuyamount\n  type: http\n  seq: 3\n}\n\npost {\n  url: https://api.bilibili.com/pgc/activity/score/task/receive/v2\n  body: formUrlEncoded\n  auth: none\n}\n\nheaders {\n  Host: api.bilibili.com\n  Cookie: {{cookieStr}}\n  native_api_from: h5\n  buvid: {{buvid}}\n  accept: application/json, text/plain, */*\n  referer: https://big.bilibili.com/mobile/bigPoint/task\n  user-agent: {{user-agent}}\n  x-bili-trace-id: 39dab959605906ee420167e8af677533:420167e8af677533:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NjI4NDksImlhdCI6MTczNTczMzc0OSwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.EIrjYHrmFeTJXZjxsWki_ZloVvL9IK_aDgpqslMASy0\n  bili-http-engine: cronet\n}\n\nbody:form-urlencoded {\n  access_key: {{access_key}}\n  appKey: {{appKey}}\n  appkey: {{appKey}}\n  bili_local_id: {{device_id}}\n  build: 7720200\n  buvid: {{buvid}}\n  channel: yingyongbao\n  containerName: AbstractWebActivity\n  csrf: {{csrf}}\n  device: phone\n  deviceId: f9abaee74692f9e9\n  deviceName: samsungNexus\n  devicePlatform: Android10samsungNexus\n  device_id: {{device_id}}\n  device_name: samsungNexus\n  device_platform: Android10samsungNexus\n  disable_rcmd: 0\n  fingerprint: {{device_id}}\n  isPad: false\n  localFingerprint: {{device_id}}\n  local_id: {{buvid}}\n  mobi_app: android\n  modelName: Nexus\n  networkState: 2\n  networkstate: 2\n  osVer: 10\n  platform: android\n  sessionID: 92c5ad7a\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"7.72.0\",\"abtest\":\"\"}\n  statusBarHeight: 77\n  taskCode: dressbuyamount\n  ts: 1735734245\n  sign: 293cc4d525cf41cfb8adb69f42185ec0:\n}\n\ndocs {\n  终端：APP\n  \n  作用：领取大会员赚大积分任务-购买指定装扮商品\n  \n  入口：\n    - 我的->会员中心->赚大积分->查看8项任务->领取任务\n}\n"
  },
  {
    "path": "bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/ogvwatchnew.bru",
    "content": "meta {\n  name: ogvwatchnew\n  type: http\n  seq: 4\n}\n\npost {\n  url: https://app.bilibili.com/pgc/activity/score/task/receive/v2\n  body: formUrlEncoded\n  auth: none\n}\n\nheaders {\n  Host: api.bilibili.com\n  Cookie: {{cookieStr}}\n  native_api_from: h5\n  buvid: {{buvid}}\n  accept: application/json, text/plain, */*\n  referer: https://big.bilibili.com/mobile/bigPoint/task\n  user-agent: {{user-agent}}\n  x-bili-trace-id: 9e3ed57f35a83d4edb8160805867752f:db8160805867752f:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NjE2NjQsImlhdCI6MTczNTczMjU2NCwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.YVtwH53dLJ1l6n6aFcvwNZ4MBkgnBPtxE8UfD7u9J4I\n  bili-http-engine: cronet\n}\n\nbody:form-urlencoded {\n  access_key: {{access_key}}\n  appKey: {{appKey}}\n  appkey: {{appKey}}\n  bili_local_id: {{device_id}}\n  build: 7720200\n  buvid: {{buvid}}\n  channel: yingyongbao\n  containerName: AbstractWebActivity\n  csrf: {{csrf}}\n  device: phone\n  deviceId: f9abaee74692f9e9\n  deviceName: samsungNexus\n  devicePlatform: Android10samsungNexus\n  device_id: {{device_id}}\n  device_name: samsungNexus\n  device_platform: Android10samsungNexus\n  disable_rcmd: 0\n  fingerprint: {{device_id}}\n  isPad: false\n  localFingerprint: {{device_id}}\n  local_id: {{buvid}}\n  mobi_app: android\n  modelName: Nexus\n  networkState: 2\n  networkstate: 2\n  osVer: 10\n  platform: android\n  sessionID: 120548f6\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"7.72.0\",\"abtest\":\"\"}\n  statusBarHeight: 77\n  taskCode: ogvwatchnew\n  ts: 1735733021\n  sign: 5cc38f578700cfdb506f7e489abdf442:\n}\n\ndocs {\n  终端：APP\n  \n  作用：领取大会员赚大积分任务-观看剧集内容\n  \n  入口：\n    - 我的->会员中心->赚大积分->查看8项任务->领取任务\n    \n  完整的观看剧集内容任务调用接口如下：\n  \n  - 领取：app.bilibili.com/pgc/activity/score/task/receive/v2\n  - 开始：app.bilibili.com/pgc/activity/deliver/material/receive\n  - 上报完成：app.bilibili.com/pgc/activity/deliver/task/complete\n}\n"
  },
  {
    "path": "bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/tvodbuy.bru",
    "content": "meta {\n  name: tvodbuy\n  type: http\n  seq: 2\n}\n\npost {\n  url: https://api.bilibili.com/pgc/activity/score/task/receive/v2\n  body: formUrlEncoded\n  auth: none\n}\n\nheaders {\n  Host: api.bilibili.com\n  Cookie: {{cookieStr}}\n  native_api_from: h5\n  buvid: {{buvid}}\n  accept: application/json, text/plain, */*\n  referer: https://big.bilibili.com/mobile/bigPoint/task\n  user-agent: {{user-agent}}\n  x-bili-trace-id: 39dab959605906ee420167e8af677533:420167e8af677533:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NjI4NDksImlhdCI6MTczNTczMzc0OSwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.EIrjYHrmFeTJXZjxsWki_ZloVvL9IK_aDgpqslMASy0\n  bili-http-engine: cronet\n}\n\nbody:form-urlencoded {\n  access_key: {{access_key}}\n  appKey: {{appKey}}\n  appkey: {{appKey}}\n  bili_local_id: {{device_id}}\n  build: 7720200\n  buvid: {{buvid}}\n  channel: yingyongbao\n  containerName: AbstractWebActivity\n  csrf: {{csrf}}\n  device: phone\n  deviceId: f9abaee74692f9e9\n  deviceName: samsungNexus\n  devicePlatform: Android10samsungNexus\n  device_id: {{device_id}}\n  device_name: samsungNexus\n  device_platform: Android10samsungNexus\n  disable_rcmd: 0\n  fingerprint: {{device_id}}\n  isPad: false\n  localFingerprint: {{device_id}}\n  local_id: {{buvid}}\n  mobi_app: android\n  modelName: Nexus\n  networkState: 2\n  networkstate: 2\n  osVer: 10\n  platform: android\n  sessionID: 92c5ad7a\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"7.72.0\",\"abtest\":\"\"}\n  statusBarHeight: 77\n  taskCode: tvodbuy\n  ts: 1735734245\n  sign: 293cc4d525cf41cfb8adb69f42185ec0:\n}\n\ndocs {\n  终端：APP\n  \n  作用：领取大会员赚大积分任务-购买单点付费影片\n  \n  入口：\n    - 我的->会员中心->赚大积分->查看8项任务->领取任务\n}\n"
  },
  {
    "path": "bruno/app.bilibili.com/pgc/activity/score/task/receive/v2/vipmallbuy.bru",
    "content": "meta {\n  name: vipmallbuy\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://app.bilibili.com/pgc/activity/score/task/receive/v2\n  body: formUrlEncoded\n  auth: none\n}\n\nheaders {\n  Host: api.bilibili.com\n  Cookie: {{cookieStr}}\n  native_api_from: h5\n  buvid: {{buvid}}\n  accept: application/json, text/plain, */*\n  referer: https://big.bilibili.com/mobile/bigPoint/task\n  user-agent: {{user-agent}}\n  x-bili-trace-id: 9e3ed57f35a83d4edb8160805867752f:db8160805867752f:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NjE2NjQsImlhdCI6MTczNTczMjU2NCwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.YVtwH53dLJ1l6n6aFcvwNZ4MBkgnBPtxE8UfD7u9J4I\n  bili-http-engine: cronet\n}\n\nbody:form-urlencoded {\n  access_key: {{access_key}}\n  appKey: {{appKey}}\n  appkey: {{appKey}}\n  bili_local_id: {{device_id}}\n  build: 7720200\n  buvid: {{buvid}}\n  channel: yingyongbao\n  containerName: AbstractWebActivity\n  csrf: {{csrf}}\n  device: phone\n  deviceId: f9abaee74692f9e9\n  deviceName: samsungNexus\n  devicePlatform: Android10samsungNexus\n  device_id: {{device_id}}\n  device_name: samsungNexus\n  device_platform: Android10samsungNexus\n  disable_rcmd: 0\n  fingerprint: {{device_id}}\n  isPad: false\n  localFingerprint: {{device_id}}\n  local_id: {{buvid}}\n  mobi_app: android\n  modelName: Nexus\n  networkState: 2\n  networkstate: 2\n  osVer: 10\n  platform: android\n  sessionID: 120548f6\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"7.72.0\",\"abtest\":\"\"}\n  statusBarHeight: 77\n  taskCode: vipmallbuy\n  ts: 1735733021\n  sign: 5cc38f578700cfdb506f7e489abdf442:\n}\n\ndocs {\n  终端：APP\n  \n  作用：领取大会员赚大积分任务-购买指定会员购商品\n  \n  入口：\n    - 我的->会员中心->赚大积分->查看8项任务->领取任务\n}\n"
  },
  {
    "path": "bruno/app.bilibili.com/x/report/heartbeat/mobile/end.bru",
    "content": "meta {\n  name: end\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://app.bilibili.com/x/report/heartbeat/mobile\n  body: formUrlEncoded\n  auth: none\n}\n\nheaders {\n  Host: api.bilibili.com\n  buvid: {{buvid}}\n  fp_local: {{device_id}}\n  fp_remote: {{device_id}}\n  session_id: e04d2e05\n  env: prod\n  app-key: android64\n  user-agent: {{user-agent}}\n  x-bili-trace-id: 2c64470432d0c5346a475a449467755d:6a475a449467755d:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NzI2NDcsImlhdCI6MTczNTc0MzU0NywiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.eafhpooLoe2q6cA45_Xrgq1VO-y490pxP5gwJ4qm_ik\n  bili-http-engine: cronet\n}\n\nbody:form-urlencoded {\n  access_key: {{access_key}}\n  actual_played_time: 548\n  aid: 726710400\n  appkey: {{appKey}}\n  auto_play: 99\n  build: 7720200\n  c_locale: zh_CN\n  channel: yingyongbao\n  cid: 785731972\n  disable_rcmd: 0\n  epid: 511578\n  epid_status: 13\n  extra: {\"from_outer_spmid\":\"activity.h5.0.0\"}\n  from: 12\n  from_spmid: united.player-video-detail.player.continue\n  last_play_progress_time: 638\n  list_play_time: 0\n  max_play_progress_time: 638\n  mid: {{mid}}\n  miniplayer_play_time: 0\n  mobi_app: android\n  network_type: 1\n  paused_time: 0\n  platform: android\n  play_mode: 1\n  play_status: 1\n  play_type: \n  played_time: 548\n  quality: 64\n  report_flow_data: \n  s_locale: zh_CN\n  session: 2edcb8dc8ff0b6f13dd685a23aff692b72bf2869\n  sid: 41410\n  spmid: united.player-video-detail.0.0\n  start_ts: 1735744223\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"7.72.0\",\"abtest\":\"\"}\n  sub_type: 1\n  total_time: 548\n  track_id: \n  ts: 1735744771\n  type: 4\n  user_status: 1\n  video_duration: 1450\n  sign: 1021d178fb342c0c48617d3692c97d46\n}\n"
  },
  {
    "path": "bruno/app.bilibili.com/x/report/heartbeat/mobile/start.bru",
    "content": "meta {\n  name: start\n  type: http\n  seq: 2\n}\n\npost {\n  url: https://app.bilibili.com/x/report/heartbeat/mobile\n  body: formUrlEncoded\n  auth: none\n}\n\nheaders {\n  Host: api.bilibili.com\n  buvid: {{buvid}}\n  fp_local: {{device_id}}\n  fp_remote: {{device_id}}\n  session_id: e04d2e05\n  env: prod\n  app-key: android64\n  user-agent: {{user-agent}}\n  x-bili-trace-id: 8fcda5ac30f6510905b2834bbb67755a:05b2834bbb67755a:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3NzI2NDcsImlhdCI6MTczNTc0MzU0NywiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.eafhpooLoe2q6cA45_Xrgq1VO-y490pxP5gwJ4qm_ik\n  bili-http-engine: cronet\n}\n\nbody:form-urlencoded {\n  access_key: {{access_key}}\n  actual_played_time: 0\n  aid: 726710400\n  appkey: {{appKey}}\n  auto_play: 99\n  build: 7720200\n  c_locale: zh_CN\n  channel: yingyongbao\n  cid: 785731972\n  disable_rcmd: 0\n  epid: 511578\n  epid_status: 13\n  extra: {\"from_outer_spmid\":\"activity.h5.0.0\"}\n  from: 12\n  from_spmid: united.player-video-detail.player.continue\n  last_play_progress_time: 0\n  list_play_time: 0\n  max_play_progress_time: 0\n  mid: {{mid}}\n  miniplayer_play_time: 0\n  mobi_app: android\n  network_type: 1\n  paused_time: 0\n  platform: android\n  play_mode: 1\n  play_status: 1\n  play_type: \n  played_time: 0\n  quality: 64\n  report_flow_data: \n  s_locale: zh_CN\n  session: 2edcb8dc8ff0b6f13dd685a23aff692b72bf2869\n  sid: 41410\n  spmid: united.player-video-detail.0.0\n  start_ts: 0\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"7.72.0\",\"abtest\":\"\"}\n  sub_type: 1\n  total_time: 0\n  track_id: \n  ts: 1735744223\n  type: 4\n  user_status: 1\n  video_duration: 1450\n  sign: 37b0acd3bab7b40a082ce510041a0a2b\n}\n"
  },
  {
    "path": "bruno/big.bilibili.com/pgc/activity/score/task/sign.bru",
    "content": "meta {\n  name: sign\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://api.bilibili.com/pgc/activity/score/task/sign\n  body: formUrlEncoded\n  auth: none\n}\n\nheaders {\n  Host: api.bilibili.com\n  Cookie: {{cookieStr}}\n  native_api_from: h5\n  buvid: {{buvid}}\n  accept: application/json, text/plain, */*\n  referer: https://big.bilibili.com/mobile/index?exp_symbol=release_version&oflAb=1\n  user-agent: {{user-agent}}\n  x-bili-trace-id: 9c642fae280ce80077653eef826774e3:77653eef826774e3:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzgxMTEsImlhdCI6MTczNTcwOTAxMSwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.J28IlxZ6SjllQEQkq_OvFUiRYAEL2VhQG_WWBcmNppE\n  bili-http-engine: cronet\n}\n\nbody:form-urlencoded {\n  access_key: {{access_key}}\n  appkey: {{appKey}}\n  csrf: {{csrf}}\n  disable_rcmd: 0\n  mobi_app: android\n  platform: android\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"7.72.0\",\"abtest\":\"\"}\n  ts: 1735713546\n  sign: aeaeff881a147dd5cd2c6e24df9dc21b\n}\n"
  },
  {
    "path": "bruno/big.bilibili.com/x/vip/experience/add.bru",
    "content": "meta {\n  name: add\n  type: http\n  seq: 2\n}\n\npost {\n  url: https://big.bilibili.com/x/vip/experience/add\n  body: formUrlEncoded\n  auth: none\n}\n\nheaders {\n  Host: api.bilibili.com\n  Cookie: {{cookieStr}}\n  native_api_from: h5\n  buvid: {{buvid}}\n  accept: application/json, text/plain, */*\n  referer: https://big.bilibili.com/mobile/index?exp_symbol=release_version&oflAb=1\n  user-agent: {{user-agent}}\n  x-bili-trace-id: 46656ec97cb019beac1da03fd56774d6:ac1da03fd56774d6:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzgxMTEsImlhdCI6MTczNTcwOTAxMSwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.J28IlxZ6SjllQEQkq_OvFUiRYAEL2VhQG_WWBcmNppE\n  bili-http-engine: cronet\n}\n\nbody:form-urlencoded {\n  access_key: {{access_key}}\n  appkey: {{appKey}}\n  buvid: {{buvid}}\n  csrf: {{csrf}}\n  disable_rcmd: 0\n  mobi_app: android\n  platform: android\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"7.72.0\",\"abtest\":\"\"}\n  ts: 1735710395\n  sign: 694521e34b88ec0593a8cc3edbc4e117\n}\n"
  },
  {
    "path": "bruno/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"Bili\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}"
  },
  {
    "path": "bruno/environments/default.bru",
    "content": "vars {\n  phone: {{process.env.phone}}\n  pwd: {{process.env.pwd}}\n  mid: {{process.env.mid}}\n  buvid: {{process.env.buvid}}\n  csrf: {{process.env.csrf}}\n  appKey: 1d8b6e7d45233436\n  access_key: {{process.env.access_key}}\n  cookieStr: {{process.env.cookieStr}}\n  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\n  device_id: {{process.env.device_id}}\n  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\n  appSec: 560c52ccd288fed045859ed18bffd973\n  build: 8451100\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"8.45.1\",\"abtest\":\"\"}\n}\n"
  },
  {
    "path": "bruno/mall.bilibili.com/combine.bru",
    "content": "meta {\n  name: combine\n  type: http\n  seq: 2\n}\n\nget {\n  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\n  body: none\n  auth: inherit\n}\n\nparams:query {\n  access_key: {{access_key}}\n  appKey: {{appKey}}\n  bili_local_id: 910f57d9d7feaf6928e592860452d06b202505211207597aa9050a2e5c31f431\n  brand: Samsung\n  build: {{build}}\n  buvid: {{buvid}}\n  channel: bili\n  containerName: AbstractWebActivity\n  csrf: {{csrf}}\n  device: phone\n  deviceId: {{device_id}}\n  deviceName: SamsungSM-A5560\n  devicePlatform: Android12SamsungSM-A5560\n  device_id: {{device_id}}\n  device_name: SamsungSM-A5560\n  device_platform: Android12SamsungSM-A5560\n  disable_rcmd: 0\n  fingerprint: 910f57d9d7feaf6928e592860452d06b202505211207597aa9050a2e5c31f431\n  isPad: false\n  localFingerprint: 910f57d9d7feaf6928e592860452d06b202505211207597aa9050a2e5c31f431\n  local_id: XUA5651A9EDF7387153A945CDE96CADCB6000\n  mobi_app: android\n  modelName: SM-A5560\n  networkState: 2\n  networkstate: 2\n  osVer: 12\n  platform: android\n  sessionID: 01db4bc0\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"8.45.1\",\"abtest\":\"\"}\n  statusBarHeight: 72\n  ts: 1748764069\n  sign: de7e2667157cccf4af3def524b6796e0\n}\n\nheaders {\n  Host: api.bilibili.com\n  Cookie: {{cookieStr}}\n  accept: application/json, text/plain, */*\n  bili-http-engine: ignet\n  buvid: {{buvid}}\n  content-type: application/json\n  native_api_from: h5\n  referer: https://big.bilibili.com/mobile/bigPoint/task\n  user-agent: {{user-agent}}\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-metadata-legal-region: CN\n  x-bili-mid: 341688380\n  x-bili-net-bin: DQAAgL8gAQ\n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDg3NzU1OTksImlhdCI6MTc0ODc0NjQ5OSwiYnV2aWQiOiJYVUE1NjUxQTlFREY3Mzg3MTUzQTk0NUNERTk2Q0FEQ0I2MDAwIn0.k0x2o3e2Q3W-6Wzc56IhbLgSjDKTaAuUV9om7K213fI\n  x-bili-trace-id: d605d9f1dc3818a42f3a22b5c4683c05:2f3a22b5c4683c05:0:0\n}\n\ndocs {\n  终端：APP\n  \n  作用：获取大会员赚大积分的任务列表\n  \n  入口：\n    - 我的->会员中心->赚大积分->查看8项任务\n  \n  Response sample:\n  \n  ```json\n  {\n  \t\"code\": 0,\n  \t\"message\": \"0\",\n  \t\"ttl\": 1,\n  \t\"data\": {\n  \t\t\"vip_info\": {\n  \t\t\t\"type\": 2,\n  \t\t\t\"status\": 1,\n  \t\t\t\"due_date\": 1779033600000,\n  \t\t\t\"vip_pay_type\": 0,\n  \t\t\t\"label\": {\n  \t\t\t\t\"path\": \"http://i0.hdslb.com/bfs/vip/label_annual.png\",\n  \t\t\t\t\"text_color\": \"\",\n  \t\t\t\t\"bg_style\": 0,\n  \t\t\t\t\"bg_color\": \"\",\n  \t\t\t\t\"border_color\": \"\",\n  \t\t\t\t\"use_img_label\": false,\n  \t\t\t\t\"img_label_uri_hans\": \"\",\n  \t\t\t\t\"img_label_uri_hant\": \"\",\n  \t\t\t\t\"img_label_uri_hans_static\": \"\",\n  \t\t\t\t\"img_label_uri_hant_static\": \"\"\n  \t\t\t},\n  \t\t\t\"start_time\": 1657418210,\n  \t\t\t\"paid_type\": 0,\n  \t\t\t\"mid\": 341688380,\n  \t\t\t\"role\": 3,\n  \t\t\t\"tv_vip_status\": 0,\n  \t\t\t\"tv_vip_pay_type\": 0,\n  \t\t\t\"tv_due_date\": 0,\n  \t\t\t\"vip_recent_time\": 1747398345\n  \t\t},\n  \t\t\"point_info\": {\n  \t\t\t\"point\": 415,\n  \t\t\t\"expire_point\": 0,\n  \t\t\t\"expire_time\": 0,\n  \t\t\t\"expire_days\": 0\n  \t\t},\n  \t\t\"task_info\": {\n  \t\t\t\"modules\": [{\n  \t\t\t\t\"module_title\": \"福利任务\",\n  \t\t\t\t\"common_task_item\": [{\n  \t\t\t\t\t\"task_code\": \"bonus\",\n  \t\t\t\t\t\"state\": 3,\n  \t\t\t\t\t\"title\": \"大会员福利大积分\",\n  \t\t\t\t\t\"icon\": \"https://i0.hdslb.com/bfs/activity-plat/static/20220607/b66bfe4ccfd6bed05bdb54008ff5c7aa/IbtMl6R3yt.png\",\n  \t\t\t\t\t\"subtitle\": \"大会员/年度大会员\\u003cbr /\\u003e\\u003cspan class=\\\"active\\\"\\u003e+100/200大积分\",\n  \t\t\t\t\t\"explain\": \"在期大会员可领取100大积分，在期年度大会员可领取200大积分，每人限领1次。如当身份为大会员时已领取过本任务大积分，则后续成为年度大会员也无法补领取差值大积分。\",\n  \t\t\t\t\t\"vip_limit\": 1,\n  \t\t\t\t\t\"complete_times\": 1,\n  \t\t\t\t\t\"max_times\": 1,\n  \t\t\t\t\t\"recall_num\": 0\n  \t\t\t\t}]\n  \t\t\t}, {\n  \t\t\t\t\"module_title\": \"体验任务\",\n  \t\t\t\t\"common_task_item\": [{\n  \t\t\t\t\t\"task_code\": \"privilege\",\n  \t\t\t\t\t\"state\": 3,\n  \t\t\t\t\t\"title\": \"浏览大会员权益页面\",\n  \t\t\t\t\t\"icon\": \"https://i0.hdslb.com/bfs/activity-plat/static/20220607/b66bfe4ccfd6bed05bdb54008ff5c7aa/IbtMl6R3yt.png\",\n  \t\t\t\t\t\"subtitle\": \"\\u003cspan class=\\\"active\\\"\\u003e+50大积分\\u003c/span\\u003e\",\n  \t\t\t\t\t\"explain\": \"从本任务入口跳转至大会员权益页，浏览后可得50大积分，每人限完成1次。\",\n  \t\t\t\t\t\"link\": \"https://big.bilibili.com/mobile/rights?closable=1\\u0026navhide=1\",\n  \t\t\t\t\t\"vip_limit\": 0,\n  \t\t\t\t\t\"complete_times\": 1,\n  \t\t\t\t\t\"max_times\": 1,\n  \t\t\t\t\t\"recall_num\": 0\n  \t\t\t\t}]\n  \t\t\t}, {\n  \t\t\t\t\"module_title\": \"日常任务\",\n  \t\t\t\t\"module_sub_title\": \"截止至06-01 23:59:59\",\n  \t\t\t\t\"common_task_item\": [{\n  \t\t\t\t\t\"task_code\": \"dress-view\",\n  \t\t\t\t\t\"state\": 1,\n  \t\t\t\t\t\"title\": \"浏览装扮商城主页\",\n  \t\t\t\t\t\"icon\": \"https://i0.hdslb.com/bfs/activity-plat/static/20230316/b66bfe4ccfd6bed05bdb54008ff5c7aa/d8FFfwIwFC.png\",\n  \t\t\t\t\t\"subtitle\": \"\\u003cspan class=\\\"active\\\"\\u003e+10大积分\\u003c/span\\u003e\",\n  \t\t\t\t\t\"explain\": \"从本任务入口跳转至装扮商城主页即可完成任务，每人限完成1次。\",\n  \t\t\t\t\t\"link\": \"https://www.bilibili.com/h5/mall/home?f_source=vip\\u0026navhide=1\",\n  \t\t\t\t\t\"vip_limit\": 1,\n  \t\t\t\t\t\"complete_times\": 0,\n  \t\t\t\t\t\"max_times\": 1,\n  \t\t\t\t\t\"recall_num\": 0\n  \t\t\t\t}, {\n  \t\t\t\t\t\"task_code\": \"vipmallview\",\n  \t\t\t\t\t\"state\": 1,\n  \t\t\t\t\t\"title\": \"浏览会员购页面10秒\",\n  \t\t\t\t\t\"icon\": \"https://i0.hdslb.com/bfs/activity-plat/static/20220613/b66bfe4ccfd6bed05bdb54008ff5c7aa/RnlARrUdOY.png\",\n  \t\t\t\t\t\"subtitle\": \"\\u003cspan class=\\\"active\\\"\\u003e+10大积分\\u003c/span\\u003e\",\n  \t\t\t\t\t\"explain\": \"从本任务入口跳转至会员购页面，并连续浏览页面达10秒可得10大积分，每天可完成1次。如浏览过程中离开会员购页面则中断计时，任务判定失败，需重新从本任务入口再次跳转。\",\n  \t\t\t\t\t\"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\",\n  \t\t\t\t\t\"vip_limit\": 1,\n  \t\t\t\t\t\"complete_times\": 0,\n  \t\t\t\t\t\"max_times\": 1,\n  \t\t\t\t\t\"recall_num\": 0\n  \t\t\t\t}, {\n  \t\t\t\t\t\"task_code\": \"vipmallbuy\",\n  \t\t\t\t\t\"state\": 0,\n  \t\t\t\t\t\"title\": \"购买指定会员购商品\",\n  \t\t\t\t\t\"icon\": \"https://i0.hdslb.com/bfs/activity-plat/static/20220613/b66bfe4ccfd6bed05bdb54008ff5c7aa/RnlARrUdOY.png\",\n  \t\t\t\t\t\"subtitle\": \"实付1元\\u003cspan class=\\\"active\\\"\\u003e+10大积分\\u003c/span\\u003e，当前0/2000\",\n  \t\t\t\t\t\"explain\": \"领取本任务后，当天内购买会员购商品的首笔订单（除魔力赏/一番赏/票务/先行预定订单外）可获得大积分，每实付1元+10大积分，每天上限2000大积分。本任务不支持先行购买后补领取大积分。如用户通过购买会员购商品获得了本任务大积分，后续将购买的商品退款，则同时会扣除当时获得的任务大积分。如购买商品获得大积分后的当天内产生退款，则大积分扣除后，当天无法再完成此任务。\",\n  \t\t\t\t\t\"link\": \"bilibili://mall/home?msource=member_integral_buy\",\n  \t\t\t\t\t\"vip_limit\": 1,\n  \t\t\t\t\t\"complete_times\": 0,\n  \t\t\t\t\t\"max_times\": 20000,\n  \t\t\t\t\t\"recall_num\": 0\n  \t\t\t\t}, {\n  \t\t\t\t\t\"task_code\": \"animatetab\",\n  \t\t\t\t\t\"state\": 1,\n  \t\t\t\t\t\"title\": \"浏览追番频道页10秒\",\n  \t\t\t\t\t\"icon\": \"https://i0.hdslb.com/bfs/activity-plat/static/20220607/b66bfe4ccfd6bed05bdb54008ff5c7aa/uOwc1tuJwm.png\",\n  \t\t\t\t\t\"subtitle\": \"\\u003cspan class=\\\"active\\\"\\u003e+10大积分\\u003c/span\\u003e\",\n  \t\t\t\t\t\"explain\": \"从本任务入口跳转至追番频道页，并连续浏览页面达10秒可得10大积分，每天可完成1次。如浏览过程中离开追番频道页则中断计时，任务判定失败，需重新从本任务入口再次跳转。\",\n  \t\t\t\t\t\"link\": \"bilibili://home?bottom_tab_id=home\\u0026tab_id=bangumi\\u0026vip_task_countdown=10000\\u0026win_id=bigscore-animatetab\",\n  \t\t\t\t\t\"vip_limit\": 1,\n  \t\t\t\t\t\"complete_times\": 0,\n  \t\t\t\t\t\"max_times\": 1,\n  \t\t\t\t\t\"recall_num\": 0\n  \t\t\t\t}, {\n  \t\t\t\t\t\"task_code\": \"filmtab\",\n  \t\t\t\t\t\"state\": 1,\n  \t\t\t\t\t\"title\": \"浏览影视频道页10秒\",\n  \t\t\t\t\t\"icon\": \"https://i0.hdslb.com/bfs/activity-plat/static/20220607/b66bfe4ccfd6bed05bdb54008ff5c7aa/bWPJRBuMh3.png\",\n  \t\t\t\t\t\"subtitle\": \"\\u003cspan class=\\\"active\\\"\\u003e+10大积分\\u003c/span\\u003e\",\n  \t\t\t\t\t\"explain\": \"从本任务入口跳转至影视频道页，并连续浏览页面达10秒可得10大积分，每天可完成1次。如浏览过程中离开影视频道页则中断计时，任务判定失败，需重新从本任务入口再次跳转。\",\n  \t\t\t\t\t\"link\": \"bilibili://home?bottom_tab_id=home\\u0026tab_id=bilibili://pgc/cinema-tab\\u0026vip_task_countdown=10000\\u0026win_id=bigscore-filmtab\",\n  \t\t\t\t\t\"vip_limit\": 1,\n  \t\t\t\t\t\"complete_times\": 0,\n  \t\t\t\t\t\"max_times\": 1,\n  \t\t\t\t\t\"recall_num\": 0\n  \t\t\t\t}, {\n  \t\t\t\t\t\"task_code\": \"ogvwatchnew\",\n  \t\t\t\t\t\"state\": 0,\n  \t\t\t\t\t\"title\": \"观看剧集内容\",\n  \t\t\t\t\t\"icon\": \"https://i0.hdslb.com/bfs/activity-plat/static/20220607/b66bfe4ccfd6bed05bdb54008ff5c7aa/6prGo240Md.png\",\n  \t\t\t\t\t\"subtitle\": \"观看任一剧集达10分钟\\u003cspan class=\\\"active\\\"\\u003e+40大积分\\u003c/span\\u003e\",\n  \t\t\t\t\t\"explain\": \"领取本任务后，当天内单次观看番剧/国创/电影/电视剧/综艺/纪录片的任一视频的连续时长达到10分钟可得40大积分。单次观看行为需连续达10分钟，切换至其他视频内容会导致观看时长重新计数。仅大陆版手机端APP可领取、可完成任务。\",\n  \t\t\t\t\t\"link\": \"https://www.bilibili.com/blackboard/activity-NUbGzdNJpz.html?msource=jifen\",\n  \t\t\t\t\t\"vip_limit\": 1,\n  \t\t\t\t\t\"complete_times\": 0,\n  \t\t\t\t\t\"max_times\": 1,\n  \t\t\t\t\t\"recall_num\": 0\n  \t\t\t\t}, {\n  \t\t\t\t\t\"task_code\": \"tvodbuy\",\n  \t\t\t\t\t\"state\": 0,\n  \t\t\t\t\t\"title\": \"购买单点付费影片\",\n  \t\t\t\t\t\"icon\": \"https://i0.hdslb.com/bfs/activity-plat/static/20220607/b66bfe4ccfd6bed05bdb54008ff5c7aa/6prGo240Md.png\",\n  \t\t\t\t\t\"subtitle\": \"会员特价/独家付费任意影片\\u003cbr /\\u003e\\u003cspan class=\\\"active\\\"\\u003e+70大积分\\u003c/span\\u003e\",\n  \t\t\t\t\t\"explain\": \"领取本任务后，当天内购买任意1部会员特价/独家付费的影片可得70大积分，每天上限70大积分。\",\n  \t\t\t\t\t\"link\": \"https://www.bilibili.com/blackboard/activity-F6sCMq4w92.html?msource=jifen\",\n  \t\t\t\t\t\"vip_limit\": 1,\n  \t\t\t\t\t\"complete_times\": 0,\n  \t\t\t\t\t\"max_times\": 1,\n  \t\t\t\t\t\"recall_num\": 0\n  \t\t\t\t}, {\n  \t\t\t\t\t\"task_code\": \"dressbuyamount\",\n  \t\t\t\t\t\"state\": 0,\n  \t\t\t\t\t\"title\": \"购买指定装扮商品\",\n  \t\t\t\t\t\"icon\": \"https://i0.hdslb.com/bfs/activity-plat/static/20230316/b66bfe4ccfd6bed05bdb54008ff5c7aa/d8FFfwIwFC.png\",\n  \t\t\t\t\t\"subtitle\": \"实付1元\\u003cspan class=\\\"active\\\"\\u003e+10大积分\\u003c/span\\u003e，当前0/1000\",\n  \t\t\t\t\t\"explain\": \"领取本任务后，当天内购买装扮商城商品的订单（除装扮赏/祈愿/数字藏品订单外）可获得大积分，每实付1元+10大积分，每天上限1000大积分。本任务不支持先行购买后补领取大积分。\",\n  \t\t\t\t\t\"link\": \"https://www.bilibili.com/h5/mall/home?navhide=1\\u0026f_source=vip\\u0026from=vipzx.shop\",\n  \t\t\t\t\t\"vip_limit\": 1,\n  \t\t\t\t\t\"complete_times\": 0,\n  \t\t\t\t\t\"max_times\": 10000,\n  \t\t\t\t\t\"recall_num\": 0\n  \t\t\t\t}]\n  \t\t\t}],\n  \t\t\t\"sing_task_item\": {\n  \t\t\t\t\"histories\": [{\n  \t\t\t\t\t\"day\": \"2025-05-26\",\n  \t\t\t\t\t\"signed\": false,\n  \t\t\t\t\t\"score\": 0\n  \t\t\t\t}, {\n  \t\t\t\t\t\"day\": \"2025-05-27\",\n  \t\t\t\t\t\"signed\": false,\n  \t\t\t\t\t\"score\": 0\n  \t\t\t\t}, {\n  \t\t\t\t\t\"day\": \"2025-05-28\",\n  \t\t\t\t\t\"signed\": false,\n  \t\t\t\t\t\"score\": 0\n  \t\t\t\t}, {\n  \t\t\t\t\t\"day\": \"2025-05-29\",\n  \t\t\t\t\t\"signed\": false,\n  \t\t\t\t\t\"score\": 0\n  \t\t\t\t}, {\n  \t\t\t\t\t\"day\": \"2025-05-30\",\n  \t\t\t\t\t\"signed\": false,\n  \t\t\t\t\t\"score\": 0\n  \t\t\t\t}, {\n  \t\t\t\t\t\"day\": \"2025-05-31\",\n  \t\t\t\t\t\"signed\": false,\n  \t\t\t\t\t\"score\": 0\n  \t\t\t\t}, {\n  \t\t\t\t\t\"day\": \"2025-06-01\",\n  \t\t\t\t\t\"signed\": false,\n  \t\t\t\t\t\"score\": 5,\n  \t\t\t\t\t\"is_today\": true\n  \t\t\t\t}, {\n  \t\t\t\t\t\"day\": \"2025-06-02\",\n  \t\t\t\t\t\"signed\": false,\n  \t\t\t\t\t\"score\": 5\n  \t\t\t\t}, {\n  \t\t\t\t\t\"day\": \"2025-06-03\",\n  \t\t\t\t\t\"signed\": false,\n  \t\t\t\t\t\"score\": 5\n  \t\t\t\t}, {\n  \t\t\t\t\t\"day\": \"2025-06-04\",\n  \t\t\t\t\t\"signed\": false,\n  \t\t\t\t\t\"score\": 5\n  \t\t\t\t}, {\n  \t\t\t\t\t\"day\": \"2025-06-05\",\n  \t\t\t\t\t\"signed\": false,\n  \t\t\t\t\t\"score\": 5\n  \t\t\t\t}, {\n  \t\t\t\t\t\"day\": \"2025-06-06\",\n  \t\t\t\t\t\"signed\": false,\n  \t\t\t\t\t\"score\": 5\n  \t\t\t\t}, {\n  \t\t\t\t\t\"day\": \"2025-06-07\",\n  \t\t\t\t\t\"signed\": false,\n  \t\t\t\t\t\"score\": 10\n  \t\t\t\t}],\n  \t\t\t\t\"count\": 0,\n  \t\t\t\t\"base_score\": 5\n  \t\t\t},\n  \t\t\t\"score_month\": 10,\n  \t\t\t\"score_limit\": 10000,\n  \t\t\t\"exp_value\": 3\n  \t\t},\n  \t\t\"current_ts\": 1748764070,\n  \t\t\"integration_task\": false\n  \t}\n  }\n  ```\n  \n  `build`版本号不同，会返回不同的结果。\n}\n"
  },
  {
    "path": "bruno/mall.bilibili.com/folder.bru",
    "content": "meta {\n  name: mall.bilibili.com\n}\n"
  },
  {
    "path": "bruno/mall.bilibili.com/sign2.bru",
    "content": "meta {\n  name: sign2\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://mall.bilibili.com/pgc/activity/score/task/sign2?mobi_app=android&csrf={{csrf}}&platform=android\n  body: json\n  auth: inherit\n}\n\nparams:query {\n  mobi_app: android\n  csrf: {{csrf}}\n  platform: android\n}\n\nheaders {\n  Host: api.bilibili.com\n  Cookie: {{cookieStr}}\n  accept: application/json, text/plain, */*\n  bili-http-engine: ignet\n  buvid: {{buvid}}\n  content-type: application/json; charset=utf-8\n  guestid: 24675260415603\n  native_api_from: h5\n  referer: https://big.bilibili.com/mobile/index\n  user-agent: {{user-agent}}\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-metadata-legal-region: CN\n  x-bili-mid: {{mid}}\n  x-bili-net-bin: DQAAgL8gAQ\n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDg0NzM4MTEsImlhdCI6MTc0ODQ0NDcxMSwiYnV2aWQiOiJYVUE1NjUxQTlFREY3Mzg3MTUzQTk0NUNERTk2Q0FEQ0I2MDAwIn0.IvpforrVemmRAvyQF7svr4fd-RnfP_lEt6g6pWt4AGk\n  x-bili-trace-id: 0d6c319f356dbc6191becbf764683728:91becbf764683728:0:0\n}\n\nbody:json {\n  {\n    \"t\": 1748445354567,\n    \"device\": \"phone\",\n    \"ts\": 1748445354\n  }\n}\n\ndocs {\n  Response sample:\n  \n  ```json\n  {\n      \"code\": 0,\n      \"data\": {\n          \"count\": 2,\n          \"countdown\": 0,\n          \"day3WinImg\": \"https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_win_img.png\",\n          \"day3WinImgVip\": \"https://i0.hdslb.com/bfs/activity-plat/static/4cefecc6742f8995a6bd22402a6d0b8b/day3_win_img_vip.png\",\n          \"duration\": 7,\n          \"goods\": [\n              {\n                  \"picture\": \"https://i0.hdslb.com/bfs/activity-plat/d3df8012062c6996c26f989ed6a4f0752e5d5049.png\",\n                  \"sale\": 128888,\n                  \"title\": \"御坂美琴&食蜂操祈 大霸星祭Ver. 手办\"\n              },\n              {\n                  \"picture\": \"https://i0.hdslb.com/bfs/activity-plat/245337f3c305886e4937430335218f2d7bf92362.jpg\",\n                  \"sale\": 39,\n                  \"title\": \"暗黑不朽积分兑换礼包\"\n              },\n              {\n                  \"picture\": \"https://i0.hdslb.com/bfs/activity-plat/b782d7228e8a58d2562d26f33448a50519ce4ec5.png\",\n                  \"sale\": 33600,\n                  \"title\": \"BEMOE 初音未来 樱花未来 可爱体UWA系列 毛绒4wa\"\n              }\n          ],\n          \"hasCoupon\": false,\n          \"score\": 5,\n          \"seasons\": [\n              {\n                  \"badge\": \"独家\",\n                  \"badgeType\": 1,\n                  \"cover\": \"http://i0.hdslb.com/bfs/bangumi/6a04c87e990ab74cd8d555ef45a863de0993b161.png\",\n                  \"ratingScore\": 9.8,\n                  \"seasonId\": 5398,\n                  \"seasonType\": 1,\n                  \"subtitle\": \"不一样的热血动画\",\n                  \"title\": \"JOJO的奇妙冒险 不灭钻石\"\n              },\n              {\n                  \"badge\": \"大会员\",\n                  \"badgeType\": 0,\n                  \"cover\": \"http://i0.hdslb.com/bfs/bangumi/6d8bd12e0e1ab2d4d5e8567bdba18240e75d7a1b.jpg\",\n                  \"ratingScore\": 9.8,\n                  \"seasonId\": 6262,\n                  \"seasonType\": 1,\n                  \"subtitle\": \"不想来笑一下吗？\",\n                  \"title\": \"蜡笔小新 第二季（中文）\"\n              },\n              {\n                  \"badge\": \"出品\",\n                  \"badgeType\": 1,\n                  \"cover\": \"https://i0.hdslb.com/bfs/bangumi/image/08bf0c1e24e454de51b58d1e26c0a9aecbe9b0c1.png\",\n                  \"ratingScore\": 0.0,\n                  \"seasonId\": 46585,\n                  \"seasonType\": 4,\n                  \"subtitle\": \"末世如何才能生存\",\n                  \"title\": \"灵笼 第二季\"\n              }\n          ],\n          \"vipScore\": 5,\n          \"vipStatus\": 1\n      },\n      \"message\": \"success\"\n  }\n  ```\n}\n"
  },
  {
    "path": "bruno/passport.bilibili.com/x/passport-login/oauth2/login.bru",
    "content": "meta {\n  name: login\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://passport.bilibili.com/x/passport-login/oauth2/login\n  body: formUrlEncoded\n  auth: none\n}\n\nheaders {\n  Host: passport.bilibili.com\n  buvid: {{buvid}}\n  env: prod\n  app-key: android64\n  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\n  x-bili-trace-id: fe7876684669e19624e1290d506774bf:24e1290d506774bf:0:0\n  x-bili-aurora-eid: \n  x-bili-mid: \n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzI4OTIsImlhdCI6MTczNTcwMzc5MiwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.hEDtIHZoN5Wyoja2hNuPcfy-laa1r05ObiEhJ5gPSt4\n  bili-http-engine: cronet\n  content-type: application/x-www-form-urlencoded; charset=utf-8\n}\n\nbody:form-urlencoded {\n  appkey: 783bbb7264451d82\n  bili_local_id: {{device_id}}\n  build: 7720200\n  buvid: {{buvid}}\n  c_locale: zh_CN\n  channel: yingyongbao\n  device: phone\n  device_id: {{device_id}}\n  device_meta: EA0906375AA2116F395747958E296DABD2DB115A596F20FEF9C281BC443B52439A8897F1E1E39D2C396B1320F5B47A22120559402BE3F2F6FAB4089CB04B0BA42436E3E28CDBDB46920F42E9B3C6ABB0D26C1B8E3808C143962BD06CAE0B555F01917334431CEF6E5F0372B1B8043D01FEDE6E8406EAA6580005ECEA5771FA1F12252FCA1DAF83A135061B5950D1A0E98C94B5AB91C5916B5CF9A363977BA21F511D742A58ED1E414E1350CA6CDE755086D72BB9FAA7718B497093723E77CA976C0F6946445E8862923DB59F632C4952446ADABF9B07D6022FBF40C821797FCEE2231F5F7ABBB85C1769CEF82A43A5EF9A662A2FDC9C9E1F028FDC66E46D654838A7D3737092C6AC52C2895903DAC8DAB3DCF14E917A9AA246220C3CFCD33175C55E5A1C122FF73F7E99495DE0EED8FCD4037C4A053372EBACED4C5BCB99AFFB24002936B00E90A6B52B4BBCCF2A4BD9680F16B58864E065B94B9D3C4F7216BED851F830D8BF0FD56B16893914E38095DC928F8A9C00DDABE8A7533245F53509582939293D7307A0839A3FE6CA0974C9FE545D8D796955804E63A65D776FD4160A47FC4C9A45C5C5CA3B233AF162CE8458F20D8762C02815E4F1645276D1764AFC6D0785F94D8D0534E266A316BAC348924E9199A07CBDAEC49869CEC41B0263A5A725C86F154297E3F6F62AA118F0DB34B2B5ABF04E97AADED2820A1AA45E0660FFC34289AB519146F4D59EFFAADE07E83AEE64ED995239DA23F3A8FF73E4735BF913FC8E5742251815DF403AE367526F4D660857799852F39904C5AB0AAA7E5ABCEE2EB05DD50D119A9E0D99B7ADF4AB4EEC86FE3504D4E6F7E255642CC94E26BD14FA28D67CC18BFE055A460E19320E923B44F5EBBD29E0495AC23B81861FFCE52E30E61A27A65C3D0AE4D8A9AB5E4E4E92804553EBA0AB9A53B6E5FA0215ABF5E91AD6F1F53E654617ED23EE4B2A97EE5A05C1F491FA424C13431D1760E792E90E9E1886C2CE4E72F77669BB408D45005E306001D6F9ECC714A539224751E750A73E378F62E158293B3C3AD4354A3580F802F3DE34F9031204380F50C7F0513985B8CA09DC1BF7A78755AF12243BB3227EA00A176EC17005C681569DB6CA2FAA8ADB0454939567BD96608271536D459C42CCA16EA7A55F0CC975F526C278055D36AB45B3F2D72216FFA4B0107A7EC826ED08F8570B3D44BB518FA82A1A9A4E527FEA59A32E5354F5B53B3305AAC80C36488D736E40AE6A530DEBA491D77E44F68E2DE54B074D98D87B8FB2789105C19729E38BF0A01C5B86F9E14750DDE66D8E308D48045E567169F0122500CE1A7C30838F1B84C41C4B55B9AFFA3F3A7E2DF4D86505A93E9EC3C376C8C3BE935D783722C010065EB23DA93D7B338C8DA9C1424FB15866E87160DFB9AD9C2954C9B1D2D568296329FC5FFB8F0A7F1D7C4E3269E95679031791D18EB5447760A6A8BD824D7DFF68119D5B38C78E180410CE877B232BD65770751EEB84A1A76696752304614717403DCB9C57AB5C9F2AF82D3C30C9A15254EC7CB623F52693BC9F15A7416934DB23BD1671BA4A98FCA7AB5E3D037C3655FBF3AFD0D4C6079DE045AB1A00E00469300C0F3F27A482924926319709C20E7A06BEA10E69F975BDF155FD66600729DD0514A896E061ACC14EF9EBD74E5E33B266050F602666CEDB0ED5A35A567048EFAD88AF4CFD54783C23F40F35FC950CEF3A4E4459D745F6FEE67CB09CE8E60BC4A1603AE90680382A90D2A98E9B70BB8497002E8EBCB200AD83317CFAA6CD58663CA16FA3E6145DE9B0AD761033E43C6DC01B14331116EB683037BDAA4341809DCA05E43F80BBDA548EF6D6E1509026F9883F696A19F96BDD58239DE87B1A16F2AABF5A8FF56B25FA4897E5E7EBDECA944732EF157F7AE6475EDB9B17CAB70654119992EBB00C355DB400FCD2671EB7101D9E171B390320109975FE77850A080B1D786DD5AD05D2833B65CCA3626C6FA1884991637C2DFE2DEDD6B76624568AC4E09E884174D3072A11F1584E141D53CE0DEBD9D3109827B33DEC12672663A9093D010FA7E7D6B7F2C4524AB3433D7E2D9B2E40B604D995601FCC81E29997D1BBB39D9C74986BCDEAB84CB3D95529F4B676C12F3DEEE32E62C23DBB1388CFA8DAA5E4F02D53C5545DE65AA023071F6D362A2AB090A9AB7EE1333066835E91564396AAE679644D682515D022C116E6F2DBEAF1EC3A12327825B8132E0872D7BDAE6357D9C657CB68AFCBC1985F33B005347176717CE5317EB47CA0EE3E1F0EE4EC6C07FCE9A61D3B71DDDC9917662AF3BE68407FCCBD973CEA8AF37661781D2B332D0C9D69F2963642937DCAEC62E42678C11EF2DBB1F3EA1F30D5081645C6A43D5A3B539DA8FAF33654D7C41FD5F0AACDF0CEB0E6510A270F3A306ED3C57D13E9FDD530D49EADDAC6BDB93566F4AAF1328CFB20C1D1DE8896120CF3FA6B4710297FD0BE05939FC62E0136F8AF12169534DC83519E2F520541346F1AB93E61E6C1D3ECA062EE9BB6328C2185EA9D36384B542C872D50D2E8BD660E18AE26148ED203596EF04457BC40862A5FC7F23D64019ED7FB6483F72979F3882ADDF890F0E39088B2182C796C5C4120799EF9043C246B5AEC140938D6133EFD2DBF272651754157D3D463DF90E7FD7F692D2261AAEFD91B7FB37CFD646C3008F9E3DDA06C9CCCB294A468289FA1C35F040E3B65FC81635552229D58E1E59B1DF70DEC36083DAB04AED854F9B0915E5A3F038895E1F839730954C76EEB92C2B2B48AFD994C2F2A3EACBFF3B7AAC91240F24A21DD2DCDF772BABA21040977080139F202E84D22CBF15C8D0082AC647D52D3C931C4ECDFED19A4FDB832E551894DC329CB66AEC1C490C002EA2A87BE236789E1038944CF81584701051CD131C9FD4942A6E1AE168FDB9B8FB9FBF64FD0CDA77CC1071F5469FFDBC2AD94B67829814BD3367BE5AE6CE485B4EA943D8047482BDB28B2CFD4C78A1473F126466369711E8543C9D313F7F8E06E6ECDF4F49889D72BCDE39C75514D0FCD027F0B7EBC698BD89B4843440C031CF9C0F63D689B3C1419877BC623D56E0FC1876D9F6E294D6D8D9B7AEC70D28ECB9395F1813A1E197B95ABD2BF8151ADB0D54ED9D59246B485B9D51CA5A624617E71580C40AA966663092F3CE88A559133ADA2352C32AD1F4B6114AB670DEB4BB0BACBA0134E481B12C9B38BEF3DEE328ED49997CDEDB41A94B8F4875FFA108CA78F698953976161C90E8DB99CFFC7740AF12785311B1F554CA497D69EACF83D5BD64926186BC7DFC4C965BE60071F9BEF65ABE0282C67FF0B2C199657FB7D147658607E57D016184A96F8FF7CEDCE6BCD5593A8FF848F2BD1FA23CB6AF63AFAC95BF31557206417F45F3AAB9E193513B1CDB35F74A9D064218D09699143114CB98E8D0463C43C107EDE52EB64E7E032A6217A15BB91A6E117849BF2FCFB5A23CD4A5F111572C57C961271E4E1A6FC85D749686A1F9346CC8CF4A5AF4C031A6F11189AD04C1232BCC05CF7AECA80723E446007F3248780EDF99A21DB5A8CC0ABDFD480CFE0357F7035DD0527B103EA5C4303EB4AA061230187C4DF51D65654EE047E82B19D1B4BBC22027C8D3B597D5FAB7DF992F99DD81EE2DDA6CA6C1C82922ED4C7456A0A5078C20BE8AEC82A509D55CDE8217DEF4C9C504D7358F377C54BE5B2EA10192D7C32B24A14F49DC759812285559510223866DFEF358C00F5D5F3DABBD1228C0A54835F591B788BA543376E4A816596FA3148112FBB3778F32BF3B4C21D608F767CA99CFB7E51424826D20D9F3436545598E8720687172DC5D5F8A6BCBD27E441CE5D0AE672DDD24557A9E8671E63E809DF220EB02B0FF519BFF5F646F92049225A44285E503875719DE7205BE2D472A84B7613A3DBE0DA3F0F668F5545F77B86A5AFEAC127B09E78720A5EF3E127B97CE7370C1E2243298E884FA65CA91A21178A2F4DDC8497364B9E9A37D3901DACA2F3D05D6A71416BFD1A2A33B36964C0CED3C00CCF0AEE0D35054B7BDFDA79198F4EFE7517F2009125C46F4AEF4E044FF438B6583636772E2A0D0CEE5E2555D5BF2A4B0602F6D998319D922936F8D831A9335785DE5169B4CB7CCD00AC2052774E30EE4C33AA8603075A2BB4DFFD3907AB85032A72A23C95DF301ACE087FD36291735CC04A7290069BDA5F1EAEA8ACBD169D73FADE59FA71F530728098FDF42E87123BC87C4870E24105B90B47871C2B7AE9F4029EC26A6B72A6B96758C9504881A535D8B8D3CDE5C8CCFCFD6E9A4DCFA61611413AE1AB248ACF95F95586914FCCD2791B7BA7314F930D2C24CDF520527094C0B5430D3D4B042C2CBA7E0174A633BE17CB97D1AE0A4B9E2D0220F5DAEC396D4A4781901AC61B3A065C18EFEC22E1DD40B47AAF4E464D749227028C63E1B81B6DDA8821D860B414366744A81069C8B7AC71BE44A3C3A2E3054C684900B446F3B534AC5AA325690C38539FF37F26E49B151D352F819777B071C85716640AE82DE9CCFA04B3AA3ECDA1F9553070C577FC6855A14E0C6493B4BCED3680D3896761031871B74F9CE2A630A4185FBD105B90224F2E7AD989F76997048A683EFAE693D521703E5F2AD15A3A9C449DA07A5731F75034C63A84FFA4D6CA3914CF83041FCC33815F46E06C5821136EF6B2325ECC845D227C20D2A698F99C6A8BBA74A5DFC22381E1A087EF26E20583D4A57EB8690C55AC3377C0E5E3E9897B635C2F4506C4AF4F5F40456A462A794095CF4E6ED733C58E924E932D87E00F051391425AFD3E57970AE9830F9E6D7846BBC66F9363DF568D9E6DDE1B496C18D38DD43B08191A7C147927F127B05E2D64EE8839B15968EBBCB55332F561E9E63B85AB048B95B6A80C63B3D7998F8F514D77E9902C70364F657EB3669D208C9E0E008BB76F1723FEAF644CF82111245B649C3EC5C2A52421C8F6B30B03921897067350C20B911FA67512BC7B0EFA9FDDA3300D370489B901605A895FDAF8DD205268C4DB9A9CFD225502DF03A9930843D788C50324D0DDF393897D65E4BB1B9960F0237B58C218382AE361D82B558CEBEA9DA0217D1D55B969FC0A56F8776ED3B874832AC1B71B2908A957391C5CA97FCF13346E6A4F2F39C9924551156D1AEFA500FEEEE335AD0F549AB711BEFACDD7166E8D3AA436BFB8DF6C16AE712A43D221C1D40A7F4F38993213B6CCA83DA41EC3F781613954B3C52899ECBC4B38594C6D3F0D5823795C68B67D014119483092754B388882F03148903ABA902C89F353156256587F62057B53E4F5EB043E0346254AA713A822C6A6DE931AC5F43F8462FC86BAF475B338A63F33DC534D5E5CFC7D4368E1A769A25F5ADD8EAF8020781EB07B9FD30CAC6E7343ABC70250FF724E7C31958FB1876513A43B0ADB807264AF52136FFE68A2BCDBAA8A3F372DD416F689016E544C197B8C116CC94CBD6C4A2DD7A2EFE15FC35D709AD6512144755F8364452CF80AD09C90120DB0C78D322F6F535574CDF5F9F5C04B72E86D4B8602A0656BADEF45FE5F818D628DB4A72337C4FF8CABF129C6EF529A504B0BADC8B89030F0E4BBBC70BAFE3A472DC08847DDA2E5D71C6BA767D80F18B40DA4B8FB1A094E15CFFE77AE5CCB25238B10DD3F298511914F20BC5C11E38F2D8011DEF03A4284E7CF1E525B603BCB61FCFF3342B03B8E1EF6B74F5530DD30D25DB311B148071D56FCA5377F2A7BD41D6348E4DB340DD8E45E12158925A5EB644324125AE882D933AAA0F58DCB03F2A95EBE5501A787EDFF9E067AADB42EDEBBF9753144C84E24336626F1735E3C786B4F6A61CA6C4F36C313097CA9B64FF9A2BE05A678AD55FB2276BA2D3161539320D1389AE6FB0B2D161AD1E7921DECF79A7D814DA30A59F98A7D9640A80A4502E42B7AB0024357B2017353F8361ED497A8DEEFC2C054D1C4EA0BA95637F4153CDB67A408ADFEF0ABE8EA55399382374974D034087E638E0DBBC7274E5E42A9D6D777B3C61A82BA47ED9045FE3F6F063AC86BD5A22AF161036F0B96B19449885F079395271A4CCC9B5A904F00BBF1F432B94DC5BF34BED9AB3FE2FBBF8CB9E2E4DA44ABB262EEA72318409D1A6DE2AE36718FDC5B7E6E90177F245BB92F18684DD5390289E2984F86382FA724081CF6C2F179C94F234064A9B35F62F6ACA14C3EE82AA862215DE77320DE15D6F0E82F349ECD735CEBE6DCFC493A5C93627C81D37371F47371F3625C5ACFBDC721FFF6CAEBF7A46A25D90681BEC8868B9AD9434B34AC75C30F035A9F7AAD16437D80CFA45F0E5F26430A9FF904E3A62EF8E668883DBB5D2C3EC4C4AC56D230B9A93BEC4538CC10F4E20BFDAD8FB5523EEA944825746B78E7CE581324457958FA6E736BCC2FCAA269545458BB63D085F2C08DE4053CC9B4897A007707BB32572693261B86E04B26DE98048A5B37ABBB49886D9997AA34D4A0E0B0AC3CDA15A016EC3B38B02C40D6C9839F0B48F0D8795E6CD0B899A020EB4F8B869E7347061FD2DD263C1361A7AE664B117312AFCC63939D75451087B49C8E86B95A4EEA9A0F00CE43CA81C8BB0194332A57BDF62CB3F681C940B0BB6C06F8ADDCD0D7BBCCAEF87F5FF63073EBBA5508A7D2A6A8567D9F5B297577E83987341A1E5BF61934935DA606A2B1FCA0D12B4235BC188519FC5764F6464809E642B53FAA76DB9FDF8D880AADED5FF58324E0F22C7ECC16D4BE16A81F69103FD5F118AA02E4BC9450383A8B87BE2000064C9FE23D6567A9C8C99CC246B8791D28C60B4B5E0198A07A34A571CEE3B63DDF5F0C25F6761B40797D0EF2D23938AF7600F873F65A3BC42B7A71D039B9C29CC134ED162A684E9104618D7C97C8FFA687A7D48FD55F09853D0C061C158BE47D184084604CD41C4AEBC3C1AD387B63EF41B5C1C58503BFA05A99015B74C3062ACCA33676E1C4D5EC64E20A423946A08553428BAD7D6B01495666F9375E5092E252CFAFACB20340A9B55C9886F76991DFA31BAC377CD852983162E078F5028C47944B019513113B2CB2E4015CE2FC3D8A40AAAE7F0DCE5E4935C796E959700963BA3BCEDFFC464DD76473A3FC3381888FEF753D6D743654603F56A87B3C323AA6D725381A52F7C89E5AE05B96100FC3B077A0501AE8918D807881529C4991663C6233BDB62E3CCBE8A4A73751683BB5BE15661D0992D635E4B1ED6B12A80B7BE6D87FB4CFE546E2FDA3C71E79CC5BE1F4112E3B02E2FAC33661AC208606DCDD418603D396E3E277762EC5C63168EA48374326EE80749423B63F46326E4E9D0D36D68C9C735ECEBB2DD5247C6199CE2B3D5EDC5D919DF07B4ACC87CD35E57E090200F50B8514196BEDCEA0F9D19214AFA3EE875C315E911C1842BFB9DEF6ECE97C5FCC12A0151C29E37316C2B225B8B36BBCBC44B69BD3C712C38D820028DC50264D1E6F6D6162D9209D4FD89E4EF2E79A56BE34C8145A29E83E3D05EB81076B42BD7505183579E0E3A25AA164EFA33FEC0AA7AAF6AA953416ECC762914EB03BCA6592514B4BC9007285A479E422BC15ADEADF0C652CF2719B9A79F71E7EDD2AABF8933B1A06FAD6DFE948B99C5AAA3B07D92A05A9CB1D54A7D342708CF3ECDE444AAF2B9E434D5FB4AB41AABFBD104377C69C3055814B83553E25DA9B0B235BE8FB87A63D8FFD4B95D76B30245A23BF06A33D169A2FF19159FDDCF3382B28BFF68C6DD64E30D9EDCE62A08FE6552165D622A45C9A42A665F5C0F7A24DBB8C76B55494349C9F7F87DE83B37FBC58211F556C9EFC6DEBB1E37883DE50FFCE0CA41912B4CEDCAB0BBF90207CD88CFA485BBB33E458D916952ADCE46431B6DE783143C31A0E1CF0E21EF66542FC17FC8BE2D6FE34325AC37AA22BE0DE92BDBC92282A459A5D74E7161890CC0D4BF41CD437DF31DE31A8BFD7A628FBF870470566655D5E282E3CFD89FEB57FFBA315090952922BA2FAF2F3BD0D7AD8C8E1CBF2CB32E6EC254DCBEABFC0CF303741FABCE6360998F9CF147F4450C54F96AAAD028286E12DB2BCCFC7FB0B2163F28F668D2D9ACBD616847845E71547E121D188DAD69EA6A75909B65446B30C478F254AA3C305DE58DF000B2C11F6B9EE82ED3EB1562D2EEE945C599E5D9BF2C1B0618A919721CB3E2C1A2B8306BFD95D2DEF0B4AF06FDEA71CD8A4B5BCB1F38D7379D772F0DB83960B13A0E269184E9D927A149B1916F3CC1E3352E624FE5C58C1C45B8B06B6072D08F02C29346C0D7B252EAC17BCFD723515C0428F99CD5DA4F58B7E027ABAC931EADD17FB978348629EA6B2AA210E368D80A37C686E1F3E2FF818148A9B69FACEB1E571F0C3F52F794683924CE8C5B6F236F3C93F247C722EC89BB7637F08CD84412FD9A18C9767F74E6286B9938CA984592343B56E6350A85A0FB0819F9B49A6CD4F8C549FAA5399E8A07BEE0EBE54CD6137FC3F5A909B730C1C9FAE2BEFF9316B3457EB99B46B4DCF00EDE7BC23738E2F9108C3631B3C31573C636D7F8047386EBDC115C67097F5D282053E10FE726A9369F22F2D72734E559DCC8B36D3D4FA2E08CA6F4AF32A3FD8897CB88FFECF8CF91D6C0537F160CF56B5B9427E0BFBD968F0659979053476A228ED7047F07A480473C9C04C8EE6892FD7C703B9AFC9B936944EB6E957EAEAB75310F0ADE94B840BB06FD85373C8CD0A54A746E7EA1B42C9F0046C7879A35081F3D25CB877CFE70705188D23C6DD44F2D0D9C881D2991EB64D2923023D152EA2CC0A8804310D4B50142319A2E3C85AB50D75B02C525D69955A900ED33E5F49DB02BF449173651FE1D9F512E46B3117D0214C71B0719726F19B0A59EA691224BF5EBD82ED5815F2B80CA0EF26F7ED2ACECFFBCE6B1CB1372BA53225FD4628B064D565FCE9427A365A1E49AA2FFDF171D21208A5AFDCE7FEB60FDE9F2D1EC0648C6E7A9E8B5C7FE6A5514D238C929A6CBF8CD2464C8E006DFF01A099F6DD154EE7E01E1E79A564F5D27FEDA26E3D687D08F50ABF4F3C459206011ADDE63AC54CB718A60734F3466277158AB6E6FF59BDAF1A36D6C4ADFC6E40A338CAFC8B4F59ADCF1E86A16123B55344B93BDFB37CF17CF4512836D398043E8E4F0C89E1289BD8EAEABC4B9DA0FDA507A4140E95302E9C52FAB622D7644FBB16F7FFBC87C3CE74CFC3E919699E693888D12104D3863D5929CCE1387D408A85DAE5495316B34992CC95280F607EFDA67E73166A7FC6111D94CE6A402C37A9EC67234ECC16FB9E7F3D17ABFCB672C530E56073B68FBD025487DC88C01B6ADDD7DFCC4000EB233CD11D444F068E4E1DF0E9E6E4E06B85CD80FEDDEC6FF5A4AD6F03ED29BC40B999219405AD4466E389F6CE82B14318FA32B1FA0F7CC53159BBA39B6398E03A0617A369C2CB8D991C1A131954C26229B6C133C68C5C311361F5433683314A91F75DF81344D0D7834CD8E6BFABBF6A0A2B0F08347AAE64B96392AD9D00FEE690F388715704B480DEC6AA025728049217799149D191305B7FFCD64EC57A4D671A1FD95A91706C04224C35B3F1D92CB95AB102A92E3CD7DAE6041ED24CB1588F88ED82D9DE4885B4F074EFACDF70CB700176BEEB62312045C950F37795D8666943C4D97554D0850F5F8AD2D7315201E3F214D4C005AFDBED5AD4FB9E7395B9EC7432F20A23F6EB944D7D9CD26BA6AB7E0BFA9E6050DAB043CC6E2772D4F7B6DECD8C7AFE35D6D7BBE512EE47C473456A0F2C97B7890F14D6344658B34A6067C94B0AF7BF4235C2A78F8CF7B59F677F3B5EFC3E891DB6D5DC3C1466A546FE9EF712FB1BC40C3685E014541625574A68891C06B8E11FE501E2DDA57B32EF1DDFD4102793F1CA6808B8E3B0E2058C811C945C2C0823BBA25F0A38684B5E965BB1BCE48E41C90BA910DF0B1C0367CF19A8616F48D77A65A3B39F8833CC54CE708D9CF9B6FC299788335DFE3165B0E4D070EFF602B1588E9204721379440A26C496AACBC1409AE4805FAB0E9FAB25341B92565B09C7591205AF8F2597CB8405094CAC8CF2AE0562866284EA1C35B5B759A877214E5C3056BADCEC3CA4A4747CFB3CAB2EA8F02FEC668722A78CDC6488C04D5C54EC5DD921B7755FE692954309A7AA43298682DB89AAA0AF82BBD4CE9229B4EFB90DF78D49AC1F590556827DDE9071B666FB4E1FB03D19219A132C5FC5ABDFE6B6F504F62268C1479EA87B778F726A48F563DBD7D6A1C9DB0FAE4A872A1F2B873B1E6ED1B8CF02C7C0D58A99EE8B287A50FB2AA04D9258D6FBA6BB53EA50203BBE49D4E7C7E1924E2BA18497DA1B77947B5A5BDCCBE93F7AF9A24E9BF74C47A625637CB9821585DC93BFBF28D01B909DB9190CC82EFF00018388071FE305B04D66C1F613AE7E167EB76F09DA46FC3B14E6DD666621EE24649B203C6C5286A56DA0EA36DC166E5F4D4B88E3003CAEF412E2480FC8ECB333361660275249F4AA339BFF0C56890E9BF1BB5AB7526AE4ED62CE01A2D1968ADDD5A7BC38E5C1C289BE8C185D6C8B537D801A7C5AD939923B0D1D92762753E719D1AFB7D7AA3377B6906E9CEAC848CEFB9BC725D30129542AFEED2BABE1B025839C1108EAAA6A10A0B9B30CF05CE6157BE309F09968A4B4CC291F0221C2C9783B65A3F66E64A21841F4AE7B46A897246E8440F315136FC07499917DB63B8B6FC980B8BD7BA804023A350BD1009AE049EF609C287D6D2EB77CF4FECE55F59B9E4FCC91D2BC599FC46F07F430209A52DFC12734057C59B578BB5959C0D305EECFC679990A50D739954922E57111D0D65C5D5805EF36FE0EFE7A6E8548507D514508E70ABDF13DF85FD9EB9582A22E8EE4A1AAE000627FE1A13C8D4AB02613E3E70E593D1C204712FC81D256F82760C02ED4D85681B9F0DB7120BA0F8D6203767E7EE111B859759F7F9935DE6A9CED4F61AEFF0443D865B36FAA4B7DE70BE9A2C70C6EF1AF0C18D9F1077D5540266D55F67930F7EE5D91A3DFFA2774F7A88B540E3EE3D6C1EFC7505E7FCC8EDCBB4A31308A471E9A8F7018B8D3B8000F304FB8878DD89CB5CAF98EF312DBD2D15C4E9153B0F47B51921C429CBD7B3873FC74C139352FCD83F42367D6C72E23DBF5D640C6B2FA43827673146F44FF46D5B5143AE4137D92E44C87D196CE375B7670E61C4D94C9A1ECCBD873C8703C2A5A85BC847486BB6375F10B85A343F95A8B01E028E22F1064EBFA92808D5A78153F025D9EF1D4FF57CEBFF07DECC463293ED0ADEBA54D21F9AFBD9F506B58FBB5F4A7857EB2AEE6C412F1003DCD4A780EDBC0A7F1B1F669DCEB5DA50C7CA45112AA8FBB8E48215ACAD0C379494BB6C05219BA77BD4166885B25E08685A06CBFA84B4709F5499AEECD9503DDF7C5BA88D379F35500CD35D9C9A74B2EE86FC962DE5A1D6488AFEC573CAF55EAD16413E2531AEA8C7B1F79059EDA8F179C25F6DC80FCF842BDD453C15CFE20A7DB3BF2BB8067880D74F909DC64936583DEA5143EE7D479D53ED60698626A050A962B073E1E9F1F5A1CBE5A0D0ECD4C385271B95806D7190BAA23688AADD2D461C14781424B7B6295F6F56D49F6790569E1B8765B098B476A054161465D0CDD378AD6A24C1EB84A6CC0571AE5E015987246AD2633BD123B62667A536EB330209ACE3BA68EC30F9FD0DB97B3C06CE74953B7EAE7D1E1844F7E4502A58141B1B569E650CA64EC7CC613B07384C557A2B1742A578069E2DE57ED3A0446A55F241F38BBC6AA2FA045F0CA5F01F2E1115A031F4C05A01E1CFFE344107532A0B5549DDBF4683F1C41F66D971A5DBFDE6B077FA01C068718C860E0DCD22AF7840206747CEF5983BB1EF3F198924C3B0E15013938E19DF84257A6ED54CF66FA7F567B7BD2B35DE92DA58CDBFD2F294774DF731920B3A64791E8FE65C1F263F959\n  device_name: samsungNexus\n  device_platform: Android10samsungNexus\n  device_tourist_id: 22824046377449\n  disable_rcmd: 0\n  dt: VvUCfbGyEyKA%2FoHRmvCgKyJ%2FzeDQ17WLhT6pA%2BqzF2DGxKKaHBadIZFgk%2BG6ZW%2FP72Ft1nuhWki1%0Ay551QsLAenLneTaD3Rhf7BSXtY5MykDkH9MTWDWyxuWVDwjhLduCzDywgq%2BtxjVgXV1sBQWvyISh%0AkIlGGuAp40TzbQi%2Br0Y%3D%0A\n  from_pv: main.homepage.avatar-nologin.all.click\n  from_url: bilibili%3A%2F%2Fpegasus%2Fpromo\n  local_id: {{buvid}}\n  login_session_id: f152581bf6b3a1b4bad532f836a4e878\n  mobi_app: android\n  password: {{pwd}}\n  platform: android\n  s_locale: zh_CN\n  statistics: %7B%22appId%22%3A1%2C%22platform%22%3A3%2C%22version%22%3A%227.72.0%22%2C%22abtest%22%3A%22%22%7D\n  ts: 1735704456\n  username: {{phone}}\n  sign: 8e9d9f9da8d5a9355ef8ba620de75175\n}\n"
  },
  {
    "path": "bruno/passport.bilibili.com/x/passport-login/web/key.bru",
    "content": "meta {\n  name: key\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://passport.bilibili.com/x/passport-login/web/key\n  body: none\n  auth: none\n}\n\nheaders {\n  Host: passport.bilibili.com\n  buvid: {{buvid}}\n  env: prod\n  app-key: android64\n  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\n  x-bili-trace-id: 43e1a54f60cfbf0bb9788d4d9e6774bf:b9788d4d9e6774bf:0:0\n  x-bili-aurora-eid: \n  x-bili-mid: \n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzI4OTIsImlhdCI6MTczNTcwMzc5MiwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.hEDtIHZoN5Wyoja2hNuPcfy-laa1r05ObiEhJ5gPSt4\n  bili-http-engine: cronet\n}\n"
  },
  {
    "path": "bruno/passport.bilibili.com/x/relation/followings/simple.bru",
    "content": "meta {\n  name: simple\n  type: http\n  seq: 1\n}\n\nget {\n  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\n  body: none\n  auth: none\n}\n\nparams:query {\n  _device: android\n  _hwid: e0N3QnRBJEV0QiEXaxdrXG8LPgw1UDECYFU0AWJTZQ\n  access_key: {{access_key}}\n  appkey: {{appKey}}\n  build: 7720200\n  c_locale: zh_CN\n  channel: yingyongbao\n  disable_rcmd: 0\n  mobi_app: android\n  platform: android\n  s_locale: zh_CN\n  src: yingyongbao\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"7.72.0\",\"abtest\":\"\"}\n  trace_id: 20250101120700037\n  ts: 1735704457\n  version: 7.72.0.7720200\n  sign: 3a7ddaa98dbf792b654bdaabdf3dcd1d\n}\n\nheaders {\n  Host: api.bilibili.com\n  buvid: {{buvid}}\n  fp_local: {{device_id}}\n  fp_remote: {{device_id}}\n  session_id: 3160e892\n  env: prod\n  app-key: android64\n  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\n  x-bili-trace-id: 97831b4bddcb6011ee84ae59806774bf:ee84ae59806774bf:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzI4OTIsImlhdCI6MTczNTcwMzc5MiwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.hEDtIHZoN5Wyoja2hNuPcfy-laa1r05ObiEhJ5gPSt4\n  bili-http-engine: cronet\n}\n"
  },
  {
    "path": "bruno/passport.bilibili.com/x/relation/tag/special.bru",
    "content": "meta {\n  name: special\n  type: http\n  seq: 1\n}\n\nget {\n  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\n  body: none\n  auth: none\n}\n\nparams:query {\n  _device: android\n  _hwid: e0N3QnRBJEV0QiEXaxdrXG8LPgw1UDECYFU0AWJTZQ\n  access_key: {{access_key}}\n  appkey: {{appKey}}\n  build: 7720200\n  c_locale: zh_CN\n  channel: yingyongbao\n  disable_rcmd: 0\n  mobi_app: android\n  platform: android\n  s_locale: zh_CN\n  src: yingyongbao\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"7.72.0\",\"abtest\":\"\"}\n  trace_id: 20250101120700047\n  ts: 1735704467\n  version: 7.72.0.7720200\n  sign: c49a2ca7bf93e8e65e77fe285fb71b1e\n}\n\nheaders {\n  Host: api.bilibili.com\n  buvid: {{buvid}}\n  fp_local: {{device_id}}\n  fp_remote: {{device_id}}\n  session_id: 3160e892\n  env: prod\n  app-key: android64\n  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\n  x-bili-trace-id: f8209f943aca4379b5b407142a6774bf:b5b407142a6774bf:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzI4OTIsImlhdCI6MTczNTcwMzc5MiwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.hEDtIHZoN5Wyoja2hNuPcfy-laa1r05ObiEhJ5gPSt4\n  bili-http-engine: cronet\n}\n"
  },
  {
    "path": "bruno/passport.bilibili.com/x/v2/account/myinfo.bru",
    "content": "meta {\n  name: myinfo\n  type: http\n  seq: 1\n}\n\nget {\n  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\n  body: none\n  auth: none\n}\n\nparams:query {\n  access_key: {{access_key}}\n  appkey: 783bbb7264451d82\n  build: 7720200\n  buvid: {{buvid}}\n  c_locale: zh_CN\n  channel: yingyongbao\n  disable_rcmd: 0\n  local_id: {{buvid}}\n  mobi_app: android\n  platform: android\n  s_locale: zh_CN\n  statistics: {\"appId\":1,\"platform\":3,\"version\":\"7.72.0\",\"abtest\":\"\"}\n  ts: 1735704457\n  sign: b33e7a0aa1aef3d1d4284c759fa12857\n}\n\nheaders {\n  Host: app.bilibili.com\n  buvid: {{buvid}}\n  env: prod\n  app-key: android64\n  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\n  x-bili-trace-id: 46e70b4538aaf4001079cf97f86774bf:1079cf97f86774bf:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzI4OTIsImlhdCI6MTczNTcwMzc5MiwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.hEDtIHZoN5Wyoja2hNuPcfy-laa1r05ObiEhJ5gPSt4\n  bili-http-engine: cronet\n}\n"
  },
  {
    "path": "bruno/passport.bilibili.com/x/v2/feed/index.bru",
    "content": "meta {\n  name: index\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://passport.bilibili.com/x/v2/feed/index\n  body: none\n  auth: none\n}\n\nheaders {\n  Host: app.bilibili.com\n  buvid: {{buvid}}\n  fp_local: {{device_id}}\n  fp_remote: {{device_id}}\n  session_id: 3160e892\n  env: prod\n  app-key: android64\n  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\n  x-bili-trace-id: ce74f22f44dec31cc331a61b2a6774bf:c331a61b2a6774bf:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzI4OTIsImlhdCI6MTczNTcwMzc5MiwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.hEDtIHZoN5Wyoja2hNuPcfy-laa1r05ObiEhJ5gPSt4\n  bili-http-engine: cronet\n}\n"
  },
  {
    "path": "bruno/passport.bilibili.com/x/vip/web/vip_center/v2.bru",
    "content": "meta {\n  name: v2\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://api.bilibili.com/x/vip/web/vip_center/v2\n  body: none\n  auth: none\n}\n\nheaders {\n  Host: api.bilibili.com\n  Cookie: {{cookieStr}}\n  native_api_from: h5\n  buvid: {{buvid}}\n  accept: application/json, text/plain, */*\n  referer: https://big.bilibili.com/mobile/index?exp_symbol=release_version&oflAb=1\n  content-type: application/json\n  user-agent: {{user-agent}}\n  x-bili-trace-id: 0b9853d9388b01892edb6939c16774d4:2edb6939c16774d4:0:0\n  x-bili-aurora-eid: UlAAQFkMBVkH\n  x-bili-mid: {{mid}}\n  x-bili-aurora-zone: \n  x-bili-gaia-vtoken: \n  x-bili-ticket: eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU3MzgxMTEsImlhdCI6MTczNTcwOTAxMSwiYnV2aWQiOiJYVzcyNEQxNzI0Njg3MTlDQzI1NjA1REIyNDI0NzhEMkUxMjE5In0.J28IlxZ6SjllQEQkq_OvFUiRYAEL2VhQG_WWBcmNppE\n  bili-http-engine: cronet\n}\n"
  },
  {
    "path": "common.props",
    "content": "<Project>\n  <PropertyGroup>\n    <Authors>Ray</Authors>\n    <Version>3.8.2</Version>\n    <NoWarn>$(NoWarn);CS1591;CS0436</NoWarn>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "docker/README.md",
    "content": "# Docker 使用说明\n<!-- TOC depthFrom:2 -->\n\n- [1. 前期工作](#1-前期工作)\n- [2. 方式一：一键脚本(推荐)](#2-方式一一键脚本推荐)\n- [3. 方式二：手动 Docker Compose](#3-方式二手动-docker-compose)\n    - [3.1. 启动](#31-启动)\n    - [3.2. 其他命令参考](#32-其他命令参考)\n- [4. 方式三：手动Docker指令](#4-方式三手动docker指令)\n    - [4.1. Docker启动](#41-docker启动)\n    - [4.2. 其他指令参考](#42-其他指令参考)\n    - [4.3. 使用Watchtower更新容器](#43-使用watchtower更新容器)\n- [5. 登录](#5-登录)\n- [6. 添加 Bili 账号](#6-添加-bili-账号)\n- [7. 自己构建镜像（非必须）](#7-自己构建镜像非必须)\n- [8. 其他](#8-其他)\n\n<!-- /TOC -->\n\n## 1. 前期工作\n\n```\napt-get update\napt-get install curl\n```\n\n## 2. 方式一：一键脚本(推荐)\n\n```\nbash <(curl -sSL https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/main/docker/install.sh)\n```\n\n## 3. 方式二：手动 Docker Compose\n\n### 3.1. 启动\n\n```\n# 创建目录\nmkdir bili_tool_web && cd bili_tool_web\n\n# 下载\nwget https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/main/docker/sample/docker-compose.yml\nmkdir -p config\ncd ./config\nwget https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/main/docker/sample/config/cookies.json\ncd ..\n\n# 启动\ndocker compose up -d\n\n# 查看启动日志\ndocker logs -f bili_tool_web\n```\n\n最终文件结构如下：\n\n```\nbili_tool_web\n├── Logs\n├── config\n├──── cookies.json\n└── docker-compose.yml\n```\n\n### 3.2. 其他命令参考\n\n```\n# 启动 docker-compose\ndocker compose up -d\n\n# 停止 docker-compose\ndocker compose stop\n\n# 查看实时日志\ndocker logs -f bili_tool_web\n\n# 进入容器\ndocker exec -it bili_tool_web /bin/bash\n\n# 手动更新容器\ndocker compose pull && docker compose up -d\n```\n\n## 4. 方式三：手动Docker指令\n\n### 4.1. Docker启动\n\n```\n# 创建目录\nmkdir bili_tool_web && cd bili_tool_web\n\n# 生成并运行容器\ndocker pull ghcr.io/raywangqvq/bili_tool_web\ndocker run -d --name=\"bili_tool_web\" \\\n    -p 22330:8080 \\\n    -e TZ=Asia/Shanghai \\\n    -v ./Logs:/app/Logs \\\n    -v ./config:/app/config \\\n    ghcr.io/raywangqvq/bili_tool_web\n\n# 查看实时日志\ndocker logs -f bili_tool_web\n```\n\n其中，`cookie`需要替换为自己真实的cookie字符串\n\n### 4.2. 其他指令参考\n\n```\n# 启动容器\ndocker start bili_tool_web\n\n# 停止容器\ndocker stop bili_tool_web\n\n# 重启容器\ndocker restart bili_tool_web\n\n# 删除容器\ndocker rm bili_tool_web\n\n# 进入容器\ndocker exec -it bili_tool_web /bin/bash\n```\n\n### 4.3. 使用Watchtower更新容器\n```\ndocker run --rm \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    containrrr/watchtower \\\n    --run-once --cleanup \\\n    bili_tool_web\n```\n\n## 5. 登录\n\n- 默认用户：`admin`\n- 默认密码：`BiliTool@2233`\n\n首次登陆后，请到`Admin`页面修改密码。\n\n## 6. 添加 Bili 账号\n\n扫码进行账号添加。\n\n![trigger](../docs/imgs/web-trigger-login.png)\n\n![login](../docs/imgs/docker-login.png)\n\n## 7. 自己构建镜像（非必须）\n\n目前我提供和维护的镜像：\n\n- DockerHub: `[zai7lou/bili_tool_web](https://hub.docker.com/repository/docker/zai7lou/bili_tool_web)`\n- GitHub: `[bili_tool_web](https://github.com/RayWangQvQ/BiliBiliToolPro/pkgs/container/bili_tool_web)`\n\n如果有需要（大部分都不需要），可以使用源码自己构建镜像，如下：\n\n在有项目的Dockerfile的目录运行\n\n`docker build -t TARGET_NAME .`\n\n`TARGET_NAME`为镜像名称和版本，可以自己起个名字\n\n## 8. 其他\n\n代码编译和发布环境: mcr.microsoft.com/dotnet/sdk:8.0\n\n代码运行环境: mcr.microsoft.com/dotnet/aspnet:8.0\n\n如果下载`github`资源有问题，可以尝试添加加速器。\n"
  },
  {
    "path": "docker/build/buildAndPushImage_multiArch.ps1",
    "content": "echo \"start to build\"\n# https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/\n# https://segmentfault.com/a/1190000021166703\n# linux/arm/v6,linux/riscv64,linux/s390x,linux/ppc64le,linux/386,,linux/arm/v7 偶发异常，待进一步测试\necho \"Start to build docker image with multi-arch\"\n# $image=\"zai7lou/bilibili_tool_pro\"\n# $version=\"0.0.5\"\ndocker 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 ../..\n"
  },
  {
    "path": "docker/build/buildImage.cmd",
    "content": "@echo off\n\nREM start to build\necho Start to build docker image\n@echo on\ndocker build --tag zai7lou/bilibili_tool_pro:latest ../..\n@echo off\npause\n"
  },
  {
    "path": "docker/build/buildImage_amd64.cmd",
    "content": "@echo off\n\nREM start to build\nREM https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/\nREM https://segmentfault.com/a/1190000021166703\necho Start to build docker image with amd64-arch\n@echo on\ndocker buildx build --tag zai7lou/bilibili_tool_pro:latest --output \"type=image,push=false\" --platform linux/amd64 ../..\n@echo off\npause\n"
  },
  {
    "path": "docker/build/buildImage_arm64.cmd",
    "content": "@echo off\n\nREM start to build\nREM https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/\nREM https://segmentfault.com/a/1190000021166703\necho Start to build docker image with arm64-arch\n@echo on\ndocker buildx build --platform linux/arm64 -o type=docker -t zai7lou/bilibili_tool ../..\n@echo off\npause\n"
  },
  {
    "path": "docker/entrypoint.sh",
    "content": "#!/bin/bash\nset -e\nset -o pipefail\n\necho \"Starting BiliTool container...\"\nmkdir -p /app/config\n\necho \"Running maintenance scripts...\"\n\n# 3.3.0 need migrate db file location to /app/config\nif [ -f \"/app/BiliBiliTool.db\" ]; then\n    echo \"[3.3.0] Migrate db file location to /app/config\"\n    mv /app/BiliBiliTool.db /app/config/BiliBiliTool.db\nfi\n\necho \"Starting application...\"\nexec dotnet Ray.BiliBiliTool.Web.dll \"$@\""
  },
  {
    "path": "docker/install.sh",
    "content": "#!/usr/bin/env bash\n###\n# @Author: Ray zai7lou@outlook.com\n# @Date: 2023-02-11 23:13:19\n # @LastEditors: Ray zai7lou@outlook.com\n # @LastEditTime: 2023-02-12 20:51:19\n# @FilePath: \\BiliBiliToolPro\\docker\\install.sh\n# @Description:\n###\nset -e\nset -u\nset -o pipefail\n\necho '  ____    _   _____           _  '\necho ' | __ ) _| |_|_   _|__   ___ | | '\necho ' |  _ \\(_) (_) | |/ _ \\ / _ \\| | '\necho ' | |_) | | | | | | (_) | (_) | | '\necho ' |____/|_|_|_| |_|\\___/ \\___/|_| '\n\ncurrent_dir=$(pwd)\nbase_dir=\"${current_dir}/bili_tool_web\"\ngithub_proxy=\"\"\ngithub_branch=\"main\"\nremote_compose_url=\"${github_proxy}https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/refs/heads/${github_branch}/docker/sample/docker-compose.yml\"\nremote_ckJson_url=\"${github_proxy}https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/refs/heads/${github_branch}/docker/sample/config/cookies.json\"\ndocker_img_name=\"ghcr.io/raywangqvq/bili_tool_web\"\ncontainer_name=\"bili_tool_web\"\n\n### infra\nverbose=false\n\ninvocation='echo \"\" && say_verbose \"Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}\"'\n\nif [ -t 1 ] && command -v tput >/dev/null; then\n    ncolors=$(tput colors || echo 0)\n    if [ -n \"$ncolors\" ] && [ $ncolors -ge 8 ]; then\n        bold=\"$(tput bold || echo)\"\n        normal=\"$(tput sgr0 || echo)\"\n        black=\"$(tput setaf 0 || echo)\"\n        red=\"$(tput setaf 1 || echo)\"\n        green=\"$(tput setaf 2 || echo)\"\n        yellow=\"$(tput setaf 3 || echo)\"\n        blue=\"$(tput setaf 4 || echo)\"\n        magenta=\"$(tput setaf 5 || echo)\"\n        cyan=\"$(tput setaf 6 || echo)\"\n        white=\"$(tput setaf 7 || echo)\"\n    fi\nfi\n\nsay_verbose() {\n    if [ \"$verbose\" = true ]; then\n        # using stream 3 (defined in the beginning) to not interfere with stdout of functions\n        # which may be used as return value\n        printf \"%b\\n\" \"${cyan:-}$(date \"+%Y-%m-%d %H:%M:%S\")[VER]:${normal:-} $1\" >&3\n    fi\n}\n\nsay_info() {\n    printf \"%b\\n\" \"${green:-}$(date \"+%Y-%m-%d %H:%M:%S\")[INF]:$1${normal:-}\" >&2\n}\n\nsay_warning() {\n    printf \"%b\\n\" \"${yellow:-}$(date \"+%Y-%m-%d %H:%M:%S\")[WAR]:$1${normal:-}\" >&3\n}\n\nsay_err() {\n    printf \"%b\\n\" \"${red:-}$(date \"+%Y-%m-%d %H:%M:%S\")[ERR]:$1${normal:-}\" >&2\n}\n\nmachine_has() {\n    eval $invocation\n\n    command -v \"$1\" >/dev/null 2>&1\n    return $?\n}\n\n# args:\n# remote_path - $1\nget_http_header_curl() {\n    eval $invocation\n\n    local remote_path=\"$1\"\n\n    curl_options=\"-I -sSL --retry 5 --retry-delay 2 --connect-timeout 15 \"\n    curl $curl_options \"$remote_path\" 2>&1 || return 1\n    return 0\n}\n\n# args:\n# remote_path - $1\nget_http_header_wget() {\n    eval $invocation\n\n    local remote_path=\"$1\"\n    local wget_options=\"-q -S --spider --tries 5 \"\n    # Store options that aren't supported on all wget implementations separately.\n    local wget_options_extra=\"--waitretry 2 --connect-timeout 15 \"\n    local wget_result=''\n\n    wget $wget_options $wget_options_extra \"$remote_path\" 2>&1\n    wget_result=$?\n\n    if [[ $wget_result == 2 ]]; then\n        # Parsing of the command has failed. Exclude potentially unrecognized options and retry.\n        wget $wget_options \"$remote_path\" 2>&1\n        return $?\n    fi\n\n    return $wget_result\n}\n\n# Updates global variables $http_code and $download_error_msg\ndownloadcurl() {\n    eval $invocation\n\n    unset http_code\n    unset download_error_msg\n    local remote_path=\"$1\"\n    local out_path=\"${2:-}\"\n    local remote_path_with_credential=\"${remote_path}\"\n    local curl_options=\"--retry 20 --retry-delay 2 --connect-timeout 15 -sSL -f --create-dirs \"\n    local failed=false\n    if [ -z \"$out_path\" ]; then\n        curl $curl_options \"$remote_path_with_credential\" 2>&1 || failed=true\n    else\n        curl $curl_options -o \"$out_path\" \"$remote_path_with_credential\" 2>&1 || failed=true\n    fi\n    if [ \"$failed\" = true ]; then\n        local response=$(get_http_header_curl $remote_path)\n        http_code=$(echo \"$response\" | awk '/^HTTP/{print $2}' | tail -1)\n        download_error_msg=\"Unable to download $remote_path.\"\n        if [[ $http_code != 2* ]]; then\n            download_error_msg+=\" Returned HTTP status code: $http_code.\"\n        fi\n        say_verbose \"$download_error_msg\"\n        return 1\n    fi\n    return 0\n}\n\n# Updates global variables $http_code and $download_error_msg\ndownloadwget() {\n    eval $invocation\n\n    unset http_code\n    unset download_error_msg\n    local remote_path=\"$1\"\n    local out_path=\"${2:-}\"\n    local remote_path_with_credential=\"${remote_path}\"\n    local wget_options=\"--tries 20 \"\n    # Store options that aren't supported on all wget implementations separately.\n    local wget_options_extra=\"--waitretry 2 --connect-timeout 15 \"\n    local wget_result=''\n\n    if [ -z \"$out_path\" ]; then\n        wget -q $wget_options $wget_options_extra -O - \"$remote_path_with_credential\" 2>&1\n        wget_result=$?\n    else\n        wget $wget_options $wget_options_extra -O \"$out_path\" \"$remote_path_with_credential\" 2>&1\n        wget_result=$?\n    fi\n\n    if [[ $wget_result == 2 ]]; then\n        # Parsing of the command has failed. Exclude potentially unrecognized options and retry.\n        if [ -z \"$out_path\" ]; then\n            wget -q $wget_options -O - \"$remote_path_with_credential\" 2>&1\n            wget_result=$?\n        else\n            wget $wget_options -O \"$out_path\" \"$remote_path_with_credential\" 2>&1\n            wget_result=$?\n        fi\n    fi\n\n    if [[ $wget_result != 0 ]]; then\n        local disable_feed_credential=false\n        local response=$(get_http_header_wget $remote_path $disable_feed_credential)\n        http_code=$(echo \"$response\" | awk '/^  HTTP/{print $2}' | tail -1)\n        download_error_msg=\"Unable to download $remote_path.\"\n        if [[ $http_code != 2* ]]; then\n            download_error_msg+=\" Returned HTTP status code: $http_code.\"\n        fi\n        say_verbose \"$download_error_msg\"\n        return 1\n    fi\n\n    return 0\n}\n\n# args:\n# remote_path - $1\n# [out_path] - $2 - stdout if not provided\ndownload() {\n    eval $invocation\n\n    local remote_path=\"$1\"\n    local out_path=\"${2:-}\"\n\n    if [[ \"$remote_path\" != \"http\"* ]]; then\n        cp \"$remote_path\" \"$out_path\"\n        return $?\n    fi\n\n    local failed=false\n    local attempts=0\n    while [ $attempts -lt 3 ]; do\n        attempts=$((attempts + 1))\n        failed=false\n        if machine_has \"curl\"; then\n            downloadcurl \"$remote_path\" \"$out_path\" || failed=true\n        elif machine_has \"wget\"; then\n            downloadwget \"$remote_path\" \"$out_path\" || failed=true\n        else\n            say_err \"Missing dependency: neither curl nor wget was found.\"\n            exit 1\n        fi\n\n        if [ \"$failed\" = false ] || [ $attempts -ge 3 ] || { [ ! -z $http_code ] && [ $http_code = \"404\" ]; }; then\n            break\n        fi\n\n        say_info \"Download attempt #$attempts has failed: $http_code $download_error_msg\"\n        say_info \"Attempt #$((attempts + 1)) will start in $((attempts * 10)) seconds.\"\n        sleep $((attempts * 10))\n    done\n\n    if [ \"$failed\" = true ]; then\n        say_verbose \"Download failed: $remote_path\"\n        return 1\n    fi\n    return 0\n}\n\ncreateBaseDir() {\n    eval $invocation\n    mkdir -p $base_dir\n    cd $base_dir\n}\n\ninstallDocker() {\n    eval $invocation\n    if machine_has \"docker\"; then\n        say_info \"已安装docker\"\n        docker --version\n        return 0\n    else\n        say_warning \"未安装docker，尝试安装\"\n        download \"https://get.docker.com\" ./get-docker.sh\n        chmod +x ./get-docker.sh\n        get-docker.sh\n\n        if machine_has \"docker\"; then\n            say_info \"已安装docker\"\n            docker --version\n            return 0\n        else\n            say_err \"docker 安装失败，请手动安装成功后再执行该脚本\"\n            exit 1\n        fi\n    fi\n}\n\ndownloadResources() {\n    eval $invocation\n    say_info \"开始下载资源\"\n\n    # docker compose\n    [ -f \"docker-compose.yml\" ] || download $remote_compose_url ./docker-compose.yml\n\n    # ckJson\n    mkdir -p config\n    cd ./config\n    [ -f \"cookies.json\" ] || download $remote_ckJson_url ./cookies.json\n    chmod +x ./cookies.json\n    cd ..\n\n    ls -l\n}\n\nrunContainer() {\n    eval $invocation\n\n    say_info \"开始拉取镜像\"\n    docker pull $docker_img_name\n\n    say_info \"开始运行容器\"\n    {\n        docker compose version && docker compose up -d\n    } || {\n        docker-compose version && docker-compose up -d\n    } || {\n        docker run -d --name=\"${container_name}\" \\\n            -p 22330:8080 \\\n            -e TZ=Asia/Shanghai \\\n            -v $base_dir/Logs:/app/Logs \\\n            -v $base_dir/config:/app/config \\\n            $docker_img_name\n    } || {\n        say_err \"创建容器失败，请检查\"\n        exit 1\n    }\n}\n\ncheckResult() {\n    eval $invocation\n    say_info \"检测容器运行情况\"\n\n    docker ps --filter \"name=${container_name}\"\n\n    containerId=$(docker ps -q --filter \"name=^${container_name}$\")\n    if [ -n \"$containerId\" ]; then\n        docker logs ${container_name}\n        echo \"\"\n        echo \"===============================================\"\n        echo \"Congratulations! 恭喜！\"\n        echo \"创建并运行${container_name}容器成功。\"\n        echo \"访问地址：http:{ip}:22330\"\n        echo \"云服务器防火墙请自行开放22330端口\"\n        echo \"首次运行后，请执行扫码登录任务添加账号\"\n        echo \"Enjoy it~\"\n        echo \"===============================================\"\n    else\n        echo \"\"\n        echo \"请查看运行日志，确认容器是否正常运行，点击 Ctrl+c 退出日志追踪\"\n        echo \"\"\n        docker logs -f ${container_name}\n    fi\n}\n\nmain() {\n    installDocker\n    createBaseDir\n    downloadResources\n    runContainer\n    checkResult\n}\n\nmain\n"
  },
  {
    "path": "docker/sample/config/cookies.json",
    "content": "{\n  \"BiliBiliCookies\":[\n  ]\n}\n"
  },
  {
    "path": "docker/sample/docker-compose.yml",
    "content": "services:\n  bili_tool_web:\n    image: ghcr.io/raywangqvq/bili_tool_web\n    container_name: bili_tool_web\n    restart: unless-stopped\n    tty: true\n    volumes:\n      - ./Logs:/app/Logs\n      - ./config:/app/config\n    ports:\n      - \"22330:8080\"\n    environment:\n      TZ: \"Asia/Shanghai\"\n      DailyTaskConfig__Cron: \"0 0 15 * * ?\"\n"
  },
  {
    "path": "docs/claw-cloud.md",
    "content": "# Claw免费容器部署\n\n## 教程\n\n点击 [https://console.run.claw.cloud/signin](https://console.run.claw.cloud/signin?link=FNTTMHS056E5) 注册账号，选择使用 GitHub 账号注册并登录。\n\n成功后，每个月会赠送 $5 额度，跑 BiliTool 绰绰有余。\n\n左上角可以选择一个区域，然后点击 **App Store**。\n\n![claw-app-store.png](/docs/imgs/claw-app-store.png)\n\n搜索`BiliTool`并点击如下搜索结果。\n\n![claw-search.png](/docs/imgs/claw-search.png)\n\n点击`Deploy App`按钮一键部署。\n\n![claw-deploy.png](/docs/imgs/claw-deploy.png)\n\n等待半分钟左右，进入容器 Detail 页面，点击如下链接即可访问站点：\n\n![claw-addr.png](/docs/imgs/claw-addr.png)\n\n## 消息推送\n\n建议使用环境变量配置：\n\n![claw-notification.png](/docs/imgs/claw-notification.png)\n\n配置值见：[confifuration](/docs/configuration.md)\n\n## 费用\n\n官方模板默认配置为 `0.25C 512M`，每天 $0.06，一个月 `30 * 0.06 = $1.8`，每月赠送是 $5，还很富裕。\n\n## 其他\n\n### 账号\n\n- 默认用户名：admin\n- 默认密码：BiliTool@2233\n\n首次登陆后，请立即修改账号和密码！\n\n### 速度\n\n不同区域速度可能有差异，可自己切换尝试。\n\n如果速度慢可能会导致页面短时无响应，可稍作等待，或手动刷新。\n\n### 更新\n\n右上角`Update`进行版本更新，如果更新后启动异常，请尝试`Pause`然后再`Restart`。\n"
  },
  {
    "path": "docs/configuration.md",
    "content": "# 配置说明\n\n**[目录]**\n\n<!-- TOC depthFrom:2 insertAnchor:true -->\n\n- [1. 配置方式](#1-配置方式)\n    - [1.1. 方式一：修改配置文件](#11-方式一修改配置文件)\n    - [1.2. 方式二：命令启动时通过命令行参数配置](#12-方式二命令启动时通过命令行参数配置)\n    - [1.3. 方式三：添加环境变量（推荐）](#13-方式三添加环境变量推荐)\n    - [1.4. 方式四：托管在青龙面板上，使用面板的环境变量页或配置文件页进行配置](#14-方式四托管在青龙面板上使用面板的环境变量页或配置文件页进行配置)\n- [2. 优先级](#2-优先级)\n- [3. 详细配置说明](#3-详细配置说明)\n    - [3.1. Cookie字符串](#31-cookie字符串)\n    - [3.2. 安全相关的配置](#32-安全相关的配置)\n        - [3.2.1. 是否跳过执行任务](#321-是否跳过执行任务)\n        - [3.2.2. 随机睡眠的最大时长](#322-随机睡眠的最大时长)\n        - [3.2.3. 两次调用B站Api之间的间隔秒数](#323-两次调用b站api之间的间隔秒数)\n        - [3.2.4. 间隔秒数所针对的HttpMethod](#324-间隔秒数所针对的httpmethod)\n        - [3.2.5. 请求B站接口时头部传递的User-Agent](#325-请求b站接口时头部传递的user-agent)\n        - [3.2.6. App请求B站接口时头部传递的User-Agent](#326-app请求b站接口时头部传递的user-agent)\n        - [3.2.7. WebProxy（代理）](#327-webproxy代理)\n    - [3.3. 每日任务相关](#33-每日任务相关)\n        - [3.3.1. 是否开启观看视频任务](#331-是否开启观看视频任务)\n        - [3.3.2. 是否开启分享视频任务](#332-是否开启分享视频任务)\n        - [3.3.3. 每日投币数量](#333-每日投币数量)\n        - [3.3.4. 投币时是否同时点赞](#334-投币时是否同时点赞)\n        - [3.3.5. 优先选择支持的up主Id集合](#335-优先选择支持的up主id集合)\n        - [3.3.6. 每月几号自动领取会员权益](#336-每月几号自动领取会员权益)\n        - [3.3.7. 每月几号进行直播中心银瓜子兑换硬币](#337-每月几号进行直播中心银瓜子兑换硬币)\n        - [3.3.8. Lv6后开启硬币白嫖模式](#338-lv6后开启硬币白嫖模式)\n        - [3.3.9. 是否开启专栏投币](#339-是否开启专栏投币)\n    - [3.4. 天选时刻抽奖相关](#34-天选时刻抽奖相关)\n        - [3.4.1. 根据关键字排除奖品](#341-根据关键字排除奖品)\n        - [3.4.2. 根据关键字指定奖品](#342-根据关键字指定奖品)\n        - [3.4.3. 天选抽奖后是否自动分组关注的主播](#343-天选抽奖后是否自动分组关注的主播)\n        - [3.4.4. 天选筹抽奖主播Uid黑名单](#344-天选筹抽奖主播uid黑名单)\n    - [3.5. 批量取关相关](#35-批量取关相关)\n        - [3.5.1. 想要批量取关的分组名称](#351-想要批量取关的分组名称)\n        - [3.5.2. 批量取关的人数](#352-批量取关的人数)\n        - [3.5.3. 取关白名单](#353-取关白名单)\n    - [3.6. 大积分相关](#36-大积分相关)\n        - [3.6.1. 自定义观看番剧](#361-自定义观看番剧)\n    - [3.7. 免费B币券充电](#37-免费b币券充电)\n        - [3.7.1. 充电对象](#371-充电对象)\n    - [3.8. 推送相关](#38-推送相关)\n        - [3.8.1. 是否开启每个账号单独推送消息](#381-是否开启每个账号单独推送消息)\n        - [3.8.2. Telegram机器人](#382-telegram机器人)\n            - [3.8.2.1. botToken](#3821-bottoken)\n            - [3.8.2.2. chatId](#3822-chatid)\n            - [3.8.2.3. proxy](#3823-proxy)\n        - [3.8.3. 企业微信机器人](#383-企业微信机器人)\n            - [3.8.3.1. webHookUrl](#3831-webhookurl)\n        - [3.8.4. 钉钉机器人](#384-钉钉机器人)\n            - [3.8.4.1. webHookUrl](#3841-webhookurl)\n        - [3.8.5. Server酱](#385-server酱)\n            - [3.8.5.1. TurboScKey（Server酱SCKEY）](#3851-turbosckeyserver酱sckey)\n        - [3.8.6. 酷推](#386-酷推)\n            - [3.8.6.1. sKey](#3861-skey)\n        - [3.8.7. 推送到自定义Api](#387-推送到自定义api)\n            - [3.8.7.1. api](#3871-api)\n            - [3.8.7.2. placeholder](#3872-placeholder)\n            - [3.8.7.3. bodyJsonTemplate](#3873-bodyjsontemplate)\n        - [3.8.8. PushPlus[推荐]](#388-pushplus推荐)\n            - [3.8.8.1. PushPlus的Token](#3881-pushplus的token)\n            - [3.8.8.2. PushPlus的Topic](#3882-pushplus的topic)\n            - [3.8.8.3. PushPlus的Channel](#3883-pushplus的channel)\n            - [3.8.8.4. PushPlus的Webhook](#3884-pushplus的webhook)\n        - [3.8.9. Microsoft Teams](#389-microsoft-teams)\n            - [3.8.9.1. Microsoft Teams的Webhook](#3891-microsoft-teams的webhook)\n        - [3.8.10. 企业微信应用推送](#3810-企业微信应用推送)\n            - [3.8.10.1. 企业微信应用推送的corpId](#38101-企业微信应用推送的corpid)\n            - [3.8.10.2. 企业微信应用推送的agentId](#38102-企业微信应用推送的agentid)\n            - [3.8.10.3. 企业微信应用推送的secret](#38103-企业微信应用推送的secret)\n    - [3.9. 日志相关](#39-日志相关)\n        - [3.9.1. 日志输出等级](#391-日志输出等级)\n        - [3.9.2. 日志输出样式](#392-日志输出样式)\n        - [3.9.3. 定时任务相关](#393-定时任务相关)\n        - [3.9.4. 定时任务](#394-定时任务)\n\n<!-- /TOC -->\n\n<a id=\"markdown-1-配置方式\" name=\"1-配置方式\"></a>\n## 1. 配置方式\n\n<a id=\"markdown-11-方式一修改配置文件\" name=\"11-方式一修改配置文件\"></a>\n### 1.1. 方式一：修改配置文件\n\n推荐使用Release包在本地运行的朋友使用，直接打开文件，将对应的配置值填入，保存即可生效。\n\n默认有3个配置文件：`appsettings.json`、`appsettings.Development.json`、`appsettings.Production.json`，分别对应默认、开发与生产环境。\n\n对于不是开发人员的大部分人来说，只需要关注`appsettings.Production.json`即可。\n\n<a id=\"markdown-12-方式二命令启动时通过命令行参数配置\" name=\"12-方式二命令启动时通过命令行参数配置\"></a>\n### 1.2. 方式二：命令启动时通过命令行参数配置\n\n在使用命令行启动时，可使用`-key=value`的形式附加配置，所有可用的命令行参数均在 [命令行参数映射表](../src/Ray.BiliBiliTool.Config/Constants.cs#L76-L105) 中。\n\n* 使用跨平台的依赖包\n\n各个系统只要安装了net5环境，均可使用dotnet命令启动，命令样例：\n\n```\ndotnet Ray.BiliBiliTool.Console.dll -cookieStr=abc -numberOfCoins=5\n```\n\n* Windows系统\n\n使用自包含包（win-x86-x64.zip），命令样例：\n\n```\nRay.BiliBiliTool.Console.exe -cookieStr=abc -numberOfCoins=5\n```\n\n* Linux系统\n\n使用自包含包（linux.zip），命令样例：\n\n```\nRay.BiliBiliTool.Console -cookieStr=abc -numberOfCoins=5\n```\n\n如映射文件所展示，支持使用命令行配置的配置项并不多，也不建议大量地使用该种方式进行配置。使用包运行的朋友，除了改配置文件和命令行参数配置外，还可以使用环境变量进行配置，这也是推荐的做法，如下。\n\n<a id=\"markdown-13-方式三添加环境变量推荐\" name=\"13-方式三添加环境变量推荐\"></a>\n### 1.3. 方式三：添加环境变量（推荐）\n\n所有的配置项均可以通过添加环境变量来进行配置。如：\n\nLinux下运行Web：\n\n```bash\n# 添加环境变量作为配置：\nexport RunTasks=\"Daily\"\nexport BiliBiliCookies__1=\"abc\"\nexport BiliBiliCookies__2=\"efg\"\nexport DailyTaskConfig__NumberOfCoins=\"3\"\n\n# 开始运行程序：\ndotnet BiliBiliTool.Web.dll\n```\n\nLinux下运行Console：\n\n```bash\n# 添加环境变量作为配置：\nexport Ray_RunTasks=\"Daily\"\nexport Ray_BiliBiliCookies__1=\"abc\"\nexport Ray_BiliBiliCookies__2=\"efg\"\nexport Ray_DailyTaskConfig__NumberOfCoins=\"3\"\n\n# 开始运行程序：\ndotnet Ray.BiliBiliTool.Console.dll\n```\n\n注意Console需要添加`Ray_`前缀，win系统使用`set`关键字代替`export`。\n\n<a id=\"markdown-14-方式四托管在青龙面板上使用面板的环境变量页或配置文件页进行配置\" name=\"14-方式四托管在青龙面板上使用面板的环境变量页或配置文件页进行配置\"></a>\n### 1.4. 方式四：托管在青龙面板上，使用面板的环境变量页或配置文件页进行配置\n\n青龙面板配置，其本质还是通过环境变量进行配置，有如下两种方式。\n\n- 环境变量页[推荐]\n\n例如：\n\n名称：`Ray_BiliBiliCookies__1`\n\n值：`abcde`\n\n<img src=\"/docs/imgs/qinglong-env.png\" alt=\"qinglong-env\" width=\"800\" />\n\n- 配置文件页\n\n例如，配置Cookie和推送：\n\n```\nexport Ray_BiliBiliCookies__1=\"_uuid=abc...\"\nexport Ray_Serilog__WriteTo__9__Args__token=\"abcde\"\n```\n\n<img src=\"/docs/imgs/qinglong-config.png\" alt=\"qinglong-config\" width=\"800\" />\n\n配置文件页添加、修改配置，需要重启青龙容器使之生效，环境变量页则可以立即生效，所以推荐使用环境变量页配置。\n\n<a id=\"markdown-2-优先级\" name=\"2-优先级\"></a>\n## 2. 优先级\n\n以上 4 种配置源，其优先级由低到高依次是：json文件 < 环境变量 < 命令行。\n\n高优先级的配置会覆盖低优先级的配置。\n\n<a id=\"markdown-3-详细配置说明\" name=\"3-详细配置说明\"></a>\n## 3. 详细配置说明\n\nConsole项目（青龙）的环境变量需要添加`Ray_`前缀，其他不用。\n比如，原始配置Key为`BiliBiliCookies__1`，Console则为`Ray_BiliBiliCookies__1`。\n\n<a id=\"markdown-31-cookie字符串\" name=\"31-cookie字符串\"></a>\n### 3.1. Cookie字符串\n\n必填，数组，可以多个。\n\n| TITLE | CONTENT             | 示例                                           |\n| ----- | ------------------- | -------------------------------------------- |\n| 配置Key | `BiliBiliCookies__1` |                                              |\n| 值域    | 字符串，英文分号分隔，来自浏览器抓取  | `export BiliBiliCookies__1=abc=123;def=456;` |\n| 默认值   | 空                   |                                              |\n\n| TITLE | CONTENT             | 示例                                           |\n| ----- | ------------------- | -------------------------------------------- |\n| 配置Key | `BiliBiliCookies__2` |                                              |\n| 值域    | 字符串，英文分号分隔，来自浏览器抓取  | `export BiliBiliCookies__1=abc=123;def=456;` |\n| 默认值   | 空                   |                                              |\n\n<a id=\"markdown-32-安全相关的配置\" name=\"32-安全相关的配置\"></a>\n### 3.2. 安全相关的配置\n<a id=\"markdown-321-是否跳过执行任务\" name=\"321-是否跳过执行任务\"></a>\n#### 3.2.1. 是否跳过执行任务\n\n用于特殊情况下，通过配置灵活的开启和关闭整个应用。\n\n配置为关闭后，程序会跳过所有任务，不会调用B站任何接口。\n\n| TITLE | CONTENT                         | 示例                                   |\n| ----- | ------------------------------- | ------------------------------------ |\n| 配置Key | `Security__IsSkipDailyTask`     |                                      |\n| 值域    | [true,false]                    | `export Security__IsSkipDailyTask=true` |\n| 默认值   | false                           |                                      |\n\n<a id=\"markdown-322-随机睡眠的最大时长\" name=\"322-随机睡眠的最大时长\"></a>\n#### 3.2.2. 随机睡眠的最大时长\n\n用于设置程序启动后，随机睡眠时间的最大上限值，单位为分钟。\n\n这样可以避免程序每天准点地在同一时间运行太像机器。\n\n配置为0则不进行睡眠。\n\n| TITLE | CONTENT                           |\n| ----- | --------------------------------- |\n| 配置Key | `Security__RandomSleepMaxMin`     |\n| 值域    | 数字                                |\n| 默认值   | 20                                |\n\n<a id=\"markdown-323-两次调用b站api之间的间隔秒数\" name=\"323-两次调用b站api之间的间隔秒数\"></a>\n#### 3.2.3. 两次调用B站Api之间的间隔秒数\n\n用于设置两次Api请求之间的最短时间间隔，避免程序在1到2秒内连续调用B站的Api过快。\n\n| TITLE | CONTENT                                          |\n| ----- | ------------------------------------------------ |\n| 配置Key | `Security__IntervalSecondsBetweenRequestApi`     |\n| 值域    | [0,+]                                            |\n| 默认值   | 20                                               |\n\n<a id=\"markdown-324-间隔秒数所针对的httpmethod\" name=\"324-间隔秒数所针对的httpmethod\"></a>\n#### 3.2.4. 间隔秒数所针对的HttpMethod\n\n间隔秒数所针对的HttpMethod类型，服务于上一个配置。服务器一般对GET请求不是很敏感，建议只针对POST请求做间隔就可以了。\n\n| TITLE | CONTENT                             |\n| ----- | ----------------------------------- |\n| 配置Key | `Security__IntervalMethodTypes`     |\n| 值域    | [GET,POST]，多个以英文逗号分隔                |\n| 默认值   | POST                                |\n\n<a id=\"markdown-325-请求b站接口时头部传递的user-agent\" name=\"325-请求b站接口时头部传递的user-agent\"></a>\n#### 3.2.5. 请求B站接口时头部传递的User-Agent\n\n| TITLE | CONTENT                                                                                                                                  |\n| ----- | ---------------------------------------------------------------------------------------------------------------------------------------- |\n| 配置Key | `Security__UserAgent`                                                                                                                    |\n| 值域    | 字符串，可以F12从自己的浏览器获取                                                                                                                       |\n| 默认值   | 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 |\n\n获取浏览器中自己的UA的方法见下图：\n\n<img src=\"/docs/imgs/get-user-agent.png\" alt=\"get-user-agent\" width=\"800\" />\n\n<a id=\"markdown-326-app请求b站接口时头部传递的user-agent\" name=\"326-app请求b站接口时头部传递的user-agent\"></a>\n#### 3.2.6. App请求B站接口时头部传递的User-Agent\n\n| TITLE | CONTENT                                                                                                                                                                                                                                                                                                                                                                                                                                                   |\n| ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| 配置Key | `Security__UserAgentApp`                                                                                                                                                                                                                                                                                                                                                                                                                                  |\n| 值域    | 字符串，可以F12从自己的浏览器获取                                                                                                                                                                                                                                                                                                                                                                                                                                        |\n| 默认值   | 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 |\n\n获取浏览器中自己的UA的方法见下图：\n\n<img src=\"/docs/imgs/get-user-agent.png\" alt=\"get-user-agent\" width=\"800\" />\n\n<a id=\"markdown-327-webproxy代理\" name=\"327-webproxy代理\"></a>\n#### 3.2.7. WebProxy（代理）\n\n支持需要账户密码的代理。\n\n| TITLE          | CONTENT                        |\n| -------------- | ------------------------------ |\n| 配置Key          | `Security__WebProxy`           |\n| 值域             | 字符串，形如：user:password@host:port |\n| 默认值            | 无                              |\n\n<a id=\"markdown-33-每日任务相关\" name=\"33-每日任务相关\"></a>\n### 3.3. 每日任务相关\n\n<a id=\"markdown-331-是否开启观看视频任务\" name=\"331-是否开启观看视频任务\"></a>\n#### 3.3.1. 是否开启观看视频任务\n\n当该配置被设置为`false`时会导致大积分任务中的签到领额外10点经验的任务不能自动完成。\n\n| TITLE | CONTENT                         |\n| ----- | ------------------------------- |\n| 配置Key | `DailyTaskConfig__IsWatchVideo` |\n| 值域    | [true,false]                    |\n| 默认值   | true                            |\n\n<a id=\"markdown-332-是否开启分享视频任务\" name=\"332-是否开启分享视频任务\"></a>\n#### 3.3.2. 是否开启分享视频任务\n\n| TITLE | CONTENT                         |\n| ----- | ------------------------------- |\n| 配置Key | `DailyTaskConfig__IsShareVideo` |\n| 值域    | [true,false]                    |\n| 默认值   | true                            |\n\n<a id=\"markdown-333-每日投币数量\" name=\"333-每日投币数量\"></a>\n#### 3.3.3. 每日投币数量\n\n每天投币的总目标数量，因为投币获取经验只与次数有关，所以程序每次投币只会投1个，也就是说该配置也表示每日投币次数。\n\n| TITLE | CONTENT                          |\n| ----- | -------------------------------- |\n| 配置Key | `DailyTaskConfig__NumberOfCoins` |\n| 值域    | [0,5]，为安全考虑，程序内部还会做验证，最大不能超过5    |\n| 默认值   | 5                                |\n\n<a id=\"markdown-334-投币时是否同时点赞\" name=\"334-投币时是否同时点赞\"></a>\n#### 3.3.4. 投币时是否同时点赞\n\n| TITLE | CONTENT                       |\n| ----- | ----------------------------- |\n| 配置Key | `DailyTaskConfig__SelectLike` |\n| 值域    | [true,false]                  |\n| 默认值   | false                         |\n\n<a id=\"markdown-335-优先选择支持的up主id集合\" name=\"335-优先选择支持的up主id集合\"></a>\n#### 3.3.5. 优先选择支持的up主Id集合\n\n通过填入自己选择的up主ID，以后观看、分享和投币，都会优先从配置的up主下面挑选视频，如果没有找到,则会去你的**特别关注**列表中随机再获取，再然后会去**普通关注**列表中随机获取，最后会去排行榜中随机获取。\n\n**注意：该配置的默认值是作者的upId，如需换掉的话，直接更改即可。**\n\n| TITLE | CONTENT                                                         |\n| ----- | --------------------------------------------------------------- |\n| 配置Key | `DailyTaskConfig__SupportUpIds`                                 |\n| 值域    | up主ID，多个用英文逗号分隔，默认是作者本人的UpId，如需删除可以配置为空格字符串或\"-1\"，也可以配置为其他人的UpId |\n| 默认值   | 作者的upId                                                         |\n\n获取UP主的Id方法：打开bilibili，进入欲要选择的UP主主页，在url中和简介中，都可获得该UP主的Id，如下图所示：\n\n<img src=\"/docs/imgs/get-up-id.png\" alt=\"get-up-id\" width=\"800\" />\n\n<a id=\"markdown-336-每月几号自动领取会员权益\" name=\"336-每月几号自动领取会员权益\"></a>\n#### 3.3.6. 每月几号自动领取会员权益\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `DailyTaskConfig__DayOfReceiveVipPrivilege` |\n| 值域   | [-1,31]，-1表示不指定，默认每月1号；0表示不领取 |\n| 默认值   | 1 |\n\n<a id=\"markdown-337-每月几号进行直播中心银瓜子兑换硬币\" name=\"337-每月几号进行直播中心银瓜子兑换硬币\"></a>\n#### 3.3.7. 每月几号进行直播中心银瓜子兑换硬币\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `DailyTaskConfig__DayOfExchangeSilver2Coin` |\n| 值域   | [-1,31]，-1表示不指定，默认每月最后一天；-2表示每天；0表示不进行兑换 |\n| 默认值   | -1 |\n\n<a id=\"markdown-338-lv6后开启硬币白嫖模式\" name=\"338-lv6后开启硬币白嫖模式\"></a>\n#### 3.3.8. Lv6后开启硬币白嫖模式\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `DailyTaskConfig__SaveCoinsWhenLv6` |\n| 值域   | [true,false]，true表示开启，Lv6的账号不会投币 |\n| 默认值   | false |\n\n<a id=\"markdown-339-是否开启专栏投币\" name=\"339-是否开启专栏投币\"></a>\n#### 3.3.9. 是否开启专栏投币\n\n| TITLE | CONTENT                                   |     |\n| ----- | ----------------------------------------- | --- |\n| 配置Key | `DailyTaskConfig__IsDonateCoinForArticle` |     |\n| 值域    | [true,false]                              |     |\n| 默认值   | false                                     |     |\n\n<a id=\"markdown-34-天选时刻抽奖相关\" name=\"34-天选时刻抽奖相关\"></a>\n### 3.4. 天选时刻抽奖相关\n\n<a id=\"markdown-341-根据关键字排除奖品\" name=\"341-根据关键字排除奖品\"></a>\n#### 3.4.1. 根据关键字排除奖品\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `LiveLotteryTaskConfig__ExcludeAwardNames` |\n| 值域   | 一串字符串，多个关键字使用`\\|`符号隔开 |\n| 默认值   | `舰\\|船\\|航海\\|代金券\\|自拍\\|照\\|写真\\|图` |\n\n<a id=\"markdown-342-根据关键字指定奖品\" name=\"342-根据关键字指定奖品\"></a>\n#### 3.4.2. 根据关键字指定奖品\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `LiveLotteryTaskConfig__IncludeAwardNames` |\n| 值域   | 一串字符串，多个关键字使用`\\|`符号隔开 |\n| 默认值   | 空 |\n\n<a id=\"markdown-343-天选抽奖后是否自动分组关注的主播\" name=\"343-天选抽奖后是否自动分组关注的主播\"></a>\n#### 3.4.3. 天选抽奖后是否自动分组关注的主播\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `LiveLotteryTaskConfig__AutoGroupFollowings` |\n| 值域   | [true,false] |\n| 默认值   | true |\n\n<a id=\"markdown-344-天选筹抽奖主播uid黑名单\" name=\"344-天选筹抽奖主播uid黑名单\"></a>\n#### 3.4.4. 天选筹抽奖主播Uid黑名单\n\n不想参与抽奖的主播Upid集合，多个用英文逗号分隔，配置后不会参加黑名单中的主播的抽奖活动。默认值是目前已知的中奖后拒绝发奖的Up，后期还会继续补充，也反映反馈。\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `LiveLotteryTaskConfig__DenyUids` |\n| 值域   | 字符串，如\"65566781,1277481241\" |\n| 默认值   | \"65566781,1277481241,1643654862,603676925\" |\n\n<a id=\"markdown-35-批量取关相关\" name=\"35-批量取关相关\"></a>\n### 3.5. 批量取关相关\n\n<a id=\"markdown-351-想要批量取关的分组名称\" name=\"351-想要批量取关的分组名称\"></a>\n#### 3.5.1. 想要批量取关的分组名称\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `UnfollowBatchedTaskConfig__GroupName` |\n| 值域   | 字符串 |\n| 默认值   | 天选时刻 |\n\n<a id=\"markdown-352-批量取关的人数\" name=\"352-批量取关的人数\"></a>\n#### 3.5.2. 批量取关的人数\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `UnfollowBatchedTaskConfig__Count` |\n| 值域   | 数字，[-1,+]，-1表示全部 |\n| 默认值   | 5 |\n\n<a id=\"markdown-353-取关白名单\" name=\"353-取关白名单\"></a>\n#### 3.5.3. 取关白名单\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `UnfollowBatchedTaskConfig__RetainUids` |\n| 值域   | 字符串，多个使用英文逗号分隔 |\n| 默认值   | 108569350 |\n\n<a id=\"markdown-36-大积分相关\" name=\"36-大积分相关\"></a>\n### 3.6. 大积分相关\n\n<a id=\"markdown-361-自定义观看番剧\" name=\"361-自定义观看番剧\"></a>\n#### 3.6.1. 自定义观看番剧\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `VipBigPointConfig__ViewBangumis` |\n| 值域   | 番剧的ssid（season_id） |\n| 默认值   | `33378`（名侦探柯南） |\n\n<a id=\"markdown-37-免费b币券充电\" name=\"37-免费b币券充电\"></a>\n### 3.7. 免费B币券充电\n\n<a id=\"markdown-371-充电对象\" name=\"371-充电对象\"></a>\n#### 3.7.1. 充电对象\n\n充电对象的upId，-1表示不指定，~~默认为自己充电~~；其他Id则会尝试为配置的UpId充电。\n\n注意：之前可以不配置，默认为自己充电，但后来阿B改了规则，不再允许自己冲自己。。。\n\n建议配置为自己小号（小号需要认证为作者并开启充电），或者也可以配置为 -1，以支持作者~\n\n| TITLE | CONTENT                           |\n| ----- | --------------------------------- |\n| 配置Key | `ChargeTaskConfig__AutoChargeUpId` |\n| 值域    | up的Id字符串                          |\n| 默认值   | 无                                 |\n\n<a id=\"markdown-38-推送相关\" name=\"38-推送相关\"></a>\n### 3.8. 推送相关\n\nv1.0.x仅支持推送到Server酱，v1.1.x之后重新定义了推送地概念，将推送仅看作不同地日志输出端，与Console、File没有本质区别。\n\n配置多个，多个端均会收到日志消息。推荐Telegram、企业微信、Server酱。\n\n<a id=\"markdown-381-是否开启每个账号单独推送消息\" name=\"381-是否开启每个账号单独推送消息\"></a>\n#### 3.8.1. 是否开启每个账号单独推送消息\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Notification__IsSingleAccountSingleNotify` |\n| 意义 | 开启后，每个账号会单独推送消息。否则多账号合并只推送一条消息 |\n| 值域   | [true,false] |\n| 默认值   | true |\n\n<a id=\"markdown-382-telegram机器人\" name=\"382-telegram机器人\"></a>\n#### 3.8.2. Telegram机器人\n\n<img src=\"/docs/imgs/push-tg.png\" alt=\"push-tg\" width=\"400\" />\n\n<a id=\"markdown-3821-bottoken\" name=\"3821-bottoken\"></a>\n##### 3.8.2.1. botToken\n\n点击 https://core.telegram.org/api#bot-api 查看如何创建机器人并获取到机器人的botToken。\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__3__Args__botToken` |\n| 意义 | 用于将日志输出到Telegram机器人 |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n\n<a id=\"markdown-3822-chatid\" name=\"3822-chatid\"></a>\n##### 3.8.2.2. chatId\n点击 https://api.telegram.org/bot{TOKEN}/getUpdates 获取到与机器人的chatId（需要用上面获取到的Token替换进链接里的{TOKEN}后访问）\n\nP.S.访问链接需要能访问\"外网\"，有vpn的挂vpn。\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__3__Args__chatId` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n| 命令行示范   | 无 |\n\n<a id=\"markdown-3823-proxy\" name=\"3823-proxy\"></a>\n##### 3.8.2.3. proxy\n\n使用代理\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__3__Args__proxy` |\n| 值域   | 一串字符串，格式为user:password@host:port |\n| 默认值   | 空 |\n| 命令行示范   | 无 |\n\n<a id=\"markdown-383-企业微信机器人\" name=\"383-企业微信机器人\"></a>\n#### 3.8.3. 企业微信机器人\n\n在群内添加机器人，获取到机器人的WebHook地址，添加到配置中。\n\n<img src=\"/docs/imgs/push-workweixin.png\" alt=\"push-workweixin\" width=\"400\" />\n\n<a id=\"markdown-3831-webhookurl\" name=\"3831-webhookurl\"></a>\n##### 3.8.3.1. webHookUrl\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__4__Args__webHookUrl` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n| 命令行示范   | 无 |\n\n<a id=\"markdown-384-钉钉机器人\" name=\"384-钉钉机器人\"></a>\n#### 3.8.4. 钉钉机器人\n\n在群内添加机器人，获取到机器人的WebHook地址，添加到配置中。\n\n机器人的安全策略，当前不支持加签，请使用关键字策略，推荐关键字：`Ray` 或 `BiliBili`\n\n<img src=\"/docs/imgs/push-ding.png\" alt=\"push-ding\" width=\"400\" />\n\n<a id=\"markdown-3841-webhookurl\" name=\"3841-webhookurl\"></a>\n##### 3.8.4.1. webHookUrl\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__5__Args__webHookUrl` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n\n<a id=\"markdown-385-server酱\" name=\"385-server酱\"></a>\n#### 3.8.5. Server酱\n官网： http://sc.ftqq.com/9.version\n\n<img src=\"/docs/imgs/wechat-push.png\" alt=\"wechat-push\" width=\"400\" />\n\n<a id=\"markdown-3851-turbosckeyserver酱sckey\" name=\"3851-turbosckeyserver酱sckey\"></a>\n##### 3.8.5.1. TurboScKey（Server酱SCKEY）\n获取方式请参考官网。\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__6__Args__turboScKey` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n\n<a id=\"markdown-386-酷推\" name=\"386-酷推\"></a>\n#### 3.8.6. 酷推\nhttps://cp.xuthus.cc/\n<a id=\"markdown-3861-skey\" name=\"3861-skey\"></a>\n##### 3.8.6.1. sKey\n该平台可能还在完善当中，对接时我发现其接口定义不规范，且机器人容易被封，所以不推荐使用，且不接受提酷推推送相关bug。\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__7__Args__sKey` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n\n<a id=\"markdown-387-推送到自定义api\" name=\"387-推送到自定义api\"></a>\n#### 3.8.7. 推送到自定义Api\n这是我简单封装了一个通用的推送接口，可以推送到任意的api地址，如果有自己的机器人或自己的用于接受日志的api，可以根据需要自定义配置。\n<a id=\"markdown-3871-api\" name=\"3871-api\"></a>\n##### 3.8.7.1. api\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__8__Args__api` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n\n<a id=\"markdown-3872-placeholder\" name=\"3872-placeholder\"></a>\n##### 3.8.7.2. placeholder\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__8__Args__placeholder` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n\n<a id=\"markdown-3873-bodyjsontemplate\" name=\"3873-bodyjsontemplate\"></a>\n##### 3.8.7.3. bodyJsonTemplate\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__8__Args__bodyJsonTemplate` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n\n<a id=\"markdown-388-pushplus推荐\" name=\"388-pushplus推荐\"></a>\n#### 3.8.8. PushPlus[推荐]\n\n官网： http://www.pushplus.plus/doc/\n\n<a id=\"markdown-3881-pushplus的token\" name=\"3881-pushplus的token\"></a>\n##### 3.8.8.1. PushPlus的Token\n\n获取方式请参考官网。\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__9__Args__token` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n\n<a id=\"markdown-3882-pushplus的topic\" name=\"3882-pushplus的topic\"></a>\n##### 3.8.8.2. PushPlus的Topic\n\n获取方式请参考官网。\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__9__Args__topic` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n\n<a id=\"markdown-3883-pushplus的channel\" name=\"3883-pushplus的channel\"></a>\n##### 3.8.8.3. PushPlus的Channel\n\n获取方式请参考官网。\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__9__Args__channel` |\n| 值域   | 一串字符串，[wechat,webhook,cp,sms,mail] |\n| 默认值   | 空 |\n\n<a id=\"markdown-3884-pushplus的webhook\" name=\"3884-pushplus的webhook\"></a>\n##### 3.8.8.4. PushPlus的Webhook\n\n获取方式请参考官网。\n\nwebhook编码(不是地址)，在官网平台设定，仅在channel使用webhook渠道和CP渠道时需要填写\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__9__Args__webhook` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n| 命令行示范   |  |\n\n<a id=\"markdown-389-microsoft-teams\" name=\"389-microsoft-teams\"></a>\n#### 3.8.9. Microsoft Teams\n\n官网： https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook\n\n<a id=\"markdown-3891-microsoft-teams的webhook\" name=\"3891-microsoft-teams的webhook\"></a>\n##### 3.8.9.1. Microsoft Teams的Webhook\n\nwebhook的完整地址，在Teams的Channel中获取，详细获取方式请参考官网。\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__10__Args__webhook` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n| 命令行示范   |  |\n\n<a id=\"markdown-3810-企业微信应用推送\" name=\"3810-企业微信应用推送\"></a>\n#### 3.8.10. 企业微信应用推送\n\n官网： https://developer.work.weixin.qq.com/tutorial/application-message\n\n当`corpId`、`agentId`、`secret`均不为空时，自动开启推送，否则关闭。\n\n`toUser`、`toParty`、`toTag`3个配置非必填，但不可同时为空，默认`toUser`为`@all`，向所有用户推送。\n\n<a id=\"markdown-38101-企业微信应用推送的corpid\" name=\"38101-企业微信应用推送的corpid\"></a>\n##### 3.8.10.1. 企业微信应用推送的corpId\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__11__Args__corpId` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n| 命令行示范   |  |\n\n<a id=\"markdown-38102-企业微信应用推送的agentid\" name=\"38102-企业微信应用推送的agentid\"></a>\n##### 3.8.10.2. 企业微信应用推送的agentId\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__11__Args__agentId` |\n| 值域   | 一串字符串 |\n| 默认值   | 空 |\n| 命令行示范   |  |\n\n<a id=\"markdown-38103-企业微信应用推送的secret\" name=\"38103-企业微信应用推送的secret\"></a>\n##### 3.8.10.3. 企业微信应用推送的secret\n\n| TITLE | CONTENT                          |\n| ----- | -------------------------------- |\n| 配置Key | `Serilog__WriteTo__11__Args__secret` |\n| 值域    | 一串字符串                            |\n| 默认值   | 空                                |\n| 命令行示范 |                                  |\n\n<a id=\"markdown-39-日志相关\" name=\"39-日志相关\"></a>\n### 3.9. 日志相关\n\n<a id=\"markdown-391-日志输出等级\" name=\"391-日志输出等级\"></a>\n#### 3.9.1. 日志输出等级\n\n为了美观， BiliBiliTool 默认只输出最低等级为 Information 的日志，保证只展示最精简的信息。\n\n通过更改等级，可以指定日志输出的详细程度。\n\nBiliBiliTool 使用 Serilog 作为日志组件，所以其值域与 Serilog 的日志等级选项相同，这里只建议在需要调试时改为`Debug`，应用会输出详细的调试日志信息，包括每次调用B站Api的请求参数与返回数据。\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__0__Args__restrictedToMinimumLevel` |\n| 值域   | [Information,Debug] |\n| 默认值   | 1 |\n\n<a id=\"markdown-392-日志输出样式\" name=\"392-日志输出样式\"></a>\n#### 3.9.2. 日志输出样式\n\n这里的日志样式指的是 Console 的等级，即 GitHub Actions 里和微信推送里看到的日志。\n\n通过更改模板样式，可以指定日志输出的样式，比如不输出时间和等级，做到最精简的样式。\n\nBiliBiliTool 使用 Serilog 作为日志组件，所以可以参考 Serilog 的日志样式模板。\n\n|   TITLE   | CONTENT   |\n| ---------- | -------------- |\n| 配置Key | `Serilog__WriteTo__0__Args__outputTemplate` |\n| 值域   | 字符串 |\n| 默认值   | `[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}` |\n\n<a id=\"markdown-393-定时任务相关\" name=\"393-定时任务相关\"></a>\n#### 3.9.3. 定时任务相关\n\n适用于 [方式四：docker容器化运行（推荐）](../docker/README.md)，用于配置定时任务。\n\n<a id=\"markdown-394-定时任务\" name=\"394-定时任务\"></a>\n#### 3.9.4. 定时任务\n\n以下环境变量的值应为有效的 [cron 表达式](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm)。\n\n当被设置时，对应定时任务将开启。\n\n| 环境变量                              | 定时任务   |\n| --------------------------------- | ------ |\n| `DailyTaskConfig__Cron`           | 每日任务   |\n| `LiveLotteryTaskConfig__Cron`     | 天选时刻抽奖 |\n| `UnfollowBatchedTaskConfig__Cron` | 批量取关   |\n| `VipBigPointConfig__Cron`         | 大会员大积分 |\n"
  },
  {
    "path": "docs/donate-list.md",
    "content": "# 赞赏\n\n| 赞赏人 | 时间 | 金额 | 方式 | 留言 | 回复\n| ---------- | -------------- | -------------- | -------------- | -------------- | -------------- |\n| Jonty | 2020-11-04 | ￥5 | 微信 | 不知可否加个联系方式，讨论一下b站小公举 | 你没有留微信号啊大兄弟 |\n| 春歌 | 2020-11-06 | ￥5 | 微信 | 赞赏一下~ |  |\n| 雅南 | 2020-11-08 | ￥1 | 微信 | 赞赏一下~ |  |\n| RainMeter | 2020-11-09 | ￥1 | 微信 | 大佬牛逼 |  |\n| 1998 | 2020-11-10 | ￥1 | 微信 | 谢谢大佬，但是投币总是失败 | 有问题可以加群讨论 |\n| Andy | 2020-11-10 | ￥1 | 微信 | 赞赏一下～ |  |\n| 不若艳阳 | 2020-11-11 | ￥10 | 微信 | 赞赏一下～ |  |\n| 努力努力再努力 | 2020-11-11 | ￥1 | 微信 | 赞赏一下～ |  |\n| Wandering Ghost | 2020-11-12 | ￥1 | 微信 | 好活，当赏 |  |\n| sadhu | 2020-11-12 | ￥1 | 支付宝 | 加油 |  |\n| 浮蘭·鳥ドス | 2020-11-12 | ￥1 | 微信 | 加油！在一个微信公众号看到这个 |  |\n| Ⅶ | 2020-11-12 | ￥1 | 微信 | 太棒啦！支持您！ |  |\n| 舞飞扬 | 2020-11-12 | ￥1 | 支付宝 | 多谢bilibilitool |  |\n| 王雨桐 | 2020-11-13 | ￥1 | 微信 | 感谢ps借用作者项目完成我开源选修作业——分析一款开源软件 |  |\n| 郁宁 | 2020-11-13 | ￥5 | 微信 | Godd Job |  |\n| 半岛 | 2020-11-13 | ￥1 | 支付宝 | 赞赏一下～ |  |\n| Wenson | 2020-11-19 | ￥10 | 微信 | 搞的不错👍 |  |\n| xingxing | 2020-11-20 | ￥1 | 支付宝 | 这东西太好用了，点个 |  |\n|  | 2020-11-21 | ￥10 | 微信 | 单纯问下，有做成云函数的可行性吗 | 目前没了解过云函数相关，有懂的朋友欢迎PR~ |\n| Gaogao | 2020-11-21 | ￥1 | 支付宝 | 老哥加油加油^0^ |  |\n| 老狗 | 2020-11-23 | ￥10 | 微信 | 好活当赏吗 |  |\n|  | 2020-11-23 | ￥10 | 微信 |  |  |\n| 还输给回忆不成 | 2020-11-23 | ￥1 | 微信 |  |  |\n| Winfor | 2020-11-23 | ￥5 | 微信 | 感谢分享 |  |\n| 那个冰 | 2020-11-23 | ￥1 | 微信 | 如果一开始努力的方向就是错误的，那么只会越来越忙。感谢 |  |\n| Luv(sic) part 2 | 2020-11-24 | ￥1 | 微信 | 牛逼嗷，上班划水新技能get |  |\n| 刘小明 | 2020-11-24 | ￥3 | 微信 | 大佬喝冰阔落 |  |\n| 青翘 | 2020-11-25 | ￥1 | 支付宝 | 赞赏一下~ |  |\n| CT | 2020-11-25 | ￥1 | 微信 | 感谢 |  |\n| Panda | 2020-11-25 | ￥1 | 微信 | 很好用的工具 |  |\n| Che | 2020-11-25 | ￥1 | 微信 | 微信推送的配置方法能不能说的再详细一点，没用过有点懵 |  |\n| 张浩 | 2020-11-25 | ￥1 | 微信 | 集资给你买霸王 | 真棒 |\n| 骷髅刀皇 | 2020-11-26 | ￥1 | 支付宝 | 外行人第一个再GitHub跑成功的源码 |  |\n|  | 2020-11-26 | ￥1 | 微信 | 项目有意思 |  |\n| ohh | 2020-11-27 | ￥1 | 微信 | 集资买霸王 duang😏 | 😏 |\n| 夏风 | 2020-11-28 | ￥1 | 支付宝 | 赞赏一下~ |  |\n| 征服神的眼睛 | 2020-11-27 | ￥1 | 微信 | 头发+1 |  |\n| 长空X | 2020-11-30 | ￥1 | 支付宝 | 加油！我是hjkl950217 | 贡献的代码很棒，欢迎加入 |\n| 旧城空梦 | 2020-11-30 | ￥5 | 微信 | 拉我进下微信群，谢谢，我微信*** | 已拉 |\n| Mr.华 | 2020-12-01 | ￥1 | 微信 | 白嫖党 今天给你投币来了 |  |\n| 暮雨 | 2020-12-01 | ￥1 | 微信 | 不多说，好用412还没有解决 | 大于等于1.0.14版本解决啦 |\n| 闪电 | 2020-12-01 | ￥1 | 微信 | 欧拉拉 |  |\n| 山水之间 | 2020-12-01 | ￥1 | 微信 | 加油💪 | 💪 |\n| 八八九九 | 2020-12-02 | ￥1 | 微信 | cool |  |\n| 。 | 2020-12-02 | ￥1 | 微信 | 支持 |  |\n| Carnina | 2020-12-06 | ￥5 | 微信 | 谢谢大大的bili工具 |  |\n| 大疼 | 2020-12-07 | ￥1 | 支付宝 | 教程很细致，谢谢 |  |\n|  | 2020-12-07 | ￥1 | 微信 | 谢谢大佬B站的项目 |  |\n| Nirvana | 2020-12-08 | ￥1 | 微信 | 求进群我的微信*** | 已拉 |\n|  | 2020-12-17 | ￥1 | 微信 | 微信昵称时空白的 | 是的 |\n| aiyΑ | 2020-12-23 | ￥1 | 微信 |  |  |\n| 就这样被作业征服 | 2021-01-02 | ￥1 | 微信 | 太棒了，十分感谢 |  |\n| けっこ | 2021-01-03 | ￥1 | 微信 | bilibilitools加油! | 加油~ |\n| Ruo | 2021-01-05 | ￥1 | 微信 | 加油加油 | 加油~ |\n|  | 2021-01-06 | ￥1 | 微信 |  |  |\n| 199863nothing | 2021-01-06 | ￥1 | 支付宝 | 等俺五级给你买霸王洗发水 | 棒 |\n| 小伊 | 2021-01-06 | ￥1 | 支付宝 | 赞赏一下~ |  |\n| 多喝热水吧你 | 2021-01-07 | ￥10 | 微信 | 感谢作者，支持一下，太辛苦了 | 感谢 |\n| YNight-FZQ | 2021-01-08 | ￥5 | 微信 | 2021，要加油哦！感谢作者分享 | 一起加油~ |\n| 外比巴卜 | 2021-01-09 | ￥3 | 微信 | 加油^o^~，做的很棒 | 支持开源的你们更棒~ |\n| 199863nothing | 2021-01-09 | ￥1 | 微信 | 求拉进群(ง •_•)ง | 你没有留微信号啊大兄弟 |\n| 黑影 | 2021-01-10 | ￥5 | 微信 | Mreblack7感谢7楼大大可以加微信群嘛qwq | 你没有留微信号啊大兄弟 |\n| 199863nothing | 2021-01-11 | ￥1 | 微信 | 微信号：\\*\\*\\*，大佬捞一捞我 | 已拉~ |\n| . | 2021-01-12 | ￥5 | 微信 | 进群进群，冲冲冲 | 你没有留微信号啊大兄弟 |\n\n\n个人维护开源不易\n\n如果你觉得我写的东西对你确实有帮助\n\n或者，你就是单纯的想集资给我买瓶霸王增发液\n\n那么下面的赞赏码可以扫一扫啦\n\n（赞赏时记得留下【昵称】和【留言】，上面这么多留言要想要进群或者加好友的，一定一定要记得留微信号哈，微信赞赏页面是看不到微信号的~）\n\n* 微信扫码自动赞赏1元\n\n![微信赞赏码](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)\n\n* 支付宝扫码自动赞赏1元\n\n![支付宝赞赏吗](https://img2018.cnblogs.com/blog/1327955/201907/1327955-20190722174147547-1575068076.jpg)\n"
  },
  {
    "path": "docs/questions.md",
    "content": "# 常见问题\n\n**[目录]**\n<!-- TOC depthFrom:2 -->\n\n- [1. 运行出现异常怎么办？](#1-运行出现异常怎么办)\n- [2. 如何提交issue（如何提交Bug或建议）](#2-如何提交issue如何提交bug或建议)\n- [3. Actions定时任务没有每天自动运行](#3-actions定时任务没有每天自动运行)\n- [4. Actions修改定时任务的执行时间](#4-actions修改定时任务的执行时间)\n    - [4.1. 方法一：修改yaml文件中的cron表达式](#41-方法一修改yaml文件中的cron表达式)\n    - [4.2. 方法二：添加 GitHub Environments 并设置延时](#42-方法二添加-github-environments-并设置延时)\n- [5. 我 Fork 之后怎么同步原作者的更新内容？](#5-我-fork-之后怎么同步原作者的更新内容)\n    - [5.1. 方法一：删掉自己的仓库再重新Fork](#51-方法一删掉自己的仓库再重新fork)\n    - [5.2. 方法二：使用提供的 Repo Sync 工作流脚本同步](#52-方法二使用提供的-repo-sync-工作流脚本同步)\n    - [5.3. 方法三：手动PR同步](#53-方法三手动pr同步)\n    - [5.4. 方法四：使用插件 Pull App 同步](#54-方法四使用插件-pull-app-同步)\n        - [5.4.1. Pull App 方式一： 源作者内容直接覆盖自己内容](#541-pull-app-方式一-源作者内容直接覆盖自己内容)\n        - [5.4.2. Pull App 方式二： 保留自己内容](#542-pull-app-方式二-保留自己内容)\n- [6. 本地或服务器如何安装.net环境](#6-本地或服务器如何安装net环境)\n- [7. 如何关停Actions运行](#7-如何关停actions运行)\n    - [7.1. 方法一：使用配置关停每日任务](#71-方法一使用配置关停每日任务)\n    - [7.2. 方法二：关停Actions](#72-方法二关停actions)\n\n<!-- /TOC -->\n\n## 1. 运行出现异常怎么办？\n第一步：根据异常信息，请先仔细阅读文档（特别是 [常见问题文档](https://github.com/RayWangQvQ/BiliBiliTool.Docs/blob/main/questions.md) 和 [配置说明文档](https://github.com/RayWangQvQ/BiliBiliTool.Docs/blob/main/configuration.md) ），查找相关信息\n\n第二步：如果文档没有找到，请到 [issues](https://github.com/RayWangQvQ/BiliBiliTool/issues) 下面查找相关问题，看是否有人其他人也遇到类似问题，并确认issue下是否已经有解决方案\n\n第三步：如果仍没有解决，请将日志输出等级配置为Debug，该等级会输出详细的日志信息，修改后请再次运行，并查看详细的日志信息。如何配置请详见 [配置说明文档](https://github.com/RayWangQvQ/BiliBiliTool.Docs/blob/main/configuration.md)\n\n第四步：拿到详细日志后，如果自己无法根据日志信息确定问题，请将日志信息贴到讨论群里，群里会有大佬及时帮忙解答\n\n第五步：如果根据详细日志信息可以确认是 Bug（缺陷），可以到 [issues](https://github.com/RayWangQvQ/BiliBiliTool/issues) 下新建一条 issue 。如何新建issue请见下面的常见问题中的**如何提交issue**，如果是不符合要求的issue，会被关闭，严重的会被删除。\n\n## 2. 如何提交issue（如何提交Bug或建议）\nissues 被 GitHub 译为**议题**，用来为开源项目反馈 Bug、提出建议、讨论设计与需求等。\n\n首先先提前感谢所有提交议题的朋友们，你们的反馈和建议会让开源程序优化的越来越好。\n\n但为了使 issues 下面的议题便于维护，便于其他人搜索查找历史议题，避免淹没在一堆无用或重复的 issues 里，请大家自觉遵守下面的提交规范：\n\nⅠ. 提交前请先确认自己的议题是否是新的议题（是否在文档中已有说明、是否已经有其他人提过类似的议题），重复议题会被标记为重复并关闭，严重的会被删除\n\nⅡ. issue标题请填写完整，语义需清晰，以便在不点击进入详情时，仅根据标题就可以定位到该 issue 所反应的问题\n\nⅢ. 如果是提交 bug ，请描述清楚问题，**标明版本号、环境，并贴上详细日志信息（Debug等级的日志信息）**。如果获取Debug等级的日志信息请参见配置说明文档，如果没有日志信息，或日志信息不是Debug等级的日志信息，或在没有日志的情况下描述也不清晰，导致无法复现或无法定位问题，该 issue 会被标记为不清晰的议题，且会被忽略或关闭，严重的会被删除。\n\n## 3. Actions定时任务没有每天自动运行\nFork的仓库，actions默认是关闭的，需要对仓库进行1次操作才会触发webhook。\n\n可以通过在页面上点击创建wiki来触发，也可以通过任意一次提交推送代码来触发。\n\n## 4. Actions修改定时任务的执行时间\n每日任务执行的时间，由`.github/workflows/bilibili-daily-task.yml` 中的cron表达式指定，默认为每日的0点整:\n\n```yml\n  schedule:\n    - cron: '0 16 * * *' \n    # cron表达式，时区是UTC时间，比我们早8小时，如上所表示的是每天0点0分（24点整）\n```\n\n若要修改为自己指定的时间执行，有如下两种方式：\n\n### 4.1. 方法一：修改yaml文件中的cron表达式\n我们可以直接修改上述该文件中的cron表达式，然后提交。\n\n个人不建议这么做，因为以后更新要注意冲突与覆盖问题，建议使用下面的方法二。\n\n### 4.2. 方法二：添加 GitHub Environments 并设置延时\nv1.1.3及之后版本，支持通过添加GitHub Environments来设置延时运行，即在每日0点整触发 Actions 后，会再多执行一个延时操作，延时时长可由我们自己设置。\n\n比如想设置为每天23点执行，只需要将这个延时时常设置为1380分钟（23个小时）即可。方法如下：\n\n* Ⅰ.找到 Production Environments\n\n运行完 bilibili-daily-task.yml 之后，在 `Settings` ——> `Environments` 中会自动多出一个名为 `Production` 的环境，如下图所示：\n\n![环境列表](imgs/github-env-list.png)\n\n如果没有，也可以自己手动点击添加。\n\n* Ⅱ.设置延时时长\n\n勾选 Wait timer，并填写延时时长，单位为分钟，如下图所示：\n\n![环境列表](imgs/github-env-wait-timer.png)\n\n下面给出一些常用的分钟数换算供参考：\n\n   | 时间 | 从0点开始计算的分钟数               |\n   | -------------- | --------------------- |\n   | 6点整  | 360 |\n   | 8点整  | 480 |\n   | 9点整  | 540 |\n   | 12点整  | 720 |\n   | 14点整  | 840 |\n   | 18点整  | 1080 |\n   | 22点整  | 1320 |\n   | 23点整  | 1380 |\n\n注意，Actions 目前本身是有20分钟左右的延时的，是 GitHub 暂未解决的缺陷，属于正常现象。\n\n设置成功后，再次运行 Actions 会发现触发后会自动进入倒计时状态，等倒计时结束后才会真正运行之后的内容，如下图所示：\n\n![环境列表](imgs/github-env-count-down.png)\n\n## 5. 我 Fork 之后怎么同步原作者的更新内容？\nFork 被 GitHub 译为复刻，相当于拷贝了一份源作者的代码到自己的 Repository （仓库）里，Fork 后，源作者更新自己的代码内容（比如发新的版本），一般情况下 Fork 的项目并不会自动更新源作者的修改。\n\nBiliBiliTool内置了自动同步的 actions（即下面的方法二），默认情况下，Fork的仓库会在每周一会自动拉取更新源仓库内容，如想要更新请参考方法二。\n\n这里共提供如下4种方法同步更新的方法：\n\n### 5.1. 方法一：删掉自己的仓库再重新Fork\n这是最最最保守的方法，删掉后重新Fork会导致之前配置过的GitHub Secrets和提交的代码更改全部丢掉，只能重新部署。\n所以，请把该方法放到保底的位置，即如果你已经尝试了下面所有方法都还不能成功，再保底考虑使用该方法。\n\n### 5.2. 方法二：使用提供的 Repo Sync 工作流脚本同步\n> BiliBiliTool提供了一个用于自动同步上游仓库的脚本 [repo-sync.yml](https://github.com/RayWangQvQ/BiliBiliTool/blob/main/.github/workflows/repo-sync.yml)，执行后，会拉取源仓库最新内容直接覆盖掉自己的代码修改内容。该脚本默认开启，且每周一自动执行一次，如要关闭，可以将yml文件里的schedule使用#号注释掉。\n\n脚本内部需要一个Token参数完成授权，我们要做的共两步：1.获取自己的 Token 并添加到 Secrets 中，2.运行脚本。\n\n详细步骤如下：\n\nⅠ. [>>点击生成 Token](https://github.com/settings/tokens/new?description=repo-sync&scopes=repo,workflow) ，将生成的 `Token` 复制下来。\n\nToken 只显示一次，没复制只能重新生成。更多关于加密机密的说明可以查看 Github 官方文档：[加密机密](https://docs.github.com/cn/free-pro-team@latest/actions/reference/encrypted-secrets)。\n\n![Generate a token 01](https://cdn.jsdelivr.net/gh/Ryanjiena/BiliBiliTool.Docs@main/imgs/generate_a_token_01.png)\n\n![Generate a token 02](https://cdn.jsdelivr.net/gh/Ryanjiena/BiliBiliTool.Docs@main/imgs/generate_a_token_02.png)\n\nⅡ. 将上一步生成的 `Token `添加到 `Github Secrets` 中。\n\n   | GitHub Secrets | CONTENT               |\n   | -------------- | --------------------- |\n   | Name           | `PAT`                 |\n   | Value          | 上一步生成的 `Token ` |\n\n![New repository secret 01](https://cdn.jsdelivr.net/gh/Ryanjiena/BiliBiliTool.Docs@main/imgs/new_repository_secret_01.png)\n\n![New repository secret 02](https://cdn.jsdelivr.net/gh/Ryanjiena/BiliBiliTool.Docs@main/imgs/new_repository_secret_02.png)\n\nⅢ. 手动触发 `workflow` 工作流进行代码同步。\n\n![Run sync workflow](https://cdn.jsdelivr.net/gh/Ryanjiena/BiliBiliTool.Docs@main/imgs/run_sync_workflows.png)\n\n_该脚本是在v1.0.12添加的，如果你的版本低于该版本，没有该yaml文件，也可以直接在自己的 Fork 的仓库下面新建一个，然后将我的文件内容拷贝过去，提交文件，剩下的再继续按照上面流程走就可以了。_\n\n### 5.3. 方法三：手动PR同步\n由于大量不懂PR的人乱操作，导致每次更新版本，源仓库都收到大量辣鸡无效的PR请求，现删除了方法三。\n\n### 5.4. 方法四：使用插件 Pull App 同步\n需要安装 [![](https://prod.download/pull-18h-svg) Pull app](https://github.com/apps/pull) 插件。\n\n安装过程中会让你选择要选择那一种方式;\n\n`All repositories`表示同步已经 frok 的仓库以及未来 fork 的仓库；\n\n`Only select repositories`表示仅选择要自己需要同步的仓库，其他 fork 的仓库不会被同步。\n\n根据自己需求选择，实在不知道怎么选择，就选 `All repositories`。\n\n点击 `install`，完成安装。\n\n![Install Pull App](https://cdn.jsdelivr.net/gh/Ryanjiena/BiliBiliTool.Docs@main/imgs/install_pull_app.png)\n\nPull App 可以指定是否保留自己已经修改的内容，分为下面两种方式，如果你不知道他们的区别，就请选择方式一；如果你知道他们的区别，并且懂得如何解决 git 冲突，可根据需求自由选择任一方式：\n\n#### 5.4.1. Pull App 方式一： 源作者内容直接覆盖自己内容\n> 该方式会将源作者的内容直接强制覆盖到自己的仓库中，也就是不会保留自己已经修改过的内容。\n步骤如下：\n\nⅠ. 确认已安装 [![](https://prod.download/pull-18h-svg) Pull app](https://github.com/apps/pull) 插件。\n\nⅡ. 编辑 [pull.yml](https://github.com/RayWangQvQ/BiliBiliTool/blob/main/.github/pull.yml) 文件，将第 5 行内容修改为 `mergeMethod: hardreset`，然后保存提交。\n\n（默认就是hardreset，如果未修改过，可以不用再次提交）\n\n完成后，上游代码更新后 pull 插件会自动发起 PR 更新**覆盖**自己仓库的代码！\n\n当然也可以立即手动触发同步：`https://pull.git.ci/process/${owner}/${repo}`\n\n#### 5.4.2. Pull App 方式二： 保留自己内容\n\n> 该方式会在上游代码更新后，判断上游更新内容和自己分支代码是否存在冲突，如果有冲突则需要自己手动合并解决（也就是不会直接强制直接覆盖）。如果上游代码更新涉及 workflow 里的文件内容改动，这时也需要自己手动合并解决。\n\n步骤如下：\n\nⅠ. 确认已安装 [![](https://prod.download/pull-18h-svg) Pull app](https://github.com/apps/pull) 插件。\n\nⅡ. 编辑 [pull.yml](https://github.com/RayWangQvQ/BiliBiliTool/blob/main/.github/pull.yml) 文件，将第 5 行内容修改为 `mergeMethod: merge`，然后保存提交。\n\n完成后，上游代码更新后 pull 插件就会自动发起 PR 更新自己分支代码！只是如果存在冲突，需要自己手动去合并解决冲突。\n\n当然也可以立即手动触发同步：`https://pull.git.ci/process/${owner}/${repo}`\n\n## 6. 本地或服务器如何安装.net环境\n\n请见[官方文档](https://learn.microsoft.com/zh-cn/dotnet/core/tools/dotnet-install-script)\n\n## 7. 如何关停Actions运行\n推荐做法有两种：一是使用配置关停应用的每日任务，二是关停Actions。\n\n当然，直接删库也是可以的，但是不推荐，除非是已确认以后都不会再使用的情况。因为删库会让已有配置都丢失，且使自动更新版本的Actions失效。\n\n### 7.1. 方法一：使用配置关停每日任务\n\n详情见 [配置说明文档](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)。\n\n该方法是在应用层面关闭每日任务，即Actions还是会每天运行，只是进入程序后，应用不会去执行每日任务，即不会调用任何接口。如果配置了推送，每天仍能收到推送消息。\n\n### 7.2. 方法二：关停Actions\n \n点击Actions进入Workflows列表，点击名称为`bilibili-daily-task`的Workflow，在搜索框右侧有一个三个点的设置按钮，点击按钮后，在弹出的下拉列表里选中`Disable workflow`项即可，如下图所示：\n![关闭某个Actions](imgs/github-actions-close.png)\n\n该方法是直接关闭了Actions，即不会触发每天定时的Actions。\n"
  },
  {
    "path": "docs/runInLocal.md",
    "content": "# 下载程序包到本地或服务器运行\n\n<!-- TOC depthFrom:2 -->\n\n- [1. 任意系统，但已安装`.NET 8.0`](#1-任意系统但已安装net-80)\n- [2. Win](#2-win)\n- [3. Linux:](#3-linux)\n- [4. macOS](#4-macos)\n- [5. 配置](#5-配置)\n\n<!-- /TOC -->\n\n如果是 DotNet 开发者，直接 Clone 源码，然后 VS 打开解决方案，即可调式和运行。\n\n跑什么任务，可以在`Ray.BiliBiliTool.Console`项目下的`appsettings.json`文件里的`RunTasks`指定。\n\n对于不是开发者的朋友，可以通过下载 [BiliBiliTool/release](https://github.com/RayWangQvQ/BiliBiliToolPro/releases) 到本地或任意服务器运行。\n\n## 1. 任意系统，但已安装`.NET 8.0`\n\n任何操作系统，不管是Win还是Linux还是mac，只要已安装了`.NET 8.0` 环境，均可通过下载`net-dependent.zip`运行。\n\n下载解压后，进入应用目录，执行`dotnet ./Ray.BiliBiliTool.Console.dll --runTasks=Login`\n\n会出现二维码，扫码登录后即可运行各个任务。\n\n![login](imgs/dotnet-login.png)\n\n![运行图示](imgs/run-exe.png)\n\nP.S.这里的运行环境指的是 `.NET Runtime 8.0.0` ，安装方法可详见 [常见问题](questions.md) 中的 **本地或服务器如何安装.net环境**\n\n## 2. Win\n\n请下载 `win-x86-x64.zip`，此文件已自包含（self-contained）运行环境。\n\n解压后，在应用目录打开cmd或powershell，执行`.\\Ray.BiliBiliTool.Console.exe --runTasks=Login`，扫码登录。\n也可以直接双击`Ray.BiliBiliTool.Console.exe`来运行，建议使用windows自带的定时任务来执行它\n\n## 3. Linux:\n\n```\nwget https://github.com/RayWangQvQ/BiliBiliToolPro/releases/download/0.3.1/bilibili-tool-pro-v0.3.1-linux-x64.zip\nunzip bilibili-tool-pro-v0.3.1-linux-x64.zip\ncd ./linux-x64/\n./Ray.BiliBiliTool.Console --runTasks=Login\n```\n\n## 4. macOS\n请下载 `osx-x64.zip`，解压后在应用目录运行`./Ray.BiliBiliTool.Console --runTasks=Login`\n\n## 5. 配置\n\n最简单的方式是直接修改应用目录下的`appsettings.json`，详细方法可参考下面的**配置说明**章节。\n"
  },
  {
    "path": "gitHubActions/README.md",
    "content": "# GitHub Actions 部署\n\n<!-- TOC depthFrom:2 -->\n\n- [介绍](#介绍)\n- [步骤](#步骤)\n    - [复刻项目](#复刻项目)\n    - [添加 Secrets 配置](#添加-secrets-配置)\n    - [测试运行 Actions](#测试运行-actions)\n- [其他](#其他)\n\n<!-- /TOC -->\n\n## 介绍\nGA 是微软（巨硬）收购 G 站之后新增的内置 CI/CD 方案，其核心就是一个可以运行脚本的小型服务器。\n\n有了它，我们就可以实现每天线上自动运行我们的应用程序，通过配置还可以实现版本的自动同步更新。\n\n## 步骤\n### 复刻项目\n首先点击本页面右上角的 fork 按钮，复刻本项目到自己的仓库\n\n### 添加 Secrets 配置\n进入自己 fork 的仓库，点击 Settings-> Secrets-> New Secrets， 添加 1 个 Secrets，其名称为`COOKIESTR`，值为刚才我们保存的 `cookie 字符串`。它们将作为配置项，在应用启动时传入程序。\n\n![Secrets图示](../docs/imgs/git-secrets.png)\n\n![添加CookieStr图示](../docs/imgs/git-secrets-add-cookie.png)\n\n\n### 测试运行 Actions\n刚 Fork 完，所有 Actions 都是默认关闭的，都配置好后，需要手动点击 Enable 开启 Actions。开启后请手动执行一次工作流，验证是否可以正常工作，操作步骤如下图所示：\n\n![Actions图示](../docs/imgs/run-workflow.png)\n\n运行结束后，请查看运行日志：\n\n![Actions日志图示](../docs/imgs/github-actions-log-1.png)\n![Actions日志图示](../docs/imgs/github-actions-log-2.png)\n\n\n## 其他\nActions 的执行策略默认是每天 0 点整触发运行，如要设置为指定的运行时间，请详见下面**常见问题**章节中的《**Actions 如何修改定时任务的执行时间？**》\n\n**建议每个人都设置下每日执行时间！不要使用默认时间！最好也不要设定在整点，错开峰值，避免 G 站的同一个IP在相同时间去请求 B 站接口，导致 IP 被禁！**\n\n**应用运行后，会进行0到30分钟的随机睡眠，是为了使每天定时运行时间在范围内波动。刚开始如果需要频繁调试，建议使用empty-task.yml来调试，或者参考下面的个性化自定义配置章节，将睡眠配置为1分钟，避免每次测试都需要等待半小时**\n"
  },
  {
    "path": "gitHubActions/bak/bilibili-daily-task.yml",
    "content": "# 每日任务\n\nname: bilibili-daily-task\n\n\non:\n  workflow_dispatch: # 手动触发\n  schedule: # 计划任务触发\n    - cron: '0 16 * * *' \n    # cron表达式，时区是UTC时间，比我们早8小时，如上所表示的是每天0点0分（16+8=24点整）\n    # 建议每个人通过设置名称为 Production 的 GitHub Environments 来设定为自己的目标运行时间（详细设置方法见文档说明）\n\nenv:\n  ASPNETCORE_ENVIRONMENT: ${{secrets.ENV}} # 运行环境\n  Ray_BiliBiliCookies__1: ${{secrets.COOKIESTR}}\n  Ray_BiliBiliCookies__2: ${{secrets.COOKIESTR2}}\n  Ray_BiliBiliCookies__3: ${{secrets.COOKIESTR3}}\n  # 推送：\n  Ray_Serilog__WriteTo__3__Args__botToken: ${{secrets.PUSHTGTOKEN}} # Telegram\n  Ray_Serilog__WriteTo__3__Args__chatId: ${{secrets.PUSHTGCHATID}}\n  Ray_Serilog__WriteTo__3__Args__restrictedToMinimumLevel: ${{secrets.PUSHTGLEVEL}}\n  Ray_Serilog__WriteTo__4__Args__webHookUrl: ${{secrets.PUSHWEIXINURL}} # 企业微信\n  Ray_Serilog__WriteTo__4__Args__restrictedToMinimumLevel: ${{secrets.PUSHWEIXINLEVEL}}\n  Ray_Serilog__WriteTo__5__Args__webHookUrl: ${{secrets.PUSHDINGURL}} # 钉钉\n  Ray_Serilog__WriteTo__5__Args__restrictedToMinimumLevel: ${{secrets.PUSHDINGLEVEL}}\n  Ray_Serilog__WriteTo__6__Args__scKey: ${{secrets.PUSHSCKEY}} # Server酱\n  Ray_Serilog__WriteTo__6__Args__turboScKey: ${{secrets.PUSHSERVERTSCKEY}}\n  Ray_Serilog__WriteTo__6__Args__restrictedToMinimumLevel: ${{secrets.PUSHSERVERLEVEL}}\n  Ray_Serilog__WriteTo__7__Args__sKey: ${{secrets.PUSHCOOLSKEY}} # 酷推\n  Ray_Serilog__WriteTo__7__Args__restrictedToMinimumLevel: ${{secrets.PUSHCOOLLEVEL}}\n  Ray_Serilog__WriteTo__8__Args__api: ${{secrets.PUSHOTHERAPI}} # 自定义api\n  Ray_Serilog__WriteTo__8__Args__placeholder: ${{secrets.PUSHOTHERPLACEHOLDER}}\n  Ray_Serilog__WriteTo__8__Args__bodyJsonTemplate: ${{secrets.PUSHOTHERBODYJSONTEMPLATE}}\n  Ray_Serilog__WriteTo__8__Args__restrictedToMinimumLevel: ${{secrets.PUSHOTHERLEVEL}}\n  Ray_Serilog__WriteTo__9__Args__token: ${{secrets.PUSHPLUSTOKEN}} # PushPlus\n  Ray_Serilog__WriteTo__9__Args__topic: ${{secrets.PUSHPLUSTOPIC}}\n  Ray_Serilog__WriteTo__9__Args__channel: ${{secrets.PUSHPLUSCHANNEL}}\n  Ray_Serilog__WriteTo__9__Args__webhook: ${{secrets.PUSHPLUSWEBHOOK}}\n  Ray_Serilog__WriteTo__9__Args__restrictedToMinimumLevel: ${{secrets.PUSHPLUSLEVEL}}\n  # 安全相关：\n  Ray_Security__IsSkipDailyTask: ${{secrets.ISSKIPDAILYTASK}}\n  Ray_Security__IntervalSecondsBetweenRequestApi: ${{secrets.INTERVALSECONDSBETWEENREQUESTAPI}}\n  Ray_Security__IntervalMethodTypes: ${{secrets.INTERVALMETHODTYPES}}\n  Ray_Security__UserAgent: ${{secrets.USERAGENT}}\n  Ray_Security__WebProxy: ${{secrets.WEBPROXY}}\n  Ray_Security__RandomSleepMaxMin: ${{secrets.RANDOMSLEEPMAXMIN}}\n  # 每日任务：\n  Ray_DailyTaskConfig__NumberOfCoins: ${{secrets.NUMBEROFCOINS}}\n  Ray_DailyTaskConfig__SaveCoinsWhenLv6: ${{secrets.SAVECOINSWHENLV6}}\n  Ray_DailyTaskConfig__SelectLike: ${{secrets.SELECTLIKE}}\n  Ray_DailyTaskConfig__SupportUpIds: ${{secrets.SUPPORTUPIDS}}\n  Ray_DailyTaskConfig__DayOfAutoCharge: ${{secrets.DAYOFAUTOCHARGE}}\n  Ray_DailyTaskConfig__AutoChargeUpId: ${{secrets.AUTOCHARGEUPID}}\n  Ray_DailyTaskConfig__ChargeComment: ${{secrets.CHARGECOMMENT}}\n  Ray_DailyTaskConfig__DayOfReceiveVipPrivilege: ${{secrets.DAYOFRECEIVEVIPPRIVILEGE}}\n  Ray_DailyTaskConfig__DayOfExchangeSilver2Coin: ${{secrets.DAYOFEXCHANGESILVER2COIN}}\n  Ray_DailyTaskConfig__DevicePlatform: ${{secrets.DEVICEPLATFORM}}\n  Ray_Serilog__WriteTo__0__Args__restrictedToMinimumLevel: ${{secrets.CONSOLELOGLEVEL}}\n  Ray_Serilog__WriteTo__0__Args__outputTemplate: ${{secrets.CONSOLELOGTEMPLATE}}         \n\njobs:\n\n  pre-check:\n    runs-on: ubuntu-latest\n    outputs:\n      result: ${{ steps.check.outputs.result }} # 不能直接传递secrets的值，否则会被skip，需要转一下\n    steps:\n    - id: check\n      if: env.IsOpenDailyTask=='true'\n      run: |\n        echo \"::set-output name=result::开启\"\n\n  run-daily-task:\n\n    runs-on: ubuntu-latest\n    environment: Production\n    needs: pre-check\n    if: needs.pre-check.outputs.result=='开启'\n\n    steps:\n\n    # 设置服务器时区为东八区 \n    - name: Set time zone\n      run: sudo timedatectl set-timezone 'Asia/Shanghai'\n\n    # 检出\n    - name: Checkout\n      uses: actions/checkout@v2\n      \n    # .Net 环境\n    - name: Setup .NET\n      uses: actions/setup-dotnet@v1\n      with:\n        dotnet-version: 6.0.x\n \n    # 测试运行\n    - name: Test APP\n      run: |\n        cd ./src/Ray.BiliBiliTool.Console\n        dotnet run --runTasks=Daily\n"
  },
  {
    "path": "gitHubActions/bak/empty-task.yml",
    "content": "name: empty-task\n\non:\n  workflow_dispatch: # 手动触发\n    inputs:\n      tasks:\n        description: '任务Code'\n        required: true\n\nenv:\n  ASPNETCORE_ENVIRONMENT: ${{secrets.ENV}} # 运行环境\n  Ray_CloseConsoleWhenEnd: 1\n  Ray_BiliBiliCookies__1: ${{secrets.COOKIESTR}}\n  Ray_BiliBiliCookies__2: ${{secrets.COOKIESTR2}}\n  Ray_BiliBiliCookies__3: ${{secrets.COOKIESTR3}}\n  # 推送：\n  Ray_Serilog__WriteTo__3__Args__botToken: ${{secrets.PUSHTGTOKEN}} # Telegram\n  Ray_Serilog__WriteTo__3__Args__chatId: ${{secrets.PUSHTGCHATID}}\n  Ray_Serilog__WriteTo__3__Args__restrictedToMinimumLevel: ${{secrets.PUSHTGLEVEL}}\n  Ray_Serilog__WriteTo__4__Args__webHookUrl: ${{secrets.PUSHWEIXINURL}} # 企业微信\n  Ray_Serilog__WriteTo__4__Args__restrictedToMinimumLevel: ${{secrets.PUSHWEIXINLEVEL}}\n  Ray_Serilog__WriteTo__5__Args__webHookUrl: ${{secrets.PUSHDINGURL}} # 钉钉\n  Ray_Serilog__WriteTo__5__Args__restrictedToMinimumLevel: ${{secrets.PUSHDINGLEVEL}}\n  Ray_Serilog__WriteTo__6__Args__scKey: ${{secrets.PUSHSCKEY}} # Server酱\n  Ray_Serilog__WriteTo__6__Args__turboScKey: ${{secrets.PUSHSERVERTSCKEY}}\n  Ray_Serilog__WriteTo__6__Args__restrictedToMinimumLevel: ${{secrets.PUSHSERVERLEVEL}}\n  Ray_Serilog__WriteTo__7__Args__sKey: ${{secrets.PUSHCOOLSKEY}} # 酷推\n  Ray_Serilog__WriteTo__7__Args__restrictedToMinimumLevel: ${{secrets.PUSHCOOLLEVEL}}\n  Ray_Serilog__WriteTo__8__Args__api: ${{secrets.PUSHOTHERAPI}} # 自定义api\n  Ray_Serilog__WriteTo__8__Args__placeholder: ${{secrets.PUSHOTHERPLACEHOLDER}}\n  Ray_Serilog__WriteTo__8__Args__bodyJsonTemplate: ${{secrets.PUSHOTHERBODYJSONTEMPLATE}}\n  Ray_Serilog__WriteTo__8__Args__restrictedToMinimumLevel: ${{secrets.PUSHOTHERLEVEL}}\n  Ray_Serilog__WriteTo__9__Args__token: ${{secrets.PUSHPLUSTOKEN}} # PushPlus\n  Ray_Serilog__WriteTo__9__Args__topic: ${{secrets.PUSHPLUSTOPIC}}\n  Ray_Serilog__WriteTo__9__Args__channel: ${{secrets.PUSHPLUSCHANNEL}}\n  Ray_Serilog__WriteTo__9__Args__webhook: ${{secrets.PUSHPLUSWEBHOOK}}\n  Ray_Serilog__WriteTo__9__Args__restrictedToMinimumLevel: ${{secrets.PUSHPLUSLEVEL}}\n  # 安全相关：\n  Ray_Security__IsSkipDailyTask: ${{secrets.ISSKIPDAILYTASK}}\n  Ray_Security__IntervalSecondsBetweenRequestApi: ${{secrets.INTERVALSECONDSBETWEENREQUESTAPI}}\n  Ray_Security__IntervalMethodTypes: ${{secrets.INTERVALMETHODTYPES}}\n  Ray_Security__UserAgent: ${{secrets.USERAGENT}}\n  Ray_Security__WebProxy: ${{secrets.WEBPROXY}}\n  Ray_Security__RandomSleepMaxMin: ${{secrets.RANDOMSLEEPMAXMIN}}\n  # 每日任务：\n  Ray_DailyTaskConfig__NumberOfCoins: ${{secrets.NUMBEROFCOINS}}\n  Ray_DailyTaskConfig__SaveCoinsWhenLv6: ${{secrets.SAVECOINSWHENLV6}}\n  Ray_DailyTaskConfig__SelectLike: ${{secrets.SELECTLIKE}}\n  Ray_DailyTaskConfig__SupportUpIds: ${{secrets.SUPPORTUPIDS}}\n  Ray_DailyTaskConfig__DayOfAutoCharge: ${{secrets.DAYOFAUTOCHARGE}}\n  Ray_DailyTaskConfig__AutoChargeUpId: ${{secrets.AUTOCHARGEUPID}}\n  Ray_DailyTaskConfig__ChargeComment: ${{secrets.CHARGECOMMENT}}\n  Ray_DailyTaskConfig__DayOfReceiveVipPrivilege: ${{secrets.DAYOFRECEIVEVIPPRIVILEGE}}\n  Ray_DailyTaskConfig__DayOfExchangeSilver2Coin: ${{secrets.DAYOFEXCHANGESILVER2COIN}}\n  Ray_DailyTaskConfig__DevicePlatform: ${{secrets.DEVICEPLATFORM}}\n  Ray_Serilog__WriteTo__0__Args__restrictedToMinimumLevel: ${{secrets.CONSOLELOGLEVEL}}\n  Ray_Serilog__WriteTo__0__Args__outputTemplate: ${{secrets.CONSOLELOGTEMPLATE}}\n  # 天选任务：\n  Ray_LiveLotteryTaskConfig__ExcludeAwardNames: ${{secrets.EXCLUDEAWARDNAMES}} # 天选抽奖指定排除关键字\n  Ray_LiveLotteryTaskConfig__IncludeAwardNames: ${{secrets.INCLUDEAWARDNAMES}} # 天选抽奖指定包含关键字\n  Ray_LiveLotteryTaskConfig__AutoGroupFollowings: ${{secrets.AUTOGROUPFOLLOWINGS}} # 抽奖结束后是否将关注主播自动分组\n  # 批量取关任务：\n  Ray_UnfollowBatchedTaskConfig__GroupName: ${{secrets.UNFOLLOWGROUPNAME}}\n  Ray_UnfollowBatchedTaskConfig__Count: ${{secrets.UNFOLLOWCOUNT}}\n\njobs:\n  run-task:\n\n    runs-on: ubuntu-latest\n\n    steps:\n\n    # 设置服务器时区为东八区 \n    - name: Set time zone\n      run: sudo timedatectl set-timezone 'Asia/Shanghai'\n\n    # 检出\n    - name: Checkout\n      uses: actions/checkout@v2\n      \n    # .Net 环境\n    - name: Setup .NET\n      uses: actions/setup-dotnet@v1\n      with:\n        dotnet-version: 6.0.x\n        \n    # 运行\n    - name: Run APP\n      run: |\n        cd ./src/Ray.BiliBiliTool.Console\n        dotnet run --runTasks=${{ github.event.inputs.tasks }}\n"
  },
  {
    "path": "gitHubActions/bak/live-lottery-task.yml",
    "content": "# 天选时刻抽奖任务\n\nname: live-lottery-task\n\n\non:\n  \n  workflow_dispatch: # 手动触发\n  schedule: # 计划任务触发\n    - cron: '0 16 * * *' \n    # cron表达式，时区是UTC时间，比我们早8小时，如上所表示的是每天0点0分（24点整）\n    # 建议每个人通过设置名称为 LiveLottery 的 GitHub Environments 来设定为自己的目标运行时间（详细设置方法见文档说明）\n\nenv:\n  IsOpenLiveLotteryTask: ${{secrets.ISOPENLIVELOTTERYTASK}} #是否开启该GitHub工作流\n  ASPNETCORE_ENVIRONMENT: ${{secrets.ENV}} # 运行环境\n  Ray_CloseConsoleWhenEnd: 1\n  Ray_BiliBiliCookies__1: ${{secrets.COOKIESTR}}\n  Ray_BiliBiliCookies__2: ${{secrets.COOKIESTR2}}\n  Ray_BiliBiliCookies__3: ${{secrets.COOKIESTR3}}\n  # 天选任务：\n  Ray_LiveLotteryTaskConfig__ExcludeAwardNames: ${{secrets.EXCLUDEAWARDNAMES}} # 天选抽奖指定排除关键字\n  Ray_LiveLotteryTaskConfig__IncludeAwardNames: ${{secrets.INCLUDEAWARDNAMES}} # 天选抽奖指定包含关键字\n  Ray_LiveLotteryTaskConfig__AutoGroupFollowings: ${{secrets.AUTOGROUPFOLLOWINGS}} # 抽奖结束后是否将关注主播自动分组\n  Ray_LiveLotteryTaskConfig__DenyUids: ${{secrets.LIVELOTTERYDENYUIDS}} # 天选筹抽奖主播Uid黑名单\n  # 推送：\n  Ray_Serilog__WriteTo__3__Args__botToken: ${{secrets.PUSHTGTOKEN}} # Telegram\n  Ray_Serilog__WriteTo__3__Args__chatId: ${{secrets.PUSHTGCHATID}}\n  Ray_Serilog__WriteTo__3__Args__restrictedToMinimumLevel: ${{secrets.PUSHTGLEVEL}}\n  Ray_Serilog__WriteTo__4__Args__webHookUrl: ${{secrets.PUSHWEIXINURL}} # 企业微信\n  Ray_Serilog__WriteTo__4__Args__restrictedToMinimumLevel: ${{secrets.PUSHWEIXINLEVEL}}\n  Ray_Serilog__WriteTo__5__Args__webHookUrl: ${{secrets.PUSHDINGURL}} # 钉钉\n  Ray_Serilog__WriteTo__5__Args__restrictedToMinimumLevel: ${{secrets.PUSHDINGLEVEL}}\n  Ray_Serilog__WriteTo__6__Args__scKey: ${{secrets.PUSHSCKEY}} # Server酱\n  Ray_Serilog__WriteTo__6__Args__turboScKey: ${{secrets.PUSHSERVERTSCKEY}}\n  Ray_Serilog__WriteTo__6__Args__restrictedToMinimumLevel: ${{secrets.PUSHSERVERLEVEL}}\n  Ray_Serilog__WriteTo__7__Args__sKey: ${{secrets.PUSHCOOLSKEY}} # 酷推\n  Ray_Serilog__WriteTo__7__Args__restrictedToMinimumLevel: ${{secrets.PUSHCOOLLEVEL}}\n  Ray_Serilog__WriteTo__8__Args__api: ${{secrets.PUSHOTHERAPI}} # 自定义api\n  Ray_Serilog__WriteTo__8__Args__placeholder: ${{secrets.PUSHOTHERPLACEHOLDER}}\n  Ray_Serilog__WriteTo__8__Args__bodyJsonTemplate: ${{secrets.PUSHOTHERBODYJSONTEMPLATE}}\n  Ray_Serilog__WriteTo__8__Args__restrictedToMinimumLevel: ${{secrets.PUSHOTHERLEVEL}}\n  Ray_Serilog__WriteTo__9__Args__token: ${{secrets.PUSHPLUSTOKEN}} # PushPlus\n  Ray_Serilog__WriteTo__9__Args__topic: ${{secrets.PUSHPLUSTOPIC}}\n  Ray_Serilog__WriteTo__9__Args__channel: ${{secrets.PUSHPLUSCHANNEL}}\n  Ray_Serilog__WriteTo__9__Args__webhook: ${{secrets.PUSHPLUSWEBHOOK}}\n  Ray_Serilog__WriteTo__9__Args__restrictedToMinimumLevel: ${{secrets.PUSHPLUSLEVEL}}\n  # 安全相关：\n  Ray_Security__IsSkipDailyTask: ${{secrets.ISSKIPDAILYTASK}}\n  Ray_Security__IntervalSecondsBetweenRequestApi: ${{secrets.INTERVALSECONDSBETWEENREQUESTAPI}}\n  Ray_Security__IntervalMethodTypes: ${{secrets.INTERVALMETHODTYPES}}\n  Ray_Security__UserAgent: ${{secrets.USERAGENT}}\n  Ray_Security__WebProxy: ${{secrets.WEBPROXY}}\n  Ray_Security__RandomSleepMaxMin: ${{secrets.RANDOMSLEEPMAXMIN}}\n  # Console日志：\n  Ray_Serilog__WriteTo__0__Args__restrictedToMinimumLevel: ${{secrets.CONSOLELOGLEVEL}}\n  Ray_Serilog__WriteTo__0__Args__outputTemplate: ${{secrets.CONSOLELOGTEMPLATE}} \n\njobs:\n\n  pre-check:\n    runs-on: ubuntu-latest\n    outputs:\n      result: ${{ steps.check.outputs.result }} # 不能直接传递secrets的值，否则会被skip，需要转一下\n    steps:\n    - id: check\n      if: env.IsOpenLiveLotteryTask=='true'\n      run: |\n        echo \"::set-output name=result::开启\"\n\n  run-live-lottery:\n    runs-on: ubuntu-latest\n    needs: pre-check\n    # if: env.IsOpenLiveLotteryTask=='true' # 这里job.if读取不到env或secrets，很坑...但是发现可以读到needs的outputs值\n    if: needs.pre-check.outputs.result=='开启'\n\n    environment: LiveLottery\n\n    steps:\n\n    # 输出IP、设置服务器时区为东八区 \n    - name: PreWork\n      run: |\n        sudo curl ifconfig.me\n        sudo timedatectl set-timezone 'Asia/Shanghai'\n\n    # 检出\n    - name: Checkout\n      uses: actions/checkout@v2\n      \n    # .Net 环境\n    - name: Setup .NET\n      uses: actions/setup-dotnet@v1\n      with:\n        dotnet-version: 6.0.x\n\n    # 测试运行\n    - name: Test APP\n      run: |\n        cd ./src/Ray.BiliBiliTool.Console\n        dotnet run --runTasks=LiveLottery\n"
  },
  {
    "path": "gitHubActions/bak/unfollow-batched-task.yml",
    "content": "# 批量取关\n\nname: unfollow-batched-task\n\non:\n  workflow_dispatch: # 手动触发\n    inputs:\n      group:\n        description: '分组名称'\n        required: true\n      count:\n        description: '目标取关个数（-1表示全部）'\n        required: true\n\nenv:\n  ASPNETCORE_ENVIRONMENT: ${{secrets.ENV}} # 运行环境\n  Ray_CloseConsoleWhenEnd: 1\n  Ray_BiliBiliCookies__1: ${{secrets.COOKIESTR}}\n  Ray_BiliBiliCookies__2: ${{secrets.COOKIESTR2}}\n  Ray_BiliBiliCookies__3: ${{secrets.COOKIESTR3}}\n  # 推送：\n  Ray_Serilog__WriteTo__3__Args__botToken: ${{secrets.PUSHTGTOKEN}} # Telegram\n  Ray_Serilog__WriteTo__3__Args__chatId: ${{secrets.PUSHTGCHATID}}\n  Ray_Serilog__WriteTo__3__Args__restrictedToMinimumLevel: ${{secrets.PUSHTGLEVEL}}\n  Ray_Serilog__WriteTo__4__Args__webHookUrl: ${{secrets.PUSHWEIXINURL}} # 企业微信\n  Ray_Serilog__WriteTo__4__Args__restrictedToMinimumLevel: ${{secrets.PUSHWEIXINLEVEL}}\n  Ray_Serilog__WriteTo__5__Args__webHookUrl: ${{secrets.PUSHDINGURL}} # 钉钉\n  Ray_Serilog__WriteTo__5__Args__restrictedToMinimumLevel: ${{secrets.PUSHDINGLEVEL}}\n  Ray_Serilog__WriteTo__6__Args__scKey: ${{secrets.PUSHSCKEY}} # Server酱\n  Ray_Serilog__WriteTo__6__Args__turboScKey: ${{secrets.PUSHSERVERTSCKEY}}\n  Ray_Serilog__WriteTo__6__Args__restrictedToMinimumLevel: ${{secrets.PUSHSERVERLEVEL}}\n  Ray_Serilog__WriteTo__7__Args__sKey: ${{secrets.PUSHCOOLSKEY}} # 酷推\n  Ray_Serilog__WriteTo__7__Args__restrictedToMinimumLevel: ${{secrets.PUSHCOOLLEVEL}}\n  Ray_Serilog__WriteTo__8__Args__api: ${{secrets.PUSHOTHERAPI}} # 自定义api\n  Ray_Serilog__WriteTo__8__Args__placeholder: ${{secrets.PUSHOTHERPLACEHOLDER}}\n  Ray_Serilog__WriteTo__8__Args__bodyJsonTemplate: ${{secrets.PUSHOTHERBODYJSONTEMPLATE}}\n  Ray_Serilog__WriteTo__8__Args__restrictedToMinimumLevel: ${{secrets.PUSHOTHERLEVEL}}\n  Ray_Serilog__WriteTo__9__Args__token: ${{secrets.PUSHPLUSTOKEN}} # PushPlus\n  Ray_Serilog__WriteTo__9__Args__topic: ${{secrets.PUSHPLUSTOPIC}}\n  Ray_Serilog__WriteTo__9__Args__channel: ${{secrets.PUSHPLUSCHANNEL}}\n  Ray_Serilog__WriteTo__9__Args__webhook: ${{secrets.PUSHPLUSWEBHOOK}}\n  Ray_Serilog__WriteTo__9__Args__restrictedToMinimumLevel: ${{secrets.PUSHPLUSLEVEL}}\n  # 安全相关：\n  Ray_Security__IsSkipDailyTask: ${{secrets.ISSKIPDAILYTASK}}\n  Ray_Security__IntervalSecondsBetweenRequestApi: ${{secrets.INTERVALSECONDSBETWEENREQUESTAPI}}\n  Ray_Security__IntervalMethodTypes: ${{secrets.INTERVALMETHODTYPES}}\n  Ray_Security__UserAgent: ${{secrets.USERAGENT}}\n  Ray_Security__WebProxy: ${{secrets.WEBPROXY}}\n  Ray_Security__RandomSleepMaxMin: ${{secrets.RANDOMSLEEPMAXMIN}}\n  # 批量取关任务：\n  Ray_UnfollowBatchedTaskConfig__GroupName: ${{ github.event.inputs.group }}\n  Ray_UnfollowBatchedTaskConfig__Count: ${{ github.event.inputs.count }}\n  Ray_UnfollowBatchedTaskConfig__RetainUids: ${{secrets.UNFOLLOWBATCHEDRETAINUIDS}}\n\njobs:\n  run-task:\n\n    runs-on: ubuntu-latest\n\n    steps:\n\n    # 设置服务器时区为东八区 \n    - name: Set time zone\n      run: sudo timedatectl set-timezone 'Asia/Shanghai'\n\n    # 检出\n    - name: Checkout\n      uses: actions/checkout@v2\n      \n    # .Net 环境\n    - name: Setup .NET\n      uses: actions/setup-dotnet@v1\n      with:\n        dotnet-version: 6.0.x\n\n    # 发布\n    - name: Publish\n      run: |\n        cd ./src/Ray.BiliBiliTool.Console\n        dotnet publish --configuration Release --self-contained false --output ./bin/Publish/net5-dependent\n       \n    # 测试运行\n    - name: Test APP\n      run: |\n        cd ./src/Ray.BiliBiliTool.Console\n        dotnet run --runTasks=UnfollowBatched\n"
  },
  {
    "path": "helm/README.md",
    "content": "<!--- app-name: bilibili-tool -->\n\n# BiliBili Tool\n\nBiliBiliTool 是一个自动执行任务的工具，当我们忘记做某项任务时，它会像一个贴心小助手，按照我们预先吩咐它的命令，在指定频率、时间范围内帮助我们完成计划的任务。\n\n[Overview of BiliBili Tool](https://github.com/RayWangQvQ/BiliBiliToolPro)\n\n## TL;DR\n\n### 在集群中通过chart部署\n\n```console\n$ git clone https://github.com/RayWangQvQ/BiliBiliToolPro.git\n$ cd ${local_code_repo}/helm/bilibili-tool\n[optional]$ vim values.yaml # provides your own settings like cookies\n$ helm install <my_release_name> .\n```\n\n如果没有在values.yaml中提供cookie，那么需要手动扫描日志中的二维码进行登录\n\n```console\n$kubectl logs -f <pod_name>\n```\n\n如果在values.yaml中提供了cookie，那么可以不扫描也可以扫描进行登录，上面的步骤可以暂时不执行\n\n## Introduction\n\n这个chart通过[Helm](https://helm.sh)在[Kubernetes](https://kubernetes.io)集群上拉起一个[BiliBiliToolPro](https://github.com/RayWangQvQ/BiliBiliToolPro)deployment\n\n## Prerequisites\n\n- Kubernetes\n- Helm\n\n或者\n\n- Kind\n- Helm\n\n## 安装Chart\n\n安装Chart并命名为 `my-release`:\n\n```console\n$helm repo add my-repo <my_chart_repo>\n$helm install my-release my-repo/bilibili-tool(:1.0.1)\n```\n\n上述命令需要用户在values.yaml里提供cookie等必须信息\n[Parameters](#parameters) 部分列出了所有可供自定义的值\n\n> **Tip**: `helm list` 可以列出当前已经列出的所有的release\n\n## 卸载 Chart\n\n卸载 `my-release` deployment:\n\n```console\n$helm delete my-release\n```\n\n上述命令卸载掉所有的release相关资源\n\n## Parameters\n\n| Name                      | Description                                     | Value | Required |\n| ------------------------- | ----------------------------------------------- | ----- | -------- |\n| `replicaCount`    | Deployment Relicas Count                   | `1`  | true |\n| `namespace`    | Deployment and ConfigMap deployed namespace                   | `default`  | true |\n| `configmap.name`    | ConfigMap name contains the entry files                   | `entry`  | true |\n| `image.repository` | Global Dockevr registry | `zai7lou/bilibili_tool_pro`  | true |\n| `image.tag`     | Image Tag    | `1.0.1`  | true |\n| `image.pullPolicy`     | Image Pull Policy    | `IfNotPresent`  | true |\n| `imagePullSecrets` | Image Pull Secrets | `[]` | false |\n| `nameOverride` | Deployment name in the Chart | `\"\"` | false |\n| `fullnameOverride` | Release name when set | `\"\"` | false |\n| `resources.limits`      | The resources limits for the BiliBili Tool containers                                 | `{}`            | true |\n| `resources.limits.memory`                         | The limited memory for the BiliBili Tool containers                                                                                                                                                 | `180Mi`         | true |\n| `resources.limits.cpu`                            | The limited cpu for the BiliBili Tool containers     | `100m` | true |\n| `resources.requests`      | The resources requests for the BiliBili Tool containers                                                                                                       | `{}`            | true |\n| `resources.requests.memory`                         | The requested memory for the BiliBili Tool containers                                                                                                                                                 | `180Mi`         | true |\n| `resources.requests.cpu`                            | The requested cpu for the BiliBili Tool containers     | `100m` | true |\n| `affinity`                                          | Affinity for pod assignment                                                                                                                                                                       | `{}`            | false |\n| `nodeSelector`                                      | Node labels for pod assignment                                                                                                                                                                    | `{}`            | false |\n| `tolerations`                                       | Tolerations for pod assignment                                                                                                                                                                    | `[]`            | false |\n| `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 |\n| `volumes.log.enabled` | Enable persistent log volume for BiliBili Tool or not | `\"false\"` | true |\n| `volumes.log.path` | The host path mounted into pod | `\"/tmp/Logs\"` | false |\n| `volumes.log.name` | The volume name | `\"bili-tool-vol\"` | false |\n| `volumes.login.enabled` | Enable persistent log volume contains the entries for BiliBili Tool or not | `\"false\"` | true |\n| `volumes.login.name` | The volume name | `\"entry\"` | false |\n| `podAnnotations` | The annotations for the BiliBili Tool pod | `{}` | false |\n\n可以用指定helm install命令行参数 `--set key=value[,key=value]`， 比如\n\n```console\n$ helm install my-release \\\n  --set  \\\n    relicas=1\n```\n\n也可以通过指定一个YAML格式的values文件来配置以上参数，比如\n\n```console\n$helm install my-release -f values.yaml my_chart_repo/bilibili-tool\n```\n\n> **Tip**: 你可以使用默认的 [values.yaml](bilibili-tool/values.yaml)进行配置\n\n当想更新一些变量时，可以通过指定参数或者直接修改YAML的values文件进行更新\n\n```console\n$helm upgrade my-release my_chart_repo/bilibili-tool <-f values> or <--set-file ...> \n```\n\n## Upgrade\n\n建议重新装release\n\n## [Optional]本地Cluster运行\n\n通过安装[kind](https://kind.sigs.k8s.io/docs/user/quick-start/)工具在本地运行一个Kubernetes Cluster\n\ngo 1.17+ and Docker installed\n\n```console\n$ go install sigs.k8s.io/kind@v0.17.0 && kind create cluster <--config kind_config_file>\n$ cat <kind_config_file>\n$ kind: Cluster\napiVersion: kind.x-k8s.io/v1alpha4\nnodes:\n- role: control-plane\n- role: worker \n$ EOF\n```\n\nat least one worker node otherwise you have to provides tolerations in the values.yaml to schedule on master node\n"
  },
  {
    "path": "helm/bilibili-tool/Chart.yaml",
    "content": "apiVersion: v2\nname: bilibili-tool\ndescription: A Helm chart for running bilibili tool in Kubernetes\n\n# A chart can be either an 'application' or a 'library' chart.\n#\n# Application charts are a collection of templates that can be packaged into versioned archives\n# to be deployed.\n#\n# Library charts provide useful utilities or functions for the chart developer. They're included as\n# a dependency of application charts to inject those utilities and functions into the rendering\n# pipeline. Library charts do not define any templates and therefore cannot be deployed.\ntype: application\n\n# This is the chart version. This version number should be incremented each time you make changes\n# to the chart and its templates, including the app version.\n# Versions are expected to follow Semantic Versioning (https://semver.org/)\nversion: 1.0.1\n\n# This is the version number of the application being deployed. This version number should be\n# incremented each time you make changes to the application. Versions are not expected to\n# follow Semantic Versioning. They should reflect the version the application is using.\n# It is recommended to use it with quotes.\nappVersion: \"1.0.1\"\n"
  },
  {
    "path": "helm/bilibili-tool/templates/NOTES.txt",
    "content": ""
  },
  {
    "path": "helm/bilibili-tool/templates/_helpers.tpl",
    "content": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"bilibili_tool.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCreate a default fully qualified app name.\nWe truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).\nIf release name contains chart name it will be used as a full name.\n*/}}\n{{- define \"bilibili_tool.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"bilibili_tool.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"bilibili_tool.labels\" -}}\nhelm.sh/chart: {{ include \"bilibili_tool.chart\" . }}\n{{ include \"bilibili_tool.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"bilibili_tool.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"bilibili_tool.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}\n\n{{/*\nCreate the name of the service account to use\n*/}}\n{{- define \"bilibili_tool.serviceAccountName\" -}}\n{{- if .Values.serviceAccount.create }}\n{{- default (include \"bilibili_tool.fullname\" .) .Values.serviceAccount.name }}\n{{- else }}\n{{- default \"default\" .Values.serviceAccount.name }}\n{{- end }}\n{{- end }}\n"
  },
  {
    "path": "helm/bilibili-tool/templates/configmap.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: {{ .Values.configmap.name }}\n  namespace: {{ .Values.namespace }}\ndata:\n  entry_before.sh: |\n    #!/bin/bash\n    set -e\n    echo -e \"entry_before\\n\"\n  entry_after.sh: |\n    #!/bin/bash\n    set -e\n    echo -e \"entry_after\\n\"\n  entry.sh: |\n    #!/bin/bash\n    set -e\n\n    . /app/scripts/entry_before.sh\n\n    CONSOLE_DLL=\"Ray.BiliBiliTool.Console.dll\"\n    CRON_FILE=\"/etc/cron.d/bilicron\"\n\n    # https://stackoverflow.com/questions/27771781/how-can-i-access-docker-set-environment-variables-from-a-cron-job\n    echo \"[step 1/5]导入环境变量\"\n    printenv | grep -v \"no_proxy\" >/etc/environment\n    declare -p | grep -v \"no_proxy\" >/etc/cron.env\n    echo -e \"=>完成\\n\"\n\n    echo \"[step 2/5]配置cron定时任务\"\n    echo \"SHELL=/bin/bash\" >$CRON_FILE\n    echo \"BASH_ENV=/etc/cron.env\" >>$CRON_FILE\n    if [ -z \"$Ray_DailyTaskConfig__Cron$Ray_LiveLotteryTaskConfig__Cron$Ray_UnfollowBatchedTaskConfig__Cron$Ray_VipBigPointConfig__Cron$Ray_LiveFansMedalTaskConfig__Cron\" ]; then\n        echo \"=>使用默认的定时任务配置\"\n        cat /app/scripts/crontab >>$CRON_FILE\n    else\n        echo \"=>使用用户指定的定时任务配置\"\n        if ! [ -z \"$Ray_DailyTaskConfig__Cron\" ]; then\n            echo \"$Ray_DailyTaskConfig__Cron cd /app && dotnet $CONSOLE_DLL --runTasks=Daily\" >>$CRON_FILE\n        fi\n        if ! [ -z \"$Ray_LiveLotteryTaskConfig__Cron\" ]; then\n            echo \"$Ray_LiveLotteryTaskConfig__Cron cd /app && dotnet $CONSOLE_DLL --runTasks=LiveLottery\" >>$CRON_FILE\n        fi\n        if ! [ -z \"$Ray_UnfollowBatchedTaskConfig__Cron\" ]; then\n            echo \"$Ray_UnfollowBatchedTaskConfig__Cron cd /app && dotnet $CONSOLE_DLL --runTasks=UnfollowBatched\" >>$CRON_FILE\n        fi\n        if ! [ -z \"$Ray_VipBigPointConfig__Cron\" ]; then\n            echo \"$Ray_VipBigPointConfig__Cron cd /app && dotnet $CONSOLE_DLL --runTasks=VipBigPoint\" >>$CRON_FILE\n        fi\n        if ! [ -z \"$Ray_LiveFansMedalTaskConfig__Cron\" ]; then\n            echo \"$Ray_LiveFansMedalTaskConfig__Cron  cd /app && dotnet $CONSOLE_DLL --runTasks=LiveFansMedal\" >>$CRON_FILE\n        fi\n    fi\n\n    if ! [ -z \"$Ray_Crontab\" ]; then\n        echo \"=>检测到自定义定时任务\"\n        echo \"$Ray_Crontab\" >>$CRON_FILE\n    fi\n\n    cat $CRON_FILE\n    chmod 0644 $CRON_FILE\n    crontab $CRON_FILE # 指定定时列表文件\n\n    echo -e \"=>完成\\n\"\n\n    echo \"[step 3/5]启动定时任务，开启每日定时运行\"\n    cron\n    echo -e \"=>完成\\n\"\n\n    echo \"[step 4/5]初始运行，进行Login\"\n    cd /app && dotnet Ray.BiliBiliTool.Console.dll --runTasks=Login\n    echo -e \"=>完成Login\\n\"\n\n    echo \"[step 5/5]初始运行，尝试测试Cookie\"\n    dotnet Ray.BiliBiliTool.Console.dll --runTasks=Test\n    echo -e \"=>完成\\n\"\n\n    echo -e \"[step 全部已完成]\\n\"\n\n    . /app/scripts/entry_after.sh\n\n    touch /var/log/cron.log   #todo：debian似乎并没有记录cron的日志。。。\n    tail -f /var/log/cron.log # 追踪cron日志，避免当前脚本终止导致容器终止\n"
  },
  {
    "path": "helm/bilibili-tool/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"bilibili_tool.fullname\" . }}\n  namespace: {{ .Values.namespace }}\n  labels:\n    {{- include \"bilibili_tool.labels\" . | nindent 4 }}\nspec:\n  selector:\n    matchLabels:\n      {{- include \"bilibili_tool.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      {{- with .Values.podAnnotations }}\n      annotations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      labels:\n        {{- include \"bilibili_tool.selectorLabels\" . | nindent 8 }}\n    spec:\n      {{- if or (eq .Values.volumes.log.enabled true) (eq .Values.volumes.login.enabled true) }}\n      volumes:\n      {{- if .Values.volumes.log.enabled }}\n      - name: {{ .Values.volumes.log.name }}\n        hostPath: \n          path: {{ .Values.volumes.log.path }}\n      {{- end }}\n      {{- if .Values.volumes.login.enabled }}\n      - name: {{ .Values.volumes.login.name }}\n        configMap:\n          name: {{ .Values.configmap.name }}\n          items:\n          - key: \"entry.sh\"\n            path: \"entry.sh\"\n            mode: 0755\n          - key: \"entry_before.sh\"\n            path: \"entry_before.sh\"\n            mode: 0755\n          - key: \"entry_after.sh\"\n            path: \"entry_after.sh\"\n            mode: 0755\n      {{- end }}\n      {{- end }}\n      {{- with .Values.imagePullSecrets }}\n      imagePullSecrets:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      containers:\n        - name: {{ .Chart.Name }}\n          image: \"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n          imagePullPolicy: {{ .Values.image.pullPolicy }}\n          env:\n            {{- toYaml .Values.env | nindent 12 }}\n          {{- if or (eq .Values.volumes.log.enabled true) (eq .Values.volumes.login.enabled true) }}\n          volumeMounts:\n          {{- if .Values.volumes.log.enabled }}\n          - mountPath: \"/bilibili_tool/Logs\"\n            name: {{ .Values.volumes.log.name }}\n          {{- end }}\n          {{- if .Values.volumes.login.enabled }}\n          - mountPath: \"/app/scripts\"\n            name: {{ .Values.volumes.login.name }}\n          {{- end }}\n          {{- end }}\n          resources:\n            {{- toYaml .Values.resources | nindent 12 }}\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n"
  },
  {
    "path": "helm/bilibili-tool/values.yaml",
    "content": "# Default values for bilibili_tool.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\n\nnamespace: default\nreplicaCount: 1\n\nconfigmap:\n  name: entry\n\nimage:\n  repository: zai7lou/bilibili_tool_pro\n  pullPolicy: IfNotPresent\n  # Overrides the image tag whose default is the chart appVersion.\n  tag: \"1.0.1\"\n\nimagePullSecrets: []\nnameOverride: \"\"\nfullnameOverride: \"\"\n\n# For more envs pls take a view at https://github.com/RayWangQvQ/BiliBiliToolPro/blob/main/docs/configuration.md\nenv:\n  # cookie - required\n  - name: Ray_BiliBiliCookies__1\n    # past your cookie value\n    value: \"\"\n  # DailyTrigger - required\n  - name: Ray_DailyTaskConfig__Cron\n    # This means BiliBili Toll triggers at every day's 08:10 AM \n    value: \"10 8 * * *\"\n  \n  # Add your custom env vars like\n  # - name: Ray_Security__IntervalSecondsBetweenRequestApi\n  #   value: \"20\"\n  # - name: Ray_Security__RandomSleepMaxMin\n  #   value: \"20\"\n  # - name: Ray_LiveLotteryTaskConfig__Cron\n  #   value: \"\"\n\nvolumes:\n  # if `enabled=true`, then path and name is required\n  log:\n    enabled: true\n    path: \"/tmp/logs\"\n    name: \"bili-tool-vol\"\n  login:\n    enabled: true\n    name: \"entry\"\n\n\n\npodAnnotations: {}\n\nresources:\n  # Recommended to set this resources field\n  limits:\n    cpu: 100m\n    memory: 120Mi\n  requests:\n    cpu: 100m\n    memory: 120Mi\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n"
  },
  {
    "path": "krew/.gitignore",
    "content": "bin\nbilipro"
  },
  {
    "path": "krew/Makefile",
    "content": ".DEFAULT_GOAL:=help\nSHELL := /usr/bin/env bash\n\nROOT_DIR := $(shell git rev-parse --show-toplevel)\nGOOS ?= $(shell go env GOOS)\nGOARCH ?= $(shell go env GOARCH)\nBIN_NAME ?= kubectl-bilipro\nKUBECTL_DIR ?= $(shell which kubectl | awk -F 'kubectl' '{printf \"%s\\n\", $$1 }')\n\n\n\nGITCOMMIT ?= `git rev-parse HEAD`\n\nhelp: #### display help\n\t@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)\n\t@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=<os-arch>\\\" 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)\n##### GLOBAL\n\n.PHONY: deploy\ndeploy: build install #### build + install\n\n\n.PHONY: fmt\nfmt: #### run go fmt against code\n\t@go fmt ./...\n\n\n.PHONY: vet\nvet: #### run go vet against code\n\t@go vet ./...\n\n.PHONY: update-modules\nupdate-modules: tidy #### update go modules\n\n.PHONY: tidy\ntidy: #### run go mod tidy\n\t@go mod tidy\n\n.PHONY: build\nbuild: #### build the plugin\n\t@echo \"build on ${GOOS}/${GOARCH}\" && \\\n\t\tGOOS=${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\n\n.PHONY: install\ninstall: #### install the plugin\n\t@cd ${ROOT_DIR}/krew/bin && \\\n      sudo install ./$(BIN_NAME) ${KUBECTL_DIR}/$(BIN_NAME)\n\n.PHONY: test\ntest: #### run tests\n\t@cd ${ROOT_DIR}/krew/pkg/utils && \\\n\trm -rf fixtures && \\\n\tmkdir fixtures && \\\n\tgo test ./... -cover && \\\n\t  rm -rf fixtures"
  },
  {
    "path": "krew/README.md",
    "content": "# BiliBiliPro Kubectl Plugin\n\n## Prerequisites\n\n- Kubernetes >= v1.23.0.\n- go >= v1.18\n- kubectl installed on your local machine, configured to an existing healthy Kubernetes cluster.\n- [krew](https://krew.sigs.k8s.io/docs/user-guide/setup/install/) plugin installed\n\n## Install Plugin\n\nCommand: `cd ./krew && make deploy`\nThe binary will be generated in cmd/ install it alonside the kubectl binary.\n\nFor example: the kubectl is installed under `/usr/bin`, then put the bilibilipro plugin under `/usr/bin` too.\n\n## Plugin Commands\n\n### Deployment && Update\n\nPrerequsites: please make sure you have the right permission to at least manage namespaces/deployments\n\nCommand: `kubectl bilipro init --config config.yaml`\n\nCreates Deployment with the needed environments.\n\nOptional Options:\n\n- `--image=zai7lou/bilibili_tool_pro:2.0.1`\n- `--namespace=bilipro`\n- `--image-pull-secret=<docker secrets>`\n- `--login` to scan QR code to login\n\nRequired Options:\n\n- `--config=<config.yaml>`\n\nThe content of <config.yaml> is a yaml array, please refer to the example config yaml under the krew directory.\n\nFor example\n\n````yaml\n- name: Ray_BiliBiliCookies__2\n  value: \"cookie\"\n  # DailyTrigger - required\n- name: Ray_DailyTaskConfig__Cron\n  value: \"11 11 * * *\"\n````\n\nSuggestions: Deploy this workload in namespace other than default or kube-* namespace, because the delete logic should be improved\n\n### Deletion\n\nCommand: `kubectl bilipro delete [options]`\n\nDeletes Deployment.\nv\nOptional Options:\n\n- `--namespace=<deploy-namespace>`\n- `--name=<deploy-name>`\n\n### Version\n\nCommand: `kubectl bilipro version`\n\nOutput the plugin version.\n\n## Package\n\nPls refer to [installation](https://krew.sigs.k8s.io/docs), you can package your own krew plugin\n"
  },
  {
    "path": "krew/cmd/kubectl-bilipro.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/pflag\"\n\n\t\"github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/cmd\"\n\thelper \"github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/utils\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\nfunc main() {\n\tflags := pflag.NewFlagSet(\"kubectl-bilipro\", pflag.ExitOnError)\n\tpflag.CommandLine = flags\n\n\tcmd := cmd.NewExecutor(genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr})\n\tif err := cmd.Execute(); err != nil {\n\t\tfmt.Println(helper.GenErrorMsg(helper.SERVER_ERROR, err.Error()).Error())\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "krew/config.yaml",
    "content": "- name: Ray_BiliBiliCookies__1\n  value: \"cookie\"\n  # DailyTrigger - required\n- name: Ray_DailyTaskConfig__Cron\n  value: \"11 11 * * *\""
  },
  {
    "path": "krew/go.mod",
    "content": "module github.com/RayWangQvQ/BiliBiliToolPro/krew\n\ngo 1.20\n\nrequire (\n\tgithub.com/spf13/cobra v1.4.0\n\tgithub.com/spf13/pflag v1.0.5\n\tk8s.io/api v0.25.4\n\tk8s.io/apimachinery v0.25.4\n\tk8s.io/cli-runtime v0.25.4\n\tk8s.io/client-go v0.25.4\n\tsigs.k8s.io/kustomize/api v0.12.1\n\tsigs.k8s.io/kustomize/kyaml v0.13.9\n\tsigs.k8s.io/yaml v1.3.0\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/emicklei/go-restful/v3 v3.9.0 // indirect\n\tgithub.com/evanphx/json-patch v4.12.0+incompatible // indirect\n\tgithub.com/go-errors/errors v1.0.1 // indirect\n\tgithub.com/go-logr/logr v1.2.3 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.19.5 // indirect\n\tgithub.com/go-openapi/jsonreference v0.20.0 // indirect\n\tgithub.com/go-openapi/swag v0.19.14 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.2 // indirect\n\tgithub.com/google/btree v1.0.1 // indirect\n\tgithub.com/google/gnostic v0.5.7-v3refs // indirect\n\tgithub.com/google/go-cmp v0.5.9 // indirect\n\tgithub.com/google/gofuzz v1.1.0 // indirect\n\tgithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect\n\tgithub.com/google/uuid v1.1.2 // indirect\n\tgithub.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect\n\tgithub.com/imdario/mergo v0.3.6 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.0.0 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect\n\tgithub.com/mailru/easyjson v0.7.6 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/peterbourgon/diskv v2.0.1+incompatible // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/xlab/treeprint v1.1.0 // indirect\n\tgo.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect\n\tgolang.org/x/net v0.7.0 // indirect\n\tgolang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect\n\tgolang.org/x/sys v0.5.0 // indirect\n\tgolang.org/x/term v0.5.0 // indirect\n\tgolang.org/x/text v0.7.0 // indirect\n\tgolang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect\n\tgoogle.golang.org/appengine v1.6.7 // indirect\n\tgoogle.golang.org/protobuf v1.28.1 // indirect\n\tgopkg.in/inf.v0 v0.9.1 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tk8s.io/klog/v2 v2.80.1 // indirect\n\tk8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect\n\tk8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect\n\tsigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect\n\tsigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect\n)\n\n// replace github.com/RayWangQvQ/BiliBiliToolPro/krew => ../krew\n"
  },
  {
    "path": "krew/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=\ngithub.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=\ngithub.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=\ngithub.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=\ngithub.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=\ngithub.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=\ngithub.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=\ngithub.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=\ngithub.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=\ngithub.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=\ngithub.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=\ngithub.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=\ngithub.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=\ngithub.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=\ngithub.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=\ngithub.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=\ngithub.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=\ngithub.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=\ngithub.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=\ngithub.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=\ngithub.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=\ngithub.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=\ngithub.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU=\ngithub.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=\ngithub.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=\ngithub.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=\ngithub.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk=\ngithub.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc=\ngo.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=\ngolang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=\ngolang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=\ngoogle.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nk8s.io/api v0.25.4 h1:3YO8J4RtmG7elEgaWMb4HgmpS2CfY1QlaOz9nwB+ZSs=\nk8s.io/api v0.25.4/go.mod h1:IG2+RzyPQLllQxnhzD8KQNEu4c4YvyDTpSMztf4A0OQ=\nk8s.io/apimachinery v0.25.4 h1:CtXsuaitMESSu339tfhVXhQrPET+EiWnIY1rcurKnAc=\nk8s.io/apimachinery v0.25.4/go.mod h1:jaF9C/iPNM1FuLl7Zuy5b9v+n35HGSh6AQ4HYRkCqwo=\nk8s.io/cli-runtime v0.25.4 h1:GTSBN7aKBrc2LqpdO30CmHQqJtRmotxV7XsMSP+QZIk=\nk8s.io/cli-runtime v0.25.4/go.mod h1:JGOw1CR8v4Mcz6cEKA7bFQe0bPrNn1l5sGAX1/Ke4Eg=\nk8s.io/client-go v0.25.4 h1:3RNRDffAkNU56M/a7gUfXaEzdhZlYhoW8dgViGy5fn8=\nk8s.io/client-go v0.25.4/go.mod h1:8trHCAC83XKY0wsBIpbirZU4NTUpbuhc2JnI7OruGZw=\nk8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4=\nk8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=\nk8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E=\nk8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4=\nk8s.io/utils v0.0.0-20221107191617-1a15be271d1d h1:0Smp/HP1OH4Rvhe+4B8nWGERtlqAGSftbSbbmm45oFs=\nk8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=\nsigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=\nsigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM=\nsigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s=\nsigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk=\nsigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4=\nsigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=\nsigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=\nsigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=\nsigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=\n"
  },
  {
    "path": "krew/pkg/cmd/cmd.go",
    "content": "package cmd\n\nimport (\n\t\"log\"\n\n\t\"github.com/spf13/cobra\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\nconst (\n\tbiliproDesc = `Manage and deploy bilibili pro tools on k8s`\n\tkubeconfig  = \"kubeconfig\"\n)\n\nvar (\n\tconfPath string\n\trootCmd  = &cobra.Command{\n\t\tUse:          \"bilipro\",\n\t\tLong:         biliproDesc,\n\t\tSilenceUsage: true,\n\t}\n)\n\nfunc init() {\n\trootCmd.PersistentFlags().StringVar(&confPath, kubeconfig, \"\", \"Custom kubeconfig path\")\n\n\tlog.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)\n}\n\n// New creates a new root command for kubectl-bilipro\nfunc NewExecutor(streams genericclioptions.IOStreams) *cobra.Command {\n\tcobra.EnableCommandSorting = false\n\trootCmd.AddCommand(newInitCmd(rootCmd.OutOrStdout(), rootCmd.ErrOrStderr()))\n\t// If you want to update, just init again\n\trootCmd.AddCommand(newGetCmd(rootCmd.OutOrStdout(), rootCmd.ErrOrStderr()))\n\trootCmd.AddCommand(newDeleteCmd(rootCmd.OutOrStdout(), rootCmd.ErrOrStderr()))\n\trootCmd.AddCommand(newVersionCmd(rootCmd.OutOrStdout(), rootCmd.ErrOrStderr()))\n\treturn rootCmd\n}\n"
  },
  {
    "path": "krew/pkg/cmd/delete.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"sigs.k8s.io/kustomize/api/krusty\"\n\t\"sigs.k8s.io/kustomize/api/types\"\n\t\"sigs.k8s.io/yaml\"\n\n\t\"github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/options\"\n\thelper \"github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/utils\"\n\t\"github.com/spf13/cobra\"\n)\n\nconst (\n\tdeleteDesc = `\n'delete' command delete bilibilipro tool.`\n\tdeleteExample = `  kubectl bilipro delete <--name deployment_name>`\n)\n\ntype deleteCmd struct {\n\tout        io.Writer\n\terrOut     io.Writer\n\toutput     bool\n\tdeployOpts options.DeployOptions\n}\n\nfunc newDeleteCmd(out io.Writer, errOut io.Writer) *cobra.Command {\n\to := &deleteCmd{out: out, errOut: errOut}\n\n\tcmd := &cobra.Command{\n\t\tUse:     \"delete\",\n\t\tShort:   \"Delete bilibilipro\",\n\t\tLong:    deleteDesc,\n\t\tExample: deleteExample,\n\t\tArgs:    cobra.MaximumNArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\n\t\t\terr := o.run(out)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(\"bilibili tool is removed from your cluster\")\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tf := cmd.Flags()\n\tf.StringVarP(&o.deployOpts.Namespace, \"namespace\", \"n\", \"bilipro\", \"namespace scope for this request\")\n\tf.StringVarP(&o.deployOpts.Name, \"name\", \"\", \"bilibilipro\", \"name of deployment to delete\")\n\treturn cmd\n}\n\nfunc (o *deleteCmd) run(writer io.Writer) error {\n\tinDiskSys, err := helper.GetResourceFileSys()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// remove namespace files lastly\n\tresources := []string{\"base/bilibiliPro/deployment.yaml\"}\n\n\t// write the kustomization file\n\n\tkustomizationYaml := types.Kustomization{\n\t\tTypeMeta: types.TypeMeta{\n\t\t\tKind:       \"Kustomization\",\n\t\t\tAPIVersion: \"kustomize.config.k8s.io/v1beta1\",\n\t\t},\n\t\tResources: resources,\n\t}\n\n\tif o.deployOpts.Namespace != \"\" {\n\t\tkustomizationYaml.Namespace = o.deployOpts.Namespace\n\t}\n\n\t// Compile the kustomization to a file and create on the in memory filesystem\n\tkustYaml, err := yaml.Marshal(kustomizationYaml)\n\tif err != nil {\n\t\treturn helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error())\n\t}\n\n\tkustFile, err := inDiskSys.Create(\"./bilipro/kustomization.yaml\")\n\tif err != nil {\n\t\treturn helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error())\n\t}\n\n\t_, err = kustFile.Write(kustYaml)\n\tif err != nil {\n\t\treturn helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error())\n\t}\n\n\t// kustomize build the target location\n\tk := krusty.MakeKustomizer(\n\t\tkrusty.MakeDefaultOptions(),\n\t)\n\n\tm, err := k.Run(inDiskSys, \"./bilipro\")\n\tif err != nil {\n\t\treturn helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error())\n\t}\n\n\tyml, err := m.AsYaml()\n\tif err != nil {\n\t\treturn helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error())\n\t}\n\n\tif o.output {\n\t\t_, err = writer.Write(yml)\n\t\treturn helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error())\n\t}\n\n\t// do kubectl delete\n\tcmd := exec.Command(\"kubectl\", \"delete\", \"-f\", \"-\")\n\n\tif err := helper.Run(cmd, strings.NewReader(string(yml))); err != nil {\n\t\treturn err\n\t}\n\n\t// delete the namespace\n\tcmd = exec.Command(\"kubectl\", \"delete\", \"ns\", o.deployOpts.Namespace)\n\tif err := helper.Run(cmd, nil); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "krew/pkg/cmd/get.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os/exec\"\n\n\t\"github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/options\"\n\thelper \"github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/utils\"\n\t\"github.com/spf13/cobra\"\n)\n\nconst (\n\tgetDesc = `\n'get' command get bilibilipro tool deployment.`\n\tgetExample = `  kubectl bilipro get <--name deployment_name --namespace namespace_name>`\n)\n\ntype getCmd struct {\n\tout    io.Writer\n\terrOut io.Writer\n\n\tdeployOpts options.DeployOptions\n}\n\nfunc newGetCmd(out io.Writer, errOut io.Writer) *cobra.Command {\n\to := &getCmd{out: out, errOut: errOut}\n\n\tcmd := &cobra.Command{\n\t\tUse:     \"get\",\n\t\tShort:   \"Get bilibilipro\",\n\t\tLong:    getDesc,\n\t\tExample: getExample,\n\t\tArgs:    cobra.MaximumNArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\n\t\t\terr := o.run(out)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tf := cmd.Flags()\n\tf.StringVarP(&o.deployOpts.Namespace, \"namespace\", \"n\", \"bilipro\", \"namespace scope for this request\")\n\tf.StringVarP(&o.deployOpts.Name, \"name\", \"\", \"bilibilipro\", \"name of deployment to get\")\n\treturn cmd\n}\n\nfunc (o *getCmd) run(writer io.Writer) error {\n\t// do kubectl get\n\tcmd := exec.Command(\"kubectl\", \"get\", \"deploy\", o.deployOpts.Name, \"-n\", o.deployOpts.Namespace)\n\n\tif err := helper.Run(cmd, nil); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "krew/pkg/cmd/init.go",
    "content": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\tcorev1 \"k8s.io/api/core/v1\"\n\n\t\"sigs.k8s.io/kustomize/kyaml/resid\"\n\n\t\"sigs.k8s.io/yaml\"\n\n\t\"sigs.k8s.io/kustomize/api/types\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/options\"\n\thelper \"github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/utils\"\n\t\"sigs.k8s.io/kustomize/api/krusty\"\n)\n\nconst (\n\tinitDesc = `\n 'init' command creates BilibiliPro deployment along with all the dependencies.`\n\tinitExample = `  kubectl bilipro init --config <config-file>`\n)\n\ntype initCmd struct {\n\tout        io.Writer\n\terrOut     io.Writer\n\toutput     bool\n\tlogin      bool\n\tdeployOpts options.DeployOptions\n}\n\nfunc newInitCmd(out io.Writer, errOut io.Writer) *cobra.Command {\n\to := &initCmd{out: out, errOut: errOut}\n\n\tcmd := &cobra.Command{\n\t\tUse:     \"init\",\n\t\tShort:   \"Initialize bilipro\",\n\t\tLong:    initDesc,\n\t\tExample: initExample,\n\t\tArgs:    cobra.MaximumNArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\terr := o.run(out)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tf := cmd.Flags()\n\tf.StringVarP(&o.deployOpts.Image, \"image\", \"i\", \"zai7lou/bilibili_tool_pro:2.0.1\", \"bilibilipro image\")\n\tf.StringVarP(&o.deployOpts.Namespace, \"namespace\", \"n\", \"bilipro\", \"namespace scope for this request\")\n\tf.StringVar(&o.deployOpts.ImagePullSecret, \"image-pull-secret\", \"\", \"image pull secret to be used for pulling bilibilipro image\")\n\tf.StringVarP(&o.deployOpts.ConfigFilePath, \"config\", \"c\", \"\", \"the config file contanis the environment variables\")\n\tf.BoolVarP(&o.output, \"output\", \"o\", false, \"dry run this command and generate requisite yaml\")\n\tf.BoolVarP(&o.login, \"login\", \"l\", false, \"scan QR login code\")\n\treturn cmd\n}\n\ntype opStr struct {\n\tOp    string `json:\"op\"`\n\tPath  string `json:\"path\"`\n\tValue string `json:\"value\"`\n}\n\ntype opInterface struct {\n\tOp    string      `json:\"op\"`\n\tPath  string      `json:\"path\"`\n\tValue interface{} `json:\"value\"`\n}\n\ntype normalEnvVars struct {\n\tName  string `json:\"name\"`\n\tValue string `json:\"value\"`\n}\n\n// run initializes local config and installs BiliBiliPro tool to Kubernetes Cluster.\nfunc (o *initCmd) run(writer io.Writer) error {\n\tinDiskSys, err := helper.GetResourceFileSys()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// TODO: All about paths are a little bit tricky should give it more thoughts\n\n\tfmt.Println(\"Creating the kustomization file\")\n\t// if the bilibili tool is deployed under system/pre-defined namespace, ignore the namespace file\n\tvar resources []string // nolint: go-staticcheck\n\tif o.deployOpts.Namespace == \"default\" || o.deployOpts.Namespace == \"kube-system\" || o.deployOpts.Namespace == \"kube-public\" {\n\t\tresources = []string{\"base/bilibiliPro/deployment.yaml\"}\n\t} else {\n\t\tresources = []string{\"base/ns/namespace.yaml\", \"base/bilibiliPro/deployment.yaml\"}\n\t}\n\n\t// write the kustomization file\n\tkustomizationYaml := types.Kustomization{\n\t\tTypeMeta: types.TypeMeta{\n\t\t\tKind:       \"Kustomization\",\n\t\t\tAPIVersion: \"kustomize.config.k8s.io/v1beta1\",\n\t\t},\n\t\tResources:       resources,\n\t\tPatchesJson6902: []types.Patch{},\n\t}\n\n\tvar deployDepPatches []interface{}\n\t// create patches for the supplied arguments\n\tif o.deployOpts.Image != \"\" {\n\t\tdeployDepPatches = append(deployDepPatches, opStr{\n\t\t\tOp:    \"replace\",\n\t\t\tPath:  \"/spec/template/spec/containers/0/image\",\n\t\t\tValue: o.deployOpts.Image,\n\t\t})\n\t}\n\t// create patches for the env\n\tcontent, err := os.ReadFile(o.deployOpts.ConfigFilePath)\n\tif err != nil {\n\t\treturn helper.GenErrorMsg(helper.FILE_ERROR, err.Error())\n\t}\n\n\tenvs := []normalEnvVars{}\n\terr = yaml.Unmarshal(content, &envs)\n\tif err != nil {\n\t\treturn helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error())\n\t}\n\n\tdeployDepPatches = append(deployDepPatches, opInterface{\n\t\tOp:    \"add\",\n\t\tPath:  \"/spec/template/spec/containers/0/env\",\n\t\tValue: envs,\n\t})\n\n\tif o.deployOpts.ImagePullSecret != \"\" {\n\t\tdeployDepPatches = append(deployDepPatches, opInterface{\n\t\t\tOp:    \"add\",\n\t\t\tPath:  \"/spec/template/spec/imagePullSecrets\",\n\t\t\tValue: []corev1.LocalObjectReference{{Name: o.deployOpts.ImagePullSecret}},\n\t\t})\n\t}\n\n\t// attach the patches to the kustomization file\n\tif len(deployDepPatches) > 0 {\n\t\tkustomizationYaml.PatchesJson6902 = append(kustomizationYaml.PatchesJson6902, types.Patch{\n\t\t\tPatch: o.serializeJSONPatchOps(deployDepPatches),\n\t\t\tTarget: &types.Selector{\n\t\t\t\tResId: resid.ResId{\n\t\t\t\t\tGvk: resid.Gvk{\n\t\t\t\t\t\tGroup:   \"apps\",\n\t\t\t\t\t\tVersion: \"v1\",\n\t\t\t\t\t\tKind:    \"Deployment\",\n\t\t\t\t\t},\n\t\t\t\t\tName:      \"bilibilipro\",\n\t\t\t\t\tNamespace: o.deployOpts.Namespace,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t}\n\n\t// Not deploying in kube-* namespace\n\tif o.deployOpts.Namespace == \"kube-system\" || o.deployOpts.Namespace == \"kube-public\" {\n\t\tfmt.Println(\"better not deployed under system namesapce\")\n\t}\n\n\tif o.deployOpts.Namespace != \"\" {\n\t\tkustomizationYaml.Namespace = o.deployOpts.Namespace\n\t}\n\t// Compile the kustomization to a file and create on the in memory filesystem\n\tkustYaml, err := yaml.Marshal(kustomizationYaml)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tkustFile, err := inDiskSys.Create(\"./bilipro/kustomization.yaml\")\n\tif err != nil {\n\t\treturn helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error())\n\t}\n\n\t_, err = kustFile.Write(kustYaml)\n\tif err != nil {\n\t\treturn helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error())\n\t}\n\n\t// kustomize build the target location\n\tk := krusty.MakeKustomizer(\n\t\tkrusty.MakeDefaultOptions(),\n\t)\n\n\tm, err := k.Run(inDiskSys, \"./bilipro\")\n\tif err != nil {\n\t\treturn helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error())\n\t}\n\n\tyml, err := m.AsYaml()\n\tif err != nil {\n\t\treturn helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error())\n\t}\n\n\tif o.output {\n\t\t_, err = writer.Write(yml)\n\t\tif err != nil {\n\t\t\treturn helper.GenErrorMsg(helper.TEMPLATE_ERROR, err.Error())\n\t\t}\n\t}\n\n\tfmt.Println(\"Applying the kustomization file\")\n\t// do kubectl apply\n\t// make sure kubectl is under your PATH\n\tcmd := exec.Command(\"kubectl\", \"apply\", \"-f\", \"-\")\n\n\tif err := helper.Run(cmd, strings.NewReader(string(yml))); err != nil {\n\t\treturn err\n\t}\n\n\t// if there is login required, exectue the login command as the last step\n\tif o.login {\n\t\tfmt.Println(\"please login...\")\n\t\tclient, _, err := helper.GetK8sClient()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// get the pod name\n\t\tpodName, err := helper.GetBiliName(client, o.deployOpts.Namespace, \"bilibilipro\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfmt.Println(\"wait for the deployment to be ready\")\n\t\t// Wait for the deployment ready\n\t\tcheckCmdArgs := []string{\n\t\t\t\"rollout\",\n\t\t\t\"status\",\n\t\t\t\"deployment/bilibilipro\",\n\t\t\t\"-n\",\n\t\t\to.deployOpts.Namespace,\n\t\t}\n\t\tcheckCmd := exec.Command(\"kubectl\", checkCmdArgs...)\n\n\t\tfor {\n\t\t\tif err := checkCmd.Start(); err != nil {\n\t\t\t\tfmt.Printf(\"deployment is not ready yet, current status: %v\\n\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\terr := checkCmd.Wait()\n\t\t\tif err == nil {\n\t\t\t\tfmt.Printf(\"deployment is ready\\n\")\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfmt.Printf(\"deployment is not ready yet, current status: %v\\n\", err)\n\t\t}\n\n\t\tfmt.Println(\"please scan the QR code\")\n\t\t// Exec the login command\n\t\targs := []string{\n\t\t\t\"exec\",\n\t\t\tpodName,\n\t\t\t\"-n\",\n\t\t\to.deployOpts.Namespace,\n\t\t\t\"--\",\n\t\t\t\"dotnet\",\n\t\t\t\"Ray.BiliBiliTool.Console.dll\",\n\t\t\t\"--runTasks=Login\",\n\t\t}\n\t\tcmd := exec.Command(\"kubectl\", args...)\n\n\t\tif err := helper.Run(cmd, nil); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (o *initCmd) serializeJSONPatchOps(jp []interface{}) string {\n\tjpJSON, _ := json.Marshal(jp)\n\treturn string(jpJSON)\n}\n"
  },
  {
    "path": "krew/pkg/cmd/version.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// version provides the version of this plugin\nvar version = \"NO.VERSION\"\n\nconst (\n\tversionDesc = `\n'version' command displays the kubectl plugin version.`\n\tversionExample = `  kubectl bilipro version`\n)\n\ntype versionCmd struct {\n\tout    io.Writer\n\terrOut io.Writer\n}\n\nfunc newVersionCmd(out io.Writer, errOut io.Writer) *cobra.Command {\n\to := &versionCmd{out: out, errOut: errOut}\n\n\tcmd := &cobra.Command{\n\t\tUse:     \"version\",\n\t\tShort:   \"Display plugin version\",\n\t\tLong:    versionDesc,\n\t\tExample: versionExample,\n\t\tArgs:    cobra.MaximumNArgs(0),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\terr := o.run()\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\n\treturn cmd\n}\n\n// run initializes local config and installs BilibiliPro Plugin to Kubernetes cluster.\nfunc (o *versionCmd) run() error {\n\tfmt.Println(version)\n\treturn nil\n}\n"
  },
  {
    "path": "krew/pkg/options/deployment.go",
    "content": "package options\n\n// DeployOptions encapsulates the CLI options for a BiliBiliPro\ntype DeployOptions struct {\n\tName            string\n\tImage           string\n\tNamespace       string\n\tImagePullSecret string\n\tConfigFilePath  string\n}\n"
  },
  {
    "path": "krew/pkg/resources/asset.go",
    "content": "package resources\n\nimport (\n\t\"embed\"\n)\n\n//go:embed *\nvar fs embed.FS\n\n// GetStaticResources returns the fs with the embedded assets\nfunc GetStaticResources() embed.FS {\n\treturn fs\n}\n"
  },
  {
    "path": "krew/pkg/resources/base/bilibiliPro/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: bilibilipro\nspec:\n  selector:\n    matchLabels:\n      app: bilibilipro\n  template:\n    metadata:\n      labels:\n        app: bilibilipro\n    spec:\n      containers:\n      - name: bilibilipro\n        image: zai7lou/bilibili_tool_pro:2.0.1\n        imagePullPolicy: IfNotPresent\n        resources:\n          requests:\n            cpu: 80m\n            memory: 100M\n          limits:\n            cpu: 100m\n            memory: 120M\n            \n  "
  },
  {
    "path": "krew/pkg/resources/base/ns/namespace.yaml",
    "content": "\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: bilipro"
  },
  {
    "path": "krew/pkg/utils/client.go",
    "content": "package utils\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/client-go/kubernetes\"\n\t\"k8s.io/client-go/rest\"\n\t\"k8s.io/client-go/tools/clientcmd\"\n)\n\nfunc GetK8sClient() (*kubernetes.Clientset, *rest.Config, error) {\n\tvar kubeconfig string\n\n\tenvKubeConfig := os.Getenv(\"KUBECONFIG\")\n\tif envKubeConfig == \"\" {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn nil, nil, GenErrorMsg(SERVER_ERROR, err.Error())\n\t\t}\n\t\tkubeconfig = filepath.Join(home, \".kube\", \"config\")\n\t} else {\n\t\tkubeconfig = envKubeConfig\n\t}\n\n\t// use the current context in kubeconfig\n\tconfig, err := clientcmd.BuildConfigFromFlags(\"\", kubeconfig)\n\tif err != nil {\n\t\treturn nil, nil, GenErrorMsg(SERVER_ERROR, err.Error())\n\t}\n\tconfig.QPS = float32(10.0)\n\tconfig.Burst = 20\n\n\t// create the clientset\n\tclientset, err := kubernetes.NewForConfig(config)\n\tif err != nil {\n\t\treturn nil, config, GenErrorMsg(SERVER_ERROR, err.Error())\n\t}\n\treturn clientset, config, nil\n}\n\nfunc GetBiliName(client *kubernetes.Clientset, namespace, deploymentName string) (string, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)\n\tdefer cancel()\n\tdeployment, err := client.AppsV1().Deployments(namespace).Get(ctx, deploymentName, metav1.GetOptions{})\n\tif err != nil {\n\t\treturn \"\", GenErrorMsg(SERVER_ERROR, err.Error())\n\t}\n\n\t// we dont do much checks here\n\tselector := deployment.Spec.Selector.MatchLabels\n\tif len(selector) == 0 {\n\t\treturn \"\", GenErrorMsg(SERVER_ERROR, \"deployment doesn't have any selectors, please check the deploy template\")\n\t}\n\tlistOptions := metav1.ListOptions{\n\t\tLabelSelector: labels.Set(selector).String(),\n\t}\n\tpods, err := client.CoreV1().Pods(namespace).List(ctx, listOptions)\n\tif err != nil {\n\t\treturn \"\", GenErrorMsg(SERVER_ERROR, \"cannot list the pods with deploy selectors\")\n\t}\n\tif len(pods.Items) != 1 {\n\t\treturn \"\", GenErrorMsg(SERVER_ERROR, fmt.Sprintf(\"pod number is expected to be 1, currently %d\", len(pods.Items)))\n\t}\n\n\t// only one pod is supposed to be existing, soft constraint\n\treturn pods.Items[0].ObjectMeta.GetName(), nil\n}\n"
  },
  {
    "path": "krew/pkg/utils/client_test.go",
    "content": "package utils\n\nimport (\n\t\"testing\"\n)\n\nfunc TestGetK8sClient(t *testing.T) {\n\tclient, _, err := GetK8sClient()\n\tif err != nil {\n\t\tt.Logf(\"test failed due to %s\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\tif client == nil {\n\t\tt.Logf(\"test failed for returned client is empty, this should not happen\")\n\t\tt.FailNow()\n\t}\n\n}\n"
  },
  {
    "path": "krew/pkg/utils/cmd.go",
    "content": "package utils\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"os/exec\"\n)\n\nfunc Run(cmd *exec.Cmd, in io.Reader) error {\n\tcmd.Stdin = in\n\n\tstdoutReader, _ := cmd.StdoutPipe()\n\tstdoutScanner := bufio.NewScanner(stdoutReader)\n\tgo func() {\n\t\tfor stdoutScanner.Scan() {\n\t\t\tfmt.Println(stdoutScanner.Text())\n\t\t}\n\t}()\n\tstderrReader, _ := cmd.StderrPipe()\n\tstderrScanner := bufio.NewScanner(stderrReader)\n\tgo func() {\n\t\tfor stderrScanner.Scan() {\n\t\t\tfmt.Println(stderrScanner.Text())\n\t\t}\n\t}()\n\terr := cmd.Start()\n\tif err != nil {\n\t\treturn GenErrorMsg(EXEC_ERROR, err.Error())\n\t}\n\n\t// Stuck here until there are out and err\n\terr = cmd.Wait()\n\tif err != nil {\n\t\treturn GenErrorMsg(EXEC_ERROR, err.Error())\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "krew/pkg/utils/cmd_test.go",
    "content": "package utils\n\nimport (\n\t\"os/exec\"\n\t\"testing\"\n)\n\nfunc TestRun(t *testing.T) {\n\n\t// Just make sure there is no error...\n\ttestCmd := exec.Command(\"ls\")\n\terr := Run(testCmd, nil)\n\tif err != nil {\n\t\tt.Logf(\"test failed due to %s\", err.Error())\n\t\tt.FailNow()\n\t}\n}\n"
  },
  {
    "path": "krew/pkg/utils/error.go",
    "content": "package utils\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\nconst (\n\t// For errors about kustomize\n\tTEMPLATE_ERROR = \"template error\"\n\t// For errors about file system\n\tFILE_ERROR = \"file system error\"\n\t// For errors about create/delete/... resources in cluster\n\tSERVER_ERROR = \"cluster operation error\"\n\t// For exec errors\n\tEXEC_ERROR = \"exec error\"\n)\n\nfunc GenErrorMsg(errType, customMsg string) error {\n\terrorMsg := fmt.Sprintf(\"[ERROR] %s: %s\", errType, customMsg)\n\treturn errors.New(errorMsg)\n}\n"
  },
  {
    "path": "krew/pkg/utils/error_test.go",
    "content": "package utils\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestServerGenErrorMsg(t *testing.T) {\n\texpectedErr := GenErrorMsg(SERVER_ERROR, \"test for server\")\n\tif !strings.Contains(expectedErr.Error(), SERVER_ERROR) {\n\t\tt.Logf(\"server error generate failed\")\n\t\tt.FailNow()\n\t}\n}\n\nfunc TestTemplateGenErrorMsg(t *testing.T) {\n\texpectedErr := GenErrorMsg(TEMPLATE_ERROR, \"test for template\")\n\tif !strings.Contains(expectedErr.Error(), TEMPLATE_ERROR) {\n\t\tt.Logf(\"template error generate failed\")\n\t\tt.FailNow()\n\t}\n}\n\nfunc TestExecGenErrorMsg(t *testing.T) {\n\texpectedErr := GenErrorMsg(EXEC_ERROR, \"test for exec\")\n\tif !strings.Contains(expectedErr.Error(), EXEC_ERROR) {\n\t\tt.Logf(\"error error generate failed\")\n\t\tt.FailNow()\n\t}\n}\n\nfunc TestFileGenErrorMsg(t *testing.T) {\n\texpectedErr := GenErrorMsg(FILE_ERROR, \"test for file\")\n\tif !strings.Contains(expectedErr.Error(), FILE_ERROR) {\n\t\tt.Logf(\"file error generate failed\")\n\t\tt.FailNow()\n\t}\n}\n"
  },
  {
    "path": "krew/pkg/utils/fileSys.go",
    "content": "package utils\n\nimport (\n\t\"io\"\n\t\"io/fs\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/RayWangQvQ/BiliBiliToolPro/krew/pkg/resources\"\n\t\"sigs.k8s.io/kustomize/kyaml/filesys\"\n)\n\nvar assetFS = resources.GetStaticResources()\n\n// GetResourceFileSys file\nfunc GetResourceFileSys() (filesys.FileSystem, error) {\n\tinDiskSys := filesys.MakeFsOnDisk()\n\t// copy from the resources into the target folder on the in memory FS\n\tif err := copyDirtoDiskFS(\".\", \"bilipro\", inDiskSys); err != nil {\n\t\treturn nil, GenErrorMsg(FILE_ERROR, err.Error())\n\t}\n\treturn inDiskSys, nil\n}\n\nfunc copyFileToDiskFS(src, dst string, diskFS filesys.FileSystem) error {\n\t// skip all .go files\n\tif strings.HasSuffix(src, \".go\") {\n\t\treturn nil\n\t}\n\tvar err error\n\tvar srcFileDesc fs.File\n\tvar dstFileDesc filesys.File\n\n\tif srcFileDesc, err = assetFS.Open(src); err != nil {\n\t\treturn GenErrorMsg(FILE_ERROR, err.Error())\n\t}\n\tdefer srcFileDesc.Close()\n\n\tif dstFileDesc, err = diskFS.Create(dst); err != nil {\n\t\treturn GenErrorMsg(FILE_ERROR, err.Error())\n\t}\n\tdefer dstFileDesc.Close()\n\n\t// Note: I had to read the whole string, for some reason io.Copy was not copying the whole content\n\tinput, err := io.ReadAll(srcFileDesc)\n\tif err != nil {\n\t\treturn GenErrorMsg(FILE_ERROR, err.Error())\n\t}\n\n\t_, err = dstFileDesc.Write(input)\n\tif err != nil {\n\t\treturn GenErrorMsg(FILE_ERROR, err.Error())\n\t}\n\treturn nil\n}\n\nfunc copyDirtoDiskFS(src string, dst string, diskFS filesys.FileSystem) error {\n\tvar err error\n\tvar fds []fs.DirEntry\n\n\tif err = diskFS.MkdirAll(dst); err != nil {\n\t\treturn GenErrorMsg(FILE_ERROR, err.Error())\n\t}\n\n\tif fds, err = assetFS.ReadDir(src); err != nil {\n\t\treturn GenErrorMsg(FILE_ERROR, err.Error())\n\t}\n\tfor _, fd := range fds {\n\t\tsrcfp := path.Join(src, fd.Name())\n\t\tdstfp := path.Join(dst, fd.Name())\n\n\t\tif fd.IsDir() {\n\t\t\tif err = copyDirtoDiskFS(srcfp, dstfp, diskFS); err != nil {\n\t\t\t\treturn GenErrorMsg(FILE_ERROR, err.Error())\n\t\t\t}\n\t\t} else {\n\t\t\tif err = copyFileToDiskFS(srcfp, dstfp, diskFS); err != nil {\n\t\t\t\treturn GenErrorMsg(FILE_ERROR, err.Error())\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "krew/pkg/utils/fileSys_test.go",
    "content": "package utils\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"sigs.k8s.io/kustomize/kyaml/filesys\"\n)\n\nfunc TestCopyFiletoDiskFS(t *testing.T) {\n\texpectedFile, err := assetFS.Open(\"base/ns/namespace.yaml\")\n\tif err != nil {\n\t\tt.Logf(\"test failed due to %s\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\texpectedOutput, err := io.ReadAll(expectedFile)\n\tif err != nil {\n\t\tt.Logf(\"test failed due to %s\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\tdir, err := os.Getwd()\n\tif err != nil {\n\t\tt.Logf(\"test failed due to %s\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\tinDiskSys := filesys.MakeFsOnDisk()\n\n\terr = copyFileToDiskFS(\"base/ns/namespace.yaml\", filepath.Join(dir, \"fixtures/testNamespace.yaml\"), inDiskSys)\n\tif err != nil {\n\t\tt.Logf(\"test failed due to %s\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\tactualOutput, err := os.ReadFile(filepath.Join(dir, \"fixtures/testNamespace.yaml\"))\n\tif err != nil {\n\t\tt.Logf(\"test failed due to %s\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\tif string(expectedOutput) != string(actualOutput) {\n\t\tt.Logf(\"test failed due to copy file failed\")\n\t\tt.FailNow()\n\t}\n}\n\nfunc TestCopyDirtoDiskFS(t *testing.T) {\n\texpectedFile, err := assetFS.Open(\"base/bilibiliPro/deployment.yaml\")\n\tif err != nil {\n\t\tt.Logf(\"test failed due to %s\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\texpectedOutput, err := io.ReadAll(expectedFile)\n\tif err != nil {\n\t\tt.Logf(\"test failed due to %s\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\tdir, err := os.Getwd()\n\tif err != nil {\n\t\tt.Logf(\"test failed due to %s\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\tinDiskSys := filesys.MakeFsOnDisk()\n\n\terr = copyDirtoDiskFS(\"base/bilibiliPro\", filepath.Join(dir, \"fixtures\"), inDiskSys)\n\tif err != nil {\n\t\tt.Logf(\"test failed due to %s\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\tactualOutput, err := os.ReadFile(filepath.Join(dir, \"fixtures/deployment.yaml\"))\n\tif err != nil {\n\t\tt.Logf(\"test failed due to %s\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\tif string(expectedOutput) != string(actualOutput) {\n\t\tt.Logf(\"test failed due to copy file failed\")\n\t\tt.FailNow()\n\t}\n}\n"
  },
  {
    "path": "podman/README.md",
    "content": "# Podman 使用说明\n<!-- TOC depthFrom:2 -->\n\n- [1. 前期工作](#1-前期工作)\n    - [1.1. Podman环境](#11-podman环境)\n    - [1.2. 从Docker迁移](#12-从docker迁移)\n- [2. 运行容器](#2-运行容器)\n    - [2.1. 极简版](#21-极简版)\n    - [2.2. 综合版](#22-综合版)\n- [3. 登录](#3-登录)\n- [4. 添加 Bili 账号](#4-添加-bili-账号)\n- [5. 自己构建镜像（非必须）](#5-自己构建镜像非必须)\n- [6. 其他](#6-其他)\n\n<!-- /TOC -->\n\n## 1. 前期工作\n\n### 1.1. Podman环境\n\n请确认已安装了Podman所需环境（[Podman](https://podman.io/)\n\n安装完成后，请执行`podman -v`检查是否安装成功，请执行`podman info`检查虚拟机环境是否正常。\n\n常用命令参考：\n\n```\n# 查看版本\npodman -v\n\n# 初始化虚拟机\npodman machine init\n\n# 启动虚拟机\npodman machine start\n\n# 查看信息\npodman info\n```\n\n### 1.2. 从Docker迁移\n\nPodman可以和Docker共存，命令也基本可以通用。\n\n但挂载逻辑有点区别，podman挂载时，如果宿主机下没有指定的文件夹，podman不会像docker一样去自动创建文件夹，而是会报异常。\n\n所以在挂载文件夹时，需要先手动在宿主机上mkdir创建文件夹。\n\n## 2. 运行容器\n\n以下提供极简版和综合版两个版本，一个简单一个复杂，供参考\n\n### 2.1. 极简版\n\n```\n# 生成并运行容器\npodman run -itd --name=\"bili_tool_web\" docker.io/zai7lou/bili_tool_web\n\n# 查看实时日志\npodman logs -f bili_tool_web\n```\n\n### 2.2. 综合版\n\n```\n# 创建文件和文件夹\nmkdir -p /bili_tool_web && cd /bili_tool_web\nmkdir -p Logs\n\n# 下载appsettings.json\nmkdir -p config\ncd ./config\nwget https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/main/docker/sample/config/cookies.json\ncd ..\n\n# 运行\npodman run -itd --name=\"bili_tool_web\" \\\n    -v ./Logs:/app/Logs \\\n    -v ./config:/app/config \\\n    -e DailyTaskConfig__Cron=\"0 0 15 * * ?\" \\\n    -e LiveLotteryTaskConfig__Cron=\"0 0 22 * * ?\" \\\n    -e UnfollowBatchedTaskConfig__Cron=\"0 0 6 1 * ?\" \\\n    -e VipBigPointConfig__Cron=\"0 7 1 * * *\" \\\n    -e DailyTaskConfig__NumberOfCoins=\"5\"\n    docker.io/zai7lou/bili_tool_web\n\n# 查看实时日志\npodman logs -f bili\n```\n\n其他指令参考：\n\n```\n# 查看容器运行状态\npodman ps -a\n\n# 进入容器\npodman exec -it bili bash\n```\n\n## 3. 登录\n\n- 默认用户：`admin`\n- 默认密码：`BiliTool@2233`\n\n首次登陆后，请到`Admin`页面修改密码。\n\n## 4. 添加 Bili 账号\n\n扫码进行登录。\n\n![trigger](../docs/imgs/web-trigger-login.png)\n\n![login](../docs/imgs/docker-login.png)\n\n## 5. 自己构建镜像（非必须）\n\n目前我提供和维护的镜像：`[zai7lou/bilibili_tool_web](https://hub.docker.com/repository/docker/zai7lou/bilibili_tool_web)`;\n\n如果有需要（大部分都不需要），可以使用源码自己构建镜像，如下：\n\n在有项目的Dockerfile的目录运行\n\n`podman build -t TARGET_NAME .`\n\n `TARGET_NAME`为镜像名称和版本，可以自己起个名字\n\n## 6. 其他\n\n镜像使用的是docker仓库的镜像。\n"
  },
  {
    "path": "podman/build/buildImage.cmd",
    "content": "@echo off\n\nREM start to build\necho Start to build image\n@echo on\npodman build -t docker.io/zai7lou/bilibili_tool_pro:latest ../..\n@echo off\npause\n"
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_base.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 0 1 1 *\n# new Env(\"bili_base\")\n\n# Stop script on NZEC\nset -e\n# Stop script if unbound variable found (use ${var:-} if intentional)\nset -u\n# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success\n# This is causing it to fail\nset -o pipefail\n\nverbose=false                          # 开启debug日志\nbili_repo=\"raywangqvq/bilibilitoolpro\" # 仓库地址\nbili_branch=\"\"                         # 分支名，空或_develop\nprefer_mode=${BILI_MODE:-\"dotnet\"}     # dotnet或bilitool，需要通过环境变量配置\ngithub_proxy=${BILI_GITHUB_PROXY:-\"\"}  # 下载github release包时使用的代理，会拼在地址前面，需要通过环境变量配置\nexport DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 # 解决抽风问题\n\n# Use in the the functions: eval $invocation\ninvocation='say_verbose \"Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}\"'\n\n# standard output may be used as a return value in the functions\n# we need a way to write text on the screen in the functions so that\n# it won't interfere with the return value.\n# Exposing stream 3 as a pipe to standard output of the script itself\nexec 3>&1\n\n# Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors.\n# See if stdout is a terminal\nif [ -t 1 ] && command -v tput >/dev/null; then\n    # see if it supports colors\n    ncolors=$(tput colors || echo 0)\n    if [ -n \"$ncolors\" ] && [ $ncolors -ge 8 ]; then\n        bold=\"$(tput bold || echo)\"\n        normal=\"$(tput sgr0 || echo)\"\n        black=\"$(tput setaf 0 || echo)\"\n        red=\"$(tput setaf 1 || echo)\"\n        green=\"$(tput setaf 2 || echo)\"\n        yellow=\"$(tput setaf 3 || echo)\"\n        blue=\"$(tput setaf 4 || echo)\"\n        magenta=\"$(tput setaf 5 || echo)\"\n        cyan=\"$(tput setaf 6 || echo)\"\n        white=\"$(tput setaf 7 || echo)\"\n    fi\nfi\n\nsay_warning() {\n    printf \"%b\\n\" \"${yellow:-}bilitool: Warning: $1${normal:-}\" >&3\n}\n\nsay_err() {\n    printf \"%b\\n\" \"${red:-}bilitool: Error: $1${normal:-}\" >&2\n}\n\nsay() {\n    # using stream 3 (defined in the beginning) to not interfere with stdout of functions\n    # which may be used as return value\n    printf \"%b\\n\" \"${cyan:-}bilitool:${normal:-} $1\" >&3\n}\n\nsay_verbose() {\n    if [ \"$verbose\" = true ]; then\n        say \"$1\"\n    fi\n}\n\nQL_DIR=${QL_DIR:-\"/ql\"}\nQL_BRANCH=${QL_BRANCH:-\"develop\"}\nDefaultCronRule=${DefaultCronRule:-\"\"}\nCpuWarn=${CpuWarn:-\"\"}\nMemoryWarn=${MemoryWarn:-\"\"}\nDiskWarn=${DiskWarn:-\"\"}\n\ndir_repo=${dir_repo:-\"$QL_DIR/data/repo\"}\n# 需要兼容老版本青龙，https://github.com/RayWangQvQ/BiliBiliToolPro/issues/728\nif [ ! -d \"$dir_repo\" ] && [ -d \"$QL_DIR/repo\" ]; then\n  dir_repo=\"$QL_DIR/repo\"\nfi\ndir_shell=$QL_DIR/shell\ntouch $dir_shell/env.sh && . $dir_shell/env.sh\ntouch /root/.bashrc && . /root/.bashrc\n\n# 目录\nsay \"青龙repo目录: $dir_repo\"\nqinglong_bili_repo=\"$(echo \"$bili_repo\" | sed 's/\\//_/g')${bili_branch}\"\nqinglong_bili_repo_dir=\"$(find $dir_repo -type d \\( -iname $qinglong_bili_repo -o -iname ${qinglong_bili_repo}_main \\) | head -1)\"\nsay \"bili仓库目录: $qinglong_bili_repo_dir\"\n\ncurrent_linux_os=\"debian\"  # 或alpine\ncurrent_os=\"linux\"         # 或linux-musl\nmachine_architecture=\"x64\" # 或arm、arm64\n\nbilitool_installed_version=0\n\n# 以下操作仅在bilitool仓库的根bin文件下执行\ncd $qinglong_bili_repo_dir\nmkdir -p bin && cd $qinglong_bili_repo_dir/bin\n\n# 判断是否存在某指令\nmachine_has() {\n    eval $invocation\n\n    command -v \"$1\" >/dev/null 2>&1\n    return $?\n}\n\n# 判断系统架构\n# 输出：arm、arm64、x64\nget_machine_architecture() {\n    eval $invocation\n\n    if command -v uname >/dev/null; then\n        CPUName=$(uname -m)\n        case $CPUName in\n        armv*l)\n            echo \"arm\"\n            return 0\n            ;;\n        aarch64 | arm64)\n            echo \"arm64\"\n            return 0\n            ;;\n        esac\n    fi\n\n    # Always default to 'x64'\n    echo \"x64\"\n    return 0\n}\n\n# 获取linux系统名称\n# 输出：debian.10、debian.11、debian.12、ubuntu.20.04、ubuntu.22.04、alpine.3.4.3...\nget_linux_platform_name() {\n    eval $invocation\n\n    if [ -e /etc/os-release ]; then\n        . /etc/os-release\n        echo \"$ID${VERSION_ID:+.${VERSION_ID}}\"\n        return 0\n    elif [ -e /etc/redhat-release ]; then\n        local redhatRelease=$(</etc/redhat-release)\n        if [[ $redhatRelease == \"CentOS release 6.\"* || $redhatRelease == \"Red Hat Enterprise Linux \"*\" release 6.\"* ]]; then\n            echo \"rhel.6\"\n            return 1\n        fi\n    fi\n\n    echo \"Linux specific platform name and version could not be detected: UName = $uname\"\n    return 1\n}\n\n# 判断是否为musl（一般指alpine）\nis_musl_based_distro() {\n    eval $invocation\n\n    (ldd --version 2>&1 || true) | grep -q musl\n}\n\n# 获取当前系统名称\n# 输出：linux、linux-musl、osx、freebsd\nget_current_os_name() {\n    eval $invocation\n\n    local uname=$(uname)\n    if [ \"$uname\" = \"Darwin\" ]; then\n        say_warning \"当前系统：osx\"\n        echo \"osx\"\n        return 1\n    elif [ \"$uname\" = \"FreeBSD\" ]; then\n        say_warning \"当前系统：freebsd\"\n        echo \"freebsd\"\n        return 1\n    elif [ \"$uname\" = \"Linux\" ]; then\n        local linux_platform_name=\"\"\n        linux_platform_name=\"$(get_linux_platform_name)\" || true\n        say \"当前系统发行版本：$linux_platform_name\"\n\n        if [ \"$linux_platform_name\" = \"rhel.6\" ]; then\n            echo $linux_platform_name\n            return 1\n        elif is_musl_based_distro; then\n            echo \"linux-musl\"\n            return 0\n        elif [ \"$linux_platform_name\" = \"linux-musl\" ]; then\n            echo \"linux-musl\"\n            return 0\n        else\n            echo \"linux\"\n            return 0\n        fi\n    fi\n\n    say_err \"OS name could not be detected: UName = $uname\"\n    return 1\n}\n\n# 检查操作系统\ncheck_os() {\n    eval $invocation\n\n    current_os=\"$(get_current_os_name)\"\n    say \"当前系统：$current_os\"\n\n    machine_architecture=\"$(get_machine_architecture)\"\n    say \"当前架构：$machine_architecture\"\n\n    if [ \"$current_os\" = \"linux\" ]; then\n        current_linux_os=\"debian\" # 当前青龙只有debian和aplpine两种\n        if ! machine_has curl; then\n            say \"curl未安装，开始安装依赖...\"\n            apt-get update\n            apt-get install -y curl\n        fi\n    else\n        current_linux_os=\"alpine\"\n        if ! machine_has curl; then\n            say \"curl未安装，开始安装依赖...\"\n            apk update\n            apk add -y curl\n        fi\n    fi\n\n    say \"当前选择的运行方式：$prefer_mode\"\n}\n\n# 检查安装jq\ncheck_jq() {\n    if [ \"$current_linux_os\" = \"debian\" ]; then\n        if ! machine_has jq; then\n            say \"jq未安装，开始安装依赖...\"\n            apt-get update\n            apt-get install -y jq\n        fi\n    else\n        if ! machine_has jq; then\n            say \"jq未安装，开始安装依赖...\"\n            apk update\n            apk add -y jq\n        fi\n    fi\n}\n\n# 检查安装unzip\ncheck_unzip() {\n    if [ \"$current_linux_os\" = \"debian\" ]; then\n        if ! machine_has unzip; then\n            say \"unzip未安装，开始安装依赖...\"\n            apt-get update\n            apt-get install -y unzip\n        fi\n    else\n        if ! machine_has unzip; then\n            say \"jq未安装，开始安装依赖...\"\n            apk update\n            apk add -y unzip\n        fi\n    fi\n}\n\n# 检查dotnet\ncheck_dotnet() {\n    eval $invocation\n\n    dotnetVersion=$(dotnet --version)\n    say \"当前dotnet版本：$dotnetVersion\"\n    if [[ $(echo \"$dotnetVersion\" | grep -oE '^[0-9]+') -ge 8 ]]; then\n        say \"已安装，且版本满足\"\n        say \"which dotnet: $(which dotnet)\"\n        return 0\n    else\n        say \"未安装\"\n        return 1\n    fi\n}\n\n# 检查bilitool\ncheck_bilitool() {\n    eval $invocation\n\n    TAG_FILE=\"./tag.txt\"\n    touch $TAG_FILE\n    local STORED_TAG=$(cat $TAG_FILE 2>/dev/null)\n\n    #如果STORED_TAG为空，则返回1\n    if [[ -z $STORED_TAG ]]; then\n        say \"tag.txt为空，未安装过\"\n        return 1\n    fi\n\n    say \"tag.txt记录的版本：$STORED_TAG\"\n\n    # 查找当前目录下是否有叫Ray.BiliBiliTool.Console的文件\n    if [ -f \"./Ray.BiliBiliTool.Console\" ]; then\n        say \"bilitool已安装\"\n        bilitool_installed_version=$STORED_TAG\n        return 0\n    else\n        say \"bilitool未安装\"\n        return 1\n    fi\n}\n\n# 检查环境\ncheck_installed() {\n    eval $invocation\n\n    if [ \"$prefer_mode\" == \"dotnet\" ]; then\n        check_dotnet\n        return $?\n    fi\n\n    if [ \"$prefer_mode\" == \"bilitool\" ]; then\n        check_bilitool\n        return $?\n    fi\n\n    return 1\n}\n\n# 使用官方脚本安装dotnet\ninstall_dotnet_by_script() {\n    eval $invocation\n\n    say \"再尝试使用官方脚本安装\"\n    curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 8.0 --verbose\n\n    say \"添加到PATH\"\n    local exportFile=\"/root/.bashrc\"\n    touch $exportFile\n    echo '' >>$exportFile\n    echo 'export DOTNET_ROOT=$HOME/.dotnet' >>$exportFile\n    echo 'export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools' >>$exportFile\n    . $exportFile\n}\n\n# 安装dotnet环境\ninstall_dotnet() {\n    eval $invocation\n\n    say \"开始安装dotnet\"\n    say \"当前系统：$current_linux_os\"\n    if [[ $current_linux_os == \"debian\" ]]; then\n        say \"使用apt安装\"\n\n        if ! (curl -s -m 5 www.google.com >/dev/nul); then\n            say \"机器位于墙内，切换为包源为国内镜像源\"\n            cp /etc/apt/sources.list /etc/apt/sources.list.bak\n            sed -i 's/https:\\/\\/deb.debian.org/https:\\/\\/mirrors.ustc.edu.cn/g' /etc/apt/sources.list\n            sed -i 's/http:\\/\\/deb.debian.org/https:\\/\\/mirrors.ustc.edu.cn/g' /etc/apt/sources.list\n            apt-get update\n        fi\n        {\n            . /etc/os-release\n            curl -o packages-microsoft-prod.deb https://packages.microsoft.com/config/debian/$VERSION_ID/packages-microsoft-prod.deb\n            dpkg -i packages-microsoft-prod.deb\n            rm packages-microsoft-prod.deb\n            apt-get update && apt-get install -y dotnet-sdk-8.0\n        } || {\n            install_dotnet_by_script\n        }\n    else\n        say \"使用apk安装\"\n        if ! (curl -s -m 5 www.google.com >/dev/nul); then\n            say \"机器位于墙内，切换为包源为国内镜像源\"\n            cp /etc/apk/repositories /etc/apk/repositories.bak\n            sed -i 's/https:\\/\\/dl-cdn.alpinelinux.org/https:\\/\\/mirrors.ustc.edu.cn/g' /etc/apk/repositories\n            sed -i 's/http:\\/\\/dl-cdn.alpinelinux.org/https:\\/\\/mirrors.ustc.edu.cn/g' /etc/apk/repositories\n            apk update\n        fi\n        {\n            apk add dotnet8-sdk # https://pkgs.alpinelinux.org/packages\n        } || {\n            install_dotnet_by_script\n        }\n    fi\n    dotnet --version && say \"which dotnet: $(which dotnet)\" && say \"安装成功\"\n    return $?\n}\n\n# 从github获取bilitool下载地址\nget_download_url() {\n    eval $invocation\n\n    tag=$1\n    url=\"${github_proxy}https://github.com/RayWangQvQ/BiliBiliToolPro/releases/download/$tag/bilibili-tool-pro-v$tag-$current_os-$machine_architecture.zip\"\n    say \"下载地址：$url\"\n    echo $url\n    return 0\n}\n\n# 安装bilitool\ninstall_bilitool() {\n    eval $invocation\n\n    say \"开始安装bilitool\"\n    # 获取最新的release信息\n    LATEST_RELEASE=$(curl -s https://api.github.com/repos/$bili_repo/releases/latest)\n\n    # 解析最新的tag名称\n    check_jq\n    LATEST_TAG=$(echo $LATEST_RELEASE | jq -r '.tag_name')\n    say \"最新版本：$LATEST_TAG\"\n\n    # 读取之前存储的tag并比较\n    if [ \"$LATEST_TAG\" != \"$bilitool_installed_version\" ]; then\n        # 如果不一样，则需要更新安装\n        ASSET_URL=$(get_download_url $LATEST_TAG)\n\n        # 使用curl下载文件到当前目录下的test.zip文件\n        local zip_file_name=\"bilitool-$LATEST_TAG.zip\"\n        curl -L -o \"$zip_file_name\" $ASSET_URL\n\n        # 解压\n        check_unzip\n        unzip -jo \"$zip_file_name\" -d ./ &&\n            rm \"$zip_file_name\" &&\n            rm -f appsettings.*\n\n        # 更新tag.txt文件\n        echo $LATEST_TAG >./tag.txt\n    else\n        say \"已经是最新版本，无需下载。\"\n    fi\n}\n\n## 安装dotnet（如果未安装过）\ninstall() {\n    eval $invocation\n\n    if check_installed; then\n        say \"环境正常，本次无需安装\"\n    else\n        say \"开始安装环境\"\n        if [ \"$prefer_mode\" == \"dotnet\" ]; then\n            install_dotnet || {\n                say_err \"安装失败\"\n                say_err \"请根据文档自行在青龙容器中安装dotnet：https://learn.microsoft.com/zh-cn/dotnet/core/install/linux-$current_linux_os\"\n                say_err \"或者尝试切换运行模式为bilitool，它不需要安装dotnet：https://github.com/RayWangQvQ/BiliBiliToolPro/blob/develop/qinglong/README.md\"\n            }\n        fi\n\n        if [ \"$prefer_mode\" == \"bilitool\" ]; then\n            install_bilitool || {\n                say_err \"安装失败，请检查日志并重试\"\n                say_err \"或者尝试切换运行模式为dotnet：https://github.com/RayWangQvQ/BiliBiliToolPro/blob/develop/qinglong/README.md\"\n            }\n        fi\n    fi\n}\n\n# 运行bilitool任务\nrun_task() {\n    eval $invocation\n\n    local target_code=$1\n\n    export Ray_PlatformType=QingLong\n    export Ray_RunTasks=$target_code\n\n    cd $qinglong_bili_repo_dir/src/Ray.BiliBiliTool.Console\n\n    if [ \"$prefer_mode\" == \"dotnet\" ]; then\n        dotnet run --ENVIRONMENT=Production\n    else\n        cp -f $qinglong_bili_repo_dir/bin/Ray.BiliBiliTool.Console .\n        chmod +x ./Ray.BiliBiliTool.Console && ./Ray.BiliBiliTool.Console --ENVIRONMENT=Production\n    fi\n}\n\ncheck_os\ninstall\n"
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_charge.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 12 * * *\n# new Env(\"bili免费B币券充电任务\")\n\n. bili_task_base.sh\n\ntarget_task_code=\"Charge\"\nrun_task \"${target_task_code}\"\n"
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_daily.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 9 * * *\n# new Env(\"bili每日任务\")\n\n. bili_task_base.sh\n\ntarget_task_code=\"Daily\"\nrun_task \"${target_task_code}\""
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_liveFansMedal.sh",
    "content": "#!/usr/bin/env bash\n# cron:5 0 * * *\n# new Env(\"bili直播粉丝牌\")\n\n. bili_task_base.sh\n\ntarget_task_code=\"LiveFansMedal\"\nrun_task \"${target_task_code}\""
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_liveLottery.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 13 * * *\n# new Env(\"bili天选时刻\")\n\n. bili_task_base.sh\n\ntarget_task_code=\"LiveLottery\"\nrun_task \"${target_task_code}\""
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_login.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 0 1 1 *\n# new Env(\"bili扫码登录\")\n\n. bili_task_base.sh\n\ntarget_task_code=\"Login\"\nrun_task \"${target_task_code}\""
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_manga.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 14 * * *\n# new Env(\"bili漫画任务\")\n\n. bili_task_base.sh\n\ntarget_task_code=\"Manga\"\nrun_task \"${target_task_code}\"\n"
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_manga_privilege.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 15 * * *\n# new Env(\"bili领取大会员漫画权益任务\")\n\n. bili_task_base.sh\n\ntarget_task_code=\"MangaPrivilege\"\nrun_task \"${target_task_code}\"\n"
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_silver2coin.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 8 * * *\n# new Env(\"bili银瓜子兑换硬币任务\")\n\n. bili_task_base.sh\n\ntarget_task_code=\"Silver2Coin\"\nrun_task \"${target_task_code}\"\n"
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_test.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 8 * * *\n# new Env(\"bili测试ck\")\n\n. bili_task_base.sh\n\ntarget_task_code=\"Test\"\nrun_task \"${target_task_code}\""
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_tryFix.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 0 1 1 *\n# new Env(\"bili尝试修复异常\")\n\ndir_shell=$QL_DIR/shell\n. $dir_shell/share.sh\n. /root/.bashrc\n\nbili_repo=\"raywangqvq_bilibilitoolpro\"\nbili_branch=\"\"\n\necho \"青龙repo目录: $dir_repo\"\nqinglong_bili_repo=\"$(echo \"$bili_repo\" | sed 's/\\//_/g')${bili_branch}\"\nqinglong_bili_repo_dir=\"$(find $dir_repo -type d \\( -iname $qinglong_bili_repo -o -iname ${qinglong_bili_repo}_main \\) | head -1)\"\necho \"bili仓库目录: $qinglong_bili_repo_dir\"\n\necho -e \"清理缓存...\\n\"\ncd $qinglong_bili_repo_dir\nfind . -type d -name \"bin\" -exec rm -rf {} +\nfind . -type d -name \"obj\" -exec rm -rf {} +\necho -e \"清理完成\\n\"\n\necho \"检测dotnet...\"\ndotnetVersion=$(dotnet --version)\necho \"当前dotnet版本：$dotnetVersion\"\nif [[ $(echo \"$dotnetVersion\" | grep -oE '^[0-9]+') -ge 8 ]]; then\n    echo \"已安装，且版本满足\"\nelse\n    echo \"which dotnet: $(which dotnet)\"\n    echo \"Path: $PATH\"\n    rm -f /usr/local/bin/dotnet\nfi\necho \"检测dotnet结束\""
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_unfollowBatched.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 12 1 * *\n# new Env(\"bili批量取关主播\")\n\n. bili_task_base.sh\n\ntarget_task_code=\"UnfollowBatched\"\nrun_task \"${target_task_code}\""
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_vipBigPoint.sh",
    "content": "#!/usr/bin/env bash\n# cron:7 1 * * *\n# new Env(\"bili大会员大积分\")\n\n. bili_task_base.sh\n\ntarget_task_code=\"VipBigPoint\"\nrun_task \"${target_task_code}\""
  },
  {
    "path": "qinglong/DefaultTasks/bili_task_vip_privilege.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 1 * * *\n# new Env(\"bili领取大会员福利任务\")\n\n. bili_task_base.sh\n\ntarget_task_code=\"VipPrivilege\"\nrun_task \"${target_task_code}\"\n"
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_base.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 0 1 1 *\n# new Env(\"bili_dev_task_base\");\n\n# Stop script on NZEC\nset -e\n# Stop script if unbound variable found (use ${var:-} if intentional)\nset -u\n# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success\n# This is causing it to fail\nset -o pipefail\n\nverbose=false                          # 开启debug日志\nbili_repo=\"raywangqvq/bilibilitoolpro\" # 仓库地址\nbili_branch=\"_develop\"                 # 分支名，空或_develop\nprefer_mode=${BILI_MODE:-\"dotnet\"}     # dotnet或bilitool，需要通过环境变量配置\ngithub_proxy=${BILI_GITHUB_PROXY:-\"\"}  # 下载github release包时使用的代理，会拼在地址前面，需要通过环境变量配置\nexport DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 # 解决抽风问题\n\n# Use in the the functions: eval $invocation\ninvocation='say_verbose \"Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}\"'\n\n# standard output may be used as a return value in the functions\n# we need a way to write text on the screen in the functions so that\n# it won't interfere with the return value.\n# Exposing stream 3 as a pipe to standard output of the script itself\nexec 3>&1\n\n# Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors.\n# See if stdout is a terminal\nif [ -t 1 ] && command -v tput >/dev/null; then\n    # see if it supports colors\n    ncolors=$(tput colors || echo 0)\n    if [ -n \"$ncolors\" ] && [ $ncolors -ge 8 ]; then\n        bold=\"$(tput bold || echo)\"\n        normal=\"$(tput sgr0 || echo)\"\n        black=\"$(tput setaf 0 || echo)\"\n        red=\"$(tput setaf 1 || echo)\"\n        green=\"$(tput setaf 2 || echo)\"\n        yellow=\"$(tput setaf 3 || echo)\"\n        blue=\"$(tput setaf 4 || echo)\"\n        magenta=\"$(tput setaf 5 || echo)\"\n        cyan=\"$(tput setaf 6 || echo)\"\n        white=\"$(tput setaf 7 || echo)\"\n    fi\nfi\n\nsay_warning() {\n    printf \"%b\\n\" \"${yellow:-}bilitool: Warning: $1${normal:-}\" >&3\n}\n\nsay_err() {\n    printf \"%b\\n\" \"${red:-}bilitool: Error: $1${normal:-}\" >&2\n}\n\nsay() {\n    # using stream 3 (defined in the beginning) to not interfere with stdout of functions\n    # which may be used as return value\n    printf \"%b\\n\" \"${cyan:-}bilitool:${normal:-} $1\" >&3\n}\n\nsay_verbose() {\n    if [ \"$verbose\" = true ]; then\n        say \"$1\"\n    fi\n}\n\nQL_DIR=${QL_DIR:-\"/ql\"}\nQL_BRANCH=${QL_BRANCH:-\"develop\"}\nDefaultCronRule=${DefaultCronRule:-\"\"}\nCpuWarn=${CpuWarn:-\"\"}\nMemoryWarn=${MemoryWarn:-\"\"}\nDiskWarn=${DiskWarn:-\"\"}\n\ndir_repo=${dir_repo:-\"$QL_DIR/data/repo\"}\n# 需要兼容老版本青龙，https://github.com/RayWangQvQ/BiliBiliToolPro/issues/728\nif [ ! -d \"$dir_repo\" ] && [ -d \"$QL_DIR/repo\" ]; then\n  dir_repo=\"$QL_DIR/repo\"\nfi\ndir_shell=$QL_DIR/shell\ntouch $dir_shell/env.sh && . $dir_shell/env.sh\ntouch /root/.bashrc && . /root/.bashrc\n\n# 目录\nsay \"青龙repo目录: $dir_repo\"\nqinglong_bili_repo=\"$(echo \"$bili_repo\" | sed 's/\\//_/g')${bili_branch}\"\nqinglong_bili_repo_dir=\"$(find $dir_repo -type d \\( -iname $qinglong_bili_repo -o -iname ${qinglong_bili_repo}_main \\) | head -1)\"\nsay \"bili仓库目录: $qinglong_bili_repo_dir\"\n\ncurrent_linux_os=\"debian\"  # 或alpine\ncurrent_os=\"linux\"         # 或linux-musl\nmachine_architecture=\"x64\" # 或arm、arm64\n\nbilitool_installed_version=0\n\n# 以下操作仅在bilitool仓库的根bin文件下执行\ncd $qinglong_bili_repo_dir\nmkdir -p bin && cd $qinglong_bili_repo_dir/bin\n\n# 判断是否存在某指令\nmachine_has() {\n    eval $invocation\n\n    command -v \"$1\" >/dev/null 2>&1\n    return $?\n}\n\n# 判断系统架构\n# 输出：arm、arm64、x64\nget_machine_architecture() {\n    eval $invocation\n\n    if command -v uname >/dev/null; then\n        CPUName=$(uname -m)\n        case $CPUName in\n        armv*l)\n            echo \"arm\"\n            return 0\n            ;;\n        aarch64 | arm64)\n            echo \"arm64\"\n            return 0\n            ;;\n        esac\n    fi\n\n    # Always default to 'x64'\n    echo \"x64\"\n    return 0\n}\n\n# 获取linux系统名称\n# 输出：debian.10、debian.11、debian.12、ubuntu.20.04、ubuntu.22.04、alpine.3.4.3...\nget_linux_platform_name() {\n    eval $invocation\n\n    if [ -e /etc/os-release ]; then\n        . /etc/os-release\n        echo \"$ID${VERSION_ID:+.${VERSION_ID}}\"\n        return 0\n    elif [ -e /etc/redhat-release ]; then\n        local redhatRelease=$(</etc/redhat-release)\n        if [[ $redhatRelease == \"CentOS release 6.\"* || $redhatRelease == \"Red Hat Enterprise Linux \"*\" release 6.\"* ]]; then\n            echo \"rhel.6\"\n            return 1\n        fi\n    fi\n\n    echo \"Linux specific platform name and version could not be detected: UName = $uname\"\n    return 1\n}\n\n# 判断是否为musl（一般指alpine）\nis_musl_based_distro() {\n    eval $invocation\n\n    (ldd --version 2>&1 || true) | grep -q musl\n}\n\n# 获取当前系统名称\n# 输出：linux、linux-musl、osx、freebsd\nget_current_os_name() {\n    eval $invocation\n\n    local uname=$(uname)\n    if [ \"$uname\" = \"Darwin\" ]; then\n        say_warning \"当前系统：osx\"\n        echo \"osx\"\n        return 1\n    elif [ \"$uname\" = \"FreeBSD\" ]; then\n        say_warning \"当前系统：freebsd\"\n        echo \"freebsd\"\n        return 1\n    elif [ \"$uname\" = \"Linux\" ]; then\n        local linux_platform_name=\"\"\n        linux_platform_name=\"$(get_linux_platform_name)\" || true\n        say \"当前系统发行版本：$linux_platform_name\"\n\n        if [ \"$linux_platform_name\" = \"rhel.6\" ]; then\n            echo $linux_platform_name\n            return 1\n        elif is_musl_based_distro; then\n            echo \"linux-musl\"\n            return 0\n        elif [ \"$linux_platform_name\" = \"linux-musl\" ]; then\n            echo \"linux-musl\"\n            return 0\n        else\n            echo \"linux\"\n            return 0\n        fi\n    fi\n\n    say_err \"OS name could not be detected: UName = $uname\"\n    return 1\n}\n\n# 检查操作系统\ncheck_os() {\n    eval $invocation\n\n    current_os=\"$(get_current_os_name)\"\n    say \"当前系统：$current_os\"\n\n    machine_architecture=\"$(get_machine_architecture)\"\n    say \"当前架构：$machine_architecture\"\n\n    if [ \"$current_os\" = \"linux\" ]; then\n        current_linux_os=\"debian\" # 当前青龙只有debian和aplpine两种\n        if ! machine_has curl; then\n            say \"curl未安装，开始安装依赖...\"\n            apt-get update\n            apt-get install -y curl\n        fi\n    else\n        current_linux_os=\"alpine\"\n        if ! machine_has curl; then\n            say \"curl未安装，开始安装依赖...\"\n            apk update\n            apk add -y curl\n        fi\n    fi\n\n    say \"当前选择的运行方式：$prefer_mode\"\n}\n\n# 检查安装jq\ncheck_jq() {\n    if [ \"$current_linux_os\" = \"debian\" ]; then\n        if ! machine_has jq; then\n            say \"jq未安装，开始安装依赖...\"\n            apt-get update\n            apt-get install -y jq\n        fi\n    else\n        if ! machine_has jq; then\n            say \"jq未安装，开始安装依赖...\"\n            apk update\n            apk add -y jq\n        fi\n    fi\n}\n\n# 检查安装unzip\ncheck_unzip() {\n    if [ \"$current_linux_os\" = \"debian\" ]; then\n        if ! machine_has unzip; then\n            say \"unzip未安装，开始安装依赖...\"\n            apt-get update\n            apt-get install -y unzip\n        fi\n    else\n        if ! machine_has unzip; then\n            say \"jq未安装，开始安装依赖...\"\n            apk update\n            apk add -y unzip\n        fi\n    fi\n}\n\n# 检查dotnet\ncheck_dotnet() {\n    eval $invocation\n\n    dotnetVersion=$(dotnet --version)\n    say \"当前dotnet版本：$dotnetVersion\"\n    if [[ $(echo \"$dotnetVersion\" | grep -oE '^[0-9]+') -ge 8 ]]; then\n        say \"已安装，且版本满足\"\n        say \"which dotnet: $(which dotnet)\"\n        return 0\n    else\n        say \"未安装\"\n        return 1\n    fi\n}\n\n# 检查bilitool\ncheck_bilitool() {\n    eval $invocation\n\n    TAG_FILE=\"./tag.txt\"\n    touch $TAG_FILE\n    local STORED_TAG=$(cat $TAG_FILE 2>/dev/null)\n\n    #如果STORED_TAG为空，则返回1\n    if [[ -z $STORED_TAG ]]; then\n        say \"tag.txt为空，未安装过\"\n        return 1\n    fi\n\n    say \"tag.txt记录的版本：$STORED_TAG\"\n\n    # 查找当前目录下是否有叫Ray.BiliBiliTool.Console的文件\n    if [ -f \"./Ray.BiliBiliTool.Console\" ]; then\n        say \"bilitool已安装\"\n        bilitool_installed_version=$STORED_TAG\n        return 0\n    else\n        say \"bilitool未安装\"\n        return 1\n    fi\n}\n\n# 检查环境\ncheck_installed() {\n    eval $invocation\n\n    if [ \"$prefer_mode\" == \"dotnet\" ]; then\n        check_dotnet\n        return $?\n    fi\n\n    if [ \"$prefer_mode\" == \"bilitool\" ]; then\n        check_bilitool\n        return $?\n    fi\n\n    return 1\n}\n\n# 使用官方脚本安装dotnet\ninstall_dotnet_by_script() {\n    eval $invocation\n\n    say \"再尝试使用官方脚本安装\"\n    curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 8.0 --verbose\n\n    say \"添加到PATH\"\n    local exportFile=\"/root/.bashrc\"\n    touch $exportFile\n    echo '' >>$exportFile\n    echo 'export DOTNET_ROOT=$HOME/.dotnet' >>$exportFile\n    echo 'export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools' >>$exportFile\n    . $exportFile\n}\n\n# 安装dotnet环境\ninstall_dotnet() {\n    eval $invocation\n\n    say \"开始安装dotnet\"\n    say \"当前系统：$current_linux_os\"\n    if [[ $current_linux_os == \"debian\" ]]; then\n        say \"使用apt安装\"\n\n        if ! (curl -s -m 5 www.google.com >/dev/nul); then\n            say \"机器位于墙内，切换为包源为国内镜像源\"\n            cp /etc/apt/sources.list /etc/apt/sources.list.bak\n            sed -i 's/https:\\/\\/deb.debian.org/https:\\/\\/mirrors.ustc.edu.cn/g' /etc/apt/sources.list\n            sed -i 's/http:\\/\\/deb.debian.org/https:\\/\\/mirrors.ustc.edu.cn/g' /etc/apt/sources.list\n            apt-get update\n        fi\n        {\n            . /etc/os-release\n            curl -o packages-microsoft-prod.deb https://packages.microsoft.com/config/debian/$VERSION_ID/packages-microsoft-prod.deb\n            dpkg -i packages-microsoft-prod.deb\n            rm packages-microsoft-prod.deb\n            apt-get update && apt-get install -y dotnet-sdk-8.0\n        } || {\n            install_dotnet_by_script\n        }\n    else\n        say \"使用apk安装\"\n        if ! (curl -s -m 5 www.google.com >/dev/nul); then\n            say \"机器位于墙内，切换为包源为国内镜像源\"\n            cp /etc/apk/repositories /etc/apk/repositories.bak\n            sed -i 's/https:\\/\\/dl-cdn.alpinelinux.org/https:\\/\\/mirrors.ustc.edu.cn/g' /etc/apk/repositories\n            sed -i 's/http:\\/\\/dl-cdn.alpinelinux.org/https:\\/\\/mirrors.ustc.edu.cn/g' /etc/apk/repositories\n            apk update\n        fi\n        {\n            apk add dotnet8-sdk # https://pkgs.alpinelinux.org/packages\n        } || {\n            install_dotnet_by_script\n        }\n    fi\n    dotnet --version && say \"which dotnet: $(which dotnet)\" && say \"安装成功\"\n    return $?\n}\n\n# 从github获取bilitool下载地址\nget_download_url() {\n    eval $invocation\n\n    tag=$1\n    url=\"${github_proxy}https://github.com/RayWangQvQ/BiliBiliToolPro/releases/download/$tag/bilibili-tool-pro-v$tag-$current_os-$machine_architecture.zip\"\n    say \"下载地址：$url\"\n    echo $url\n    return 0\n}\n\n# 安装bilitool\ninstall_bilitool() {\n    eval $invocation\n\n    say \"开始安装bilitool\"\n    # 获取最新的release信息\n    LATEST_RELEASE=$(curl -s https://api.github.com/repos/$bili_repo/releases/latest)\n\n    # 解析最新的tag名称\n    check_jq\n    LATEST_TAG=$(echo $LATEST_RELEASE | jq -r '.tag_name')\n    say \"最新版本：$LATEST_TAG\"\n\n    # 读取之前存储的tag并比较\n    if [ \"$LATEST_TAG\" != \"$bilitool_installed_version\" ]; then\n        # 如果不一样，则需要更新安装\n        ASSET_URL=$(get_download_url $LATEST_TAG)\n\n        # 使用curl下载文件到当前目录下的test.zip文件\n        local zip_file_name=\"bilitool-$LATEST_TAG.zip\"\n        curl -L -o \"$zip_file_name\" $ASSET_URL\n\n        # 解压\n        check_unzip\n        unzip -jo \"$zip_file_name\" -d ./ &&\n            rm \"$zip_file_name\" &&\n            rm -f appsettings.*\n\n        # 更新tag.txt文件\n        echo $LATEST_TAG >./tag.txt\n    else\n        say \"已经是最新版本，无需下载。\"\n    fi\n}\n\n## 安装dotnet（如果未安装过）\ninstall() {\n    eval $invocation\n\n    if check_installed; then\n        say \"环境正常，本次无需安装\"\n    else\n        say \"开始安装环境\"\n        if [ \"$prefer_mode\" == \"dotnet\" ]; then\n            install_dotnet || {\n                say_err \"安装失败\"\n                say_err \"请根据文档自行在青龙容器中安装dotnet：https://learn.microsoft.com/zh-cn/dotnet/core/install/linux-$current_linux_os\"\n                say_err \"或者尝试切换运行模式为bilitool，它不需要安装dotnet：https://github.com/RayWangQvQ/BiliBiliToolPro/blob/develop/qinglong/README.md\"\n            }\n        fi\n\n        if [ \"$prefer_mode\" == \"bilitool\" ]; then\n            install_bilitool || {\n                say_err \"安装失败，请检查日志并重试\"\n                say_err \"或者尝试切换运行模式为dotnet：https://github.com/RayWangQvQ/BiliBiliToolPro/blob/develop/qinglong/README.md\"\n            }\n        fi\n    fi\n}\n\n# 运行bilitool任务\nrun_task() {\n    eval $invocation\n\n    local target_code=$1\n\n    export Ray_PlatformType=QingLong\n    export Ray_RunTasks=$target_code\n\n    cd $qinglong_bili_repo_dir/src/Ray.BiliBiliTool.Console\n\n    if [ \"$prefer_mode\" == \"dotnet\" ]; then\n        dotnet run --ENVIRONMENT=Production\n    else\n        cp -f $qinglong_bili_repo_dir/bin/Ray.BiliBiliTool.Console .\n        chmod +x ./Ray.BiliBiliTool.Console && ./Ray.BiliBiliTool.Console --ENVIRONMENT=Production\n    fi\n}\n\ncheck_os\ninstall\n"
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_charge.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 12 * * *\n# new Env(\"bili免费B币券充电任务[dev先行版]\")\n\n. bili_dev_task_base.sh\n\ntarget_task_code=\"Charge\"\nrun_task \"${target_task_code}\"\n"
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_daily.sh",
    "content": "#!/usr/bin/env bash\n# cron:5 9 * * *\n# new Env('bili每日任务[dev先行版]');\n\n. bili_dev_task_base.sh\n\ntarget_task_code=\"Daily\"\nrun_task \"${target_task_code}\"\n"
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_liveFansMedal.sh",
    "content": "#!/usr/bin/env bash\n# cron:5 0 * * *\n# new Env(\"bili直播粉丝牌[dev先行版]\")\n\n. bili_dev_task_base.sh\n\ntarget_task_code=\"LiveFansMedal\"\nrun_task \"${target_task_code}\""
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_liveLottery.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 13 * * *\n# new Env(\"bili天选时刻[dev先行版]\")\n\n. bili_dev_task_base.sh\n\ntarget_task_code=\"LiveLottery\"\nrun_task \"${target_task_code}\""
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_login.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 0 1 1 *\n# new Env(\"bili扫码登录[dev先行版]\")\n\n. bili_dev_task_base.sh\n\ntarget_task_code=\"Login\"\nrun_task \"${target_task_code}\""
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_manga.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 14 * * *\n# new Env(\"bili漫画任务[dev先行版]\")\n\n. bili_dev_task_base.sh\n\ntarget_task_code=\"Manga\"\nrun_task \"${target_task_code}\"\n"
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_manga_privilege.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 15 * * *\n# new Env(\"bili领取大会员漫画权益任务[dev先行版]\")\n\n. bili_dev_task_base.sh\n\ntarget_task_code=\"MangaPrivilege\"\nrun_task \"${target_task_code}\"\n"
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_silver2coin.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 8 * * *\n# new Env(\"bili银瓜子兑换硬币任务[dev先行版]\")\n\n. bili_dev_task_base.sh\n\ntarget_task_code=\"Silver2Coin\"\nrun_task \"${target_task_code}\"\n"
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_test.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 8 * * *\n# new Env(\"bili测试ck[dev先行版]\")\n\n. bili_dev_task_base.sh\n\ntarget_task_code=\"Test\"\nrun_task \"${target_task_code}\""
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_tryFix.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 0 1 1 *\n# new Env(\"bili尝试修复异常[dev先行版]\")\n\ndir_shell=$QL_DIR/shell\n. $dir_shell/share.sh\n. /root/.bashrc\n\nbili_repo=\"raywangqvq/bilibilitoolpro\"\nbili_branch=\"_develop\" \n\necho \"青龙repo目录: $dir_repo\"\nqinglong_bili_repo=\"$(echo \"$bili_repo\" | sed 's/\\//_/g')${bili_branch}\"\nqinglong_bili_repo_dir=\"$(find $dir_repo -type d \\( -iname $qinglong_bili_repo -o -iname ${qinglong_bili_repo}_main \\) | head -1)\"\necho \"bili仓库目录: $qinglong_bili_repo_dir\"\n\n\necho -e \"清理缓存...\\n\"\ncd $qinglong_bili_repo_dir\nfind . -type d -name \"bin\" -exec rm -rf {} +\nfind . -type d -name \"obj\" -exec rm -rf {} +\necho -e \"清理完成\\n\"\n\necho \"检测dotnet...\"\ndotnetVersion=$(dotnet --version)\necho \"当前dotnet版本：$dotnetVersion\"\nif [[ $(echo \"$dotnetVersion\" | grep -oE '^[0-9]+') -ge 8 ]]; then\n    echo \"已安装，且版本满足\"\nelse\n    echo \"which dotnet: $(which dotnet)\"\n    echo \"Path: $PATH\"\n    rm -f /usr/local/bin/dotnet\nfi\necho \"检测dotnet结束\""
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_unfollowBatched.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 12 1 * *\n# new Env(\"bili批量取关主播[dev先行版]\")\n\n. bili_dev_task_base.sh\n\ntarget_task_code=\"UnfollowBatched\"\nrun_task \"${target_task_code}\""
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_vipBigPoint.sh",
    "content": "#!/usr/bin/env bash\n# cron:7 1 * * *\n# new Env(\"bili大会员大积分[dev先行版]\")\n\n. bili_dev_task_base.sh\n\ntarget_task_code=\"VipBigPoint\"\nrun_task \"${target_task_code}\""
  },
  {
    "path": "qinglong/DefaultTasks/dev/bili_dev_task_vip_privilege.sh",
    "content": "#!/usr/bin/env bash\n# cron:0 1 * * *\n# new Env(\"bili领取大会员福利任务[dev先行版]\")\n\n. bili_dev_task_base.sh\n\ntarget_task_code=\"VipPrivilege\"\nrun_task \"${target_task_code}\"\n"
  },
  {
    "path": "qinglong/README.md",
    "content": "# 在青龙中运行\n\n原理是，利用青龙的拉库命令，拉取本仓库源码，自动添加cron定时任务，然后在青龙容器中安装`dotnet`环境或`bilitool`的二进制包，定时运行相应的Task。\n\n开始前，请先确保你的青龙面板是运行正常的。\n\n<!-- TOC depthFrom:2 -->\n\n- [1. 步骤](#1-步骤)\n    - [1.1. 登录青龙面板并修改配置](#11-登录青龙面板并修改配置)\n    - [1.2. 在青龙面板中添加拉库定时任务](#12-在青龙面板中添加拉库定时任务)\n        - [1.2.1. 方式一：订阅管理](#121-方式一订阅管理)\n        - [1.2.2. 方式二：定时任务拉库](#122-方式二定时任务拉库)\n    - [1.3. 检查定时任务](#13-检查定时任务)\n    - [1.4. 配置青龙Client Secret（可选）](#14-配置青龙client-secret可选)\n        - [1.4.1. 新建 Application](#141-新建-application)\n        - [1.4.2. 密钥配置到环境变量](#142-密钥配置到环境变量)\n    - [1.5. Bili登录](#15-bili登录)\n- [2. 先行版](#2-先行版)\n- [3. GitHub加速](#3-github加速)\n- [4. 常见问题](#4-常见问题)\n    - [4.1. 安装dotnet失败怎么办法](#41-安装dotnet失败怎么办法)\n    - [4.2. Couldn't find a valid ICU package installed on the system](#42-couldnt-find-a-valid-icu-package-installed-on-the-system)\n    - [4.3. 提示文件不存在或路径异常，怎么排查](#43-提示文件不存在或路径异常怎么排查)\n    - [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)\n\n<!-- /TOC -->\n\n## 1. 步骤\n\n### 1.1. 登录青龙面板并修改配置\n青龙面板，`配置文件`页。\n\n修改 `RepoFileExtensions=\"js py\"` 为 `RepoFileExtensions=\"js py sh\"`\n\n保存配置。\n\n### 1.2. 在青龙面板中添加拉库定时任务\n\n两种方式，任选其一即可：\n\n#### 1.2.1. 方式一：订阅管理\n\n```\n名称：Bilibili\n类型：公开仓库\n链接：https://github.com/RayWangQvQ/BiliBiliToolPro.git\n定时类型：crontab\n定时规则：2 2 28 * *\n白名单：bili_task_.+\\.sh\n文件后缀：sh\n```\n\n没提到的不要动。\n\n保存后，点击运行按钮，运行拉库。\n\n#### 1.2.2. 方式二：定时任务拉库\n青龙面板，`定时任务`页，右上角`添加任务`，填入以下信息：\n\n```\n名称：拉取Bili库\n命令：ql repo https://github.com/RayWangQvQ/BiliBiliToolPro.git \"bili_task_\"\n定时规则：2 2 28 * *\n```\n\n点击确定。\n\n保存成功后，找到该定时任务，点击运行按钮，运行拉库。\n\n### 1.3. 检查定时任务\n\n如果正常，拉库成功后，会自动添加bilibili相关的task任务。\n\n![qinglong-tasks.png](../docs/imgs/qinglong-tasks.png)\n\n### 1.4. 配置青龙Client Secret（可选）\n\n扫码登录Bili后，需要有权限向青龙的环境变量中持久化Cookie，所以需要添加一个鉴权。\n\n青龙官方说明：https://qinglong.online/api/preparation\n\n#### 1.4.1. 新建 Application\n\n青龙 -> 系统设置 -> 应用设置，点击新建。\n\n![qinglong-application](../docs/imgs/qinglong-application.png)\n\n#### 1.4.2. 密钥配置到环境变量\n\n将上面2个值添加到环境变量中即可。\n\nName分别为：\n\n- Ray_QingLongConfig__ClientId\n- Ray_QingLongConfig__ClientSecret\n\n![qinglong-app-env](../docs/imgs/qinglong-application-key.png)\n\n\n### 1.5. Bili登录\n\n在青龙定时任务中，点击运行`bili扫码登录`任务，查看运行日志，扫描日志中的二维码进行登录。\n![qinglong-login.png](../docs/imgs/qinglong-login.png)\n\n登录成功后，如果已配置了上述的Application，会将cookie保存到青龙的环境变量中：\n\n![qinglong-env.png](../docs/imgs/qinglong-env.png)\n\n如果未配置Application，会打印出cookie，请手动自己到环境变量中添加。\n\n首次运行会自动安装环境，时间可能长一点，之后就不需要重复安装了。\n\n## 2. 先行版\n\n青龙拉库时可以指定分支，develop分支的代码会超前于默认的main分支，包含当前正在开发的新功能。\n\n想提前体验新功能，或想要Bug能快速得到解决的朋友，可以尝试切换先行版，但同时也意味着稳定性会相应降低（其实可以忽略不计~🤨）。\n\n```\n分支：develop\n白名单：bili_dev_task_.+\\.sh\n```\n\n其他选项同上。\n\n## 3. GitHub加速\n\n拉库时，如果服务器在国内，访问GitHub速度慢，可在仓库地址前加上加速代理进行加速。\n\n如：\n\n```\nhttps://github.moeyy.xyz/https://github.com/RayWangQvQ/BiliBiliToolPro.git\nhttps://gh-proxy.com/https://github.com/RayWangQvQ/BiliBiliToolPro.git\n...\n```\n\n加速代理地址通常不能保证长期稳定，请自行查找使用。\n\n## 4. 常见问题\n\n### 4.1. 安装dotnet失败怎么办法\n\n首先，青龙有两个版本的镜像：\n\n- alpine：whyour/qinglong:latest\n- debian：whyour/qinglong:debian\n\n安装dotnet失败的情况，几乎全发生在alpine版上。。。\n\n所以，如果你“执迷不悟”，就是一定要用alpine版，那请先通过日志自行排查，不行就根据微软官方文档，进入qinglong容器后，手动安装。\n\n如果还不行，那么可以切换到基于`bilitool`的二进制包运行方式，该方式不需要安装`dotnet`，方式：\n\n编辑青龙面板的`配置文件`，新增如下两行：\n\n```\nexport BILI_MODE=\"bilitool\" # bili运行模式，dotnet或bilitool\nexport BILI_GITHUB_PROXY=\"https://github.moeyy.xyz/\" # 下载二进制包时使用的加速代理，不要的话则置空\n```\n\n![qinglong-login.png](../docs/imgs/qinglong-run-as-bilitool.png)\n\nbilitool没有先行版的概念，因为只有main分支才会打包，更新会稍慢一点。\n\n另外，alpine版的问题，我不建议来提交issue，因为已经大大超出本项目的scope了，建议可以去给alpine官方或微软的dotnet官方提交issue。\n\n### 4.2. Couldn't find a valid ICU package installed on the system\n\n如 #266 ，需要在青龙面板的环境变量添加如下环境变量：\n\n```\n名称：DOTNET_SYSTEM_GLOBALIZATION_INVARIANT\n值：1\n```\n\n### 4.3. 提示文件不存在或路径异常，怎么排查\n\n需要`docker exec -it qinglong bash`后，查看几个常用路径：\n\n```\n/ql\n    /data\n        /repo\n    /scripts\n    /shell\n```\n\n- `/ql/dada/repo`目录下存储了拉库后，bilitool的源代码\n- `/ql/scripts`目录下存储了bilitool的定时运行脚本\n- `/ql/shell`目录下是青龙的基础脚本\n\n请cd到相应目录，查看该目录下文件是否存在，状态是否正常。\n\n### 4.4. The configured user limit (128) on the number of inotify instances has been reached\n\n报错：\n\n```\nAsp.Net Core - The configured user limit (128) on the number of inotify instances has been reached\n```\n\n可以尝试添加如下环境变量解决：\n\n```\nDOTNET_USE_POLLING_FILE_WATCHER=1\n```\n\n添加后，对配置变更事件的监听，会从监听 Linux 系统的 inotify 事件，变成定时轮询。"
  },
  {
    "path": "qinglong/bak/bili_dev_task_get_cookie.py.bak",
    "content": "'''\n1 9 11 11 1 bili_dev_task_get_cookie.py\n手动运行，查看日志，并使用手机B站app扫描日志中二维码，注意，只能修改第一个cookie\n如果产生错误，重新运行并用手机扫描二维码\n有可能识别不出来二维码，我测试了几次都能识别\n\n默认环境变量存放位置为/ql/data/config/env.sh\n可以自己通过docker命令进入容器查找这个文件位置。docker exec -it qinglong /bin/bash,进入青龙容器，然后查找一下这个文件位置\nfilename = '../config/env.sh'\n'''\n\nimport qrcode\nimport requests\nimport json\nimport time\nimport os\n\nfilename = '/ql/data/config/env.sh'\n\nurl_get = 'http://passport.bilibili.com/x/passport-login/web/qrcode/generate'\nheaders = {\n    \"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\"\n    }\nsession = requests.session()\nresponse = session.get(url_get, headers=headers)\njson_data = json.loads(response.text)\nqr_data = json_data['data']['url']\nqr_code = json_data['data']['qrcode_key']\n# print(qr_data)\n#  img = qrcode.make(qr_data)\n#  img.save('../upload/B.png')\n# 生成二维码，并且打印，只有invert是True手机才能识别，默认的打印识别不出来\nqr = qrcode.QRCode()\nqr.add_data(qr_data)\nqr.print_ascii(invert=True)\n\nurl_get_2 = f'http://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key={qr_code}&source=main_mini'\nrefresh_token = ''\n# 尝试次数\ntry_time = 8\nwhile True:\n    try_time -= 1\n    if not try_time:\n        print('一直没有扫码，退出登录！')\n        exit(1)\n    response = session.get(url_get_2, headers=headers)\n    json_data = json.loads(response.text)\n    response_data_2 = json_data['data']\n    if response_data_2['code'] == 0:\n        try_time += 5\n        refresh_token = response_data_2['refresh_token']\n        print(response_data_2, end='')\n    if response_data_2['message'] == '二维码已失效':\n        print(response_data_2['message'])\n        print('-' * 20)\n        break\n    print(response_data_2['message'])\n    print('-' * 20)\n    time.sleep(5)\nsession.get('https://api.bilibili.com/x/web-interface/nav')\ncookies = requests.utils.dict_from_cookiejar(session.cookies)\nlst = []\nfor item in cookies.items():\n    lst.append(f\"{item[0]}={item[1]}\")\n\ncookie_str = ';'.join(lst)\nprint('=' * 20)\nprint(cookie_str)\nprint('=' * 20)\n# 修改环境变量\nwith open(filename, 'r') as f:\n    lines = f.readlines()\n\nflag = True\nwith open(filename, 'w') as f:\n    for l in lines:\n        if 'Ray_BiliBiliCookies__1' in l:\n            flag = False\n            l = f'export Ray_BiliBiliCookies__1=\"{cookie_str}\"\\n'\n            print(l)\n        f.write(l)\n    if flag:\n        flag = False\n        l = f'export Ray_BiliBiliCookies__1=\"{cookie_str}\"\\n'\n        print(l)\n        f.write(l)\nos.popen(f'source {filename}')\n"
  },
  {
    "path": "qinglong/bak/bili_task_get_cookie.py.bak",
    "content": "'''\n1 9 11 11 1 bili_task_get_cookie.py\n手动运行，查看日志，并使用手机B站app扫描日志中二维码，注意，只能修改第一个cookie\n如果产生错误，重新运行并用手机扫描二维码\n有可能识别不出来二维码，我测试了几次都能识别\n\n默认环境变量存放位置为/ql/data/config/env.sh\n可以自己通过docker命令进入容器查找这个文件位置。docker exec -it qinglong /bin/bash,进入青龙容器，然后查找一下这个文件位置\nfilename = '../config/env.sh'\n'''\n\nimport qrcode\nimport requests\nimport json\nimport time\nimport os\n\nfilename = '/ql/data/config/env.sh'\n\nurl_get = 'http://passport.bilibili.com/x/passport-login/web/qrcode/generate'\nheaders = {\n    \"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\"\n    }\nsession = requests.session()\nresponse = session.get(url_get, headers=headers)\njson_data = json.loads(response.text)\nqr_data = json_data['data']['url']\nqr_code = json_data['data']['qrcode_key']\n# print(qr_data)\n#  img = qrcode.make(qr_data)\n#  img.save('../upload/B.png')\n# 生成二维码，并且打印，只有invert是True手机才能识别，默认的打印识别不出来\nqr = qrcode.QRCode()\nqr.add_data(qr_data)\nqr.print_ascii(invert=True)\n\nurl_get_2 = f'http://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key={qr_code}&source=main_mini'\nrefresh_token = ''\n# 尝试次数\ntry_time = 8\nwhile True:\n    try_time -= 1\n    if not try_time:\n        print('一直没有扫码，退出登录！')\n        exit(1)\n    response = session.get(url_get_2, headers=headers)\n    json_data = json.loads(response.text)\n    response_data_2 = json_data['data']\n    if response_data_2['code'] == 0:\n        try_time += 5\n        refresh_token = response_data_2['refresh_token']\n        print(response_data_2, end='')\n    if response_data_2['message'] == '二维码已失效':\n        print(response_data_2['message'])\n        print('-' * 20)\n        break\n    print(response_data_2['message'])\n    print('-' * 20)\n    time.sleep(5)\nsession.get('https://api.bilibili.com/x/web-interface/nav')\ncookies = requests.utils.dict_from_cookiejar(session.cookies)\nlst = []\nfor item in cookies.items():\n    lst.append(f\"{item[0]}={item[1]}\")\n\ncookie_str = ';'.join(lst)\nprint('=' * 20)\nprint(cookie_str)\nprint('=' * 20)\n# 修改环境变量\nwith open(filename, 'r') as f:\n    lines = f.readlines()\n\nflag = True\nwith open(filename, 'w') as f:\n    for l in lines:\n        if 'Ray_BiliBiliCookies__1' in l:\n            flag = False\n            l = f'export Ray_BiliBiliCookies__1=\"{cookie_str}\"\\n'\n            print(l)\n        f.write(l)\n    if flag:\n        flag = False\n        l = f'export Ray_BiliBiliCookies__1=\"{cookie_str}\"\\n'\n        print(l)\n        f.write(l)\nos.popen(f'source {filename}')\n"
  },
  {
    "path": "qinglong/dotnet-install.sh",
    "content": "#!/usr/bin/env bash\n# Copyright (c) .NET Foundation and contributors. All rights reserved.\n# Licensed under the MIT license. See LICENSE file in the project root for full license information.\n#\n\n# Stop script on NZEC\nset -e\n# Stop script if unbound variable found (use ${var:-} if intentional)\nset -u\n# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success\n# This is causing it to fail\nset -o pipefail\n\n# Use in the the functions: eval $invocation\ninvocation='say_verbose \"Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}\"'\n\n# standard output may be used as a return value in the functions\n# we need a way to write text on the screen in the functions so that\n# it won't interfere with the return value.\n# Exposing stream 3 as a pipe to standard output of the script itself\nexec 3>&1\n\n# Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors.\n# See if stdout is a terminal\nif [ -t 1 ] && command -v tput > /dev/null; then\n    # see if it supports colors\n    ncolors=$(tput colors || echo 0)\n    if [ -n \"$ncolors\" ] && [ $ncolors -ge 8 ]; then\n        bold=\"$(tput bold       || echo)\"\n        normal=\"$(tput sgr0     || echo)\"\n        black=\"$(tput setaf 0   || echo)\"\n        red=\"$(tput setaf 1     || echo)\"\n        green=\"$(tput setaf 2   || echo)\"\n        yellow=\"$(tput setaf 3  || echo)\"\n        blue=\"$(tput setaf 4    || echo)\"\n        magenta=\"$(tput setaf 5 || echo)\"\n        cyan=\"$(tput setaf 6    || echo)\"\n        white=\"$(tput setaf 7   || echo)\"\n    fi\nfi\n\nsay_warning() {\n    printf \"%b\\n\" \"${yellow:-}dotnet_install: Warning: $1${normal:-}\" >&3\n}\n\nsay_err() {\n    printf \"%b\\n\" \"${red:-}dotnet_install: Error: $1${normal:-}\" >&2\n}\n\nsay() {\n    # using stream 3 (defined in the beginning) to not interfere with stdout of functions\n    # which may be used as return value\n    printf \"%b\\n\" \"${cyan:-}dotnet-install:${normal:-} $1\" >&3\n}\n\nsay_verbose() {\n    if [ \"$verbose\" = true ]; then\n        say \"$1\"\n    fi\n}\n\n# This platform list is finite - if the SDK/Runtime has supported Linux distribution-specific assets,\n#   then and only then should the Linux distribution appear in this list.\n# Adding a Linux distribution to this list does not imply distribution-specific support.\nget_legacy_os_name_from_platform() {\n    eval $invocation\n\n    platform=\"$1\"\n    case \"$platform\" in\n        \"centos.7\")\n            echo \"centos\"\n            return 0\n            ;;\n        \"debian.8\")\n            echo \"debian\"\n            return 0\n            ;;\n        \"debian.9\")\n            echo \"debian.9\"\n            return 0\n            ;;\n        \"fedora.23\")\n            echo \"fedora.23\"\n            return 0\n            ;;\n        \"fedora.24\")\n            echo \"fedora.24\"\n            return 0\n            ;;\n        \"fedora.27\")\n            echo \"fedora.27\"\n            return 0\n            ;;\n        \"fedora.28\")\n            echo \"fedora.28\"\n            return 0\n            ;;\n        \"opensuse.13.2\")\n            echo \"opensuse.13.2\"\n            return 0\n            ;;\n        \"opensuse.42.1\")\n            echo \"opensuse.42.1\"\n            return 0\n            ;;\n        \"opensuse.42.3\")\n            echo \"opensuse.42.3\"\n            return 0\n            ;;\n        \"rhel.7\"*)\n            echo \"rhel\"\n            return 0\n            ;;\n        \"ubuntu.14.04\")\n            echo \"ubuntu\"\n            return 0\n            ;;\n        \"ubuntu.16.04\")\n            echo \"ubuntu.16.04\"\n            return 0\n            ;;\n        \"ubuntu.16.10\")\n            echo \"ubuntu.16.10\"\n            return 0\n            ;;\n        \"ubuntu.18.04\")\n            echo \"ubuntu.18.04\"\n            return 0\n            ;;\n        \"alpine.3.4.3\")\n            echo \"alpine\"\n            return 0\n            ;;\n    esac\n    return 1\n}\n\nget_legacy_os_name() {\n    eval $invocation\n\n    local uname=$(uname)\n    if [ \"$uname\" = \"Darwin\" ]; then\n        echo \"osx\"\n        return 0\n    elif [ -n \"$runtime_id\" ]; then\n        echo $(get_legacy_os_name_from_platform \"${runtime_id%-*}\" || echo \"${runtime_id%-*}\")\n        return 0\n    else\n        if [ -e /etc/os-release ]; then\n            . /etc/os-release\n            os=$(get_legacy_os_name_from_platform \"$ID${VERSION_ID:+.${VERSION_ID}}\" || echo \"\")\n            if [ -n \"$os\" ]; then\n                echo \"$os\"\n                return 0\n            fi\n        fi\n    fi\n\n    say_verbose \"Distribution specific OS name and version could not be detected: UName = $uname\"\n    return 1\n}\n\nget_linux_platform_name() {\n    eval $invocation\n\n    if [ -n \"$runtime_id\" ]; then\n        echo \"${runtime_id%-*}\"\n        return 0\n    else\n        if [ -e /etc/os-release ]; then\n            . /etc/os-release\n            echo \"$ID${VERSION_ID:+.${VERSION_ID}}\"\n            return 0\n        elif [ -e /etc/redhat-release ]; then\n            local redhatRelease=$(</etc/redhat-release)\n            if [[ $redhatRelease == \"CentOS release 6.\"* || $redhatRelease == \"Red Hat Enterprise Linux \"*\" release 6.\"* ]]; then\n                echo \"rhel.6\"\n                return 0\n            fi\n        fi\n    fi\n\n    say_verbose \"Linux specific platform name and version could not be detected: UName = $uname\"\n    return 1\n}\n\nis_musl_based_distro() {\n    (ldd --version 2>&1 || true) | grep -q musl\n}\n\nget_current_os_name() {\n    eval $invocation\n\n    local uname=$(uname)\n    if [ \"$uname\" = \"Darwin\" ]; then\n        echo \"osx\"\n        return 0\n    elif [ \"$uname\" = \"FreeBSD\" ]; then\n        echo \"freebsd\"\n        return 0\n    elif [ \"$uname\" = \"Linux\" ]; then\n        local linux_platform_name=\"\"\n        linux_platform_name=\"$(get_linux_platform_name)\" || true\n\n        if [ \"$linux_platform_name\" = \"rhel.6\" ]; then\n            echo $linux_platform_name\n            return 0\n        elif is_musl_based_distro; then\n            echo \"linux-musl\"\n            return 0\n        elif [ \"$linux_platform_name\" = \"linux-musl\" ]; then\n            echo \"linux-musl\"\n            return 0\n        else\n            echo \"linux\"\n            return 0\n        fi\n    fi\n\n    say_err \"OS name could not be detected: UName = $uname\"\n    return 1\n}\n\nmachine_has() {\n    eval $invocation\n\n    command -v \"$1\" > /dev/null 2>&1\n    return $?\n}\n\ncheck_min_reqs() {\n    local hasMinimum=false\n    if machine_has \"curl\"; then\n        hasMinimum=true\n    elif machine_has \"wget\"; then\n        hasMinimum=true\n    fi\n\n    if [ \"$hasMinimum\" = \"false\" ]; then\n        say_err \"curl (recommended) or wget are required to download dotnet. Install missing prerequisite to proceed.\"\n        return 1\n    fi\n    return 0\n}\n\n# args:\n# input - $1\nto_lowercase() {\n    #eval $invocation\n\n    echo \"$1\" | tr '[:upper:]' '[:lower:]'\n    return 0\n}\n\n# args:\n# input - $1\nremove_trailing_slash() {\n    #eval $invocation\n\n    local input=\"${1:-}\"\n    echo \"${input%/}\"\n    return 0\n}\n\n# args:\n# input - $1\nremove_beginning_slash() {\n    #eval $invocation\n\n    local input=\"${1:-}\"\n    echo \"${input#/}\"\n    return 0\n}\n\n# args:\n# root_path - $1\n# child_path - $2 - this parameter can be empty\ncombine_paths() {\n    eval $invocation\n\n    # TODO: Consider making it work with any number of paths. For now:\n    if [ ! -z \"${3:-}\" ]; then\n        say_err \"combine_paths: Function takes two parameters.\"\n        return 1\n    fi\n\n    local root_path=\"$(remove_trailing_slash \"$1\")\"\n    local child_path=\"$(remove_beginning_slash \"${2:-}\")\"\n    say_verbose \"combine_paths: root_path=$root_path\"\n    say_verbose \"combine_paths: child_path=$child_path\"\n    echo \"$root_path/$child_path\"\n    return 0\n}\n\nget_machine_architecture() {\n    eval $invocation\n\n    if command -v uname > /dev/null; then\n        CPUName=$(uname -m)\n        case $CPUName in\n        armv*l)\n            echo \"arm\"\n            return 0\n            ;;\n        aarch64|arm64)\n            echo \"arm64\"\n            return 0\n            ;;\n        esac\n    fi\n\n    # Always default to 'x64'\n    echo \"x64\"\n    return 0\n}\n\n# args:\n# architecture - $1\nget_normalized_architecture_from_architecture() {\n    eval $invocation\n\n    local architecture=\"$(to_lowercase \"$1\")\"\n\n    if [[ $architecture == \\<auto\\> ]]; then\n        echo \"$(get_machine_architecture)\"\n        return 0\n    fi\n\n    case \"$architecture\" in\n        amd64|x64)\n            echo \"x64\"\n            return 0\n            ;;\n        arm)\n            echo \"arm\"\n            return 0\n            ;;\n        arm64)\n            echo \"arm64\"\n            return 0\n            ;;\n    esac\n\n    say_err \"Architecture \\`$architecture\\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues\"\n    return 1\n}\n\n# args:\n# user_defined_os - $1\nget_normalized_os() {\n    eval $invocation\n\n    local osname=\"$(to_lowercase \"$1\")\"\n    if [ ! -z \"$osname\" ]; then\n        case \"$osname\" in\n            osx | freebsd | rhel.6 | linux-musl | linux)\n                echo \"$osname\"\n                return 0\n                ;;\n            *)\n                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.\"\n                return 1\n                ;;\n        esac\n    else\n        osname=\"$(get_current_os_name)\" || return 1\n    fi\n    echo \"$osname\"\n    return 0\n}\n\n# args:\n# quality - $1\nget_normalized_quality() {\n    eval $invocation\n\n    local quality=\"$(to_lowercase \"$1\")\"\n    if [ ! -z \"$quality\" ]; then\n        case \"$quality\" in\n            daily | signed | validated | preview)\n                echo \"$quality\"\n                return 0\n                ;;\n            ga)\n                #ga quality is available without specifying quality, so normalizing it to empty\n                return 0\n                ;;\n            *)\n                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.\"\n                return 1\n                ;;\n        esac\n    fi\n    return 0\n}\n\n# args:\n# channel - $1\nget_normalized_channel() {\n    eval $invocation\n\n    local channel=\"$(to_lowercase \"$1\")\"\n\n    if [[ $channel == release/* ]]; then\n        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.';\n    fi\n\n    if [ ! -z \"$channel\" ]; then\n        case \"$channel\" in\n            lts)\n                echo \"LTS\"\n                return 0\n                ;;\n            *)\n                echo \"$channel\"\n                return 0\n                ;;\n        esac\n    fi\n\n    return 0\n}\n\n# args:\n# runtime - $1\nget_normalized_product() {\n    eval $invocation\n\n    local product=\"\"\n    local runtime=\"$(to_lowercase \"$1\")\"\n    if [[ \"$runtime\" == \"dotnet\" ]]; then\n        product=\"dotnet-runtime\"\n    elif [[ \"$runtime\" == \"aspnetcore\" ]]; then\n        product=\"aspnetcore-runtime\"\n    elif [ -z \"$runtime\" ]; then\n        product=\"dotnet-sdk\"\n    fi\n    echo \"$product\"\n    return 0\n}\n\n# The version text returned from the feeds is a 1-line or 2-line string:\n# For the SDK and the dotnet runtime (2 lines):\n# Line 1: # commit_hash\n# Line 2: # 4-part version\n# For the aspnetcore runtime (1 line):\n# Line 1: # 4-part version\n\n# args:\n# version_text - stdin\nget_version_from_latestversion_file_content() {\n    eval $invocation\n\n    cat | tail -n 1 | sed 's/\\r$//'\n    return 0\n}\n\n# args:\n# install_root - $1\n# relative_path_to_package - $2\n# specific_version - $3\nis_dotnet_package_installed() {\n    eval $invocation\n\n    local install_root=\"$1\"\n    local relative_path_to_package=\"$2\"\n    local specific_version=\"${3//[$'\\t\\r\\n']}\"\n\n    local dotnet_package_path=\"$(combine_paths \"$(combine_paths \"$install_root\" \"$relative_path_to_package\")\" \"$specific_version\")\"\n    say_verbose \"is_dotnet_package_installed: dotnet_package_path=$dotnet_package_path\"\n\n    if [ -d \"$dotnet_package_path\" ]; then\n        return 0\n    else\n        return 1\n    fi\n}\n\n# args:\n# azure_feed - $1\n# channel - $2\n# normalized_architecture - $3\nget_version_from_latestversion_file() {\n    eval $invocation\n\n    local azure_feed=\"$1\"\n    local channel=\"$2\"\n    local normalized_architecture=\"$3\"\n\n    local version_file_url=null\n    if [[ \"$runtime\" == \"dotnet\" ]]; then\n        version_file_url=\"$azure_feed/Runtime/$channel/latest.version\"\n    elif [[ \"$runtime\" == \"aspnetcore\" ]]; then\n        version_file_url=\"$azure_feed/aspnetcore/Runtime/$channel/latest.version\"\n    elif [ -z \"$runtime\" ]; then\n         version_file_url=\"$azure_feed/Sdk/$channel/latest.version\"\n    else\n        say_err \"Invalid value for \\$runtime\"\n        return 1\n    fi\n    say_verbose \"get_version_from_latestversion_file: latest url: $version_file_url\"\n\n    download \"$version_file_url\" || return $?\n    return 0\n}\n\n# args:\n# json_file - $1\nparse_globaljson_file_for_version() {\n    eval $invocation\n\n    local json_file=\"$1\"\n    if [ ! -f \"$json_file\" ]; then\n        say_err \"Unable to find \\`$json_file\\`\"\n        return 1\n    fi\n\n    sdk_section=$(cat $json_file | awk '/\"sdk\"/,/}/')\n    if [ -z \"$sdk_section\" ]; then\n        say_err \"Unable to parse the SDK node in \\`$json_file\\`\"\n        return 1\n    fi\n\n    sdk_list=$(echo $sdk_section | awk -F\"[{}]\" '{print $2}')\n    sdk_list=${sdk_list//[\\\" ]/}\n    sdk_list=${sdk_list//,/$'\\n'}\n\n    local version_info=\"\"\n    while read -r line; do\n      IFS=:\n      while read -r key value; do\n        if [[ \"$key\" == \"version\" ]]; then\n          version_info=$value\n        fi\n      done <<< \"$line\"\n    done <<< \"$sdk_list\"\n    if [ -z \"$version_info\" ]; then\n        say_err \"Unable to find the SDK:version node in \\`$json_file\\`\"\n        return 1\n    fi\n\n    unset IFS;\n    echo \"$version_info\"\n    return 0\n}\n\n# args:\n# azure_feed - $1\n# channel - $2\n# normalized_architecture - $3\n# version - $4\n# json_file - $5\nget_specific_version_from_version() {\n    eval $invocation\n\n    local azure_feed=\"$1\"\n    local channel=\"$2\"\n    local normalized_architecture=\"$3\"\n    local version=\"$(to_lowercase \"$4\")\"\n    local json_file=\"$5\"\n\n    if [ -z \"$json_file\" ]; then\n        if [[ \"$version\" == \"latest\" ]]; then\n            local version_info\n            version_info=\"$(get_version_from_latestversion_file \"$azure_feed\" \"$channel\" \"$normalized_architecture\" false)\" || return 1\n            say_verbose \"get_specific_version_from_version: version_info=$version_info\"\n            echo \"$version_info\" | get_version_from_latestversion_file_content\n            return 0\n        else\n            echo \"$version\"\n            return 0\n        fi\n    else\n        local version_info\n        version_info=\"$(parse_globaljson_file_for_version \"$json_file\")\" || return 1\n        echo \"$version_info\"\n        return 0\n    fi\n}\n\n# args:\n# azure_feed - $1\n# channel - $2\n# normalized_architecture - $3\n# specific_version - $4\n# normalized_os - $5\nconstruct_download_link() {\n    eval $invocation\n\n    local azure_feed=\"$1\"\n    local channel=\"$2\"\n    local normalized_architecture=\"$3\"\n    local specific_version=\"${4//[$'\\t\\r\\n']}\"\n    local specific_product_version=\"$(get_specific_product_version \"$1\" \"$4\")\"\n    local osname=\"$5\"\n\n    local download_link=null\n    if [[ \"$runtime\" == \"dotnet\" ]]; then\n        download_link=\"$azure_feed/Runtime/$specific_version/dotnet-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz\"\n    elif [[ \"$runtime\" == \"aspnetcore\" ]]; then\n        download_link=\"$azure_feed/aspnetcore/Runtime/$specific_version/aspnetcore-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz\"\n    elif [ -z \"$runtime\" ]; then\n        download_link=\"$azure_feed/Sdk/$specific_version/dotnet-sdk-$specific_product_version-$osname-$normalized_architecture.tar.gz\"\n    else\n        return 1\n    fi\n\n    echo \"$download_link\"\n    return 0\n}\n\n# args:\n# azure_feed - $1\n# specific_version - $2\n# download link - $3 (optional)\nget_specific_product_version() {\n    # If we find a 'productVersion.txt' at the root of any folder, we'll use its contents\n    # to resolve the version of what's in the folder, superseding the specified version.\n    # if 'productVersion.txt' is missing but download link is already available, product version will be taken from download link\n    eval $invocation\n\n    local azure_feed=\"$1\"\n    local specific_version=\"${2//[$'\\t\\r\\n']}\"\n    local package_download_link=\"\"\n    if [ $# -gt 2  ]; then\n        local package_download_link=\"$3\"\n    fi\n    local specific_product_version=null\n\n    # Try to get the version number, using the productVersion.txt file located next to the installer file.\n    local download_links=($(get_specific_product_version_url \"$azure_feed\" \"$specific_version\" true \"$package_download_link\")\n        $(get_specific_product_version_url \"$azure_feed\" \"$specific_version\" false \"$package_download_link\"))\n\n    for download_link in \"${download_links[@]}\"\n    do\n        say_verbose \"Checking for the existence of $download_link\"\n\n        if machine_has \"curl\"\n        then\n            specific_product_version=$(curl -s --fail \"${download_link}${feed_credential}\" 2>&1)\n            if [ $? = 0 ]; then\n                echo \"${specific_product_version//[$'\\t\\r\\n']}\"\n                return 0\n            fi\n        elif machine_has \"wget\"\n        then\n            specific_product_version=$(wget -qO- \"${download_link}${feed_credential}\" 2>&1)\n            if [ $? = 0 ]; then\n                echo \"${specific_product_version//[$'\\t\\r\\n']}\"\n                return 0\n            fi\n        fi\n    done\n    \n    # Getting the version number with productVersion.txt has failed. Try parsing the download link for a version number.\n    say_verbose \"Failed to get the version using productVersion.txt file. Download link will be parsed instead.\"\n    specific_product_version=\"$(get_product_specific_version_from_download_link \"$package_download_link\" \"$specific_version\")\"\n    echo \"${specific_product_version//[$'\\t\\r\\n']}\"\n    return 0\n}\n\n# args:\n# azure_feed - $1\n# specific_version - $2\n# is_flattened - $3\n# download link - $4 (optional)\nget_specific_product_version_url() {\n    eval $invocation\n\n    local azure_feed=\"$1\"\n    local specific_version=\"$2\"\n    local is_flattened=\"$3\"\n    local package_download_link=\"\"\n    if [ $# -gt 3  ]; then\n        local package_download_link=\"$4\"\n    fi\n\n    local pvFileName=\"productVersion.txt\"\n    if [ \"$is_flattened\" = true ]; then\n        if [ -z \"$runtime\" ]; then\n            pvFileName=\"sdk-productVersion.txt\"\n        elif [[ \"$runtime\" == \"dotnet\" ]]; then\n            pvFileName=\"runtime-productVersion.txt\"\n        else\n            pvFileName=\"$runtime-productVersion.txt\"\n        fi\n    fi\n\n    local download_link=null\n\n    if [ -z \"$package_download_link\" ]; then\n        if [[ \"$runtime\" == \"dotnet\" ]]; then\n            download_link=\"$azure_feed/Runtime/$specific_version/${pvFileName}\"\n        elif [[ \"$runtime\" == \"aspnetcore\" ]]; then\n            download_link=\"$azure_feed/aspnetcore/Runtime/$specific_version/${pvFileName}\"\n        elif [ -z \"$runtime\" ]; then\n            download_link=\"$azure_feed/Sdk/$specific_version/${pvFileName}\"\n        else\n            return 1\n        fi\n    else\n        download_link=\"${package_download_link%/*}/${pvFileName}\"\n    fi\n\n    say_verbose \"Constructed productVersion link: $download_link\"\n    echo \"$download_link\"\n    return 0\n}\n\n# args:\n# download link - $1\n# specific version - $2\nget_product_specific_version_from_download_link()\n{\n    eval $invocation\n\n    local download_link=\"$1\"\n    local specific_version=\"$2\"\n    local specific_product_version=\"\" \n\n    if [ -z \"$download_link\" ]; then\n        echo \"$specific_version\"\n        return 0\n    fi\n\n    #get filename\n    filename=\"${download_link##*/}\"\n\n    #product specific version follows the product name\n    #for filename 'dotnet-sdk-3.1.404-linux-x64.tar.gz': the product version is 3.1.404\n    IFS='-'\n    read -ra filename_elems <<< \"$filename\"\n    count=${#filename_elems[@]}\n    if [[ \"$count\" -gt 2 ]]; then\n        specific_product_version=\"${filename_elems[2]}\"\n    else\n        specific_product_version=$specific_version\n    fi\n    unset IFS;\n    echo \"$specific_product_version\"\n    return 0\n}\n\n# args:\n# azure_feed - $1\n# channel - $2\n# normalized_architecture - $3\n# specific_version - $4\nconstruct_legacy_download_link() {\n    eval $invocation\n\n    local azure_feed=\"$1\"\n    local channel=\"$2\"\n    local normalized_architecture=\"$3\"\n    local specific_version=\"${4//[$'\\t\\r\\n']}\"\n\n    local distro_specific_osname\n    distro_specific_osname=\"$(get_legacy_os_name)\" || return 1\n\n    local legacy_download_link=null\n    if [[ \"$runtime\" == \"dotnet\" ]]; then\n        legacy_download_link=\"$azure_feed/Runtime/$specific_version/dotnet-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz\"\n    elif [ -z \"$runtime\" ]; then\n        legacy_download_link=\"$azure_feed/Sdk/$specific_version/dotnet-dev-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz\"\n    else\n        return 1\n    fi\n\n    echo \"$legacy_download_link\"\n    return 0\n}\n\nget_user_install_path() {\n    eval $invocation\n\n    if [ ! -z \"${DOTNET_INSTALL_DIR:-}\" ]; then\n        echo \"$DOTNET_INSTALL_DIR\"\n    else\n        echo \"$HOME/.dotnet\"\n    fi\n    return 0\n}\n\n# args:\n# install_dir - $1\nresolve_installation_path() {\n    eval $invocation\n\n    local install_dir=$1\n    if [ \"$install_dir\" = \"<auto>\" ]; then\n        local user_install_path=\"$(get_user_install_path)\"\n        say_verbose \"resolve_installation_path: user_install_path=$user_install_path\"\n        echo \"$user_install_path\"\n        return 0\n    fi\n\n    echo \"$install_dir\"\n    return 0\n}\n\n# args:\n# relative_or_absolute_path - $1\nget_absolute_path() {\n    eval $invocation\n\n    local relative_or_absolute_path=$1\n    echo \"$(cd \"$(dirname \"$1\")\" && pwd -P)/$(basename \"$1\")\"\n    return 0\n}\n\n# args:\n# input_files - stdin\n# root_path - $1\n# out_path - $2\n# override - $3\ncopy_files_or_dirs_from_list() {\n    eval $invocation\n\n    local root_path=\"$(remove_trailing_slash \"$1\")\"\n    local out_path=\"$(remove_trailing_slash \"$2\")\"\n    local override=\"$3\"\n    local osname=\"$(get_current_os_name)\"\n    local override_switch=$(\n        if [ \"$override\" = false ]; then\n            if [ \"$osname\" = \"linux-musl\" ]; then\n                printf -- \"-u\";\n            else\n                printf -- \"-n\";\n            fi\n        fi)\n\n    cat | uniq | while read -r file_path; do\n        local path=\"$(remove_beginning_slash \"${file_path#$root_path}\")\"\n        local target=\"$out_path/$path\"\n        if [ \"$override\" = true ] || (! ([ -d \"$target\" ] || [ -e \"$target\" ])); then\n            mkdir -p \"$out_path/$(dirname \"$path\")\"\n            if [ -d \"$target\" ]; then\n                rm -rf \"$target\"\n            fi\n            cp -R $override_switch \"$root_path/$path\" \"$target\"\n        fi\n    done\n}\n\n# args:\n# zip_path - $1\n# out_path - $2\nextract_dotnet_package() {\n    eval $invocation\n\n    local zip_path=\"$1\"\n    local out_path=\"$2\"\n\n    local temp_out_path=\"$(mktemp -d \"$temporary_file_template\")\"\n\n    local failed=false\n    tar -xzf \"$zip_path\" -C \"$temp_out_path\" > /dev/null || failed=true\n\n    local folders_with_version_regex='^.*/[0-9]+\\.[0-9]+[^/]+/'\n    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\n    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\"\n\n    rm -rf \"$temp_out_path\"\n    rm -f \"$zip_path\" && say_verbose \"Temporary zip file $zip_path was removed\"\n\n    if [ \"$failed\" = true ]; then\n        say_err \"Extraction failed\"\n        return 1\n    fi\n    return 0\n}\n\n# args:\n# remote_path - $1\n# disable_feed_credential - $2\nget_http_header()\n{\n    eval $invocation\n    local remote_path=\"$1\"\n    local disable_feed_credential=\"$2\"\n\n    local failed=false\n    local response\n    if machine_has \"curl\"; then\n        get_http_header_curl $remote_path $disable_feed_credential || failed=true\n    elif machine_has \"wget\"; then\n        get_http_header_wget $remote_path $disable_feed_credential || failed=true\n    else\n        failed=true\n    fi\n    if [ \"$failed\" = true ]; then\n        say_verbose \"Failed to get HTTP header: '$remote_path'.\"\n        return 1\n    fi\n    return 0\n}\n\n# args:\n# remote_path - $1\n# disable_feed_credential - $2\nget_http_header_curl() {\n    eval $invocation\n    local remote_path=\"$1\"\n    local disable_feed_credential=\"$2\"\n\n    remote_path_with_credential=\"$remote_path\"\n    if [ \"$disable_feed_credential\" = false ]; then\n        remote_path_with_credential+=\"$feed_credential\"\n    fi\n\n    curl_options=\"-I -sSL --retry 5 --retry-delay 2 --connect-timeout 15 \"\n    curl $curl_options \"$remote_path_with_credential\" 2>&1 || return 1\n    return 0\n}\n\n# args:\n# remote_path - $1\n# disable_feed_credential - $2\nget_http_header_wget() {\n    eval $invocation\n    local remote_path=\"$1\"\n    local disable_feed_credential=\"$2\"\n    local wget_options=\"-q -S --spider --tries 5 \"\n    # Store options that aren't supported on all wget implementations separately.\n    local wget_options_extra=\"--waitretry 2 --connect-timeout 15 \"\n    local wget_result=''\n\n    remote_path_with_credential=\"$remote_path\"\n    if [ \"$disable_feed_credential\" = false ]; then\n        remote_path_with_credential+=\"$feed_credential\"\n    fi\n\n    wget $wget_options $wget_options_extra \"$remote_path_with_credential\" 2>&1\n    wget_result=$?\n\n    if [[ $wget_result == 2 ]]; then\n        # Parsing of the command has failed. Exclude potentially unrecognized options and retry.\n        wget $wget_options \"$remote_path_with_credential\" 2>&1\n        return $?\n    fi\n\n    return $wget_result\n}\n\n# args:\n# remote_path - $1\n# [out_path] - $2 - stdout if not provided\ndownload() {\n    eval $invocation\n\n    local remote_path=\"$1\"\n    local out_path=\"${2:-}\"\n\n    if [[ \"$remote_path\" != \"http\"* ]]; then\n        cp \"$remote_path\" \"$out_path\"\n        return $?\n    fi\n\n    local failed=false\n    local attempts=0\n    while [ $attempts -lt 3 ]; do\n        attempts=$((attempts+1))\n        failed=false\n        if machine_has \"curl\"; then\n            downloadcurl \"$remote_path\" \"$out_path\" || failed=true\n        elif machine_has \"wget\"; then\n            downloadwget \"$remote_path\" \"$out_path\" || failed=true\n        else\n            say_err \"Missing dependency: neither curl nor wget was found.\"\n            exit 1\n        fi\n\n        if [ \"$failed\" = false ] || [ $attempts -ge 3 ] || { [ ! -z $http_code ] && [ $http_code = \"404\" ]; }; then\n            break\n        fi\n\n        say \"Download attempt #$attempts has failed: $http_code $download_error_msg\"\n        say \"Attempt #$((attempts+1)) will start in $((attempts*10)) seconds.\"\n        sleep $((attempts*10))\n    done\n\n\n\n    if [ \"$failed\" = true ]; then\n        say_verbose \"Download failed: $remote_path\"\n        return 1\n    fi\n    return 0\n}\n\n# Updates global variables $http_code and $download_error_msg\ndownloadcurl() {\n    eval $invocation\n    unset http_code\n    unset download_error_msg\n    local remote_path=\"$1\"\n    local out_path=\"${2:-}\"\n    # Append feed_credential as late as possible before calling curl to avoid logging feed_credential\n    # Avoid passing URI with credentials to functions: note, most of them echoing parameters of invocation in verbose output.\n    local remote_path_with_credential=\"${remote_path}${feed_credential}\"\n    local curl_options=\"--retry 20 --retry-delay 2 --connect-timeout 15 -sSL -f --create-dirs \"\n    local failed=false\n    if [ -z \"$out_path\" ]; then\n        curl $curl_options \"$remote_path_with_credential\" 2>&1 || failed=true\n    else\n        curl $curl_options -o \"$out_path\" \"$remote_path_with_credential\" 2>&1 || failed=true\n    fi\n    if [ \"$failed\" = true ]; then\n        local disable_feed_credential=false\n        local response=$(get_http_header_curl $remote_path $disable_feed_credential)\n        http_code=$( echo \"$response\" | awk '/^HTTP/{print $2}' | tail -1 )\n        download_error_msg=\"Unable to download $remote_path.\"\n        if  [[ $http_code != 2* ]]; then\n            download_error_msg+=\" Returned HTTP status code: $http_code.\"\n        fi\n        say_verbose \"$download_error_msg\"\n        return 1\n    fi\n    return 0\n}\n\n\n# Updates global variables $http_code and $download_error_msg\ndownloadwget() {\n    eval $invocation\n    unset http_code\n    unset download_error_msg\n    local remote_path=\"$1\"\n    local out_path=\"${2:-}\"\n    # Append feed_credential as late as possible before calling wget to avoid logging feed_credential\n    local remote_path_with_credential=\"${remote_path}${feed_credential}\"\n    local wget_options=\"--tries 20 \"\n    # Store options that aren't supported on all wget implementations separately.\n    local wget_options_extra=\"--waitretry 2 --connect-timeout 15 \"\n    local wget_result=''\n\n    if [ -z \"$out_path\" ]; then\n        wget -q $wget_options $wget_options_extra -O - \"$remote_path_with_credential\" 2>&1\n        wget_result=$?\n    else\n        wget $wget_options $wget_options_extra -O \"$out_path\" \"$remote_path_with_credential\" 2>&1\n        wget_result=$?\n    fi\n\n    if [[ $wget_result == 2 ]]; then\n        # Parsing of the command has failed. Exclude potentially unrecognized options and retry.\n        if [ -z \"$out_path\" ]; then\n            wget -q $wget_options -O - \"$remote_path_with_credential\" 2>&1\n            wget_result=$?\n        else\n            wget $wget_options -O \"$out_path\" \"$remote_path_with_credential\" 2>&1\n            wget_result=$?\n        fi\n    fi\n\n    if [[ $wget_result != 0 ]]; then\n        local disable_feed_credential=false\n        local response=$(get_http_header_wget $remote_path $disable_feed_credential)\n        http_code=$( echo \"$response\" | awk '/^  HTTP/{print $2}' | tail -1 )\n        download_error_msg=\"Unable to download $remote_path.\"\n        if  [[ $http_code != 2* ]]; then\n            download_error_msg+=\" Returned HTTP status code: $http_code.\"\n        fi\n        say_verbose \"$download_error_msg\"\n        return 1\n    fi\n\n    return 0\n}\n\nget_download_link_from_aka_ms() {\n    eval $invocation\n\n    #quality is not supported for LTS or current channel\n    if [[ ! -z \"$normalized_quality\"  && (\"$normalized_channel\" == \"LTS\" || \"$normalized_channel\" == \"current\") ]]; then\n        normalized_quality=\"\"\n        say_warning \"Specifying quality for current or LTS channel is not supported, the quality will be ignored.\"\n    fi\n\n    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'.\" \n\n    #construct aka.ms link\n    aka_ms_link=\"https://aka.ms/dotnet\"\n    if  [ \"$internal\" = true ]; then\n        aka_ms_link=\"$aka_ms_link/internal\"\n    fi\n    aka_ms_link=\"$aka_ms_link/$normalized_channel\"\n    if [[ ! -z \"$normalized_quality\" ]]; then\n        aka_ms_link=\"$aka_ms_link/$normalized_quality\"\n    fi\n    aka_ms_link=\"$aka_ms_link/$normalized_product-$normalized_os-$normalized_architecture.tar.gz\"\n    say_verbose \"Constructed aka.ms link: '$aka_ms_link'.\"\n\n    #get HTTP response\n    #do not pass credentials as a part of the $aka_ms_link and do not apply credentials in the get_http_header function\n    #otherwise the redirect link would have credentials as well\n    #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\n    disable_feed_credential=true\n    response=\"$(get_http_header $aka_ms_link $disable_feed_credential)\"\n\n    say_verbose \"Received response: $response\"\n    # Get results of all the redirects.\n    http_codes=$( echo \"$response\" | awk '$1 ~ /^HTTP/ {print $2}' )\n    # They all need to be 301, otherwise some links are broken (except for the last, which is not a redirect but 200 or 404).\n    broken_redirects=$( echo \"$http_codes\" | sed '$d' | grep -v '301' )\n\n    # All HTTP codes are 301 (Moved Permanently), the redirect link exists.\n    if [[ -z \"$broken_redirects\" ]]; then\n        aka_ms_download_link=$( echo \"$response\" | awk '$1 ~ /^Location/{print $2}' | tail -1 | tr -d '\\r')\n\n        if [[ -z \"$aka_ms_download_link\" ]]; then\n            say_verbose \"The aka.ms link '$aka_ms_link' is not valid: failed to get redirect location.\"\n            return 1\n        fi\n\n        say_verbose \"The redirect location retrieved: '$aka_ms_download_link'.\"\n        return 0\n    else\n        say_verbose \"The aka.ms link '$aka_ms_link' is not valid: received HTTP code: $(echo \"$broken_redirects\" | paste -sd \",\" -).\"\n        return 1\n    fi\n}\n\nget_feeds_to_use()\n{\n    feeds=(\n    \"https://dotnetcli.azureedge.net/dotnet\"\n    \"https://dotnetbuilds.azureedge.net/public\"\n    )\n\n    if [[ -n \"$azure_feed\" ]]; then\n        feeds=(\"$azure_feed\")\n    fi\n\n    if [[ \"$no_cdn\" == \"true\" ]]; then\n        feeds=(\n        \"https://dotnetcli.blob.core.windows.net/dotnet\"\n        \"https://dotnetbuilds.blob.core.windows.net/public\"\n        )\n\n        if [[ -n \"$uncached_feed\" ]]; then\n            feeds=(\"$uncached_feed\")\n        fi\n    fi\n}\n\n# THIS FUNCTION MAY EXIT (if the determined version is already installed).\ngenerate_download_links() {\n\n    download_links=()\n    specific_versions=()\n    effective_versions=()\n    link_types=()\n\n    # If generate_akams_links returns false, no fallback to old links. Just terminate.\n    # This function may also 'exit' (if the determined version is already installed).\n    generate_akams_links || return\n\n    # Check other feeds only if we haven't been able to find an aka.ms link.\n    if [[ \"${#download_links[@]}\" -lt 1 ]]; then\n        for feed in ${feeds[@]}\n        do\n            # generate_regular_links may also 'exit' (if the determined version is already installed).\n            generate_regular_links $feed || return\n        done\n    fi\n\n    if [[ \"${#download_links[@]}\" -eq 0 ]]; then\n        say_err \"Failed to resolve the exact version number.\"\n        return 1\n    fi\n\n    say_verbose \"Generated ${#download_links[@]} links.\"\n    for link_index in ${!download_links[@]}\n    do\n        say_verbose \"Link $link_index: ${link_types[$link_index]}, ${effective_versions[$link_index]}, ${download_links[$link_index]}\"\n    done\n}\n\n# THIS FUNCTION MAY EXIT (if the determined version is already installed).\ngenerate_akams_links() {\n    local valid_aka_ms_link=true;\n\n    normalized_version=\"$(to_lowercase \"$version\")\"\n    if [[ -n \"$json_file\" || \"$normalized_version\" != \"latest\" ]]; then\n        # aka.ms links are not needed when exact version is specified via command or json file\n        return\n    fi\n\n    get_download_link_from_aka_ms || valid_aka_ms_link=false\n\n    if [[ \"$valid_aka_ms_link\" == true ]]; then\n        say_verbose \"Retrieved primary payload URL from aka.ms link: '$aka_ms_download_link'.\"\n        say_verbose \"Downloading using legacy url will not be attempted.\"\n\n        download_link=$aka_ms_download_link\n\n        #get version from the path\n        IFS='/'\n        read -ra pathElems <<< \"$download_link\"\n        count=${#pathElems[@]}\n        specific_version=\"${pathElems[count-2]}\"\n        unset IFS;\n        say_verbose \"Version: '$specific_version'.\"\n\n        #Retrieve effective version\n        effective_version=\"$(get_specific_product_version \"$azure_feed\" \"$specific_version\" \"$download_link\")\"\n\n        # Add link info to arrays\n        download_links+=($download_link)\n        specific_versions+=($specific_version)\n        effective_versions+=($effective_version)\n        link_types+=(\"aka.ms\")\n\n        #  Check if the SDK version is already installed.\n        if [[ \"$dry_run\" != true ]] && is_dotnet_package_installed \"$install_root\" \"$asset_relative_path\" \"$effective_version\"; then\n            say \"$asset_name with version '$effective_version' is already installed.\"\n            exit 0\n        fi\n\n        return 0\n    fi\n\n    # if quality is specified - exit with error - there is no fallback approach\n    if [ ! -z \"$normalized_quality\" ]; then\n        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'.\"\n        say_err \"Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support.\"\n        return 1\n    fi\n    say_verbose \"Falling back to latest.version file approach.\"\n}\n\n# THIS FUNCTION MAY EXIT (if the determined version is already installed)\n# args:\n# feed - $1\ngenerate_regular_links() {\n    local feed=\"$1\"\n    local valid_legacy_download_link=true\n\n    specific_version=$(get_specific_version_from_version \"$feed\" \"$channel\" \"$normalized_architecture\" \"$version\" \"$json_file\") || specific_version='0'\n\n    if [[ \"$specific_version\" == '0' ]]; then\n        say_verbose \"Failed to resolve the specific version number using feed '$feed'\"\n        return\n    fi\n\n    effective_version=\"$(get_specific_product_version \"$feed\" \"$specific_version\")\"\n    say_verbose \"specific_version=$specific_version\"\n\n    download_link=\"$(construct_download_link \"$feed\" \"$channel\" \"$normalized_architecture\" \"$specific_version\" \"$normalized_os\")\"\n    say_verbose \"Constructed primary named payload URL: $download_link\"\n\n    # Add link info to arrays\n    download_links+=($download_link)\n    specific_versions+=($specific_version)\n    effective_versions+=($effective_version)\n    link_types+=(\"primary\")\n\n    legacy_download_link=\"$(construct_legacy_download_link \"$feed\" \"$channel\" \"$normalized_architecture\" \"$specific_version\")\" || valid_legacy_download_link=false\n\n    if [ \"$valid_legacy_download_link\" = true ]; then\n        say_verbose \"Constructed legacy named payload URL: $legacy_download_link\"\n    \n        download_links+=($legacy_download_link)\n        specific_versions+=($specific_version)\n        effective_versions+=($effective_version)\n        link_types+=(\"legacy\")\n    else\n        legacy_download_link=\"\"\n        say_verbose \"Cound not construct a legacy_download_link; omitting...\"\n    fi\n\n    #  Check if the SDK version is already installed.\n    if [[ \"$dry_run\" != true ]] && is_dotnet_package_installed \"$install_root\" \"$asset_relative_path\" \"$effective_version\"; then\n        say \"$asset_name with version '$effective_version' is already installed.\"\n        exit 0\n    fi\n}\n\nprint_dry_run() {\n\n    say \"Payload URLs:\"\n\n    for link_index in \"${!download_links[@]}\"\n        do\n            say \"URL #$link_index - ${link_types[$link_index]}: ${download_links[$link_index]}\"\n    done\n\n    resolved_version=${specific_versions[0]}\n    repeatable_command=\"./$script_name --version \"\\\"\"$resolved_version\"\\\"\" --install-dir \"\\\"\"$install_root\"\\\"\" --architecture \"\\\"\"$normalized_architecture\"\\\"\" --os \"\\\"\"$normalized_os\"\\\"\"\"\n    \n    if [ ! -z \"$normalized_quality\" ]; then\n        repeatable_command+=\" --quality \"\\\"\"$normalized_quality\"\\\"\"\"\n    fi\n\n    if [[ \"$runtime\" == \"dotnet\" ]]; then\n        repeatable_command+=\" --runtime \"\\\"\"dotnet\"\\\"\"\"\n    elif [[ \"$runtime\" == \"aspnetcore\" ]]; then\n        repeatable_command+=\" --runtime \"\\\"\"aspnetcore\"\\\"\"\"\n    fi\n\n    repeatable_command+=\"$non_dynamic_parameters\"\n\n    if [ -n \"$feed_credential\" ]; then\n        repeatable_command+=\" --feed-credential \"\\\"\"<feed_credential>\"\\\"\"\"\n    fi\n\n    say \"Repeatable invocation: $repeatable_command\"\n}\n\ncalculate_vars() {\n    eval $invocation\n\n    script_name=$(basename \"$0\")\n    normalized_architecture=\"$(get_normalized_architecture_from_architecture \"$architecture\")\"\n    say_verbose \"Normalized architecture: '$normalized_architecture'.\"\n    \n    normalized_os=\"$(get_normalized_os \"$user_defined_os\")\"\n    say_verbose \"Normalized OS: '$normalized_os'.\"\n    normalized_quality=\"$(get_normalized_quality \"$quality\")\"\n    say_verbose \"Normalized quality: '$normalized_quality'.\"\n    normalized_channel=\"$(get_normalized_channel \"$channel\")\"\n    say_verbose \"Normalized channel: '$normalized_channel'.\"\n    normalized_product=\"$(get_normalized_product \"$runtime\")\"\n    say_verbose \"Normalized product: '$normalized_product'.\"\n    install_root=\"$(resolve_installation_path \"$install_dir\")\"\n    say_verbose \"InstallRoot: '$install_root'.\"\n\n    if [[ \"$runtime\" == \"dotnet\" ]]; then\n        asset_relative_path=\"shared/Microsoft.NETCore.App\"\n        asset_name=\".NET Core Runtime\"\n    elif [[ \"$runtime\" == \"aspnetcore\" ]]; then\n        asset_relative_path=\"shared/Microsoft.AspNetCore.App\"\n        asset_name=\"ASP.NET Core Runtime\"\n    elif [ -z \"$runtime\" ]; then\n        asset_relative_path=\"sdk\"\n        asset_name=\".NET Core SDK\"\n    fi\n\n    get_feeds_to_use\n}\n\ninstall_dotnet() {\n    eval $invocation\n    local download_failed=false\n    local download_completed=false\n\n    mkdir -p \"$install_root\"\n    zip_path=\"$(mktemp \"$temporary_file_template\")\"\n    say_verbose \"Zip path: $zip_path\"\n\n    for link_index in \"${!download_links[@]}\"\n    do\n        download_link=\"${download_links[$link_index]}\"\n        specific_version=\"${specific_versions[$link_index]}\"\n        effective_version=\"${effective_versions[$link_index]}\"\n        link_type=\"${link_types[$link_index]}\"\n\n        say \"Attempting to download using $link_type link $download_link\"\n\n        # The download function will set variables $http_code and $download_error_msg in case of failure.\n        download_failed=false\n        download \"$download_link\" \"$zip_path\" 2>&1 || download_failed=true\n\n        if [ \"$download_failed\" = true ]; then\n            case $http_code in\n            404)\n                say \"The resource at $link_type link '$download_link' is not available.\"\n                ;;\n            *)\n                say \"Failed to download $link_type link '$download_link': $download_error_msg\"\n                ;;\n            esac\n            rm -f \"$zip_path\" 2>&1 && say_verbose \"Temporary zip file $zip_path was removed\"\n        else\n            download_completed=true\n            break\n        fi\n    done\n\n    if [[ \"$download_completed\" == false ]]; then\n        say_err \"Could not find \\`$asset_name\\` with version = $specific_version\"\n        say_err \"Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support\"\n        return 1\n    fi\n\n    say \"Extracting zip from $download_link\"\n    extract_dotnet_package \"$zip_path\" \"$install_root\" || return 1\n\n    #  Check if the SDK version is installed; if not, fail the installation.\n    # if the version contains \"RTM\" or \"servicing\"; check if a 'release-type' SDK version is installed.\n    if [[ $specific_version == *\"rtm\"* || $specific_version == *\"servicing\"* ]]; then\n        IFS='-'\n        read -ra verArr <<< \"$specific_version\"\n        release_version=\"${verArr[0]}\"\n        unset IFS;\n        say_verbose \"Checking installation: version = $release_version\"\n        if is_dotnet_package_installed \"$install_root\" \"$asset_relative_path\" \"$release_version\"; then\n            return 0\n        fi\n    fi\n\n    #  Check if the standard SDK version is installed.\n    say_verbose \"Checking installation: version = $effective_version\"\n    if is_dotnet_package_installed \"$install_root\" \"$asset_relative_path\" \"$effective_version\"; then\n        return 0\n    fi\n\n    # Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm.\n    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.\"\n    say_err \"\\`$asset_name\\` with version = $effective_version failed to install with an error.\"\n    return 1\n}\n\nargs=(\"$@\")\n\nlocal_version_file_relative_path=\"/.version\"\nbin_folder_relative_path=\"\"\ntemporary_file_template=\"${TMPDIR:-/tmp}/dotnet.XXXXXXXXX\"\n\nchannel=\"LTS\"\nversion=\"Latest\"\njson_file=\"\"\ninstall_dir=\"<auto>\"\narchitecture=\"<auto>\"\ndry_run=false\nno_path=false\nno_cdn=false\nazure_feed=\"\"\nuncached_feed=\"\"\nfeed_credential=\"\"\nverbose=false\nruntime=\"\"\nruntime_id=\"\"\nquality=\"\"\ninternal=false\noverride_non_versioned_files=true\nnon_dynamic_parameters=\"\"\nuser_defined_os=\"\"\n\nwhile [ $# -ne 0 ]\ndo\n    name=\"$1\"\n    case \"$name\" in\n        -c|--channel|-[Cc]hannel)\n            shift\n            channel=\"$1\"\n            ;;\n        -v|--version|-[Vv]ersion)\n            shift\n            version=\"$1\"\n            ;;\n        -q|--quality|-[Qq]uality)\n            shift\n            quality=\"$1\"\n            ;;\n        --internal|-[Ii]nternal)\n            internal=true\n            non_dynamic_parameters+=\" $name\"\n            ;;\n        -i|--install-dir|-[Ii]nstall[Dd]ir)\n            shift\n            install_dir=\"$1\"\n            ;;\n        --arch|--architecture|-[Aa]rch|-[Aa]rchitecture)\n            shift\n            architecture=\"$1\"\n            ;;\n        --os|-[Oo][SS])\n            shift\n            user_defined_os=\"$1\"\n            ;;\n        --shared-runtime|-[Ss]hared[Rr]untime)\n            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'.\"\n            if [ -z \"$runtime\" ]; then\n                runtime=\"dotnet\"\n            fi\n            ;;\n        --runtime|-[Rr]untime)\n            shift\n            runtime=\"$1\"\n            if [[ \"$runtime\" != \"dotnet\" ]] && [[ \"$runtime\" != \"aspnetcore\" ]]; then\n                say_err \"Unsupported value for --runtime: '$1'. Valid values are 'dotnet' and 'aspnetcore'.\"\n                if [[ \"$runtime\" == \"windowsdesktop\" ]]; then\n                    say_err \"WindowsDesktop archives are manufactured for Windows platforms only.\"\n                fi\n                exit 1\n            fi\n            ;;\n        --dry-run|-[Dd]ry[Rr]un)\n            dry_run=true\n            ;;\n        --no-path|-[Nn]o[Pp]ath)\n            no_path=true\n            non_dynamic_parameters+=\" $name\"\n            ;;\n        --verbose|-[Vv]erbose)\n            verbose=true\n            non_dynamic_parameters+=\" $name\"\n            ;;\n        --no-cdn|-[Nn]o[Cc]dn)\n            no_cdn=true\n            non_dynamic_parameters+=\" $name\"\n            ;;\n        --azure-feed|-[Aa]zure[Ff]eed)\n            shift\n            azure_feed=\"$1\"\n            non_dynamic_parameters+=\" $name \"\\\"\"$1\"\\\"\"\"\n            ;;\n        --uncached-feed|-[Uu]ncached[Ff]eed)\n            shift\n            uncached_feed=\"$1\"\n            non_dynamic_parameters+=\" $name \"\\\"\"$1\"\\\"\"\"\n            ;;\n        --feed-credential|-[Ff]eed[Cc]redential)\n            shift\n            feed_credential=\"$1\"\n            #feed_credential should start with \"?\", for it to be added to the end of the link.\n            #adding \"?\" at the beginning of the feed_credential if needed.\n            [[ -z \"$(echo $feed_credential)\" ]] || [[ $feed_credential == \\?* ]] || feed_credential=\"?$feed_credential\"\n            ;;\n        --runtime-id|-[Rr]untime[Ii]d)\n            shift\n            runtime_id=\"$1\"\n            non_dynamic_parameters+=\" $name \"\\\"\"$1\"\\\"\"\"\n            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.\"\n            ;;\n        --jsonfile|-[Jj][Ss]on[Ff]ile)\n            shift\n            json_file=\"$1\"\n            ;;\n        --skip-non-versioned-files|-[Ss]kip[Nn]on[Vv]ersioned[Ff]iles)\n            override_non_versioned_files=false\n            non_dynamic_parameters+=\" $name\"\n            ;;\n        -?|--?|-h|--help|-[Hh]elp)\n            script_name=\"$(basename \"$0\")\"\n            echo \".NET Tools Installer\"\n            echo \"Usage: $script_name [-c|--channel <CHANNEL>] [-v|--version <VERSION>] [-p|--prefix <DESTINATION>]\"\n            echo \"       $script_name -h|-?|--help\"\n            echo \"\"\n            echo \"$script_name is a simple command line interface for obtaining dotnet cli.\"\n            echo \"\"\n            echo \"Options:\"\n            echo \"  -c,--channel <CHANNEL>         Download from the channel specified, Defaults to \\`$channel\\`.\"\n            echo \"      -Channel\"\n            echo \"          Possible values:\"\n            echo \"          - Current - most current release\"\n            echo \"          - LTS - most current supported release\"\n            echo \"          - 2-part version in a format A.B - represents a specific release\"\n            echo \"              examples: 2.0; 1.0\"\n            echo \"          - 3-part version in a format A.B.Cxx - represents a specific SDK release\"\n            echo \"              examples: 5.0.1xx, 5.0.2xx.\"\n            echo \"              Supported since 5.0 release\"\n            echo \"          Note: The version parameter overrides the channel parameter when any version other than 'latest' is used.\"\n            echo \"  -v,--version <VERSION>         Use specific VERSION, Defaults to \\`$version\\`.\"\n            echo \"      -Version\"\n            echo \"          Possible values:\"\n            echo \"          - latest - most latest build on specific channel\"\n            echo \"          - 3-part version in a format A.B.C - represents specific version of build\"\n            echo \"              examples: 2.0.0-preview2-006120; 1.1.0\"\n            echo \"  -q,--quality <quality>         Download the latest build of specified quality in the channel.\"\n            echo \"      -Quality\"\n            echo \"          The possible values are: daily, signed, validated, preview, GA.\"\n            echo \"          Works only in combination with channel. Not applicable for current and LTS channels and will be ignored if those channels are used.\" \n            echo \"          For SDK use channel in A.B.Cxx format. Using quality for SDK together with channel in A.B format is not supported.\" \n            echo \"          Supported since 5.0 release.\" \n            echo \"          Note: The version parameter overrides the channel parameter when any version other than 'latest' is used, and therefore overrides the quality.\"\n            echo \"  --internal,-Internal               Download internal builds. Requires providing credentials via --feed-credential parameter.\"\n            echo \"  --feed-credential <FEEDCREDENTIAL> Token to access Azure feed. Used as a query string to append to the Azure feed.\"\n            echo \"      -FeedCredential                This parameter typically is not specified.\"\n            echo \"  -i,--install-dir <DIR>             Install under specified location (see Install Location below)\"\n            echo \"      -InstallDir\"\n            echo \"  --architecture <ARCHITECTURE>      Architecture of dotnet binaries to be installed, Defaults to \\`$architecture\\`.\"\n            echo \"      --arch,-Architecture,-Arch\"\n            echo \"          Possible values: x64, arm, and arm64\"\n            echo \"  --os <system>                    Specifies operating system to be used when selecting the installer.\"\n            echo \"          Overrides the OS determination approach used by the script. Supported values: osx, linux, linux-musl, freebsd, rhel.6.\"\n            echo \"          In case any other value is provided, the platform will be determined by the script based on machine configuration.\"\n            echo \"          Not supported for legacy links. Use --runtime-id to specify platform for legacy links.\"\n            echo \"          Refer to: https://aka.ms/dotnet-os-lifecycle for more information.\"\n            echo \"  --runtime <RUNTIME>                Installs a shared runtime only, without the SDK.\"\n            echo \"      -Runtime\"\n            echo \"          Possible values:\"\n            echo \"          - dotnet     - the Microsoft.NETCore.App shared runtime\"\n            echo \"          - aspnetcore - the Microsoft.AspNetCore.App shared runtime\"\n            echo \"  --dry-run,-DryRun                  Do not perform installation. Display download link.\"\n            echo \"  --no-path, -NoPath                 Do not set PATH for the current process.\"\n            echo \"  --verbose,-Verbose                 Display diagnostics information.\"\n            echo \"  --azure-feed,-AzureFeed            For internal use only.\"\n            echo \"                                     Allows using a different storage to download SDK archives from.\"\n            echo \"                                     This parameter is only used if --no-cdn is false.\"\n            echo \"  --uncached-feed,-UncachedFeed      For internal use only.\"\n            echo \"                                     Allows using a different storage to download SDK archives from.\"\n            echo \"                                     This parameter is only used if --no-cdn is true.\"\n            echo \"  --skip-non-versioned-files         Skips non-versioned files if they already exist, such as the dotnet executable.\"\n            echo \"      -SkipNonVersionedFiles\"\n            echo \"  --no-cdn,-NoCdn                    Disable downloading from the Azure CDN, and use the uncached feed directly.\"\n            echo \"  --jsonfile <JSONFILE>              Determines the SDK version from a user specified global.json file.\"\n            echo \"                                     Note: global.json must have a value for 'SDK:Version'\"\n            echo \"  -?,--?,-h,--help,-Help             Shows this help message\"\n            echo \"\"\n            echo \"Install Location:\"\n            echo \"  Location is chosen in following order:\"\n            echo \"    - --install-dir option\"\n            echo \"    - Environmental variable DOTNET_INSTALL_DIR\"\n            echo \"    - $HOME/.dotnet\"\n            exit 0\n            ;;\n        *)\n            say_err \"Unknown argument \\`$name\\`\"\n            exit 1\n            ;;\n    esac\n\n    shift\ndone\n\nsay \"Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:\"\nsay \"- The SDK needs to be installed without user interaction and without admin rights.\"\nsay \"- The SDK installation doesn't need to persist across multiple CI runs.\"\nsay \"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\"\n\nif [ \"$internal\" = true ] && [ -z \"$(echo $feed_credential)\" ]; then\n    message=\"Provide credentials via --feed-credential parameter.\"\n    if [ \"$dry_run\" = true ]; then\n        say_warning \"$message\"\n    else\n        say_err \"$message\"\n        exit 1\n    fi\nfi\n\ncheck_min_reqs\ncalculate_vars\n# generate_regular_links call below will 'exit' if the determined version is already installed.\ngenerate_download_links\n\nif [[ \"$dry_run\" = true ]]; then\n    print_dry_run\n    exit 0\nfi\n\ninstall_dotnet\n\nbin_path=\"$(get_absolute_path \"$(combine_paths \"$install_root\" \"$bin_folder_relative_path\")\")\"\nif [ \"$no_path\" = false ]; then\n    say \"Adding to current process PATH: \\`$bin_path\\`. Note: This change will be visible only when sourcing script.\"\n    export PATH=\"$bin_path\":\"$PATH\"\nelse\n    say \"Binaries of dotnet can be found in $bin_path\"\nfi\n\nsay \"Note that the script does not resolve dependencies during installation.\"\nsay \"To check the list of dependencies, go to https://docs.microsoft.com/dotnet/core/install, select your operating system and check the \\\"Dependencies\\\" section.\"\nsay \"Installation finished successfully.\""
  },
  {
    "path": "qinglong/extra.sh",
    "content": "## 添加你需要重启自动执行的任意命令，比如 ql repo\n## 安装node依赖使用 pnpm install -g xxx xxx\n## 安装python依赖使用 pip3 install xxx\n\n# 安装 dotnet 环境\n# dotnet --version || (curl -sSL https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/main/qinglong/ray-dotnet-install.sh | bash /dev/stdin --no-official) && (echo \"已安装dotnet\")\ndotnet --version || (curl -sSL https://raw.githubusercontent.com/RayWangQvQ/BiliBiliToolPro/main/qinglong/ray-dotnet-install.sh | bash /dev/stdin) && (echo \"已安装dotnet\")\n# 其他代码...\n"
  },
  {
    "path": "qinglong/ray-dotnet-install.sh",
    "content": "#!/usr/bin/env bash\necho -e \"\\n-------set up dot net env-------\"\n\n## 安装dotnet\n\n# 安装依赖\ninstall_dependency() {\n    echo \"安装依赖...\"\n    apk add bash icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib\n}\n\n# 通过官方脚本安装dotnet\ninstall_by_offical() {\n    echo \"install by offical script...\"\n    curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 8.0 --no-cdn --verbose\n}\n\n# 创建软链接\ncreate_soft_link() {\n    # echo \"创建软链接...\"\n    # rm -f /usr/bin/dotnet\n    # ln -s ~/.dotnet/dotnet /usr/bin/dotnet\n\n    echo \"添加PATH\"\n    local exportFile=\"/root/.bashrc\"\n    touch $exportFile\n    echo '' >> $exportFile\n    echo 'export DOTNET_ROOT=$HOME/.dotnet' >> $exportFile\n    echo 'export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools' >> $exportFile\n    . $exportFile\n}\n\nargs=(\"$@\")\n\ninstall_dependency\n\ninstall_by_offical\n\ncreate_soft_link\n\ndotnet --info\n\necho -e \"\\n-------set up dot net env finish-------\""
  },
  {
    "path": "scripts/clean.cmd",
    "content": "@echo off\ncd ..\nREM start to clean\necho Start to clean all bin and obj folder under Noodle repo\n@echo on\n@for /d /r %%c in (obj) do @if exist \"%%c\" (@rd /s /q \"%%c\" & echo Delete %%c)\n@for /d /r %%c in (bin) do @if exist \"%%c\" (@rd /s /q \"%%c\" & echo Delete %%c)\n@for /d /r %%c in (packages) do @if exist \"%%c\" (@rd /s /q \"%%c\" & echo Delete %%c)\n@for /d /r %%c in (.vs) do @if exist \"%%c\" (@rd /s /q \"%%c\" & echo Delete %%c)\n@for /d /r %%c in (temp) do @if exist \"%%c\" (@rd /s /q \"%%c\" & echo Delete %%c)\n@echo off\npause"
  },
  {
    "path": "scripts/publish.bat",
    "content": "::https://docs.microsoft.com/zh-cn/dotnet/core/tools/dotnet-publish\n::关闭回显\n@echo off\n\ndotnet publish --configuration Release --self-contained false -o ./bin/Publish/net5-dependent\necho \"dotnet Ray.BiliBiliTool.Console.dll\" > ./bin/Publish/net5-dependent/start.bat\n\n::dotnet publish --configuration Release --runtime win-x86 --self-contained true -p:PublishTrimmed=true -o ./bin/Publish/win-x86-x64\n::dotnet publish --configuration Release --runtime linux-x64 --self-contained true -p:PublishTrimmed=true -o ./bin/Publish/linux-x64\n::dotnet publish --configuration Release --runtime linux-arm --self-contained true -p:PublishTrimmed=true -o ./bin/Publish/linux-arm\n::dotnet publish --configuration Release --runtime linux-arm64 --self-contained true -p:PublishTrimmed=true -o ./bin/Publish/linux-arm64\n::dotnet publish --configuration Release --runtime osx-x64 --self-contained true -p:PublishTrimmed=true -o ./bin/Publish/osx-x64\n\npause\n"
  },
  {
    "path": "scripts/publish.ps1",
    "content": "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\ndotnet.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\ndotnet.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\ndotnet.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\ndotnet.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\ndotnet.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\ndotnet.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\ndotnet.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\nRemove-Item ./bin/Publish/win-x86/*.pdb\nRemove-Item ./bin/Publish/win-x64/*.pdb\nRemove-Item ./bin/Publish/win-arm64/*.pdb\nRemove-Item ./bin/Publish/linux-x64/*.pdb\nRemove-Item ./bin/Publish/linux-musl-x64/*.pdb\nRemove-Item ./bin/Publish/linux-arm64/*.pdb\nRemove-Item ./bin/Publish/linux-arm/*.pdb\nRemove-Item ./bin/Publish/osx-x64/*.pdb\n"
  },
  {
    "path": "scripts/publish.sh",
    "content": "#!/usr/bin/env bash\nset -e\nset -u\nset -o pipefail\n\necho '  ____    _   _____           _  '\necho ' | __ ) _| |_|_   _|__   ___ | | '\necho ' |  _ \\(_) (_) | |/ _ \\ / _ \\| | '\necho ' | |_) | | | | | | (_) | (_) | | '\necho ' |____/|_|_|_| |_|\\___/ \\___/|_| '\necho ''\n\n# ------------vars-----------\nrepoDir=$(dirname $PWD)\nconsoleDir=$repoDir/src/Ray.BiliBiliTool.Console\npublishDir=$consoleDir/bin/Publish\nversion=\"\"\nrunTime=\"\"\n# --------------------------\n\nread_params_from_init_cmd() {\n    while [ $# -ne 0 ]; do\n        name=\"$1\"\n        case \"$name\" in\n        -r | --runtime | -[Rr]untime)\n            shift\n            runTime=\"$1\"\n            ;;\n        *)\n            say_err \"Unknown argument \\`$name\\`\"\n            exit 1\n            ;;\n        esac\n        shift\n    done\n}\n\nread_var_from_user() {\n    # runTime\n    if [ -z \"$runTime\" ]; then\n        read -p 'please input runTime(\"all\" \"win-x86\" \"win-x64\" \"win-arm64\" \"linux-x64\" \"linux-musl-x64\" \"linux-arm64\" \"linux-arm\" \"osx-x64\")' runTime\n    else\n        echo \"runTime: $runTime\"\n    fi\n}\n\nget_version() {\n    version=$(grep -oP '(?<=<Version>).*?(?=<\\/Version>)' $repoDir/common.props)\n    echo -e \"current version: $version \\n\\n\"\n\n    mkdir -p $publishDir\n\n    # 将版本号保存到文件\n    echo \"$version\" > \"$publishDir/version.txt\"\n\n    echo \"Version saved to $publishDir/version.txt\"\n}\n\nextract_release_notes() {\n    echo \"Extracting release notes from CHANGELOG.md...\"\n    mkdir -p $publishDir\n\n    # 提取最新的 changelog (从第一个 ## 标题到下一个 ## 标题之间的所有内容)\n    sed -n '/^## /{p;:a;n;/^## /q;p;ba}' \"$repoDir/CHANGELOG.md\" > \"$publishDir/release_notes.md\"\n\n    echo \"Release notes saved to $publishDir/release_notes.md\"\n}\n\npublish_dotnet_dependent() {\n    echo \"---------start publishing 【dotnet dependent】 release---------\"\n\n    echo \"clear output dir\"\n    local outputDir=$publishDir/dotnet-dependent\n    mkdir -p $outputDir\n    rm -rf $outputDir\n\n    cd $consoleDir\n    echo \"dotnet publish...\"\n    dotnet publish --configuration Release \\\n        --self-contained false \\\n        -p:PublishSingleFile=true \\\n        -p:DebugType=None \\\n        -p:DebugSymbols=false \\\n        -o $outputDir\n\n    echo \"zip files...\"\n    cd $publishDir\n    zip -q -r bilibili-tool-pro-v$version-dotnet-dependent.zip ./dotnet-dependent/*\n    ls -l\n    echo -e \"---------publish successfully---------\\n\\n\"\n}\n\npublish_self_contained() {\n    local runtime=$1\n    echo \"---------start publishing 【$runtime】 release---------\"\n\n    echo \"clear output dir\"\n    local outputDir=$publishDir/$runtime\n    mkdir -p $outputDir\n    rm -rf $outputDir\n\n    cd $consoleDir\n    echo \"dotnet publish...\"\n    dotnet publish --configuration Release \\\n        --self-contained true \\\n        --runtime $runtime \\\n        -p:PublishSingleFile=true \\\n        -p:DebugType=None \\\n        -p:DebugSymbols=false \\\n        -o $outputDir\n\n    echo \"zip files...\"\n    cd $publishDir\n    zip -q -r bilibili-tool-pro-v$version-$runtime.zip ./$runtime/*\n    ls -l\n    echo -e \"---------publish successfully---------\\n\\n\"\n}\n\npublish_tencentScf() {\n    echo \"---------start publishing 【tencent scf】 release---------\"\n    cd $publishDir\n    cp -r $repoDir/tencentScf/bootstrap $repoDir/tencentScf/index.sh ./linux-x64/\n    cd ./linux-x64\n    chmod 755 index.sh bootstrap\n    zip -r ../bilibili-tool-pro-v$version-tencent-scf.zip ./*\n    cd .. && ls\n    echo -e \"---------publish successfully---------\\n\\n\"\n}\n\nmain() {\n    read_params_from_init_cmd $*\n    read_var_from_user\n\n    get_version\n    extract_release_notes\n\n    # dotnet dependent\n    publish_dotnet_dependent\n\n    # self contained\n    # https://learn.microsoft.com/zh-cn/dotnet/core/rid-catalog\n    array=(\"win-x86\" \"win-x64\" \"win-arm64\" \"linux-x64\" \"linux-musl-x64\" \"linux-arm64\" \"linux-arm\" \"linux-musl-arm64\" \"osx-x64\")\n    if [ \"$runTime\" != \"all\" ]; then\n        array=(\"$runTime\")\n    fi\n    for i in \"${array[@]}\"; do\n        publish_self_contained $i\n    done\n\n    if [ \"$runTime\" == \"all\" ]; then\n        publish_tencentScf\n    fi\n}\n\nmain $*\n"
  },
  {
    "path": "scripts/ut.ps1",
    "content": "Set-Location ..\r\n\r\n# 安装 ReportGenerator 工具\r\nWrite-Output \"Installing ReportGenerator tool...\"\r\ndotnet tool install -g dotnet-reportgenerator-globaltool\r\n\r\n# 运行单元测试并生成覆盖率报告\r\nWrite-Output \"Running unit tests and generating coverage report...\"\r\ndotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=./TestResults/coverage.opencover.xml\r\n\r\n# 生成html报告\r\n$coverageFiles = Get-ChildItem -Path . -Recurse -Filter \"coverage.cobertura.xml\"\r\n$coverageFiles | ForEach-Object { Write-Output $_.FullName }\r\n$reportPaths = ($coverageFiles | ForEach-Object { $_.FullName }) -join \";\"\r\nreportgenerator \"-reports:$reportPaths\" \"-targetdir:coveragereport\" -reporttypes:Html\r\n\r\n# 检查生成的覆盖率报告文件是否存在\r\nWrite-Output \"Coverage report generated successfully.\"\r\nStart-Process \"coveragereport/index.htm\""
  },
  {
    "path": "src/BlazingQuartz.Core/BlazingQuartz.Core.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Quartz\" />\n    <PackageReference Include=\"Microsoft.Extensions.DependencyInjection.Abstractions\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging.Abstractions\" />\n    <PackageReference Include=\"Microsoft.Extensions.Options\" />\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore\" />\n    <PackageReference Include=\"CronExpressionDescriptor\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\BlazingQuartz.Jobs.Abstractions\\BlazingQuartz.Jobs.Abstractions.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Application.Contracts\\Ray.BiliBiliTool.Application.Contracts.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Domain\\Ray.BiliBiliTool.Domain.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Infrastructure\\Ray.BiliBiliTool.Infrastructure.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Infrastructure.EF\\Ray.BiliBiliTool.Infrastructure.EF.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/BlazingQuartz.Core/BlazingQuartzCoreOptions.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Core\n{\n    public class BlazingQuartzCoreOptions\n    {\n        /// <summary>\n        /// <para>Assembly files that contain IJob or IJobUI implementation use for creating schedule.</para>\n        /// <para>Ex. Quartz.Jobs</para>\n        /// <para>Or Jobs/Quartz.Jobs - if this file is under Job folder</para>\n        /// </summary>\n        public string[]? AllowedJobAssemblyFiles { get; set; }\n\n        /// <summary>\n        /// <para>Job types that are not allowed to be used for creating new Jobs using UI.</para>\n        /// <para>Ex. Quartz.Job.NativeJob</para>\n        /// </summary>\n        public string[]? DisallowedJobTypes { get; set; }\n\n        /// <summary>\n        /// Storage to use to store execution history\n        /// </summary>\n        public DataStoreProvider DataStoreProvider { get; set; } = DataStoreProvider.Sqlite;\n        public bool AutoMigrateDb { get; set; } = true;\n        public string? HousekeepingCronSchedule { get; set; } = \"0 0 1 * * ?\";\n        public int ExecutionLogsDaysToKeep { get; set; } = 21;\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Constants.cs",
    "content": "﻿using System;\nusing System.ComponentModel;\n\nnamespace BlazingQuartz;\n\npublic static class Constants\n{\n    public const string SYSTEM_GROUP = \"System\";\n    public const string DEFAULT_GROUP = \"No Group\";\n}\n\npublic enum DataStoreProvider\n{\n    Sqlite,\n    PostgreSQL,\n    InMemory,\n    SqlServer,\n    Custom,\n}\n\npublic enum JobStatus\n{\n    Running,\n    Idle,\n    Paused,\n\n    /// <summary>\n    /// No trigger assigned to this job. This happens when job is durable and triggers ended\n    /// </summary>\n    NoTrigger,\n\n    /// <summary>\n    /// Not scheduled in scheduler. This happens when job is NOT durable and triggers ended\n    /// </summary>\n    NoSchedule,\n    Error,\n}\n\npublic enum TriggerType\n{\n    Cron,\n    Daily,\n    Simple,\n    Calendar,\n    Unknown,\n}\n\npublic enum MisfireAction\n{\n    [Description(\n        \"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.\"\n    )]\n    IgnoreMisfirePolicy,\n\n    [Description(\"Instruction not set (yet).\")]\n    InstructionNotSet,\n\n    [Description(\"Use smart policy.\")]\n    SmartPolicy,\n\n    [Description(\n        \"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.\"\n    )]\n    FireNow,\n\n    [Description(\n        \"Re-scheduled to the next scheduled time after 'now' - taking into account any associated ICalendar, and with the repeat count left unchanged.\"\n    )]\n    RescheduleNextWithExistingCount,\n\n    [Description(\n        \"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.\"\n    )]\n    RescheduleNextWithRemainingCount,\n\n    [Description(\n        \"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.\"\n    )]\n    RescheduleNowWithExistingRepeatCount,\n\n    [Description(\n        \"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.\"\n    )]\n    RescheduleNowWithRemainingRepeatCount,\n\n    [Description(\n        \"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.\"\n    )]\n    DoNothing,\n\n    [Description(\"Instruct to fire now\")]\n    FireOnceNow,\n}\n\npublic enum IntervalUnit\n{\n    Millisecond,\n    Second,\n    Minute,\n    Hour,\n    Day,\n    Week,\n    Month,\n    Year,\n}\n\npublic enum DataMapType\n{\n    Bool,\n    String,\n    Integer,\n    Float,\n    Double,\n    Decimal,\n    Long,\n    Short,\n    Char,\n    Object,\n}\n\npublic enum JobExecutionStatus\n{\n    Success,\n    Failed,\n    Executing,\n    Vetoed,\n}\n\npublic class BlazingQuartzCoreOptions\n{\n    /// <summary>\n    /// <para>Assembly files that contain IJob or IJobUI implementation use for creating schedule.</para>\n    /// <para>Ex. Quartz.Jobs</para>\n    /// <para>Or Jobs/Quartz.Jobs - if this file is under Job folder</para>\n    /// </summary>\n    public string[]? AllowedJobAssemblyFiles { get; set; }\n\n    /// <summary>\n    /// <para>Job types that are not allowed to be used for creating new Jobs using UI.</para>\n    /// <para>Ex. Quartz.Job.NativeJob</para>\n    /// </summary>\n    public string[]? DisallowedJobTypes { get; set; }\n\n    /// <summary>\n    /// Storage to use to store execution history\n    /// </summary>\n    public DataStoreProvider DataStoreProvider { get; set; } = DataStoreProvider.Sqlite;\n    public bool AutoMigrateDb { get; set; } = true;\n    public string? HousekeepingCronSchedule { get; set; } = \"0 0 1 * * ?\";\n    public int ExecutionLogsDaysToKeep { get; set; } = 21;\n}\n\npublic class BlazingQuartzUIOptions : BlazingQuartzCoreOptions { }\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Events/EventArgs.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Core.Events\n{\n    public class EventArgs<TArgs> : EventArgs\n    {\n        public TArgs Args { get; init; }\n        public CancellationToken CancelToken { get; init; }\n\n        public EventArgs(TArgs args, CancellationToken cancelToken = default(CancellationToken))\n        {\n            Args = args;\n            CancelToken = cancelToken;\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Events/JobWasExecutedEventArgs.cs",
    "content": "﻿using System;\nusing Quartz;\n\nnamespace BlazingQuartz.Core.Events\n{\n    public class JobWasExecutedEventArgs : EventArgs\n    {\n        public IJobExecutionContext JobExecutionContext { get; init; }\n        public JobExecutionException? JobException { get; init; }\n        public CancellationToken CancelToken { get; set; }\n\n        public JobWasExecutedEventArgs(\n            IJobExecutionContext context,\n            JobExecutionException? exception,\n            CancellationToken cancelToken = default(CancellationToken)\n        )\n        {\n            JobExecutionContext = context;\n            JobException = exception;\n            CancelToken = cancelToken;\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Events/SchedulerErrorEventArgs.cs",
    "content": "﻿using System;\nusing Quartz;\n\nnamespace BlazingQuartz.Core.Events\n{\n    public class SchedulerErrorEventArgs : EventArgs\n    {\n        public string ErrorMessage { get; init; } = null!;\n        public SchedulerException Exception { get; init; } = null!;\n        public CancellationToken CancelToken { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Events/TriggerEventArgs.cs",
    "content": "﻿using System;\nusing Quartz;\n\nnamespace BlazingQuartz.Core.Events\n{\n    public class TriggerEventArgs : EventArgs\n    {\n        public ITrigger Trigger { get; init; }\n        public IJobExecutionContext JobExecutionContext { get; init; }\n        public CancellationToken CancelToken { get; init; }\n\n        public TriggerEventArgs(\n            ITrigger trigger,\n            IJobExecutionContext context,\n            CancellationToken cancelToken = default(CancellationToken)\n        )\n        {\n            Trigger = trigger;\n            JobExecutionContext = context;\n            CancelToken = cancelToken;\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Extensions/ModelExtensions.cs",
    "content": "﻿using System;\nusing BlazingQuartz.Core.Models;\nusing Quartz;\n\nnamespace BlazingQuartz.Core\n{\n    public static class ModelExtensions\n    {\n        public static bool EqualsTriggerKey(this ScheduleModel model, TriggerKey triggerKey)\n        {\n            return model.TriggerName == triggerKey.Name && model.TriggerGroup == triggerKey.Group;\n        }\n\n        public static bool Equals(this ScheduleModel model, JobKey? jobKey, TriggerKey? triggerKey)\n        {\n            if (jobKey != null && triggerKey != null)\n                return model.JobName == jobKey.Name\n                    && model.JobGroup == jobKey.Group\n                    && model.TriggerName == triggerKey.Name\n                    && model.TriggerGroup == triggerKey.Group;\n\n            if (jobKey != null && triggerKey == null)\n                return model.JobName == jobKey.Name\n                    && model.JobGroup == jobKey.Group\n                    && model.TriggerName == null\n                    && model.TriggerGroup == null;\n\n            // less possible\n            if (jobKey == null && triggerKey != null)\n                return model.TriggerName == triggerKey.Name\n                    && model.TriggerGroup == triggerKey.Group\n                    && model.JobName == null\n                    && model.JobGroup == Constants.DEFAULT_GROUP;\n\n            return model.JobName == null && model.TriggerName == null && model.TriggerGroup == null;\n        }\n\n        public static TriggerType GetTriggerType(this ITrigger trigger)\n        {\n            if (trigger is ICronTrigger)\n                return TriggerType.Cron;\n            if (trigger is ISimpleTrigger)\n                return TriggerType.Simple;\n            if (trigger is ICalendarIntervalTrigger)\n                return TriggerType.Calendar;\n            if (trigger is IDailyTimeIntervalTrigger)\n                return TriggerType.Daily;\n\n            return TriggerType.Unknown;\n        }\n\n        public static TimeOfDay ToTimeOfDay(this TimeSpan timeSpan)\n        {\n            return new TimeOfDay(timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds);\n        }\n\n        public static Quartz.IntervalUnit ToQuartzIntervalUnit(this IntervalUnit value)\n        {\n            return Enum.Parse<Quartz.IntervalUnit>(value.ToString());\n        }\n\n        public static IntervalUnit ToBlazingQuartzIntervalUnit(this Quartz.IntervalUnit value)\n        {\n            return Enum.Parse<IntervalUnit>(value.ToString());\n        }\n\n        public static JobKey ToJobKey(this Key key)\n        {\n            return key.Group == null ? new JobKey(key.Name) : new JobKey(key.Name, key.Group);\n        }\n\n        public static TriggerKey ToTriggerKey(this Key key)\n        {\n            return key.Group == null\n                ? new TriggerKey(key.Name)\n                : new TriggerKey(key.Name, key.Group);\n        }\n\n        /// <summary>\n        /// Return closest non null stack trace of exception.\n        /// Loop until null InnerException to get stack trace.\n        /// </summary>\n        /// <param name=\"exception\"></param>\n        /// <returns>null if inner exceptions does not have stack trace</returns>\n        public static string? NonNullStackTrace(this Exception exception)\n        {\n            Exception? currentException = exception;\n            while (currentException.StackTrace == null)\n            {\n                currentException = currentException.InnerException;\n                if (currentException == null)\n                    break;\n            }\n            return currentException?.StackTrace;\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Helpers/CronExpressionHelper.cs",
    "content": "﻿using System;\nusing Quartz;\n\nnamespace BlazingQuartz.Core.Helpers\n{\n    public static class CronExpressionHelper\n    {\n        public static bool IsValidExpression(string cronExpression)\n        {\n            return CronExpression.IsValidExpression(cronExpression);\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/History/BaseExecutionLogRawSqlProvider.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Core.History\n{\n    public class BaseExecutionLogRawSqlProvider : IExecutionLogRawSqlProvider\n    {\n        public virtual string DeleteLogsByDays { get; } =\n            @\"DELETE FROM bili_execution_logs\nWHERE date_added_utc < {0}\";\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/History/ExecutionLogStore.cs",
    "content": "﻿using Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.Logging;\nusing Ray.BiliBiliTool.Domain;\nusing Ray.BiliBiliTool.Infrastructure.EF;\n\nnamespace BlazingQuartz.Core.History\n{\n    public class ExecutionLogStore : IExecutionLogStore\n    {\n        private readonly ILogger<ExecutionLogStore> _logger;\n        private readonly BiliDbContext _dbContext;\n        private readonly IExecutionLogRawSqlProvider _sqlProvider;\n\n        public ExecutionLogStore(\n            ILogger<ExecutionLogStore> logger,\n            BiliDbContext dbContext,\n            IExecutionLogRawSqlProvider sqlProvider\n        )\n        {\n            _logger = logger;\n            _dbContext = dbContext;\n            _sqlProvider = sqlProvider;\n        }\n\n        public async Task AddExecutionLog(ExecutionLog log, CancellationToken cancelToken = default)\n        {\n            await _dbContext.ExecutionLogs.AddAsync(log, cancelToken);\n        }\n\n        public bool Exists(ExecutionLog log)\n        {\n            return _dbContext.ExecutionLogs.Any(l => l.RunInstanceId == log.RunInstanceId);\n        }\n\n        public async Task<int> DeleteLogsByDays(\n            int daysToKeep,\n            CancellationToken cancelToken = default\n        )\n        {\n            DateTime oldDate = DateTime.UtcNow.Date.AddDays(-(daysToKeep + 1));\n\n            IEnumerable<object> parameters = new List<object> { oldDate };\n            return await _dbContext.Database.ExecuteSqlRawAsync(\n                _sqlProvider.DeleteLogsByDays,\n                parameters,\n                cancelToken\n            );\n        }\n\n        public async Task SaveChangesAsync(CancellationToken cancelToken = default)\n        {\n            await _dbContext.SaveChangesAsync(cancelToken);\n        }\n\n        public ValueTask UpdateExecutionLog(ExecutionLog log)\n        {\n            var entry = _dbContext\n                .ExecutionLogs.Where(l => l.RunInstanceId == log.RunInstanceId)\n                .FirstOrDefault();\n\n            if (entry != null)\n            {\n                entry.ExecutionLogDetail = log.ExecutionLogDetail;\n                entry.ErrorMessage = log.ErrorMessage;\n                entry.ExecutionLogDetail = log.ExecutionLogDetail;\n                entry.IsVetoed = log.IsVetoed;\n                entry.JobRunTime = log.JobRunTime;\n                entry.Result = log.Result;\n                entry.IsException = log.IsException;\n                entry.IsSuccess = log.IsSuccess;\n                entry.ReturnCode = log.ReturnCode;\n\n                _dbContext.ExecutionLogs.Update(entry);\n            }\n            else\n            {\n                _logger.LogWarning(\n                    \"Failed to UpdateExecutionLog. Cannot find run instance id [{runInstanceId}]\",\n                    log.RunInstanceId\n                );\n            }\n\n            return ValueTask.CompletedTask;\n        }\n\n        public async Task MarkExecutingJobAsIncomplete(CancellationToken cancellToken = default)\n        {\n            var isSuccessNullJobs = _dbContext.ExecutionLogs.Where(l =>\n                !l.IsSuccess.HasValue && l.LogType == LogType.ScheduleJob\n            );\n\n            foreach (var log in isSuccessNullJobs)\n            {\n                log.IsSuccess = false;\n                log.ErrorMessage = \"Incomplete execution.\";\n                log.JobRunTime = null;\n            }\n\n            await _dbContext.SaveChangesAsync(cancellToken);\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/History/IExecutionLogRawSqlProvider.cs",
    "content": "﻿namespace BlazingQuartz.Core.History\n{\n    public interface IExecutionLogRawSqlProvider\n    {\n        string DeleteLogsByDays { get; }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/History/IExecutionLogStore.cs",
    "content": "﻿using System;\nusing Ray.BiliBiliTool.Domain;\n\nnamespace BlazingQuartz.Core.History\n{\n    public interface IExecutionLogStore\n    {\n        bool Exists(ExecutionLog log);\n        Task<int> DeleteLogsByDays(int daysToKeep, CancellationToken cancelToken = default);\n        Task AddExecutionLog(ExecutionLog log, CancellationToken cancelToken = default);\n        ValueTask UpdateExecutionLog(ExecutionLog log);\n        Task SaveChangesAsync(CancellationToken cancelToken = default);\n        Task MarkExecutingJobAsIncomplete(CancellationToken cancellToken = default);\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/History/ISchedulerEventLoggingService.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Core.History\n{\n    internal interface ISchedulerEventLoggingService { }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/History/SchedulerEventLoggingService.cs",
    "content": "﻿using System;\nusing System.Globalization;\nusing System.Threading.Channels;\nusing BlazingQuartz.Core.Jobs;\nusing BlazingQuartz.Core.Services;\nusing BlazingQuartz.Jobs.Abstractions;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Quartz;\nusing Ray.BiliBiliTool.Domain;\n\nnamespace BlazingQuartz.Core.History;\n\ninternal class SchedulerEventLoggingService : BackgroundService, ISchedulerEventLoggingService\n{\n    private const int RESULT_MAX_LENGTH = 8000;\n    private const int MAX_BATCH_SIZE = 50;\n\n    private readonly IServiceProvider _svcProvider;\n    private readonly ISchedulerListenerService _schLisSvc;\n    private readonly ISchedulerFactory _schedulerFactory;\n    private readonly ILogger<SchedulerEventLoggingService> _logger;\n    private readonly Channel<Func<IExecutionLogStore, CancellationToken, ValueTask>> _taskQueue;\n    private readonly BlazingQuartzCoreOptions _options;\n\n    public SchedulerEventLoggingService(\n        ILogger<SchedulerEventLoggingService> logger,\n        IServiceProvider serviceProvider,\n        ISchedulerListenerService listenerSvc,\n        ISchedulerFactory schFactory,\n        IOptions<BlazingQuartzCoreOptions> options\n    )\n    {\n        _logger = logger;\n        _svcProvider = serviceProvider;\n        _schLisSvc = listenerSvc;\n        _schedulerFactory = schFactory;\n        _options = options.Value;\n        _taskQueue = Channel.CreateUnbounded<\n            Func<IExecutionLogStore, CancellationToken, ValueTask>\n        >(new UnboundedChannelOptions { SingleReader = true });\n\n        Init();\n    }\n\n    public override void Dispose()\n    {\n        _schLisSvc.OnJobToBeExecuted -= _schLisSvc_OnJobToBeExecuted;\n        _schLisSvc.OnJobWasExecuted -= _schLisSvc_OnJobWasExecuted;\n        _schLisSvc.OnJobExecutionVetoed -= _schLisSvc_OnJobExecutionVetoed;\n        _schLisSvc.OnJobDeleted -= _schLisSvc_OnJobDeleted;\n        _schLisSvc.OnJobInterrupted -= _schLisSvc_OnJobInterrupted;\n        _schLisSvc.OnSchedulerError -= _schLisSvc_OnSchedulerError;\n        _schLisSvc.OnTriggerMisfired -= _schLisSvc_OnTriggerMisfired;\n        _schLisSvc.OnTriggerPaused -= _schLisSvc_OnTriggerPaused;\n        _schLisSvc.OnTriggerResumed -= _schLisSvc_OnTriggerResumed;\n        _schLisSvc.OnTriggerFinalized -= _schLisSvc_OnTriggerFinalized;\n        _schLisSvc.OnJobScheduled -= _schLisSvc_OnJobScheduled;\n        base.Dispose();\n    }\n\n    void Init()\n    {\n        _schLisSvc.OnJobToBeExecuted += _schLisSvc_OnJobToBeExecuted;\n        _schLisSvc.OnJobWasExecuted += _schLisSvc_OnJobWasExecuted;\n        _schLisSvc.OnJobExecutionVetoed += _schLisSvc_OnJobExecutionVetoed;\n        _schLisSvc.OnJobDeleted += _schLisSvc_OnJobDeleted;\n        _schLisSvc.OnJobInterrupted += _schLisSvc_OnJobInterrupted;\n        _schLisSvc.OnSchedulerError += _schLisSvc_OnSchedulerError;\n        _schLisSvc.OnTriggerMisfired += _schLisSvc_OnTriggerMisfired;\n        _schLisSvc.OnTriggerPaused += _schLisSvc_OnTriggerPaused;\n        _schLisSvc.OnTriggerResumed += _schLisSvc_OnTriggerResumed;\n        _schLisSvc.OnTriggerFinalized += _schLisSvc_OnTriggerFinalized;\n        _schLisSvc.OnJobScheduled += _schLisSvc_OnJobScheduled;\n    }\n\n    private void _schLisSvc_OnJobScheduled(object? sender, Events.EventArgs<ITrigger> e)\n    {\n        var jKey = e.Args.JobKey;\n        var tKey = e.Args.Key;\n        var log = new ExecutionLog\n        {\n            JobName = jKey.Name,\n            JobGroup = jKey.Group,\n            TriggerName = tKey.Name,\n            TriggerGroup = tKey.Group,\n            LogType = LogType.Trigger,\n            Result = \"Job scheduled\",\n        };\n        QueueInsertTask(log);\n    }\n\n    private void _schLisSvc_OnTriggerFinalized(object? sender, Events.EventArgs<ITrigger> e)\n    {\n        var jKey = e.Args.JobKey;\n        var tKey = e.Args.Key;\n        var log = new ExecutionLog\n        {\n            JobName = jKey.Name,\n            JobGroup = jKey.Group,\n            TriggerName = tKey.Name,\n            TriggerGroup = tKey.Group,\n            LogType = LogType.Trigger,\n            Result = \"Trigger ended\",\n        };\n        QueueInsertTask(log);\n    }\n\n    private void _schLisSvc_OnTriggerResumed(object? sender, Events.EventArgs<TriggerKey> e)\n    {\n        var tKey = e.Args;\n        var log = new ExecutionLog\n        {\n            TriggerName = tKey.Name,\n            TriggerGroup = tKey.Group,\n            LogType = LogType.Trigger,\n            Result = \"Trigger resumed\",\n        };\n        QueueGetJobKeyAndInsertTask(log);\n    }\n\n    private void _schLisSvc_OnTriggerPaused(object? sender, Events.EventArgs<TriggerKey> e)\n    {\n        var tKey = e.Args;\n        var log = new ExecutionLog\n        {\n            TriggerName = tKey.Name,\n            TriggerGroup = tKey.Group,\n            LogType = LogType.Trigger,\n            Result = \"Trigger paused\",\n        };\n        QueueGetJobKeyAndInsertTask(log);\n    }\n\n    private void _schLisSvc_OnTriggerMisfired(object? sender, Events.EventArgs<ITrigger> e)\n    {\n        var jKey = e.Args.JobKey;\n        var tKey = e.Args.Key;\n        var log = new ExecutionLog\n        {\n            LogType = LogType.Trigger,\n            JobName = jKey.Name,\n            JobGroup = jKey.Group,\n            TriggerName = tKey.Name,\n            TriggerGroup = tKey.Group,\n            Result = \"Trigger misfired\",\n        };\n        QueueInsertTask(log);\n    }\n\n    private void _schLisSvc_OnSchedulerError(object? sender, Events.SchedulerErrorEventArgs e)\n    {\n        var log = new ExecutionLog\n        {\n            LogType = LogType.System,\n            IsException = true,\n            ErrorMessage = e.ErrorMessage,\n            ExecutionLogDetail = new() { ErrorStackTrace = e.Exception.NonNullStackTrace() },\n        };\n        QueueInsertTask(log);\n    }\n\n    private void _schLisSvc_OnJobInterrupted(object? sender, Events.EventArgs<JobKey> e)\n    {\n        var jKey = e.Args;\n        var log = new ExecutionLog\n        {\n            JobName = jKey.Name,\n            JobGroup = jKey.Group,\n            LogType = LogType.System,\n            Result = \"Job interrupted\",\n        };\n        QueueInsertTask(log);\n    }\n\n    private void _schLisSvc_OnJobDeleted(object? sender, Events.EventArgs<JobKey> e)\n    {\n        JobKey jKey = e.Args;\n        var log = new ExecutionLog\n        {\n            JobName = jKey.Name,\n            JobGroup = jKey.Group,\n            LogType = LogType.System,\n            Result = \"Job deleted\",\n        };\n        QueueInsertTask(log);\n    }\n\n    internal void _schLisSvc_OnJobExecutionVetoed(\n        object? sender,\n        Events.EventArgs<Quartz.IJobExecutionContext> e\n    )\n    {\n        var log = CreateScheduleJobLogEntry(e.Args, defaultIsSuccess: false);\n        log.IsVetoed = true;\n        QueueUpdateTask(log);\n    }\n\n    internal void _schLisSvc_OnJobWasExecuted(object? sender, Events.JobWasExecutedEventArgs e)\n    {\n        QueueUpdateTask(CreateScheduleJobLogEntry(e.JobExecutionContext, e.JobException, true));\n    }\n\n    internal void _schLisSvc_OnJobToBeExecuted(\n        object? sender,\n        Events.EventArgs<Quartz.IJobExecutionContext> e\n    )\n    {\n        QueueInsertTask(CreateScheduleJobLogEntry(e.Args));\n    }\n\n    private async Task AddHousekeepingSchedule(IScheduler scheduler)\n    {\n        var housekeepingJobName = \"Housekeep ExecutionLogs (bili)\";\n        var triggerKey = new TriggerKey(housekeepingJobName, Constants.SYSTEM_GROUP);\n\n        if (!string.IsNullOrEmpty(_options.HousekeepingCronSchedule))\n        {\n            var reschedule = true;\n\n            // determine if already exists\n            if (await scheduler.CheckExists(triggerKey))\n            {\n                // determine if same cron schedule\n                var trig = await scheduler.GetTrigger(triggerKey);\n                if (trig != null && trig.GetTriggerType() == TriggerType.Cron)\n                {\n                    var cronTrigger = (ICronTrigger)trig;\n                    if (cronTrigger.CronExpressionString == _options.HousekeepingCronSchedule)\n                        reschedule = false;\n                }\n            }\n\n            if (reschedule)\n            {\n                // create new one\n                IJobDetail job = JobBuilder\n                    .Create<HousekeepExecutionLogsJob>()\n                    .WithIdentity(housekeepingJobName, Constants.SYSTEM_GROUP)\n                    .Build();\n\n                ITrigger trigger = TriggerBuilder\n                    .Create()\n                    .WithIdentity(housekeepingJobName, Constants.SYSTEM_GROUP)\n                    .StartNow()\n                    .WithCronSchedule(_options.HousekeepingCronSchedule)\n                    .Build();\n\n                ITrigger nowTrigger = TriggerBuilder\n                    .Create()\n                    .WithIdentity(\"Housekeep ExecutionLogs now (bili)\", Constants.SYSTEM_GROUP)\n                    .StartNow()\n                    .Build();\n\n                await scheduler.ScheduleJob(job, new[] { trigger, nowTrigger }, true);\n            }\n        }\n        else\n        {\n            // delete housekeeping schedule\n            if (await scheduler.CheckExists(triggerKey))\n            {\n                _logger.LogInformation(\n                    \"Housekeeping ExecutionLogs has no cron schedule specified. Delete scheduled job\"\n                );\n                await scheduler.UnscheduleJob(triggerKey);\n            }\n        }\n    }\n\n    public override async Task StartAsync(CancellationToken cancellationToken)\n    {\n        var scheduler = await _schedulerFactory.GetScheduler();\n\n        await AddHousekeepingSchedule(scheduler);\n\n        await base.StartAsync(cancellationToken);\n    }\n\n    protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n    {\n        await MarkIncompleteExecution(stoppingToken);\n\n        while (!stoppingToken.IsCancellationRequested)\n        {\n            await ProcessTaskAsync(stoppingToken);\n        }\n    }\n\n    internal async Task MarkIncompleteExecution(CancellationToken stoppingToken)\n    {\n        try\n        {\n            using (IServiceScope scope = _svcProvider.CreateScope())\n            {\n                var repo = scope.ServiceProvider.GetRequiredService<IExecutionLogStore>();\n\n                await repo.MarkExecutingJobAsIncomplete();\n            }\n        }\n        catch (OperationCanceledException)\n        {\n            // Prevent throwing if stoppingToken was signaled\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(\n                ex,\n                \"Error occurred while updating executing status to incomplete status.\"\n            );\n        }\n    }\n\n    internal async Task ProcessTaskAsync(CancellationToken stoppingToken = default)\n    {\n        var batch = await GetBatch(stoppingToken);\n\n        _logger.LogInformation(\n            \"Got a batch with {taskCount} task(s). Saving to data store.\",\n            batch.Count\n        );\n\n        try\n        {\n            using (IServiceScope scope = _svcProvider.CreateScope())\n            {\n                var repo = scope.ServiceProvider.GetRequiredService<IExecutionLogStore>();\n\n                foreach (var workItem in batch)\n                {\n                    await workItem(repo, stoppingToken);\n                }\n\n                try\n                {\n                    await repo.SaveChangesAsync(stoppingToken);\n                }\n                catch (Exception ex)\n                {\n                    _logger.LogError(ex, \"Error occurred while saving execution logs.\");\n                }\n            }\n        }\n        catch (OperationCanceledException)\n        {\n            // Prevent throwing if stoppingToken was signaled\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Error occurred executing task work item.\");\n        }\n    }\n\n    private ExecutionLog CreateScheduleJobLogEntry(\n        IJobExecutionContext context,\n        JobExecutionException? jobException = null,\n        bool? defaultIsSuccess = null\n    )\n    {\n        var log = new ExecutionLog\n        {\n            RunInstanceId = context.FireInstanceId,\n            JobGroup = context.JobDetail.Key.Group,\n            JobName = context.JobDetail.Key.Name,\n            TriggerName = context.Trigger.Key.Name,\n            TriggerGroup = context.Trigger.Key.Group,\n            FireTimeUtc = context.FireTimeUtc,\n            ScheduleFireTimeUtc = context.ScheduledFireTimeUtc,\n            RetryCount = context.RefireCount,\n            JobRunTime = context.JobRunTime,\n            LogType = LogType.ScheduleJob,\n        };\n        var logDetail = new ExecutionLogDetail();\n\n        log.ReturnCode = context.GetReturnCode();\n        log.IsSuccess = context.GetIsSuccess();\n\n        if (log.IsSuccess is null)\n            log.IsSuccess = defaultIsSuccess;\n\n        var execDetail = context.GetExecutionDetails();\n        if (!string.IsNullOrEmpty(execDetail))\n        {\n            logDetail.ExecutionDetails = execDetail;\n            log.ExecutionLogDetail = logDetail;\n        }\n\n        if (jobException != null)\n        {\n            log.ErrorMessage = jobException.Message;\n            log.ExecutionLogDetail = logDetail;\n            logDetail.ErrorCode = jobException.HResult;\n            logDetail.ErrorStackTrace = jobException.ToString();\n            logDetail.ErrorHelpLink = jobException.HelpLink;\n\n            if (log.ReturnCode == null)\n                log.ReturnCode = jobException.HResult.ToString();\n\n            log.IsException = true;\n            log.IsSuccess = false;\n        }\n        else\n        {\n            if (context.Result != null)\n            {\n                var result = Convert.ToString(context.Result, CultureInfo.InvariantCulture);\n                log.Result = result?.Substring(0, Math.Min(result.Length, RESULT_MAX_LENGTH));\n            }\n        }\n\n        return log;\n    }\n\n    private async Task<List<Func<IExecutionLogStore, CancellationToken, ValueTask>>> GetBatch(\n        CancellationToken cancellationToken\n    )\n    {\n        await _taskQueue.Reader.WaitToReadAsync(cancellationToken);\n\n        var batch = new List<Func<IExecutionLogStore, CancellationToken, ValueTask>>();\n\n        while (batch.Count < MAX_BATCH_SIZE && _taskQueue.Reader.TryRead(out var dbTask))\n        {\n            batch.Add(dbTask);\n        }\n\n        return batch;\n    }\n\n    void QueueUpdateTask(ExecutionLog log)\n    {\n        QueueTask(\n            async (IExecutionLogStore repo, CancellationToken cancelToken) =>\n            {\n                try\n                {\n                    if (!repo.Exists(log))\n                        await repo.SaveChangesAsync(cancelToken);\n\n                    await repo.UpdateExecutionLog(log);\n                }\n                catch (Exception ex)\n                {\n                    if (log.LogType == LogType.ScheduleJob)\n                    {\n                        _logger.LogError(\n                            ex,\n                            \"Error occurred while updating execution log with job key [{jobGroup}.{jobName}] \"\n                                + \"run instance id [{runInstanceId}].\",\n                            log.JobGroup,\n                            log.JobName,\n                            log.RunInstanceId\n                        );\n                    }\n                    else\n                    {\n                        _logger.LogError(\n                            ex,\n                            \"Error occurred while updating {logType} execution log with \"\n                                + \"job key [{jobGroup}.{jobName}] trigger key [{triggerGroup}.{triggerName}].\",\n                            log.LogType,\n                            log.JobGroup,\n                            log.JobName,\n                            log.TriggerGroup,\n                            log.TriggerName\n                        );\n                    }\n                }\n            }\n        );\n    }\n\n    void QueueGetJobKeyAndInsertTask(ExecutionLog log)\n    {\n        QueueTask(\n            async (IExecutionLogStore repo, CancellationToken cancelToken) =>\n            {\n                try\n                {\n                    if (log.JobName == null && log.TriggerName != null && log.TriggerGroup != null)\n                    {\n                        // when there is no job name but has trigger name\n                        // try to determine the job name\n                        var scheduler = await _schedulerFactory.GetScheduler();\n                        var trigger = await scheduler.GetTrigger(\n                            new TriggerKey(log.TriggerName, log.TriggerGroup),\n                            cancelToken\n                        );\n                        if (trigger != null)\n                        {\n                            log.JobName = trigger.JobKey.Name;\n                            log.JobGroup = trigger.JobKey.Group;\n                        }\n                    }\n\n                    await repo.AddExecutionLog(log, cancelToken);\n                }\n                catch (Exception ex)\n                {\n                    if (log.LogType == LogType.ScheduleJob)\n                    {\n                        _logger.LogError(\n                            ex,\n                            \"Error occurred while adding execution log with job key [{jobGroup}.{jobName}] \"\n                                + \"run instance id [{runInstanceId}].\",\n                            log.JobGroup,\n                            log.JobName,\n                            log.RunInstanceId\n                        );\n                    }\n                    else\n                    {\n                        _logger.LogError(\n                            ex,\n                            \"Error occurred while adding {logType} execution log with \"\n                                + \"job key [{jobGroup}.{jobName}] trigger key [{triggerGroup}.{triggerName}].\",\n                            log.LogType,\n                            log.JobGroup,\n                            log.JobName,\n                            log.TriggerGroup,\n                            log.TriggerName\n                        );\n                    }\n                }\n            }\n        );\n    }\n\n    void QueueInsertTask(ExecutionLog log)\n    {\n        QueueTask(\n            async (IExecutionLogStore repo, CancellationToken cancelToken) =>\n            {\n                try\n                {\n                    await repo.AddExecutionLog(log, cancelToken);\n                }\n                catch (Exception ex)\n                {\n                    if (log.LogType == LogType.ScheduleJob)\n                    {\n                        _logger.LogError(\n                            ex,\n                            \"Error occurred while adding execution log with job key [{jobGroup}.{jobName}] \"\n                                + \"run instance id [{runInstanceId}].\",\n                            log.JobGroup,\n                            log.JobName,\n                            log.RunInstanceId\n                        );\n                    }\n                    else\n                    {\n                        _logger.LogError(\n                            ex,\n                            \"Error occurred while adding {logType} execution log with \"\n                                + \"job key [{jobGroup}.{jobName}] trigger key [{triggerGroup}.{triggerName}].\",\n                            log.LogType,\n                            log.JobGroup,\n                            log.JobName,\n                            log.TriggerGroup,\n                            log.TriggerName\n                        );\n                    }\n                }\n            }\n        );\n    }\n\n    void QueueTask(Func<IExecutionLogStore, CancellationToken, ValueTask> task)\n    {\n        if (!_taskQueue.Writer.TryWrite(task))\n        {\n            // Should not happen since it's unbounded Channel. It 'should' only fail if we call writer.Complete()\n            throw new InvalidOperationException(\"Failed to write the log message\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Jobs/HousekeepExecutionLogsJob.cs",
    "content": "﻿using System;\nusing BlazingQuartz.Core.History;\nusing BlazingQuartz.Jobs.Abstractions;\nusing Microsoft.Extensions.Options;\nusing Quartz;\n\nnamespace BlazingQuartz.Core.Jobs\n{\n    public class HousekeepExecutionLogsJob : IJob\n    {\n        private readonly IExecutionLogStore _logStore;\n        private readonly BlazingQuartzCoreOptions _options;\n\n        public HousekeepExecutionLogsJob(\n            IExecutionLogStore logStore,\n            IOptions<BlazingQuartzCoreOptions> options\n        )\n        {\n            _logStore = logStore;\n            _options = options.Value;\n        }\n\n        public async Task Execute(IJobExecutionContext context)\n        {\n            try\n            {\n                var count = await _logStore.DeleteLogsByDays(_options.ExecutionLogsDaysToKeep);\n                context.Result = $\"Deleted {count} record(s)\";\n                context.SetIsSuccess(true);\n            }\n            catch (Exception ex)\n            {\n                throw new JobExecutionException(\"Failed to delete execution logs\", ex);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Models/ExecutionLogFilter.cs",
    "content": "﻿using System;\nusing Ray.BiliBiliTool.Domain;\n\nnamespace BlazingQuartz.Core.Models\n{\n    public class ExecutionLogFilter : ICloneable\n    {\n        public string? JobName { get; set; }\n        public string? JobGroup { get; set; }\n        public string? TriggerName { get; set; }\n        public string? TriggerGroup { get; set; }\n        public HashSet<LogType>? LogTypes { get; set; }\n        public string? MessageContains { get; set; }\n\n        /// <summary>\n        /// If only StartUtc specified, means anything after this date\n        /// </summary>\n        public DateTimeOffset? DateAddedStartUtc { get; set; }\n\n        /// <summary>\n        /// DateAddedUtc before this date (exclusive).\n        /// <para>If only EndUtc specified, means anything before this date</para>\n        /// </summary>\n        public DateTimeOffset? DateAddedEndUtc { get; set; }\n        public bool IsAscending { get; set; }\n        public bool ErrorOnly { get; set; }\n        public bool IncludeSystemJobs { get; set; } = false;\n\n        public object Clone()\n        {\n            var newObj = (ExecutionLogFilter)this.MemberwiseClone();\n            if (this.LogTypes != null)\n            {\n                newObj.LogTypes = new HashSet<LogType>(this.LogTypes);\n            }\n\n            return newObj;\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Models/JobDetailModel.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Core.Models\n{\n    public class JobDetailModel\n    {\n        public string Name { get; set; } = string.Empty;\n        public string Group { get; set; } = Constants.DEFAULT_GROUP;\n        public string? Description { get; set; }\n        public Type? JobClass { get; set; }\n        public IDictionary<string, object> JobDataMap { get; set; } =\n            new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);\n\n        /// <summary>\n        /// Flag indicate whether Job should stay in scheduler even there are no more triggers assgined to it.\n        /// </summary>\n        public bool IsDurable { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Models/JobExecutionStatusSummaryModel.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Core.Models\n{\n    public class JobExecutionStatusSummaryModel\n    {\n        public DateTime StartDateTimeUtc { get; set; }\n        public List<KeyValuePair<JobExecutionStatus, int>> Data { get; set; } = new();\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Models/Key.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Core.Models\n{\n    public class Key\n    {\n        public string Name { get; set; }\n        public string? Group { get; set; }\n\n        public Key(string name)\n        {\n            Name = name;\n        }\n\n        public Key(string name, string group)\n            : this(name)\n        {\n            Group = group;\n        }\n\n        public Key(Key key)\n        {\n            Name = key.Name;\n            Group = key.Group;\n        }\n\n        public bool Equals(string name, string? group)\n        {\n            return Name == name && Group == group;\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Models/PagedList.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Core.Models\n{\n    public class PagedList<T> : List<T>\n    {\n        public PageMetadata? PageMetadata { get; set; }\n\n        public PagedList(IEnumerable<T> collection)\n            : this(collection, null) { }\n\n        public PagedList(IEnumerable<T> collection, PageMetadata? metadata)\n            : base(collection)\n        {\n            PageMetadata = metadata;\n        }\n    }\n\n    public record PageMetadata\n    {\n        /// <summary>\n        /// Page number. Start at 0\n        /// </summary>\n        public int Page { get; init; } = 0;\n\n        /// <summary>\n        /// Total number of records\n        /// </summary>\n        public int TotalCount { get; init; }\n\n        /// <summary>\n        /// Max number of records per page\n        /// </summary>\n        public int PageSize { get; init; } = 500;\n\n        public PageMetadata(int Page, int PageSize)\n        {\n            this.Page = Page;\n            this.PageSize = PageSize;\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Models/ScheduleJobFilter.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Core.Models\n{\n    public class ScheduleJobFilter : ICloneable\n    {\n        public bool IncludeSystemJobs { get; set; } = false;\n\n        public object Clone()\n        {\n            return (ScheduleJobFilter)this.MemberwiseClone();\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Models/ScheduleModel.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Core.Models\n{\n    public class ScheduleModel\n    {\n        const int SHORT_JOBTYPE_NAME_MAX_LENGTH = 25;\n\n        public string? JobName { get; set; }\n        public string? JobType { get; set; }\n        public string? JobDescription { get; set; }\n        public string JobGroup { get; set; } = Constants.DEFAULT_GROUP;\n        public string? TriggerName { get; set; }\n        public string? TriggerGroup { get; set; }\n        public string? TriggerDescription { get; set; }\n        public TriggerDetailModel? TriggerDetail { get; set; }\n        public TriggerType TriggerType { get; set; }\n        public string? TriggerTypeClassName { get; set; }\n        public JobStatus JobStatus { get; set; } = JobStatus.Idle;\n\n        public DateTimeOffset? NextTriggerTime { get; set; }\n        public DateTimeOffset? PreviousTriggerTime { get; set; }\n        public string? ExceptionMessage { get; set; }\n\n        public void ClearTrigger()\n        {\n            TriggerName = null;\n            TriggerGroup = null;\n            TriggerDescription = null;\n            TriggerDetail = null;\n            TriggerTypeClassName = null;\n            NextTriggerTime = null;\n            PreviousTriggerTime = null;\n            TriggerType = TriggerType.Unknown;\n        }\n\n        public string? GetJobTypeShortName(int suggestedMaxLength = SHORT_JOBTYPE_NAME_MAX_LENGTH)\n        {\n            if (JobType != null)\n            {\n                if (JobType.Length <= suggestedMaxLength)\n                    return JobType;\n\n                var dotIndex = JobType.LastIndexOf('.');\n                if (dotIndex < 0)\n                    return JobType;\n\n                var className = JobType.Substring(dotIndex + 1);\n                var classNameLength = className.Length;\n                if (classNameLength >= suggestedMaxLength)\n                    return className;\n\n                var remainLength = suggestedMaxLength - classNameLength - 3;\n                return $\"{JobType[..remainLength]}...{className}\";\n            }\n            return JobType;\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Models/TriggerDetailModel.cs",
    "content": "﻿using System;\nusing System.Text;\nusing Quartz;\n\nnamespace BlazingQuartz.Core.Models\n{\n    public class TriggerDetailModel\n    {\n        public string Name { get; set; } = string.Empty;\n        public string Group { get; set; } = Constants.DEFAULT_GROUP;\n        public TriggerType TriggerType { get; set; }\n        public string? Description { get; set; }\n        public TimeSpan? StartTimeSpan { get; set; }\n        public DateTime? StartDate { get; set; }\n\n        /// <summary>\n        /// Combined <see cref=\"StartDate\"/> <see cref=\"StartTimeSpan\"/> <see cref=\"StartTimezone\"/>\n        /// </summary>\n        public DateTimeOffset? StartDateTimeUtc\n        {\n            get\n            {\n                if (StartDate.HasValue)\n                {\n                    DateTimeOffset startTime;\n\n                    if (StartTimeSpan.HasValue)\n                    {\n                        var dt = StartDate.Value.Add(StartTimeSpan.Value);\n                        startTime = new DateTimeOffset(dt, StartTimezone.BaseUtcOffset);\n                    }\n                    else\n                    {\n                        startTime = new DateTimeOffset(\n                            StartDate.Value,\n                            StartTimezone.BaseUtcOffset\n                        );\n                    }\n\n                    return startTime;\n                }\n                return null;\n            }\n        }\n        public TimeSpan? EndTimeSpan { get; set; }\n        public DateTime? EndDate { get; set; }\n        public DateTimeOffset? EndDateTimeUtc\n        {\n            get\n            {\n                if (EndDate.HasValue)\n                {\n                    DateTimeOffset endTime;\n\n                    if (EndTimeSpan.HasValue)\n                    {\n                        var dt = EndDate.Value.Add(EndTimeSpan.Value);\n                        endTime = new DateTimeOffset(dt, StartTimezone.BaseUtcOffset);\n                    }\n                    else\n                    {\n                        endTime = new DateTimeOffset(EndDate.Value, StartTimezone.BaseUtcOffset);\n                    }\n\n                    return endTime;\n                }\n                else\n                {\n                    return null;\n                }\n            }\n        }\n\n        public string? ModifiedByCalendar { get; set; }\n\n        /// <summary>\n        /// Timezone of start time\n        /// </summary>\n        public TimeZoneInfo StartTimezone { get; set; } = TimeZoneInfo.Utc;\n        public int Priority { get; set; } = 5;\n        public string? CronExpression { get; set; }\n        public bool RepeatForever { get; set; }\n        public int RepeatCount { get; set; }\n\n        public bool[] DailyDayOfWeek { get; set; } = new bool[7];\n        public TimeSpan? StartDailyTime { get; set; }\n        public TimeSpan? EndDailyTime { get; set; }\n\n        /// <summary>\n        /// The timezone in which to base the scheduled. Used in Cron schedule, Calendar schedule and Daily schedule.\n        /// </summary>\n        public TimeZoneInfo InTimeZone { get; set; } = TimeZoneInfo.Local;\n\n        public int TriggerInterval { get; set; } = 1;\n        public IntervalUnit? TriggerIntervalUnit { get; set; } = IntervalUnit.Minute;\n        public MisfireAction MisfireAction { get; set; } = MisfireAction.SmartPolicy;\n\n        public IDictionary<string, object> TriggerDataMap { get; set; } =\n            new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);\n\n        public IReadOnlyCollection<DayOfWeek> GetDailyOnDaysOfWeek()\n        {\n            var dayOfWeekCount = 7;\n            var list = new List<DayOfWeek>(dayOfWeekCount);\n            for (int i = 0; i < dayOfWeekCount; i++)\n            {\n                if (DailyDayOfWeek[i])\n                {\n                    list.Add((DayOfWeek)Enum.ToObject(typeof(DayOfWeek), i));\n                }\n            }\n\n            return list;\n        }\n\n        public string ToSummaryString()\n        {\n            var bldr = new StringBuilder();\n            switch (TriggerType)\n            {\n                case TriggerType.Cron:\n                    bldr.AppendLine(\n                        CronExpressionDescriptor.ExpressionDescriptor.GetDescription(CronExpression)\n                    );\n                    break;\n                case TriggerType.Daily:\n                    bldr.AppendJoin(\n                        \", \",\n                        DailyDayOfWeek.Where(f => f).Select((f, i) => (DayOfWeek)i)\n                    );\n                    if (EndDailyTime.HasValue)\n                    {\n                        bldr.AppendLine(\n                            $\" from {StartDailyTime.ToString()} to {EndDailyTime.ToString()} {InTimeZone.DisplayName}\"\n                        );\n                    }\n                    else\n                    {\n                        bldr.AppendLine(\n                            $\" at {StartDailyTime.ToString()} {InTimeZone.DisplayName}\"\n                        );\n                    }\n                    break;\n                case TriggerType.Simple:\n                    bldr.Append($\"Every {TriggerInterval} {TriggerIntervalUnit?.ToString()}.\");\n                    if (RepeatForever)\n                    {\n                        bldr.AppendLine(\" Repeat forever.\");\n                    }\n                    else if (RepeatCount > 0)\n                    {\n                        bldr.AppendLine($\" Repeat {RepeatCount} time(s).\");\n                    }\n                    break;\n                case TriggerType.Calendar:\n                    bldr.Append(\n                        $\"{ModifiedByCalendar} calendar. Start at {StartDate} {StartTimeSpan} {InTimeZone}. Repeat every {TriggerInterval} {TriggerIntervalUnit?.ToString()}\"\n                    );\n                    break;\n            }\n\n            return bldr.ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/ServiceCollectionExtensions.cs",
    "content": "﻿using System;\nusing System.Reflection;\nusing BlazingQuartz.Core.History;\nusing BlazingQuartz.Core.Services;\nusing BlazingQuartz.Jobs;\nusing BlazingQuartz.Jobs.Abstractions;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Internal;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Quartz;\nusing Ray.BiliBiliTool.Infrastructure.EF;\n\nnamespace BlazingQuartz.Core\n{\n    public static class ServiceCollectionExtensions\n    {\n        public static IServiceCollection AddBlazingQuartz(this IServiceCollection services)\n        {\n            services.AddBlazingQuartzJobs();\n\n            services.TryAddSingleton<ISchedulerDefinitionService, SchedulerDefinitionService>();\n            services.AddTransient<ISchedulerService, SchedulerService>();\n\n            var schListenerSvc = new SchedulerListenerService();\n            services.TryAddSingleton<ISchedulerListenerService>(schListenerSvc);\n            services.AddSingleton<ITriggerListener>(schListenerSvc);\n            services.AddSingleton<IJobListener>(schListenerSvc);\n            services.AddSingleton<ISchedulerListener>(schListenerSvc);\n\n            services.AddScoped<IExecutionLogStore, ExecutionLogStore>();\n            services.AddScoped<IExecutionLogService, ExecutionLogService>();\n\n            services.AddSingleton<IExecutionLogRawSqlProvider, BaseExecutionLogRawSqlProvider>();\n\n            services.AddHostedService<SchedulerEventLoggingService>();\n\n            return services;\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Services/ExecutionLogService.cs",
    "content": "﻿using System;\nusing BlazingQuartz.Core.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Ray.BiliBiliTool.Domain;\nusing Ray.BiliBiliTool.Infrastructure.EF;\n\nnamespace BlazingQuartz.Core.Services\n{\n    public class ExecutionLogService : IExecutionLogService\n    {\n        private readonly IDbContextFactory<BiliDbContext> _contextFactory;\n\n        public ExecutionLogService(IDbContextFactory<BiliDbContext> contextFactory)\n        {\n            _contextFactory = contextFactory;\n        }\n\n        public async Task<PagedList<ExecutionLog>> GetLatestExecutionLog(\n            string jobName,\n            string jobGroup,\n            string? triggerName,\n            string? triggerGroup,\n            PageMetadata? pageMetadata = null,\n            long firstLogId = 0,\n            HashSet<LogType>? logTypes = null\n        )\n        {\n            using (var context = _contextFactory.CreateDbContext())\n            {\n                var q = context.ExecutionLogs.Where(l =>\n                    l.JobName == jobName && l.JobGroup == jobGroup\n                );\n\n                if (triggerName is not null)\n                {\n                    q = q.Where(l =>\n                        l.TriggerName == triggerName && l.TriggerGroup == triggerGroup\n                    );\n                }\n\n                if (firstLogId > 0)\n                {\n                    // to avoid incorrect page data\n                    q = q.Where(l => l.LogId <= firstLogId);\n                }\n\n                if (logTypes != null)\n                {\n                    q = q = q.Where(l => logTypes.Contains(l.LogType));\n                }\n\n                var ordered = q.OrderByDescending(l => l.DateAddedUtc)\n                    .ThenByDescending(l => l.FireTimeUtc);\n                if (pageMetadata == null)\n                {\n                    return new PagedList<ExecutionLog>(await ordered.ToListAsync());\n                }\n\n                PageMetadata newPageMetadata = pageMetadata;\n                if (pageMetadata.Page == 0)\n                {\n                    // if first page, get the total records\n                    var totalRecords = await q.CountAsync();\n                    newPageMetadata = pageMetadata with { TotalCount = totalRecords };\n                }\n\n                var result = await ordered\n                    .Skip(pageMetadata.Page * pageMetadata.PageSize)\n                    .Take(pageMetadata.PageSize)\n                    .ToListAsync();\n                return new PagedList<ExecutionLog>(result, newPageMetadata);\n            }\n        }\n\n        public async Task<PagedList<ExecutionLog>> GetExecutionLogs(\n            ExecutionLogFilter? filter = null,\n            PageMetadata? pageMetadata = null,\n            long firstLogId = 0\n        )\n        {\n            using (var context = _contextFactory.CreateDbContext())\n            {\n                IQueryable<ExecutionLog> q = context.ExecutionLogs;\n                if (filter != null)\n                {\n                    if (filter.JobName != null)\n                    {\n                        q = q.Where(l => l.JobName == filter.JobName);\n                    }\n\n                    if (filter.JobGroup != null)\n                    {\n                        q = q.Where(l => l.JobGroup == filter.JobGroup);\n                    }\n\n                    if (filter.TriggerName != null)\n                    {\n                        q = q.Where(l => l.TriggerName == filter.TriggerName);\n                    }\n\n                    if (filter.TriggerGroup != null)\n                    {\n                        q = q.Where(l => l.TriggerGroup == filter.TriggerGroup);\n                    }\n\n                    if (filter.LogTypes != null && filter.LogTypes.Any())\n                    {\n                        q = q.Where(l => filter.LogTypes.Contains(l.LogType));\n                    }\n\n                    if (filter.DateAddedStartUtc != null)\n                    {\n                        q = q.Where(l => l.DateAddedUtc >= filter.DateAddedStartUtc);\n                    }\n\n                    if (filter.DateAddedEndUtc != null)\n                    {\n                        q = q.Where(l => l.DateAddedUtc < filter.DateAddedEndUtc);\n                    }\n\n                    if (filter.ErrorOnly)\n                    {\n                        q = q.Where(l =>\n                            (l.IsException ?? false) || (l.IsSuccess.HasValue && !l.IsSuccess.Value)\n                        );\n                    }\n\n                    if (filter.MessageContains != null)\n                    {\n                        var likeStr = $\"%{filter.MessageContains}%\";\n                        q = q.Where(l =>\n                            EF.Functions.Like(l.JobName ?? string.Empty, likeStr)\n                            || EF.Functions.Like(l.TriggerName ?? string.Empty, likeStr)\n                            || EF.Functions.Like(l.Result ?? string.Empty, likeStr)\n                            || EF.Functions.Like(l.ErrorMessage ?? string.Empty, likeStr)\n                            || (\n                                l.ExecutionLogDetail != null\n                                && (\n                                    EF.Functions.Like(\n                                        l.ExecutionLogDetail.ExecutionDetails ?? string.Empty,\n                                        likeStr\n                                    )\n                                    || EF.Functions.Like(\n                                        l.ExecutionLogDetail.ErrorStackTrace ?? string.Empty,\n                                        likeStr\n                                    )\n                                    || (\n                                        l.ExecutionLogDetail.ErrorCode != null\n                                        && l.ExecutionLogDetail.ErrorCode.Value.ToString()\n                                            == filter.MessageContains\n                                    )\n                                )\n                            )\n                        );\n                    }\n\n                    if (!filter.IncludeSystemJobs)\n                    {\n                        q = q.Where(l =>\n                            !(\n                                l.TriggerGroup == Constants.SYSTEM_GROUP\n                                || l.JobGroup == Constants.SYSTEM_GROUP\n                            )\n                        );\n                    }\n                }\n\n                IOrderedQueryable<ExecutionLog> ordered;\n                if (filter != null && filter.IsAscending)\n                {\n                    ordered = q.OrderBy(l => l.DateAddedUtc).ThenBy(l => l.FireTimeUtc);\n                }\n                else\n                {\n                    if (firstLogId > 0)\n                    {\n                        // to avoid incorrect page data for descing order\n                        q = q.Where(l => l.LogId <= firstLogId);\n                    }\n                    ordered = q.OrderByDescending(l => l.DateAddedUtc)\n                        .ThenByDescending(l => l.FireTimeUtc);\n                }\n\n                if (pageMetadata == null)\n                {\n                    return new PagedList<ExecutionLog>(await ordered.ToListAsync());\n                }\n\n                PageMetadata newPageMetadata = pageMetadata;\n                if (pageMetadata.Page == 0)\n                {\n                    // if first page, get the total records\n                    var totalRecords = await q.CountAsync();\n                    newPageMetadata = pageMetadata with { TotalCount = totalRecords };\n                }\n\n                var result = await ordered\n                    .Skip(pageMetadata.Page * pageMetadata.PageSize)\n                    .Take(pageMetadata.PageSize)\n                    .ToListAsync();\n                return new PagedList<ExecutionLog>(result, newPageMetadata);\n            }\n        }\n\n        public async Task<IList<string>> GetJobNames()\n        {\n            using (var context = _contextFactory.CreateDbContext())\n            {\n                return await context\n                    .ExecutionLogs.Where(l => l.LogType != LogType.System)\n                    .Select(l => l.JobName ?? string.Empty)\n                    .Distinct()\n                    .OrderBy(l => l)\n                    .ToListAsync();\n            }\n        }\n\n        public async Task<IList<string>> GetJobGroups()\n        {\n            using (var context = _contextFactory.CreateDbContext())\n            {\n                return await context\n                    .ExecutionLogs.Where(l => l.LogType != LogType.System)\n                    .Select(l => l.JobGroup ?? string.Empty)\n                    .Distinct()\n                    .OrderBy(l => l)\n                    .ToListAsync();\n            }\n        }\n\n        public async Task<IList<string>> GetTriggerNames()\n        {\n            using (var context = _contextFactory.CreateDbContext())\n            {\n                return await context\n                    .ExecutionLogs.Where(l => l.LogType != LogType.System)\n                    .Select(l => l.TriggerName ?? string.Empty)\n                    .Distinct()\n                    .OrderBy(l => l)\n                    .ToListAsync();\n            }\n        }\n\n        public async Task<IList<string>> GetTriggerGroups()\n        {\n            using (var context = _contextFactory.CreateDbContext())\n            {\n                return await context\n                    .ExecutionLogs.Where(l => l.LogType != LogType.System)\n                    .Select(l => l.TriggerGroup ?? string.Empty)\n                    .Distinct()\n                    .OrderBy(l => l)\n                    .ToListAsync();\n            }\n        }\n\n        public async Task<JobExecutionStatusSummaryModel> GetJobExecutionStatusSummary(\n            DateTimeOffset? startTimeUtc,\n            DateTimeOffset? endTimeUtc = null\n        )\n        {\n            using (var context = _contextFactory.CreateDbContext())\n            {\n                var q = context.ExecutionLogs.Where(l => l.LogType == LogType.ScheduleJob);\n                if (startTimeUtc.HasValue)\n                {\n                    q = q.Where(l => l.DateAddedUtc >= startTimeUtc.Value);\n                }\n                if (endTimeUtc.HasValue)\n                {\n                    q = q.Where(l => l.DateAddedUtc < endTimeUtc.Value);\n                }\n\n                var statusList = q.Select(l => new\n                {\n                    DateAddedUtc = l.DateAddedUtc,\n                    ExecutionStatus = (l.IsException ?? false)\n                        ?\n                        // has exception\n                        JobExecutionStatus.Failed\n                        :\n                        // vetoed?\n                        (\n                            (l.IsVetoed ?? false)\n                                ? JobExecutionStatus.Vetoed\n                                :\n                                // is success null?\n                                (\n                                    l.IsSuccess.HasValue\n                                        ? (\n                                            l.IsSuccess.Value\n                                                ? JobExecutionStatus.Success\n                                                : JobExecutionStatus.Failed\n                                        )\n                                        : JobExecutionStatus.Executing\n                                )\n                        ),\n                });\n\n                var statusGroup = await statusList\n                    .GroupBy(l => l.ExecutionStatus)\n                    .Select(g => new\n                    {\n                        EarliestDateAdded = g.Min(l => l.DateAddedUtc),\n                        ExecutionStatus = g.Key,\n                        Count = g.Count(),\n                    })\n                    .ToListAsync();\n\n                if (!statusGroup.Any())\n                    return new();\n\n                return new JobExecutionStatusSummaryModel\n                {\n                    StartDateTimeUtc = statusGroup.Min(s => s.EarliestDateAdded).DateTime,\n                    Data = statusGroup\n                        .Select(s => new KeyValuePair<JobExecutionStatus, int>(\n                            s.ExecutionStatus,\n                            s.Count\n                        ))\n                        .ToList(),\n                };\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Services/IExecutionLogService.cs",
    "content": "﻿using BlazingQuartz.Core.Models;\nusing Ray.BiliBiliTool.Domain;\n\nnamespace BlazingQuartz.Core.Services\n{\n    public interface IExecutionLogService\n    {\n        Task<PagedList<ExecutionLog>> GetLatestExecutionLog(\n            string jobName,\n            string jobGroup,\n            string? triggerName,\n            string? triggerGroup,\n            PageMetadata? pageMetadata = null,\n            long firstLogId = 0,\n            HashSet<LogType>? logTypes = null\n        );\n        Task<PagedList<ExecutionLog>> GetExecutionLogs(\n            ExecutionLogFilter? filter = null,\n            PageMetadata? pageMetadata = null,\n            long firstLogId = 0\n        );\n        Task<IList<string>> GetJobNames();\n        Task<IList<string>> GetJobGroups();\n        Task<IList<string>> GetTriggerNames();\n        Task<IList<string>> GetTriggerGroups();\n\n        /// <summary>\n        /// Returns job execution summary. Number of success, failed,\n        /// executing and interrupted jobs of given date range.\n        /// </summary>\n        /// <param name=\"startTimeUtc\"></param>\n        /// <param name=\"endTimeUtc\">inclusive</param>\n        /// <returns></returns>\n        Task<JobExecutionStatusSummaryModel> GetJobExecutionStatusSummary(\n            DateTimeOffset? startTimeUtc,\n            DateTimeOffset? endTimeUtc = null\n        );\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Services/ISchedulerDefinitionService.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Core.Services\n{\n    public interface ISchedulerDefinitionService\n    {\n        IEnumerable<IntervalUnit> GetTriggerIntervalUnits(TriggerType triggerType);\n        IEnumerable<MisfireAction> GetMisfireActions(TriggerType triggerType);\n\n        /// <summary>\n        /// Return available IJob implementations\n        /// </summary>\n        /// <param name=\"reload\"></param>\n        /// <returns></returns>\n        IEnumerable<Type> GetJobTypes(bool reload = false);\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Services/ISchedulerListenerService.cs",
    "content": "﻿using System;\nusing BlazingQuartz.Core.Events;\nusing Quartz;\n\nnamespace BlazingQuartz.Core.Services\n{\n    public interface ISchedulerListenerService\n    {\n        event EventHandler<EventArgs<IJobDetail>>? OnJobAdded;\n        event EventHandler<EventArgs<JobKey>>? OnJobDeleted;\n\n        /// <summary>\n        /// From IJobListener\n        /// </summary>\n        event EventHandler<EventArgs<IJobExecutionContext>>? OnJobExecutionVetoed;\n        event EventHandler<EventArgs<JobKey>>? OnJobInterrupted;\n        event EventHandler<EventArgs<JobKey>>? OnJobPaused;\n        event EventHandler<EventArgs<JobKey>>? OnJobResumed;\n        event EventHandler<EventArgs<ITrigger>>? OnJobScheduled;\n        event EventHandler<EventArgs<string>>? OnJobsPaused;\n        event EventHandler<EventArgs<string>>? OnJobsResumed;\n\n        /// <summary>\n        /// From IJobListener\n        /// </summary>\n        event EventHandler<EventArgs<IJobExecutionContext>>? OnJobToBeExecuted;\n        event EventHandler<EventArgs<TriggerKey>>? OnJobUnscheduled;\n\n        /// <summary>\n        /// From IJobListener\n        /// </summary>\n        event EventHandler<JobWasExecutedEventArgs>? OnJobWasExecuted;\n        event EventHandler<SchedulerErrorEventArgs>? OnSchedulerError;\n        event EventHandler<CancellationToken>? OnSchedulerInStandbyMode;\n        event EventHandler<CancellationToken>? OnSchedulerShutdown;\n        event EventHandler<CancellationToken>? OnSchedulerShuttingdown;\n        event EventHandler<CancellationToken>? OnSchedulerStarted;\n        event EventHandler<CancellationToken>? OnSchedulerStarting;\n        event EventHandler<CancellationToken>? OnSchedulingDataCleared;\n        event EventHandler<EventArgs<ITrigger>>? OnTriggerFinalized;\n\n        /// <summary>\n        /// From ITriggerListener\n        /// </summary>\n        event EventHandler<EventArgs<ITrigger>>? OnTriggerMisfired;\n        event EventHandler<EventArgs<TriggerKey>>? OnTriggerPaused;\n        event EventHandler<EventArgs<TriggerKey>>? OnTriggerResumed;\n        event EventHandler<EventArgs<string?>>? OnTriggerGroupPaused;\n        event EventHandler<EventArgs<string?>>? OnTriggerGroupResumed;\n\n        /// <summary>\n        /// From ITriggerListener\n        /// </summary>\n        event EventHandler<TriggerEventArgs>? OnTriggerComplete;\n\n        /// <summary>\n        /// From ITriggerListener\n        /// </summary>\n        event EventHandler<TriggerEventArgs>? OnTriggerFired;\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Services/ISchedulerService.cs",
    "content": "﻿using BlazingQuartz.Core.Models;\nusing Quartz;\n\nnamespace BlazingQuartz.Core.Services\n{\n    public interface ISchedulerService\n    {\n        Task<ScheduleModel> GetScheduleModelAsync(ITrigger trigger);\n        IAsyncEnumerable<ScheduleModel> GetAllJobsAsync(ScheduleJobFilter? filter = null);\n        Task CreateSchedule(JobDetailModel jobDetailModel, TriggerDetailModel triggerDetailModel);\n        Task<IReadOnlyCollection<string>> GetJobGroups();\n        Task<IReadOnlyCollection<string>> GetTriggerGroups();\n        Task<JobDetailModel?> GetJobDetail(string jobName, string groupName);\n        Task<TriggerDetailModel?> GetTriggerDetail(string triggerName, string triggerGroup);\n        Task<bool> ContainsTriggerKey(string triggerName, string triggerGroup);\n        Task<bool> ContainsJobKey(string jobName, string jobGroup);\n        Task<IReadOnlyCollection<string>> GetCalendarNames(CancellationToken cancelToken = default);\n        Task PauseTrigger(string triggerName, string? triggerGroup);\n        Task ResumeTrigger(string triggerName, string? triggerGroup);\n        Task TriggerJob(string jobName, string jobGroup);\n        Task<bool> DeleteSchedule(ScheduleModel model);\n        Task UpdateSchedule(\n            Key oldJobKey,\n            Key? oldTriggerKey,\n            JobDetailModel newJobModel,\n            TriggerDetailModel newTriggerModel\n        );\n        Task<SchedulerMetaData> GetMetadataAsync();\n        Task<IList<KeyValuePair<string, int>>> GetScheduledJobSummary();\n        Task PauseAllSchedules();\n        Task ResumeAllSchedules();\n        Task ShutdownScheduler();\n        Task StartScheduler();\n        Task StandbyScheduler();\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Services/SchedulerDefinitionService.cs",
    "content": "﻿using System;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Reflection;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Quartz;\n\nnamespace BlazingQuartz.Core.Services\n{\n    internal class SchedulerDefinitionService : ISchedulerDefinitionService\n    {\n        private readonly ISchedulerFactory _schedulerFactory;\n        private IEnumerable<IntervalUnit> _calendarIntervalUnits;\n        private IEnumerable<IntervalUnit> _simpleIntervalUnits;\n        private IEnumerable<MisfireAction>? _cronCalDailyMisfireActions;\n        private IEnumerable<MisfireAction>? _simpleMisfireActions;\n        private readonly BlazingQuartzCoreOptions _options;\n        private readonly ILogger<SchedulerDefinitionService> _logger;\n        private List<Type>? _allowedJobTypes;\n\n        public SchedulerDefinitionService(\n            ILogger<SchedulerDefinitionService> logger,\n            ISchedulerFactory schedulerFactory,\n            IOptions<BlazingQuartzCoreOptions> options\n        )\n        {\n            _logger = logger;\n            _schedulerFactory = schedulerFactory;\n            _options = options.Value;\n            Init();\n        }\n\n        [MemberNotNull(nameof(_calendarIntervalUnits))]\n        [MemberNotNull(nameof(_simpleIntervalUnits))]\n        private void Init()\n        {\n            _calendarIntervalUnits = new List<IntervalUnit>\n            {\n                IntervalUnit.Second,\n                IntervalUnit.Minute,\n                IntervalUnit.Hour,\n                IntervalUnit.Day,\n                IntervalUnit.Week,\n                IntervalUnit.Month,\n                IntervalUnit.Year,\n            };\n            _simpleIntervalUnits = new List<IntervalUnit>\n            {\n                IntervalUnit.Second,\n                IntervalUnit.Minute,\n                IntervalUnit.Hour,\n            };\n        }\n\n        public IEnumerable<IntervalUnit> GetTriggerIntervalUnits(TriggerType triggerType)\n        {\n            switch (triggerType)\n            {\n                case TriggerType.Calendar:\n                    return _calendarIntervalUnits;\n                case TriggerType.Daily:\n                case TriggerType.Simple:\n                    return _simpleIntervalUnits;\n                default:\n                    return Enumerable.Empty<IntervalUnit>();\n            }\n        }\n\n        public IEnumerable<MisfireAction> GetMisfireActions(TriggerType triggerType)\n        {\n            switch (triggerType)\n            {\n                case TriggerType.Cron:\n                case TriggerType.Daily:\n                case TriggerType.Calendar:\n                    if (_cronCalDailyMisfireActions == null)\n                    {\n                        _cronCalDailyMisfireActions = new List<MisfireAction>\n                        {\n                            MisfireAction.SmartPolicy,\n                            MisfireAction.DoNothing,\n                            MisfireAction.IgnoreMisfirePolicy,\n                            MisfireAction.FireOnceNow,\n                        };\n                    }\n                    return _cronCalDailyMisfireActions;\n                case TriggerType.Simple:\n                    if (_simpleMisfireActions == null)\n                    {\n                        _simpleMisfireActions = new List<MisfireAction>\n                        {\n                            MisfireAction.SmartPolicy,\n                            MisfireAction.FireNow,\n                            MisfireAction.IgnoreMisfirePolicy,\n                            MisfireAction.RescheduleNextWithExistingCount,\n                            MisfireAction.RescheduleNextWithRemainingCount,\n                            MisfireAction.RescheduleNowWithExistingRepeatCount,\n                            MisfireAction.RescheduleNowWithRemainingRepeatCount,\n                        };\n                    }\n                    return _simpleMisfireActions;\n            }\n\n            return Enumerable.Empty<MisfireAction>();\n        }\n\n        public IEnumerable<Type> GetJobTypes(bool reload = false)\n        {\n            if (_options.AllowedJobAssemblyFiles == null)\n                return Enumerable.Empty<Type>();\n\n            // use cached job types if already loaded\n            if (_allowedJobTypes != null && !reload)\n                return _allowedJobTypes;\n\n            HashSet<string> disallowedJobs = new(\n                _options.DisallowedJobTypes ?? Enumerable.Empty<string>()\n            );\n\n            if (_options.DisallowedJobTypes != null)\n                _logger.LogInformation(\n                    \"{disallowedVar} was set. Will not load following job types {jobTypes}\",\n                    nameof(_options.DisallowedJobTypes),\n                    _options.DisallowedJobTypes\n                );\n\n            var path =\n                Path.GetDirectoryName(\n                    Assembly.GetAssembly(typeof(SchedulerDefinitionService))!.Location\n                ) ?? String.Empty;\n            List<Type> jobTypes = new();\n            foreach (var assemblyStr in _options.AllowedJobAssemblyFiles)\n            {\n                string assemblyPath = Path.Combine(path, assemblyStr + \".dll\");\n                try\n                {\n                    Assembly assembly = Assembly.LoadFrom(assemblyPath);\n                    if (assembly == null)\n                    {\n                        _logger.LogWarning(\n                            \"Cannot load allowed job assembly name '{assembly}'\",\n                            assemblyStr\n                        );\n                        continue;\n                    }\n\n                    jobTypes.AddRange(\n                        assembly\n                            .GetExportedTypes()\n                            .Where(x =>\n                                x.IsPublic\n                                && x.IsClass\n                                && !x.IsAbstract\n                                && typeof(IJob).IsAssignableFrom(x)\n                                && !disallowedJobs.Contains(x.FullName ?? string.Empty)\n                            )\n                    );\n                }\n                catch (Exception ex)\n                {\n                    _logger.LogWarning(\n                        ex,\n                        \"Failed to load allowed job assembly filename '{assembly}'\",\n                        assemblyStr\n                    );\n                    continue;\n                }\n            }\n            if (!jobTypes.Any())\n                return jobTypes;\n\n            _allowedJobTypes = jobTypes;\n            return _allowedJobTypes.AsReadOnly();\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Services/SchedulerListenerService.cs",
    "content": "﻿using System;\nusing BlazingQuartz.Core.Events;\nusing Quartz;\n\nnamespace BlazingQuartz.Core.Services\n{\n    public class SchedulerListenerService\n        : ISchedulerListenerService,\n            IJobListener,\n            ITriggerListener,\n            ISchedulerListener\n    {\n        public event EventHandler<EventArgs<IJobDetail>>? OnJobAdded;\n        public event EventHandler<EventArgs<JobKey>>? OnJobDeleted;\n        public event EventHandler<EventArgs<IJobExecutionContext>>? OnJobExecutionVetoed;\n        public event EventHandler<EventArgs<JobKey>>? OnJobInterrupted;\n        public event EventHandler<EventArgs<JobKey>>? OnJobPaused;\n        public event EventHandler<EventArgs<JobKey>>? OnJobResumed;\n        public event EventHandler<EventArgs<ITrigger>>? OnJobScheduled;\n        public event EventHandler<EventArgs<string>>? OnJobsPaused;\n        public event EventHandler<EventArgs<string>>? OnJobsResumed;\n        public event EventHandler<EventArgs<IJobExecutionContext>>? OnJobToBeExecuted;\n        public event EventHandler<EventArgs<TriggerKey>>? OnJobUnscheduled;\n        public event EventHandler<JobWasExecutedEventArgs>? OnJobWasExecuted;\n        public event EventHandler<SchedulerErrorEventArgs>? OnSchedulerError;\n        public event EventHandler<CancellationToken>? OnSchedulerInStandbyMode;\n        public event EventHandler<CancellationToken>? OnSchedulerShutdown;\n        public event EventHandler<CancellationToken>? OnSchedulerShuttingdown;\n        public event EventHandler<CancellationToken>? OnSchedulerStarted;\n        public event EventHandler<CancellationToken>? OnSchedulerStarting;\n        public event EventHandler<CancellationToken>? OnSchedulingDataCleared;\n        public event EventHandler<EventArgs<ITrigger>>? OnTriggerFinalized;\n        public event EventHandler<EventArgs<ITrigger>>? OnTriggerMisfired;\n        public event EventHandler<EventArgs<TriggerKey>>? OnTriggerPaused;\n        public event EventHandler<EventArgs<TriggerKey>>? OnTriggerResumed;\n        public event EventHandler<EventArgs<string?>>? OnTriggerGroupPaused;\n        public event EventHandler<EventArgs<string?>>? OnTriggerGroupResumed;\n        public event EventHandler<TriggerEventArgs>? OnTriggerComplete;\n        public event EventHandler<TriggerEventArgs>? OnTriggerFired;\n\n        public string Name => \"BlazingQuartzNetUI\";\n\n        public Task JobAdded(IJobDetail jobDetail, CancellationToken cancellationToken = default)\n        {\n            OnJobAdded?.Invoke(this, new EventArgs<IJobDetail>(jobDetail, cancellationToken));\n            return Task.CompletedTask;\n        }\n\n        public Task JobDeleted(JobKey jobKey, CancellationToken cancellationToken = default)\n        {\n            OnJobDeleted?.Invoke(this, new EventArgs<JobKey>(jobKey, cancellationToken));\n            return Task.CompletedTask;\n        }\n\n        public Task JobExecutionVetoed(\n            IJobExecutionContext context,\n            CancellationToken cancellationToken = default\n        )\n        {\n            OnJobExecutionVetoed?.Invoke(\n                this,\n                new EventArgs<IJobExecutionContext>(context, cancellationToken)\n            );\n            return Task.CompletedTask;\n        }\n\n        public Task JobInterrupted(JobKey jobKey, CancellationToken cancellationToken = default)\n        {\n            OnJobInterrupted?.Invoke(this, new EventArgs<JobKey>(jobKey, cancellationToken));\n            return Task.CompletedTask;\n        }\n\n        public Task JobPaused(JobKey jobKey, CancellationToken cancellationToken = default)\n        {\n            OnJobPaused?.Invoke(this, new EventArgs<JobKey>(jobKey, cancellationToken));\n            return Task.CompletedTask;\n        }\n\n        public Task JobResumed(JobKey jobKey, CancellationToken cancellationToken = default)\n        {\n            OnJobResumed?.Invoke(this, new EventArgs<JobKey>(jobKey, cancellationToken));\n            return Task.CompletedTask;\n        }\n\n        public Task JobScheduled(ITrigger trigger, CancellationToken cancellationToken = default)\n        {\n            OnJobScheduled?.Invoke(this, new EventArgs<ITrigger>(trigger, cancellationToken));\n            return Task.CompletedTask;\n        }\n\n        public Task JobsPaused(string jobGroup, CancellationToken cancellationToken = default)\n        {\n            OnJobsPaused?.Invoke(this, new EventArgs<string>(jobGroup, cancellationToken));\n            return Task.CompletedTask;\n        }\n\n        public Task JobsResumed(string jobGroup, CancellationToken cancellationToken = default)\n        {\n            OnJobsResumed?.Invoke(this, new EventArgs<string>(jobGroup, cancellationToken));\n            return Task.CompletedTask;\n        }\n\n        public Task JobToBeExecuted(\n            IJobExecutionContext context,\n            CancellationToken cancellationToken = default\n        )\n        {\n            OnJobToBeExecuted?.Invoke(\n                this,\n                new EventArgs<IJobExecutionContext>(context, cancellationToken)\n            );\n            return Task.CompletedTask;\n        }\n\n        public Task JobUnscheduled(\n            TriggerKey triggerKey,\n            CancellationToken cancellationToken = default\n        )\n        {\n            OnJobUnscheduled?.Invoke(\n                this,\n                new EventArgs<TriggerKey>(triggerKey, cancellationToken)\n            );\n            return Task.CompletedTask;\n        }\n\n        public Task JobWasExecuted(\n            IJobExecutionContext context,\n            JobExecutionException? jobException,\n            CancellationToken cancellationToken = default\n        )\n        {\n            OnJobWasExecuted?.Invoke(\n                this,\n                new JobWasExecutedEventArgs(context, jobException, cancellationToken)\n                {\n                    JobException = jobException,\n                }\n            );\n            return Task.CompletedTask;\n        }\n\n        public Task SchedulerError(\n            string msg,\n            SchedulerException cause,\n            CancellationToken cancellationToken = default\n        )\n        {\n            OnSchedulerError?.Invoke(\n                this,\n                new SchedulerErrorEventArgs\n                {\n                    ErrorMessage = msg,\n                    Exception = cause,\n                    CancelToken = cancellationToken,\n                }\n            );\n            return Task.CompletedTask;\n        }\n\n        public Task SchedulerInStandbyMode(CancellationToken cancellationToken = default)\n        {\n            OnSchedulerInStandbyMode?.Invoke(this, cancellationToken);\n            return Task.CompletedTask;\n        }\n\n        public Task SchedulerShutdown(CancellationToken cancellationToken = default)\n        {\n            OnSchedulerShutdown?.Invoke(this, cancellationToken);\n            return Task.CompletedTask;\n        }\n\n        public Task SchedulerShuttingdown(CancellationToken cancellationToken = default)\n        {\n            OnSchedulerShuttingdown?.Invoke(this, cancellationToken);\n            return Task.CompletedTask;\n        }\n\n        public Task SchedulerStarted(CancellationToken cancellationToken = default)\n        {\n            OnSchedulerStarted?.Invoke(this, cancellationToken);\n            return Task.CompletedTask;\n        }\n\n        public Task SchedulerStarting(CancellationToken cancellationToken = default)\n        {\n            OnSchedulerStarting?.Invoke(this, cancellationToken);\n            return Task.CompletedTask;\n        }\n\n        public Task SchedulingDataCleared(CancellationToken cancellationToken = default)\n        {\n            OnSchedulingDataCleared?.Invoke(this, cancellationToken);\n            return Task.CompletedTask;\n        }\n\n        public Task TriggerComplete(\n            ITrigger trigger,\n            IJobExecutionContext context,\n            SchedulerInstruction triggerInstructionCode,\n            CancellationToken cancellationToken = default\n        )\n        {\n            OnTriggerComplete?.Invoke(\n                this,\n                new TriggerEventArgs(trigger, context, cancellationToken)\n            );\n            return Task.CompletedTask;\n        }\n\n        public Task TriggerFinalized(\n            ITrigger trigger,\n            CancellationToken cancellationToken = default\n        )\n        {\n            OnTriggerFinalized?.Invoke(this, new EventArgs<ITrigger>(trigger, cancellationToken));\n            return Task.CompletedTask;\n        }\n\n        public Task TriggerFired(\n            ITrigger trigger,\n            IJobExecutionContext context,\n            CancellationToken cancellationToken = default\n        )\n        {\n            OnTriggerFired?.Invoke(this, new TriggerEventArgs(trigger, context, cancellationToken));\n            return Task.CompletedTask;\n        }\n\n        public Task TriggerMisfired(ITrigger trigger, CancellationToken cancellationToken = default)\n        {\n            OnTriggerMisfired?.Invoke(this, new EventArgs<ITrigger>(trigger, cancellationToken));\n            return Task.CompletedTask;\n        }\n\n        public Task TriggerPaused(\n            TriggerKey triggerKey,\n            CancellationToken cancellationToken = default\n        )\n        {\n            OnTriggerPaused?.Invoke(this, new EventArgs<TriggerKey>(triggerKey, cancellationToken));\n            return Task.CompletedTask;\n        }\n\n        public Task TriggerResumed(\n            TriggerKey triggerKey,\n            CancellationToken cancellationToken = default\n        )\n        {\n            OnTriggerResumed?.Invoke(\n                this,\n                new EventArgs<TriggerKey>(triggerKey, cancellationToken)\n            );\n            return Task.CompletedTask;\n        }\n\n        public Task TriggersPaused(\n            string? triggerGroup,\n            CancellationToken cancellationToken = default\n        )\n        {\n            OnTriggerGroupPaused?.Invoke(\n                this,\n                new EventArgs<string?>(triggerGroup, cancellationToken)\n            );\n            return Task.CompletedTask;\n        }\n\n        public Task TriggersResumed(\n            string? triggerGroup,\n            CancellationToken cancellationToken = default\n        )\n        {\n            OnTriggerGroupResumed?.Invoke(\n                this,\n                new EventArgs<string?>(triggerGroup, cancellationToken)\n            );\n            return Task.CompletedTask;\n        }\n\n        public Task<bool> VetoJobExecution(\n            ITrigger trigger,\n            IJobExecutionContext context,\n            CancellationToken cancellationToken = default\n        )\n        {\n            return Task.FromResult(false);\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Core/Services/SchedulerService.cs",
    "content": "﻿using System;\nusing BlazingQuartz.Core.Models;\nusing BlazingQuartz.Jobs;\nusing Microsoft.Extensions.Logging;\nusing Quartz;\nusing Quartz.Impl.Matchers;\n\nnamespace BlazingQuartz.Core.Services;\n\npublic class SchedulerService(ILogger<SchedulerService> logger, ISchedulerFactory schedulerFactory)\n    : ISchedulerService\n{\n    public async IAsyncEnumerable<ScheduleModel> GetAllJobsAsync(ScheduleJobFilter? filter = null)\n    {\n        IScheduler scheduler = await schedulerFactory.GetScheduler();\n        IReadOnlyCollection<string> jobGroupNames = await scheduler.GetJobGroupNames();\n\n        foreach (var jobGrp in jobGroupNames)\n        {\n            if (filter is { IncludeSystemJobs: false } && jobGrp == Constants.SYSTEM_GROUP)\n                continue;\n\n            var jobKeys = await scheduler.GetJobKeys(GroupMatcher<JobKey>.GroupEquals(jobGrp));\n\n            foreach (var jobKey in jobKeys)\n            {\n                await foreach (var job in GetScheduleModelsAsync(jobKey))\n                {\n                    yield return job;\n                }\n            }\n        }\n    }\n\n    public async Task<ScheduleModel> GetScheduleModelAsync(ITrigger trigger)\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n\n        var jobDetail = await scheduler.GetJobDetail(trigger.JobKey);\n\n        return await CreateScheduleModel(jobDetail, trigger);\n    }\n\n    public async Task<IReadOnlyCollection<string>> GetJobGroups()\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        return (await scheduler.GetJobGroupNames())\n            .Where(n => n != Constants.SYSTEM_GROUP)\n            .ToList();\n    }\n\n    public async Task<IReadOnlyCollection<string>> GetTriggerGroups()\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        return (await scheduler.GetTriggerGroupNames())\n            .Where(n => n != Constants.SYSTEM_GROUP)\n            .ToList();\n    }\n\n    public async Task<IList<KeyValuePair<string, int>>> GetScheduledJobSummary()\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        var executingCount = (await scheduler.GetCurrentlyExecutingJobs()).Count;\n        var jobCount = (await scheduler.GetJobKeys(GroupMatcher<JobKey>.AnyGroup())).Count;\n        var triggerCount = (\n            await scheduler.GetTriggerKeys(GroupMatcher<TriggerKey>.AnyGroup())\n        ).Count;\n        var sysJobCount = (\n            await scheduler.GetJobKeys(GroupMatcher<JobKey>.GroupEquals(Constants.SYSTEM_GROUP))\n        ).Count;\n        var sysTriggerCount = (\n            await scheduler.GetTriggerKeys(\n                GroupMatcher<TriggerKey>.GroupEquals(Constants.SYSTEM_GROUP)\n            )\n        ).Count;\n\n        return new List<KeyValuePair<string, int>>\n        {\n            new KeyValuePair<string, int>(\"Jobs\", jobCount),\n            new KeyValuePair<string, int>(\"Triggers\", triggerCount),\n            new KeyValuePair<string, int>(\"Executing\", executingCount),\n            new KeyValuePair<string, int>(\"System Jobs\", sysJobCount),\n            new KeyValuePair<string, int>(\"System Triggers\", sysTriggerCount),\n        };\n    }\n\n    public async Task<SchedulerMetaData> GetMetadataAsync()\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        return await scheduler.GetMetaData();\n    }\n\n    private async Task<ScheduleModel> CreateScheduleModel(\n        IJobDetail? jobDetail,\n        ITrigger trigger,\n        CancellationToken cancellationToken = default\n    )\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        var triggerState = (await scheduler.GetTriggerState(trigger.Key));\n        var runningTrigger = (await scheduler.GetCurrentlyExecutingJobs(cancellationToken))\n            .Where(context => context.Trigger.Equals(trigger))\n            .FirstOrDefault();\n\n        return new ScheduleModel\n        {\n            JobName = jobDetail?.Key.Name,\n            JobGroup = jobDetail?.Key.Group ?? \"No Group\",\n            JobType = jobDetail?.JobType.ToString(),\n            JobDescription = jobDetail?.Description,\n            TriggerName = trigger.Key.Name,\n            TriggerGroup = trigger.Key.Group,\n            TriggerDescription = trigger.Description,\n            TriggerType = trigger.GetTriggerType(),\n            TriggerTypeClassName = trigger.GetType().Name,\n            NextTriggerTime = trigger.GetNextFireTimeUtc(),\n            PreviousTriggerTime = trigger.GetPreviousFireTimeUtc(),\n            JobStatus =\n                runningTrigger != null\n                    ? JobStatus.Running\n                    : triggerState switch\n                    {\n                        TriggerState.Paused => JobStatus.Paused,\n                        TriggerState.None => JobStatus.NoTrigger,\n                        TriggerState.Error => JobStatus.Error,\n                        _ => JobStatus.Idle,\n                    },\n            TriggerDetail = CreateTriggerDetailModel(trigger),\n        };\n    }\n\n    public async Task CreateSchedule(\n        JobDetailModel jobDetailModel,\n        TriggerDetailModel triggerDetailModel\n    )\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n\n        var trigger = BuildTrigger(triggerDetailModel);\n\n        // Determine if job already exists\n        if (await ContainsJobKey(jobDetailModel.Name, jobDetailModel.Group))\n        {\n            var existingJob = await scheduler.GetJobDetail(\n                new JobKey(jobDetailModel.Name, jobDetailModel.Group)\n            );\n            if (existingJob != null)\n            {\n                //await scheduler.GetTriggersOfJob(job.Key)\n                var jobTriggers = new List<ITrigger>(1);\n                jobTriggers.Add(trigger);\n\n                await scheduler.ScheduleJob(existingJob, jobTriggers.AsReadOnly(), true);\n                return;\n            }\n        }\n\n        var job = CreateJobDetail(jobDetailModel);\n\n        await scheduler.ScheduleJob(job, trigger);\n    }\n\n    public async Task UpdateSchedule(\n        Key oldJobKey,\n        Key? oldTriggerKey,\n        JobDetailModel newJobModel,\n        TriggerDetailModel newTriggerModel\n    )\n    {\n        var scheduler = await schedulerFactory.GetScheduler().ConfigureAwait(false);\n        var oJobKey = oldJobKey.ToJobKey();\n\n        var newJob = CreateJobDetail(newJobModel);\n        var trigger = BuildTrigger(newTriggerModel, newJob.Key);\n        // determine if old triggerKey exists\n        if (\n            oldTriggerKey != null\n            && await scheduler.CheckExists(oldTriggerKey.ToTriggerKey()).ConfigureAwait(false)\n        )\n        {\n            await scheduler.UnscheduleJob(oldTriggerKey.ToTriggerKey()).ConfigureAwait(false);\n        }\n\n        var existingTriggers = await scheduler.GetTriggersOfJob(oJobKey).ConfigureAwait(false);\n\n        // assign new job to all triggers\n        var triggers = existingTriggers\n            .Select(t =>\n            {\n                var b = t.GetTriggerBuilder().ForJob(newJob.Key);\n                if (t.StartTimeUtc < DateTimeOffset.UtcNow)\n                    b.StartNow();\n                return b.Build();\n            })\n            .ToList();\n        triggers.Add(trigger);\n\n        // delete old job\n        await scheduler.DeleteJob(oJobKey).ConfigureAwait(false);\n\n        // save new job with triggers\n        await scheduler.ScheduleJob(newJob, triggers, replace: true).ConfigureAwait(false);\n    }\n\n    public async Task<JobDetailModel?> GetJobDetail(string jobName, string groupName)\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        var jd = await scheduler.GetJobDetail(new JobKey(jobName, groupName));\n\n        if (jd == null)\n            return null;\n\n        return new JobDetailModel\n        {\n            Name = jd.Key.Name,\n            Group = jd.Key.Group,\n            Description = jd.Description,\n            JobDataMap = jd.JobDataMap,\n            JobClass = jd.JobType,\n            IsDurable = jd.Durable,\n        };\n    }\n\n    public async Task<TriggerDetailModel?> GetTriggerDetail(string triggerName, string triggerGroup)\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        var trigger = await scheduler.GetTrigger(new TriggerKey(triggerName, triggerGroup));\n\n        if (trigger == null)\n            return null;\n\n        return CreateTriggerDetailModel(trigger);\n    }\n\n    public async Task<bool> ContainsTriggerKey(string triggerName, string triggerGroup)\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        return await scheduler.CheckExists(new TriggerKey(triggerName, triggerGroup));\n    }\n\n    public async Task<bool> ContainsJobKey(string jobName, string jobGroup)\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        return await scheduler.CheckExists(new JobKey(jobName, jobGroup));\n    }\n\n    public async Task<IReadOnlyCollection<string>> GetCalendarNames(\n        CancellationToken cancelToken = default\n    )\n    {\n        var scheduler = await schedulerFactory.GetScheduler(cancelToken);\n\n        return await scheduler.GetCalendarNames(cancelToken);\n    }\n\n    public async Task PauseTrigger(string triggerName, string? triggerGroup)\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        await scheduler.PauseTrigger(\n            triggerGroup == null\n                ? new TriggerKey(triggerName)\n                : new TriggerKey(triggerName, triggerGroup)\n        );\n    }\n\n    public async Task ResumeTrigger(string triggerName, string? triggerGroup)\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        await scheduler.ResumeTrigger(\n            triggerGroup == null\n                ? new TriggerKey(triggerName)\n                : new TriggerKey(triggerName, triggerGroup)\n        );\n    }\n\n    public async Task<bool> DeleteSchedule(ScheduleModel model)\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n\n        if (model.JobName == null)\n            return false;\n\n        if (model.JobStatus == JobStatus.NoSchedule)\n            return true;\n\n        var jobKey = new JobKey(model.JobName, model.JobGroup);\n\n        if (model.JobStatus == JobStatus.Error && model.TriggerName == null)\n        {\n            logger.LogInformation(\n                \"Job [{jobGroup}.{jobName}] has no trigger name. \"\n                    + \"Cannot UncheduleJob by trigger, will delete job directly.\",\n                jobKey.Group,\n                jobKey.Name\n            );\n            return await scheduler.DeleteJob(jobKey);\n        }\n\n        if (model.JobStatus == JobStatus.NoTrigger)\n        {\n            var triggers = await scheduler.GetTriggersOfJob(jobKey);\n            if (!triggers.Any())\n                return await scheduler.DeleteJob(jobKey);\n            else\n            {\n                logger.LogWarning(\n                    \"Cannot delete Job [{jobGroup}.{jobName}]. There are still {triggerCount}\"\n                        + \" trigger(s) assigned to this job.\",\n                    jobKey.Group,\n                    jobKey.Name,\n                    triggers.Count\n                );\n                return false;\n            }\n        }\n\n        if (model.TriggerName == null)\n            return false;\n\n        var success = await scheduler.UnscheduleJob(\n            model.TriggerGroup == null\n                ? new TriggerKey(model.TriggerName)\n                : new TriggerKey(model.TriggerName, model.TriggerGroup)\n        );\n\n        if (success)\n        {\n            var triggers = await scheduler.GetTriggersOfJob(jobKey);\n            if (!triggers.Any())\n            {\n                logger.LogInformation(\n                    \"UnscheduleJob [{jobGroup}.{jobName}] has no more triggers. \"\n                        + \"Determine if job was deleted.\",\n                    jobKey.Group,\n                    jobKey.Name\n                );\n\n                if (await scheduler.CheckExists(jobKey))\n                {\n                    logger.LogInformation(\n                        \"Manually delete job [{jobGroup}.{jobName}].\",\n                        jobKey.Group,\n                        jobKey.Name\n                    );\n                    return await scheduler.DeleteJob(jobKey);\n                }\n            }\n        }\n\n        return success;\n    }\n\n    public async Task TriggerJob(string jobName, string jobGroup)\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        await scheduler.TriggerJob(new JobKey(jobName, jobGroup));\n    }\n\n    #region Private methods\n\n    private async IAsyncEnumerable<ScheduleModel> GetScheduleModelsAsync(JobKey jobkey)\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n\n        IJobDetail? jobDetail = null;\n        IReadOnlyCollection<ITrigger>? jobTriggers = null;\n        ScheduleModel? exceptionJob = null;\n        try\n        {\n            jobDetail = await scheduler.GetJobDetail(jobkey);\n            jobTriggers = await scheduler.GetTriggersOfJob(jobkey);\n        }\n        catch (Exception ex)\n        {\n            logger.LogWarning(\n                ex,\n                \"Cannot GetScheduleModel of job [{jobGroup}.{jobName}]\",\n                jobkey.Group,\n                jobkey.Name\n            );\n            exceptionJob = new ScheduleModel\n            {\n                JobName = jobkey.Name,\n                JobGroup = jobkey.Group,\n                JobStatus = JobStatus.Error,\n                ExceptionMessage = ex.Message,\n            };\n        }\n\n        if (exceptionJob != null)\n        {\n            // job with exception\n            if (jobTriggers == null || !jobTriggers.Any())\n            {\n                exceptionJob.TriggerType = TriggerType.Unknown;\n                yield return exceptionJob;\n            }\n            else\n            {\n                foreach (var trigger in jobTriggers)\n                {\n                    var jobModel = await CreateScheduleModel(null, trigger);\n                    jobModel.JobName = exceptionJob.JobName;\n                    jobModel.JobGroup = exceptionJob.JobGroup;\n                    jobModel.JobStatus = exceptionJob.JobStatus;\n                    jobModel.ExceptionMessage = exceptionJob.ExceptionMessage;\n                    yield return jobModel;\n                }\n            }\n        }\n        else if (jobTriggers == null || !jobTriggers.Any())\n        {\n            yield return new ScheduleModel\n            {\n                JobName = jobkey.Name,\n                JobGroup = jobkey.Group,\n                JobType = jobDetail?.JobType.ToString(),\n                JobStatus = JobStatus.NoTrigger,\n            };\n        }\n        else\n        {\n            foreach (var trigger in jobTriggers)\n            {\n                yield return await CreateScheduleModel(jobDetail, trigger);\n            }\n        }\n    }\n\n    private TriggerDetailModel CreateTriggerDetailModel(ITrigger trigger)\n    {\n        var triggerType = trigger.GetTriggerType();\n\n        var model = new TriggerDetailModel\n        {\n            Name = trigger.Key.Name,\n            Group = trigger.Key.Group,\n            Description = trigger.Description,\n            TriggerDataMap = trigger.JobDataMap,\n            EndDate = trigger.EndTimeUtc?.Date,\n            EndTimeSpan = trigger.EndTimeUtc?.TimeOfDay,\n            StartDate = trigger.StartTimeUtc.Date,\n            StartTimeSpan = trigger.StartTimeUtc.TimeOfDay,\n            StartTimezone = TimeZoneInfo.Utc,\n            TriggerType = triggerType,\n            ModifiedByCalendar = trigger.CalendarName,\n            Priority = trigger.Priority,\n        };\n\n        switch (trigger.MisfireInstruction)\n        {\n            case MisfireInstruction.IgnoreMisfirePolicy:\n                model.MisfireAction = MisfireAction.IgnoreMisfirePolicy;\n                break;\n            // comment out same as SmartPolicy\n            //case MisfireInstruction.InstructionNotSet:\n            //    model.MisfireAction = MisfireAction.InstructionNotSet;\n            //    break;\n            case MisfireInstruction.SmartPolicy:\n                model.MisfireAction = MisfireAction.SmartPolicy;\n                break;\n        }\n\n        switch (triggerType)\n        {\n            case TriggerType.Cron:\n                var cron = (ICronTrigger)trigger;\n                model.CronExpression = cron.CronExpressionString;\n                model.InTimeZone = cron.TimeZone;\n                switch (cron.MisfireInstruction)\n                {\n                    case MisfireInstruction.CronTrigger.DoNothing:\n                        model.MisfireAction = MisfireAction.DoNothing;\n                        break;\n                    case MisfireInstruction.CronTrigger.FireOnceNow:\n                        model.MisfireAction = MisfireAction.FireOnceNow;\n                        break;\n                }\n                break;\n            case TriggerType.Daily:\n                var daily = (IDailyTimeIntervalTrigger)trigger;\n                foreach (var dow in daily.DaysOfWeek)\n                {\n                    model.DailyDayOfWeek[(int)dow] = true;\n                }\n                switch (daily.MisfireInstruction)\n                {\n                    case MisfireInstruction.DailyTimeIntervalTrigger.DoNothing:\n                        model.MisfireAction = MisfireAction.DoNothing;\n                        break;\n                    case MisfireInstruction.DailyTimeIntervalTrigger.FireOnceNow:\n                        model.MisfireAction = MisfireAction.FireOnceNow;\n                        break;\n                }\n                model.RepeatCount = daily.RepeatCount;\n                model.TriggerInterval = daily.RepeatInterval;\n                model.TriggerIntervalUnit = daily.RepeatIntervalUnit.ToBlazingQuartzIntervalUnit();\n                model.InTimeZone = daily.TimeZone;\n                model.StartDailyTime = new TimeSpan(\n                    daily.StartTimeOfDay.Hour,\n                    daily.StartTimeOfDay.Minute,\n                    daily.StartTimeOfDay.Second\n                );\n                model.EndDailyTime = new TimeSpan(\n                    daily.EndTimeOfDay.Hour,\n                    daily.EndTimeOfDay.Minute,\n                    daily.EndTimeOfDay.Second\n                );\n                break;\n            case TriggerType.Simple:\n                var simple = (ISimpleTrigger)trigger;\n                model = PopulateSimpleTrigger(simple, model);\n                break;\n            case TriggerType.Calendar:\n                var calTrigger = (ICalendarIntervalTrigger)trigger;\n                switch (calTrigger.MisfireInstruction)\n                {\n                    case MisfireInstruction.CalendarIntervalTrigger.DoNothing:\n                        model.MisfireAction = MisfireAction.DoNothing;\n                        break;\n                    case MisfireInstruction.CalendarIntervalTrigger.FireOnceNow:\n                        model.MisfireAction = MisfireAction.FireOnceNow;\n                        break;\n                }\n                model.TriggerInterval = calTrigger.RepeatInterval;\n                model.TriggerIntervalUnit =\n                    calTrigger.RepeatIntervalUnit.ToBlazingQuartzIntervalUnit();\n                model.InTimeZone = calTrigger.TimeZone;\n                break;\n        }\n\n        return model;\n    }\n\n    private IJobDetail CreateJobDetail(JobDetailModel jobDetailModel)\n    {\n        ArgumentNullException.ThrowIfNull(jobDetailModel.JobClass);\n\n        return JobBuilder\n            .Create(jobDetailModel.JobClass)\n            .WithIdentity(jobDetailModel.Name, jobDetailModel.Group)\n            .WithDescription(jobDetailModel.Description)\n            .UsingJobData(new JobDataMap(jobDetailModel.JobDataMap))\n            .StoreDurably(jobDetailModel.IsDurable)\n            .Build();\n    }\n\n    private ITrigger BuildTrigger(TriggerDetailModel triggerDetailModel, JobKey? jobKey = null)\n    {\n        var tbldr = TriggerBuilder\n            .Create()\n            .WithIdentity(triggerDetailModel.Name, triggerDetailModel.Group)\n            .WithDescription(triggerDetailModel.Description)\n            .WithPriority(triggerDetailModel.Priority)\n            .UsingJobData(new JobDataMap(triggerDetailModel.TriggerDataMap))\n            .ModifiedByCalendar(triggerDetailModel.ModifiedByCalendar);\n\n        if (jobKey != null)\n        {\n            tbldr.ForJob(jobKey);\n        }\n\n        var startTime = triggerDetailModel.StartDateTimeUtc;\n        if (startTime.HasValue)\n        {\n            tbldr = tbldr.StartAt(startTime.Value);\n        }\n        else\n        {\n            tbldr = tbldr.StartNow();\n        }\n\n        tbldr.EndAt(triggerDetailModel.EndDateTimeUtc);\n\n        switch (triggerDetailModel.TriggerType)\n        {\n            case TriggerType.Cron:\n                ArgumentNullException.ThrowIfNull(triggerDetailModel.CronExpression);\n                tbldr = tbldr.WithCronSchedule(\n                    triggerDetailModel.CronExpression,\n                    x =>\n                    {\n                        switch (triggerDetailModel.MisfireAction)\n                        {\n                            case MisfireAction.DoNothing:\n                                x.WithMisfireHandlingInstructionDoNothing();\n                                break;\n                            case MisfireAction.FireOnceNow:\n                                x.WithMisfireHandlingInstructionFireAndProceed();\n                                break;\n                            case MisfireAction.IgnoreMisfirePolicy:\n                                x.WithMisfireHandlingInstructionIgnoreMisfires();\n                                break;\n                        }\n                        x.InTimeZone(triggerDetailModel.InTimeZone);\n                    }\n                );\n                break;\n            case TriggerType.Daily:\n                tbldr = tbldr.WithDailyTimeIntervalSchedule(x =>\n                {\n                    switch (triggerDetailModel.MisfireAction)\n                    {\n                        case MisfireAction.DoNothing:\n                            x.WithMisfireHandlingInstructionDoNothing();\n                            break;\n                        case MisfireAction.FireOnceNow:\n                            x.WithMisfireHandlingInstructionFireAndProceed();\n                            break;\n                        case MisfireAction.IgnoreMisfirePolicy:\n                            x.WithMisfireHandlingInstructionIgnoreMisfires();\n                            break;\n                    }\n                    x.OnDaysOfTheWeek(triggerDetailModel.GetDailyOnDaysOfWeek());\n                    if (triggerDetailModel.StartDailyTime.HasValue)\n                    {\n                        x.StartingDailyAt(triggerDetailModel.StartDailyTime.Value.ToTimeOfDay());\n                    }\n                    if (triggerDetailModel.EndDailyTime.HasValue)\n                    {\n                        x.EndingDailyAt(triggerDetailModel.EndDailyTime.Value.ToTimeOfDay());\n                    }\n                    x.InTimeZone(triggerDetailModel.InTimeZone);\n                    if (\n                        triggerDetailModel.TriggerInterval > 0\n                        && triggerDetailModel.TriggerIntervalUnit.HasValue\n                    )\n                    {\n                        x.WithInterval(\n                            triggerDetailModel.TriggerInterval,\n                            triggerDetailModel.TriggerIntervalUnit.Value.ToQuartzIntervalUnit()\n                        );\n                    }\n                    if (triggerDetailModel.RepeatCount > 0)\n                        x.WithRepeatCount(triggerDetailModel.RepeatCount);\n                });\n                break;\n            case TriggerType.Simple:\n                tbldr = tbldr.WithSimpleSchedule(x =>\n                {\n                    switch (triggerDetailModel.MisfireAction)\n                    {\n                        case MisfireAction.FireNow:\n                            x.WithMisfireHandlingInstructionFireNow();\n                            break;\n                        case MisfireAction.RescheduleNextWithExistingCount:\n                            x.WithMisfireHandlingInstructionNextWithExistingCount();\n                            break;\n                        case MisfireAction.RescheduleNextWithRemainingCount:\n                            x.WithMisfireHandlingInstructionNextWithRemainingCount();\n                            break;\n                        case MisfireAction.RescheduleNowWithExistingRepeatCount:\n                            x.WithMisfireHandlingInstructionNowWithExistingCount();\n                            break;\n                        case MisfireAction.RescheduleNowWithRemainingRepeatCount:\n                            x.WithMisfireHandlingInstructionNowWithRemainingCount();\n                            break;\n                        case MisfireAction.IgnoreMisfirePolicy:\n                            x.WithMisfireHandlingInstructionIgnoreMisfires();\n                            break;\n                    }\n\n                    if (\n                        triggerDetailModel.TriggerInterval > 0\n                        && triggerDetailModel.TriggerIntervalUnit.HasValue\n                    )\n                    {\n                        TimeSpan timeSpan;\n                        switch (triggerDetailModel.TriggerIntervalUnit.Value)\n                        {\n                            case IntervalUnit.Millisecond:\n                                timeSpan = TimeSpan.FromMilliseconds(\n                                    triggerDetailModel.TriggerInterval\n                                );\n                                break;\n                            case IntervalUnit.Second:\n                                timeSpan = TimeSpan.FromSeconds(triggerDetailModel.TriggerInterval);\n                                break;\n                            case IntervalUnit.Minute:\n                                timeSpan = TimeSpan.FromMinutes(triggerDetailModel.TriggerInterval);\n                                break;\n                            case IntervalUnit.Hour:\n                                timeSpan = TimeSpan.FromHours(triggerDetailModel.TriggerInterval);\n                                break;\n                            case IntervalUnit.Day:\n                                timeSpan = TimeSpan.FromDays(triggerDetailModel.TriggerInterval);\n                                break;\n                            default:\n                                throw new NotSupportedException(\n                                    $\"Interval unit {triggerDetailModel.TriggerIntervalUnit} is not supported for SimpleTrigger.\"\n                                );\n                        }\n                        x.WithInterval(timeSpan);\n                    }\n\n                    if (triggerDetailModel.RepeatForever)\n                        x.RepeatForever();\n                    else\n                        x.WithRepeatCount(triggerDetailModel.RepeatCount);\n                });\n                break;\n            case TriggerType.Calendar:\n                tbldr = tbldr.WithCalendarIntervalSchedule(x =>\n                {\n                    switch (triggerDetailModel.MisfireAction)\n                    {\n                        case MisfireAction.DoNothing:\n                            x.WithMisfireHandlingInstructionDoNothing();\n                            break;\n                        case MisfireAction.FireOnceNow:\n                            x.WithMisfireHandlingInstructionFireAndProceed();\n                            break;\n                        case MisfireAction.IgnoreMisfirePolicy:\n                            x.WithMisfireHandlingInstructionIgnoreMisfires();\n                            break;\n                    }\n\n                    x.InTimeZone(triggerDetailModel.InTimeZone);\n                    if (\n                        triggerDetailModel.TriggerInterval > 0\n                        && triggerDetailModel.TriggerIntervalUnit.HasValue\n                    )\n                    {\n                        x.WithInterval(\n                            triggerDetailModel.TriggerInterval,\n                            triggerDetailModel.TriggerIntervalUnit.Value.ToQuartzIntervalUnit()\n                        );\n                    }\n                });\n                break;\n        }\n\n        return tbldr.Build();\n    }\n\n    private TriggerDetailModel PopulateSimpleTrigger(\n        ISimpleTrigger simple,\n        TriggerDetailModel model\n    )\n    {\n        switch (simple.MisfireInstruction)\n        {\n            case MisfireInstruction.SimpleTrigger.RescheduleNextWithExistingCount:\n                model.MisfireAction = MisfireAction.RescheduleNextWithExistingCount;\n                break;\n            case MisfireInstruction.SimpleTrigger.RescheduleNextWithRemainingCount:\n                model.MisfireAction = MisfireAction.RescheduleNextWithRemainingCount;\n                break;\n            case MisfireInstruction.SimpleTrigger.RescheduleNowWithExistingRepeatCount:\n                model.MisfireAction = MisfireAction.RescheduleNowWithExistingRepeatCount;\n                break;\n            case MisfireInstruction.SimpleTrigger.RescheduleNowWithRemainingRepeatCount:\n                model.MisfireAction = MisfireAction.RescheduleNowWithRemainingRepeatCount;\n                break;\n            case MisfireInstruction.SimpleTrigger.FireNow:\n                model.MisfireAction = MisfireAction.FireNow;\n                break;\n        }\n        if (simple.RepeatCount >= 0)\n            model.RepeatCount = simple.RepeatCount;\n        else\n            model.RepeatForever = true;\n\n        var total = simple.RepeatInterval.TotalHours;\n        if (Math.Round(total) == total)\n        {\n            model.TriggerInterval = Convert.ToInt32(total);\n            model.TriggerIntervalUnit = IntervalUnit.Hour;\n        }\n        else\n        {\n            total = simple.RepeatInterval.TotalMinutes;\n            if (Math.Round(total) == total)\n            {\n                model.TriggerInterval = Convert.ToInt32(total);\n                model.TriggerIntervalUnit = IntervalUnit.Minute;\n            }\n            else\n            {\n                total = simple.RepeatInterval.TotalSeconds;\n                if (Math.Round(total) == total)\n                {\n                    model.TriggerInterval = Convert.ToInt32(total);\n                    model.TriggerIntervalUnit = IntervalUnit.Second;\n                }\n                //else\n                //{\n                //    total = simple.RepeatInterval.TotalMilliseconds;\n                //    if (Math.Round(total) == total)\n                //    {\n                //        model.TriggerInterval = Convert.ToInt32(total);\n                //        model.TriggerIntervalUnit = IntervalUnit.Millisecond;\n                //    }\n                //}\n            }\n        }\n\n        return model;\n    }\n\n    public async Task PauseAllSchedules()\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        await scheduler.PauseAll();\n    }\n\n    public async Task ResumeAllSchedules()\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        await scheduler.ResumeAll();\n    }\n\n    public async Task ShutdownScheduler()\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        await scheduler.Shutdown();\n    }\n\n    public async Task StartScheduler()\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        await scheduler.Start();\n    }\n\n    public async Task StandbyScheduler()\n    {\n        var scheduler = await schedulerFactory.GetScheduler();\n        await scheduler.Standby();\n    }\n\n    #endregion Private methods\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs/BlazingQuartz.Jobs.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Extensions.Http\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\BlazingQuartz.Jobs.Abstractions\\BlazingQuartz.Jobs.Abstractions.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs/Constants.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Jobs\n{\n    public abstract class Constants\n    {\n        public const string HttpClientIgnoreVerifySsl = \"IgnoreSsl\";\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs/HttpAction.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Jobs\n{\n    public enum HttpAction\n    {\n        Get,\n        Post,\n        Put,\n        Delete,\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs/HttpJob.cs",
    "content": "﻿using System;\nusing System.Text;\nusing System.Text.Json;\nusing BlazingQuartz.Jobs.Abstractions;\nusing Microsoft.Extensions.Logging;\nusing Quartz;\nusing static System.Net.Mime.MediaTypeNames;\n\nnamespace BlazingQuartz.Jobs\n{\n    public class HttpJob : IJob\n    {\n        public const string PropertyRequestAction = \"requestAction\";\n        public const string PropertyRequestUrl = \"requestUrl\";\n        public const string PropertyRequestParameters = \"requestParams\";\n        public const string PropertyRequestHeaders = \"requestHeaders\";\n        public const string PropertyIgnoreVerifySsl = \"ignoreSsl\";\n\n        /// <summary>\n        /// HTTP request timeout. Negative value to indicate infinite timeout.\n        /// </summary>\n        public const string PropertyRequestTimeoutInSec = \"requestTimeout\";\n\n        private readonly IHttpClientFactory _httpClientFactory;\n        private readonly ILogger<HttpJob> _logger;\n        private readonly IDataMapValueResolver _dmvResolver;\n\n        public HttpJob(\n            IHttpClientFactory httpClientFactory,\n            ILogger<HttpJob> logger,\n            IDataMapValueResolver dmvResolver\n        )\n        {\n            _httpClientFactory = httpClientFactory;\n            _logger = logger;\n            _dmvResolver = dmvResolver;\n        }\n\n        public async Task Execute(IJobExecutionContext context)\n        {\n            try\n            {\n                var data = context.MergedJobDataMap;\n\n                int? timeoutInSec = data.TryGetInt(PropertyRequestTimeoutInSec, out var x)\n                    ? x\n                    : null;\n                var dmvUrl = data.GetDataMapValue(PropertyRequestUrl);\n                var url = _dmvResolver.Resolve(dmvUrl);\n                if (string.IsNullOrEmpty(url))\n                {\n                    _logger.LogWarning(\n                        \"[{runInstanceId}]. Cannot run HttpJob. No request url specified.\",\n                        context.FireInstanceId\n                    );\n                    throw new JobExecutionException(\"No request url specified\");\n                }\n                url = url.StartsWith(\"http\") ? url : \"http://\" + url;\n\n                var parameters = _dmvResolver.Resolve(\n                    data.GetDataMapValue(PropertyRequestParameters)\n                );\n                var strHeaders = _dmvResolver.Resolve(data.GetDataMapValue(PropertyRequestHeaders));\n                var headers = string.IsNullOrEmpty(strHeaders)\n                    ? null\n                    : JsonSerializer.Deserialize<Dictionary<string, string>>(strHeaders.Trim());\n\n                var strAction = data.GetString(PropertyRequestAction);\n                HttpAction action;\n                if (strAction == null)\n                {\n                    _logger.LogWarning(\n                        \"[{runInstanceId}]. Cannot run HttpJob. No http action specified.\",\n                        context.FireInstanceId\n                    );\n                    throw new JobExecutionException(\"No http action specified\");\n                }\n                action = Enum.Parse<HttpAction>(strAction);\n\n                _logger.LogDebug(\n                    \"[{runInstanceId}]. Creating HttpClient...\",\n                    context.FireInstanceId\n                );\n                HttpClient httpClient;\n                if (\n                    data.TryGetBoolean(PropertyIgnoreVerifySsl, out var IgnoreVerifySsl)\n                    && IgnoreVerifySsl\n                )\n                {\n                    httpClient = _httpClientFactory.CreateClient(\n                        Constants.HttpClientIgnoreVerifySsl\n                    );\n                    _logger.LogInformation(\n                        \"[{runInstanceId}]. Created ignore SSL validation HttpClient.\",\n                        context.FireInstanceId\n                    );\n                }\n                else\n                {\n                    httpClient = _httpClientFactory.CreateClient();\n                    _logger.LogInformation(\n                        \"[{runInstanceId}]. Created HttpClient.\",\n                        context.FireInstanceId\n                    );\n                }\n\n                // configure time out. Default 100 secs\n                if (timeoutInSec.HasValue)\n                {\n                    if (timeoutInSec > 0)\n                    {\n                        httpClient.Timeout = TimeSpan.FromSeconds(timeoutInSec.Value);\n                    }\n                    else\n                    {\n                        httpClient.Timeout = Timeout.InfiniteTimeSpan;\n                    }\n                }\n\n                if (headers != null)\n                {\n                    foreach (var header in headers)\n                    {\n                        httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);\n                    }\n                }\n\n                HttpContent? reqParam = null;\n                if (!string.IsNullOrEmpty(parameters))\n                    reqParam = new StringContent(parameters, Encoding.UTF8, Application.Json);\n\n                HttpResponseMessage response = new HttpResponseMessage();\n                _logger.LogInformation(\n                    \"[{runInstanceId}]. Sending '{action}' request to specified url '{url}'.\",\n                    context.FireInstanceId,\n                    action,\n                    url\n                );\n                switch (action)\n                {\n                    case HttpAction.Get:\n                        response = await httpClient.GetAsync(url, context.CancellationToken);\n                        break;\n                    case HttpAction.Post:\n                        response = await httpClient.PostAsync(\n                            url,\n                            reqParam,\n                            context.CancellationToken\n                        );\n                        break;\n                    case HttpAction.Put:\n                        response = await httpClient.PutAsync(\n                            url,\n                            reqParam,\n                            context.CancellationToken\n                        );\n                        break;\n                    case HttpAction.Delete:\n                        response = await httpClient.DeleteAsync(url, context.CancellationToken);\n                        break;\n                }\n\n                var result = await response.Content.ReadAsStringAsync(context.CancellationToken);\n                _logger.LogInformation(\n                    \"[{runInstanceId}]. Response tatus code '{code}'.\",\n                    context.FireInstanceId,\n                    response.StatusCode\n                );\n                context.Result = result;\n                context.SetIsSuccess(response.IsSuccessStatusCode);\n                context.SetReturnCode((int)response.StatusCode);\n                context.SetExecutionDetails($\"Request: [{response.RequestMessage}]\");\n            }\n            catch (JobExecutionException)\n            {\n                context.SetIsSuccess(false);\n                throw;\n            }\n            catch (Exception ex)\n            {\n                _logger.LogWarning(\n                    ex,\n                    \"Failed to run HttpJob. [{runInstanceId}]\",\n                    context.FireInstanceId\n                );\n                context.SetIsSuccess(false);\n                throw new JobExecutionException(\"Failed to execute http job\", ex);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs/ServiceCollectionExtensions.cs",
    "content": "﻿using System;\nusing BlazingQuartz.Jobs.Abstractions;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace BlazingQuartz.Jobs\n{\n    public static class ServiceCollectionExtensions\n    {\n        public static IServiceCollection AddBlazingQuartzJobs(this IServiceCollection services)\n        {\n            // require to run BlazingQuartz.Jobs.HttpJob\n            services.AddHttpClient();\n            services\n                .AddHttpClient(Constants.HttpClientIgnoreVerifySsl)\n                .ConfigurePrimaryHttpMessageHandler(() =>\n                    new HttpClientHandler\n                    {\n                        ServerCertificateCustomValidationCallback =\n                            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,\n                    }\n                );\n\n            return Abstractions.ServiceCollectionExtensions.AddBlazingQuartzJobs(services);\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/BlazingQuartz.Jobs.Abstractions.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Quartz\" />\n    <PackageReference Include=\"Microsoft.Extensions.DependencyInjection.Abstractions\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging.Abstractions\" />\n  </ItemGroup>\n  <ItemGroup>\n    <Folder Include=\"Processors\\\" />\n    <Folder Include=\"Resolvers\\\" />\n    <Folder Include=\"Resolvers\\V1\\\" />\n  </ItemGroup>\n  <ItemGroup>\n    <AssemblyAttribute Include=\"System.Runtime.CompilerServices.InternalsVisibleToAttribute\">\n      <_Parameter1>$(AssemblyName).Test</_Parameter1>\n    </AssemblyAttribute>\n  </ItemGroup>\n  <ItemGroup>\n    <None Remove=\"Quartz\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/DataMapValue.cs",
    "content": "﻿using System;\nusing System.Globalization;\nusing System.Text.Json;\n\nnamespace BlazingQuartz.Jobs.Abstractions\n{\n    public class DataMapValue\n    {\n        public DataMapValueType Type { get; set; }\n        public string? Value { get; set; }\n        public int Version { get; set; }\n\n        public DataMapValue()\n            : this(DataMapValueType.InterpolatedString, 1) { }\n\n        public DataMapValue(DataMapValueType type, int version)\n            : this(type, null, version) { }\n\n        public DataMapValue(DataMapValueType type, string? value = null, int version = 1)\n        {\n            Type = type;\n            Value = value;\n            Version = version;\n        }\n\n        public override string ToString()\n        {\n            return JsonSerializer.Serialize(this);\n        }\n\n        /// <summary>\n        /// Create DataMapValue instance based from specified value\n        /// </summary>\n        /// <param name=\"dataMapValue\"></param>\n        /// <returns></returns>\n        public static DataMapValue? Create(object? dataMapValue)\n        {\n            var value = Convert.ToString(dataMapValue, CultureInfo.InvariantCulture);\n            if (value == null)\n                return null;\n\n            return JsonSerializer.Deserialize<DataMapValue>(value);\n        }\n\n        /// <summary>\n        /// Create DataMapValue instance based from specified value\n        /// </summary>\n        /// <param name=\"dataMapValue\"></param>\n        /// <returns></returns>\n        public static DataMapValue? Create(string? dataMapValue)\n        {\n            if (dataMapValue == null)\n                return null;\n\n            return JsonSerializer.Deserialize<DataMapValue>(dataMapValue);\n        }\n\n        public static DataMapValue Create(\n            object? dataMapValue,\n            DataMapValueType defaultType,\n            int defaultVersion,\n            string? defaultValue = null\n        )\n        {\n            var dmv = Create(dataMapValue);\n            if (dmv != null)\n                return dmv;\n            else\n                return new(defaultType, defaultValue, defaultVersion);\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/DataMapValueResolver.cs",
    "content": "﻿using BlazingQuartz.Jobs.Abstractions.Processors;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\n\nnamespace BlazingQuartz.Jobs.Abstractions\n{\n    public class DataMapValueResolver : IDataMapValueResolver\n    {\n        private readonly IServiceProvider? _svcProvider;\n\n        public DataMapValueResolver(IServiceProvider? svcProvider)\n        {\n            _svcProvider = svcProvider;\n        }\n\n        public string? Resolve(DataMapValue? dmv)\n        {\n            if (dmv == null)\n                return null;\n\n            switch (dmv.Type)\n            {\n                case DataMapValueType.InterpolatedString:\n                    switch (dmv.Version)\n                    {\n                        case 1:\n                            var logger = _svcProvider?.GetRequiredService<\n                                ILogger<InterpolatedStringV1Processor>\n                            >();\n                            var processor = new InterpolatedStringV1Processor(logger);\n                            return processor.Process(dmv);\n                        default:\n                            throw new NotSupportedException(\n                                $\"DataMapValue {dmv.Type} version {dmv.Version} is not supported.\"\n                            );\n                    }\n                default:\n                    throw new NotSupportedException(\n                        $\"DataMapValueType {dmv.Type} is not supported.\"\n                    );\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/DataMapValueType.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Jobs.Abstractions\n{\n    public enum DataMapValueType\n    {\n        InterpolatedString,\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/IDataMapValueResolver.cs",
    "content": "﻿namespace BlazingQuartz.Jobs.Abstractions\n{\n    public interface IDataMapValueResolver\n    {\n        string? Resolve(DataMapValue? dmv);\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/IJobUI.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Jobs.Abstractions\n{\n    public interface IJobUI\n    {\n        string JobClass { get; }\n\n        bool IsReadOnly { get; set; }\n\n        IDictionary<string, object> JobDataMap { get; set; }\n\n        /// <summary>\n        /// Remove all the JobUI keys that was added to JobDataMap\n        /// </summary>\n        /// <returns></returns>\n        Task ClearChanges();\n\n        /// <summary>\n        /// Apply the changes to JobDataMap\n        /// </summary>\n        /// <returns>true if no validation error and all changes applied</returns>\n        Task<bool> ApplyChanges();\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/JobDataMapKeys.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Jobs.Abstractions\n{\n    public static class JobDataMapKeys\n    {\n        public const string ExecutionDetails = \"__execDetails\";\n        public const string IsSuccess = \"__isSuccess\";\n        public const string ReturnCode = \"__returnCode\";\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/JobExecutionContextExtensions.cs",
    "content": "﻿using System;\nusing System.Globalization;\nusing System.Text;\nusing System.Text.Json;\nusing Quartz;\n\nnamespace BlazingQuartz.Jobs.Abstractions\n{\n    public static class JobExecutionContextExtensions\n    {\n        public static IJobExecutionContext SetReturnCode(\n            this IJobExecutionContext context,\n            string value\n        )\n        {\n            context.Put(JobDataMapKeys.ReturnCode, value);\n            return context;\n        }\n\n        public static IJobExecutionContext SetReturnCode(\n            this IJobExecutionContext context,\n            int value\n        )\n        {\n            context.Put(JobDataMapKeys.ReturnCode, value.ToString());\n            return context;\n        }\n\n        public static IJobExecutionContext SetExecutionDetails(\n            this IJobExecutionContext context,\n            string execDetails\n        )\n        {\n            context.Put(JobDataMapKeys.ExecutionDetails, execDetails);\n            return context;\n        }\n\n        public static IJobExecutionContext SetIsSuccess(\n            this IJobExecutionContext context,\n            bool success\n        )\n        {\n            context.Put(JobDataMapKeys.IsSuccess, success);\n            return context;\n        }\n\n        public static string? GetReturnCode(this IJobExecutionContext context)\n        {\n            var val = context.Get(JobDataMapKeys.ReturnCode);\n            if (val != null)\n                return Convert.ToString(val, CultureInfo.InvariantCulture);\n            return null;\n        }\n\n        public static string? GetExecutionDetails(this IJobExecutionContext context)\n        {\n            var val = context.Get(JobDataMapKeys.ExecutionDetails);\n            if (val != null)\n                return Convert.ToString(val, CultureInfo.InvariantCulture);\n\n            return null;\n        }\n\n        public static bool? GetIsSuccess(this IJobExecutionContext context)\n        {\n            var value = context.Get(JobDataMapKeys.IsSuccess);\n            if (value == null)\n                return null;\n            return Convert.ToBoolean(value);\n        }\n\n        public static DataMapValue? GetDataMapValue(this IJobExecutionContext context, string key)\n        {\n            var value = context.MergedJobDataMap.GetString(key);\n            return DataMapValue.Create(value);\n        }\n\n        public static DataMapValue? GetDataMapValue(this JobDataMap dataMap, string key)\n        {\n            if (dataMap.TryGetString(key, out var value))\n            {\n                return DataMapValue.Create(value);\n            }\n\n            return null;\n        }\n\n        public static string? GetReturnCodeAndResult(this IJobExecutionContext context)\n        {\n            var returnCode = context.GetReturnCode();\n            var strBldr = new StringBuilder();\n            if (!string.IsNullOrEmpty(returnCode))\n            {\n                strBldr.Append($\"Return {returnCode}. \");\n            }\n            var result = context.Result?.ToString();\n            if (!string.IsNullOrEmpty(result))\n            {\n                strBldr.Append(result);\n            }\n            return strBldr.ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/Processors/InterpolatedStringV1Processor.cs",
    "content": "﻿using System;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing Microsoft.Extensions.Logging;\n\nnamespace BlazingQuartz.Jobs.Abstractions.Processors\n{\n    public class InterpolatedStringV1Processor\n    {\n        const string VariableRegex = @\"\\{{2}(\\$.+?)\\}{2}\";\n\n        private readonly ILogger<InterpolatedStringV1Processor>? _logger;\n\n        public InterpolatedStringV1Processor(ILogger<InterpolatedStringV1Processor>? logger)\n        {\n            _logger = logger;\n        }\n\n        public string? Process(DataMapValue interpolatedString)\n        {\n            if (interpolatedString.Type != DataMapValueType.InterpolatedString)\n                throw new ArgumentException(\n                    $\"Invalid DataMapValue type {interpolatedString.Type}. Expected type {DataMapValueType.InterpolatedString}.\"\n                );\n\n            if (string.IsNullOrEmpty(interpolatedString.Value))\n                return interpolatedString.Value;\n\n            StringBuilder strBldr = new StringBuilder();\n            int lastIndex = 0;\n\n            var provider = new SystemVariableV1Provider();\n\n            _logger?.LogDebug(\n                \"Processing interpolated string [{string}]\",\n                interpolatedString.Value\n            );\n            foreach (\n                Match match in Regex.Matches(\n                    interpolatedString.Value,\n                    VariableRegex,\n                    RegexOptions.None\n                )\n            )\n            {\n                _logger?.LogDebug(\n                    \"Matched '{matchValue}' on index {index} length {length}.\",\n                    match.Value,\n                    match.Index,\n                    match.Length\n                );\n                strBldr.Append(\n                    interpolatedString.Value.Substring(lastIndex, match.Index - lastIndex)\n                );\n                lastIndex = match.Index + match.Length;\n\n                strBldr.Append(provider.Resolve(match.Value));\n            }\n\n            // append remaining\n            strBldr.Append(interpolatedString.Value.Substring(lastIndex));\n\n            return strBldr.ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/Processors/SystemVariableV1Provider.cs",
    "content": "﻿using System;\nusing System.Text.RegularExpressions;\nusing BlazingQuartz.Jobs.Abstractions.Resolvers;\nusing BlazingQuartz.Jobs.Abstractions.Resolvers.V1;\n\nnamespace BlazingQuartz.Jobs.Abstractions.Processors\n{\n    internal class SystemVariableV1Provider\n    {\n        char[] separators = new char[] { ' ', '}' };\n\n        private static Dictionary<string, IResolver> Resolvers;\n\n        static SystemVariableV1Provider()\n        {\n            Resolvers = new()\n            {\n                { VariableNameContants.DateTime, new DateTimeVariableResolver() },\n                { VariableNameContants.LocalDateTime, new LocalDateTimeVariableResolver() },\n                { VariableNameContants.Guid, new GuidVariableResolver() },\n            };\n        }\n\n        public string Resolve(string varBlock)\n        {\n            var varName = varBlock.Split(separators, 2).First().Substring(2);\n\n            IResolver? resolver;\n            if (!Resolvers.TryGetValue(varName, out resolver))\n            {\n                return varBlock;\n            }\n\n            return resolver.Resolve(varBlock);\n        }\n\n        /// <summary>\n        /// Add variable resolver\n        /// </summary>\n        /// <param name=\"key\"></param>\n        /// <param name=\"resolver\"></param>\n        /// <returns>false if not added</returns>\n        public static bool AddResolver(string key, IResolver resolver)\n        {\n            // don't allow overwrite exiting value\n            return Resolvers.TryAdd(key, resolver);\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/Resolvers/IResolver.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Jobs.Abstractions.Resolvers\n{\n    public interface IResolver\n    {\n        string Resolve(string varBlock);\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/Resolvers/V1/DateTimeVariableResolver.cs",
    "content": "﻿using System;\nusing System.Text.RegularExpressions;\n\nnamespace BlazingQuartz.Jobs.Abstractions.Resolvers.V1\n{\n    internal class DateTimeVariableResolver : IResolver\n    {\n        const string DatetimeRegex =\n            $\"\\\\{VariableNameContants.DateTime}\\\\s(rfc1123|iso8601|\\'.+\\'|\\\\\\\".+\\\\\\\")(?:\\\\s(\\\\-?\\\\d+)\\\\s(y|M|d|h|m|s|ms))?\";\n\n        public string Resolve(string varBlock)\n        {\n            var result = Regex.Match(varBlock, GetVariableRegex());\n\n            if (!result.Success || result.Index != 2)\n                throw new FormatException(\n                    $\"Invalid {GetVariableName()} format. Expected format is \"\n                        + \"{{$datetime rfc1123|iso8601|'date format'|\\\"date format\\\" [integer y|M|w|d|h|m|s|ms]}}.\"\n                );\n\n            var dt = GetDateTimeOffset();\n\n            var format = result.Groups[1].Value;\n            var rawOffset = result.Groups[2].Value;\n            if (!string.IsNullOrEmpty(rawOffset))\n            {\n                int offset;\n                if (!int.TryParse(rawOffset, out offset))\n                {\n                    throw new FormatException(\n                        $\"Invalid {GetVariableName()} pattern. Offset '{rawOffset}' should be numeric value.\"\n                    );\n                }\n                var offsetOption = result.Groups[3].Value;\n\n                switch (offsetOption)\n                {\n                    case \"y\":\n                        dt = dt.AddYears(offset);\n                        break;\n                    case \"M\":\n                        dt = dt.AddMonths(offset);\n                        break;\n                    case \"d\":\n                        dt = dt.AddDays(offset);\n                        break;\n                    case \"h\":\n                        dt = dt.AddHours(offset);\n                        break;\n                    case \"m\":\n                        dt = dt.AddMinutes(offset);\n                        break;\n                    case \"s\":\n                        dt = dt.AddSeconds(offset);\n                        break;\n                    case \"ms\":\n                        dt = dt.AddMilliseconds(offset);\n                        break;\n                    default:\n                        throw new FormatException(\n                            $\"Invalid {GetVariableName()} pattern. Need to provide valid offset option.\"\n                        );\n                }\n            }\n\n            switch (format)\n            {\n                case \"rfc1123\":\n                    return dt.ToString(\"r\");\n                case \"iso8601\":\n                    return dt.ToString(\"u\");\n                default:\n                    // value was enclosed in quotes, ignore first and last character\n                    return dt.ToString(format.Substring(1, format.Length - 2));\n            }\n        }\n\n        internal virtual string GetVariableRegex()\n        {\n            return DatetimeRegex;\n        }\n\n        internal virtual string GetVariableName()\n        {\n            return VariableNameContants.DateTime;\n        }\n\n        internal virtual DateTimeOffset GetDateTimeOffset()\n        {\n            return DateTimeOffset.UtcNow;\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/Resolvers/V1/GuidVariableResolver.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Jobs.Abstractions.Resolvers.V1\n{\n    internal class GuidVariableResolver : IResolver\n    {\n        const string Format = $\"{{{{{VariableNameContants.Guid}}}}}\";\n\n        public GuidVariableResolver() { }\n\n        public string Resolve(string varBlock)\n        {\n            if (varBlock != Format)\n                throw new FormatException(\n                    $\"Invalid {VariableNameContants.Guid} format. Expected format is {Format}.\"\n                );\n            return Guid.NewGuid().ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/Resolvers/V1/LocalDateTimeVariableResolver.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Jobs.Abstractions.Resolvers.V1\n{\n    internal class LocalDateTimeVariableResolver : DateTimeVariableResolver\n    {\n        const string DatetimeRegex =\n            $\"\\\\{VariableNameContants.LocalDateTime}\\\\s(rfc1123|iso8601|\\'.+\\'|\\\\\\\".+\\\\\\\")(?:\\\\s(\\\\-?\\\\d+)\\\\s(y|M|d|h|m|s|ms))?\";\n\n        internal override string GetVariableName()\n        {\n            return VariableNameContants.LocalDateTime;\n        }\n\n        internal override string GetVariableRegex()\n        {\n            return DatetimeRegex;\n        }\n\n        internal override DateTimeOffset GetDateTimeOffset()\n        {\n            return DateTimeOffset.Now;\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/ServiceCollectionExtensions.cs",
    "content": "﻿using System;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\n\nnamespace BlazingQuartz.Jobs.Abstractions\n{\n    public static class ServiceCollectionExtensions\n    {\n        public static IServiceCollection AddBlazingQuartzJobs(this IServiceCollection services)\n        {\n            services.TryAddTransient<IDataMapValueResolver, DataMapValueResolver>();\n\n            return services;\n        }\n    }\n}\n"
  },
  {
    "path": "src/BlazingQuartz.Jobs.Abstractions/VariableNameContants.cs",
    "content": "﻿using System;\n\nnamespace BlazingQuartz.Jobs.Abstractions\n{\n    public static class VariableNameContants\n    {\n        public const string DateTime = \"$datetime\";\n        public const string LocalDateTime = \"$localDatetime\";\n        public const string Guid = \"$guid\";\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/Attributes/AppendHeaderAttribute.cs",
    "content": "﻿using System.Diagnostics;\nusing System.Net.Http.Headers;\nusing WebApiClientCore;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.Attributes;\n\n[DebuggerDisplay(\"{name} = {value}\")]\n[AttributeUsage(\n    AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Parameter,\n    AllowMultiple = true\n)]\npublic class AppendHeaderAttribute(\n    string name,\n    string? value,\n    AppendHeaderType appendHeaderType = AppendHeaderType.AddOrReplace\n) : ApiActionAttribute, IApiParameterAttribute\n{\n    //添加的顺序为：子类、基类、子类函数、子类函数入参\n\n    private readonly string? _aliasName;\n\n    public AppendHeaderAttribute(\n        string aliasName,\n        AppendHeaderType appendHeaderType = AppendHeaderType.AddOrReplace\n    )\n        : this(\"\", null, appendHeaderType)\n    {\n        _aliasName = aliasName;\n    }\n\n    public override Task OnRequestAsync(ApiRequestContext context)\n    {\n        AddByAppendType(context.HttpContext.RequestMessage.Headers, name, value);\n        return Task.CompletedTask;\n    }\n\n    public Task OnRequestAsync(ApiParameterContext context)\n    {\n        var parameterName = _aliasName;\n        if (string.IsNullOrEmpty(parameterName))\n        {\n            parameterName = context.ParameterName;\n        }\n\n        var text = context.ParameterValue?.ToString();\n        if (!string.IsNullOrEmpty(text))\n        {\n            AddByAppendType(context.HttpContext.RequestMessage.Headers, parameterName, text);\n        }\n\n        return Task.CompletedTask;\n    }\n\n    private void AddByAppendType(HttpRequestHeaders headers, string key, string? value)\n    {\n        switch (appendHeaderType)\n        {\n            case AppendHeaderType.Add:\n                headers.TryAddWithoutValidation(key, value);\n                break;\n            case AppendHeaderType.AddIfNotExist:\n                if (!headers.Contains(key))\n                    headers.TryAddWithoutValidation(key, value);\n                break;\n            case AppendHeaderType.AddOrReplace:\n                if (headers.Contains(key))\n                    headers.Remove(key);\n                headers.TryAddWithoutValidation(key, value);\n                break;\n            default:\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/Attributes/AppendHeaderType.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.Attributes;\n\npublic enum AppendHeaderType\n{\n    Add,\n    AddIfNotExist,\n    AddOrReplace,\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/Attributes/LogFilterAttribute.cs",
    "content": "﻿using System.Reflection;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing WebApiClientCore;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.Attributes;\n\npublic class LogFilterAttribute(bool logError = true) : LoggingFilterAttribute\n{\n    protected override Task WriteLogAsync(ApiResponseContext context, LogMessage logMessage)\n    {\n        var loggerFactory = context.HttpContext.ServiceProvider.GetService<ILoggerFactory>();\n        if (loggerFactory == null)\n        {\n            return Task.CompletedTask;\n        }\n\n        MethodInfo member = context.ActionDescriptor.Member;\n        var strArray = new string?[5];\n        var declaringType1 = member.DeclaringType;\n        strArray[0] = declaringType1?.Namespace;\n        strArray[1] = \".\";\n        var declaringType2 = member.DeclaringType;\n        strArray[2] = declaringType2?.Name;\n        strArray[3] = \".\";\n        strArray[4] = member.Name;\n        string categoryName = string.Concat(strArray);\n        ILogger logger = loggerFactory.CreateLogger(categoryName);\n\n        if (logMessage.Exception == null)\n        {\n            logger.LogDebug(logMessage.ToString());\n        }\n        else\n        {\n            if (logError)\n                logger.LogError(logMessage.ToString());\n            else\n                logger.LogDebug(logMessage.ToString());\n        }\n\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Attributes/WbiParameterAttribute.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Services;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\nusing WebApiClientCore;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Attributes;\n\n[AttributeUsage(AttributeTargets.Parameter)]\npublic class WbiParameterAttribute : Attribute, IApiParameterAttribute\n{\n    public async Task OnRequestAsync(ApiParameterContext context)\n    {\n        // 只处理实现了IWrid接口的参数\n        if (context.ParameterValue is IWrid wridRequest)\n        {\n            // 从依赖注入获取WbiService\n            var wbiService = context.HttpContext.ServiceProvider.GetRequiredService<IWbiService>();\n\n            // 从函数参数中获取Cookie\n            var cookieStr = string.Empty;\n            var allParameters = context.ActionDescriptor.Parameters;\n            foreach (var parameter in allParameters)\n            {\n                var cookieHeader = parameter.Attributes.FirstOrDefault(a =>\n                    a is HeaderAttribute header\n                    && (string)header.GetFieldValue(\"aliasName\") == \"Cookie\"\n                );\n                if (cookieHeader != null)\n                {\n                    cookieStr = context.Arguments[parameter.Index]?.ToString();\n                    break;\n                }\n            }\n\n            var cookie = CookieStrFactory<BiliCookie>.CreateNew(cookieStr ?? \"\");\n\n            // 设置w_rid和wts值\n            await wbiService.SetWridAsync(wridRequest, cookie);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/AddCoinRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class AddCoinRequest\n{\n    public AddCoinRequest(long aid, string csrf)\n    {\n        Aid = aid;\n        Csrf = csrf;\n    }\n\n    public long Aid { get; set; }\n\n    public int Multiply { get; set; } = 1;\n\n    public int Select_like { get; set; } = 1;\n\n    public string Cross_domain { get; set; } = \"true\";\n\n    public string Csrf { get; set; }\n\n    public string Eab_x { get; set; } = \"2\";\n\n    public string Ramval { get; set; } = \"3\";\n\n    public string Source { get; set; } = \"web_normal\";\n\n    public string Ga { get; set; } = \"1\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/AddCoinForArticleRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Article;\n\npublic class AddCoinForArticleRequest\n{\n    public AddCoinForArticleRequest(long cvid, long mid, string csrf)\n    {\n        Aid = cvid;\n        Upid = mid;\n        Csrf = csrf;\n    }\n\n    public long Aid { get; set; }\n\n    public long Upid { get; set; }\n\n    public int Multiply { get; set; } = 1;\n\n    // 必须为2\n    public int Avtype { get; private set; } = 2;\n\n    public string Csrf { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/SearchArticleInfoResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Article;\n\npublic class SearchArticleInfoResponse\n{\n    public int Like { get; set; }\n\n    public int Coin { get; set; }\n\n    public long Mid { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/SearchArticlesByUpIdFullFto.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Services;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Article;\n\npublic class SearchArticlesByUpIdDto : IWrid\n{\n    public long mid { get; set; }\n\n    public int pn { get; set; } = 1;\n\n    public int ps { get; set; } = 12;\n\n    public string sort { get; set; } = \"publish_time\";\n\n    public long web_location { get; set; } = 1550101;\n\n    public string platform { get; set; } = \"web\";\n\n    public string? w_rid { get; set; }\n    public long wts { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Article/SearchUpArticlesResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Article;\n\npublic class SearchUpArticlesResponse\n{\n    public List<ArticleInfo> Articles { get; set; } = [];\n    public int Count { get; set; }\n}\n\npublic class ArticleInfo\n{\n    public long Id { get; set; }\n\n    public required string Title { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/BaseAppRequest.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class BaseAppRequest\n{\n    // public string access_key { get; set; }\n    // public string appkey { get; set; }\n\n    public string build { get; } = \"8451100\";\n\n    public int disable_rcmd { get; } = 0;\n\n    public string mobi_app { get; } = \"android\";\n\n    public string platform { get; } = \"android\";\n\n    public string statistics { get; } =\n        \"{\\\"appId\\\":1,\\\"platform\\\":3,\\\"version\\\":\\\"8.45.1\\\",\\\"abtest\\\":\\\"\\\"}\";\n\n    /// <summary>\n    /// 当前时间（毫秒）\n    /// </summary>\n    /// <sample>1748445354567</sample>\n    public long t { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();\n\n    /// <summary>\n    /// 当前时间（秒）\n    /// </summary>\n    /// <sample>1748445354</sample>\n    public long ts => t / 1000;\n\n    public string? sign { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/BiliApiResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class BiliApiResponse\n{\n    public int Code { get; set; } = int.MinValue;\n\n    public string? Message { get; set; }\n}\n\npublic class BiliApiResponse<TData> : BiliApiResponse\n{\n    public required TData Data { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/BiliPageResult.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class BiliPageResult\n{\n    /// <summary>\n    /// 视频总数量\n    /// </summary>\n    public int Count { get; set; }\n\n    /// <summary>\n    /// 页码\n    /// </summary>\n    public int Pn { get; set; }\n\n    /// <summary>\n    /// 每页条数\n    /// </summary>\n    public int Ps { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ChargeCommentRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class ChargeCommentRequest\n{\n    public ChargeCommentRequest(string order_id, string message, string csrf)\n    {\n        Order_id = order_id;\n        Message = message;\n        Csrf = csrf;\n    }\n\n    public string Order_id { get; set; }\n\n    public string Message { get; set; }\n\n    public string Csrf { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ChargeRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class ChargeRequest\n{\n    public ChargeRequest(decimal bp_num, long upId, string csrf)\n    {\n        Bp_num = bp_num;\n        Up_mid = upId;\n        Oid = upId;\n        Csrf = csrf;\n    }\n\n    /// <summary>\n    /// B币个数\n    /// </summary>\n    public decimal Bp_num { get; set; }\n\n    public string Is_bp_remains_prior { get; set; } = \"true\";\n\n    /// <summary>\n    /// 对方Id\n    /// </summary>\n    public long Up_mid { get; set; }\n\n    /// <summary>\n    ///\n    /// </summary>\n    public string Otype { get; set; } = \"up\";\n\n    /// <summary>\n    /// 对方来源代码(空间充电：充电对象用户UID;视频充电：稿件avID)\n    /// </summary>\n    public long Oid { get; set; }\n\n    /// <summary>\n    /// 自己的bili_jct\n    /// </summary>\n    public string Csrf { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ChargeResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class ChargeResponse\n{\n    public int Status { get; set; }\n\n    public required string Order_no { get; set; }\n}\n\npublic class ChargeV2Response\n{\n    public required string Bp_num { get; set; }\n\n    public decimal Exp { get; set; }\n\n    public long Mid { get; set; }\n\n    public string? Msg { get; set; }\n\n    public required string Order_no { get; set; }\n\n    public int Status { get; set; }\n\n    public long Up_mid { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/CoinBalance.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\n/// <summary>\n/// 硬币余额\n/// </summary>\npublic class CoinBalance\n{\n    public decimal? Money { get; set; } = int.MinValue;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/DailyTaskInfo.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class DailyTaskInfo\n{\n    public bool Login { get; set; }\n\n    public bool Watch { get; set; }\n\n    public long Coins { get; set; }\n\n    public bool Share { get; set; }\n\n    public bool Email { get; set; }\n\n    public bool Tel { get; set; }\n\n    public bool Safe_question { get; set; }\n\n    public bool Identify_card { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/DonatedCoinsForVideo.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class DonatedCoinsForVideo\n{\n    public int Multiply { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetAlreadyDonatedCoinsRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class GetAlreadyDonatedCoinsRequest\n{\n    public GetAlreadyDonatedCoinsRequest(long aid)\n    {\n        Aid = aid;\n    }\n\n    public string Jsonp { get; set; } = \"jsonp\";\n\n    public long Aid { get; set; }\n\n    //public string Callback { get; set; } = $\"jsonCallback_bili_{new Random().Next(10000, 99999)}{new Random().Next(10000, 99999)}\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetSpaceInfoFullDto.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Services;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class GetSpaceInfoDto : IWrid\n{\n    public long mid { get; set; }\n\n    public string? w_rid { get; set; }\n    public long wts { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetSpaceInfoResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class GetSpaceInfoResponse\n{\n    public long Mid { get; set; }\n\n    public required string Name { get; set; }\n\n    public required SpaceLiveRoomInfoDto Live_room { get; set; }\n}\n\npublic class SpaceLiveRoomInfoDto\n{\n    public required string Title { get; set; }\n\n    public long Roomid { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetSpecialFollowingsRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class GetSpecialFollowingsRequest\n{\n    public GetSpecialFollowingsRequest(long userId)\n    {\n        Mid = userId;\n    }\n\n    public GetSpecialFollowingsRequest(long userId, long tagId)\n    {\n        Mid = userId;\n        Tagid = tagId;\n    }\n\n    public long Mid { get; set; }\n\n    /// <summary>\n    /// TagId\n    /// </summary>\n    /// <sample>-10:特别关注</sample>\n    public long Tagid { get; set; } = -10;\n\n    public int Pn { get; set; } = 1;\n\n    public int Ps { get; set; } = 20;\n\n    public string Jsonp { get; set; } = \"jsonp\";\n\n    //public string Callback { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/GetVideosResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class GetVideosResponse\n{\n    public List<VideoInfo> Media_list { get; set; } = [];\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/AreaDto.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class GetArteaListResponse\n{\n    public List<AreaDto> Data { get; set; } = [];\n}\n\npublic class AreaDto\n{\n    public long Id { get; set; }\n\n    public required string Name { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/CheckTianXuanDto.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class CheckTianXuanDto\n{\n    public long Id { get; set; }\n\n    public long Room_id { get; set; }\n\n    /// <summary>\n    /// 状态\n    /// </summary>\n    public TianXuanStatus Status { get; set; }\n\n    /// <summary>\n    /// 奖励名称\n    /// </summary>\n    public required string Award_name { get; set; }\n\n    /// <summary>\n    /// 奖励数量\n    /// </summary>\n    public int Award_num { get; set; }\n\n    /// <summary>\n    /// 弹幕内容\n    /// </summary>\n    public string? Danmu { get; set; }\n\n    public int Join_type { get; set; }\n\n    /// <summary>\n    /// 要求条件类型\n    /// </summary>\n    public RequireType? Require_type { get; set; }\n\n    /// <summary>\n    /// 要求值\n    /// </summary>\n    public int Require_value { get; set; }\n\n    /// <summary>\n    /// 要求名称\n    /// </summary>\n    public string? Require_text { get; set; }\n\n    #region 礼物\n    public long Gift_id { get; set; }\n\n    public string? Gift_name { get; set; }\n\n    public int Gift_num { get; set; }\n\n    public int Gift_price { get; set; }\n\n    public int Cur_gift_num { get; set; }\n\n    public string GiftDesc => $\"价值{Gift_price}的{Gift_name}{Gift_num}个\";\n    #endregion\n\n    public int Send_gift_ensure { get; set; }\n\n    public bool AwardNameIsSatisfied(List<string> includeKeys, List<string> excludeKeys)\n    {\n        //只要包含了排除的关键字，就排除\n        if (excludeKeys.Any())\n        {\n            foreach (var item in excludeKeys)\n            {\n                if (Award_name.Contains(item))\n                    return false;\n            }\n        }\n\n        //遍历所有包含关键字，包含其一就确认，否则保持排除\n        bool isInclude = true;\n        if (includeKeys.Any())\n        {\n            isInclude = false;\n            foreach (var item in includeKeys)\n            {\n                if (Award_name.Contains(item))\n                {\n                    isInclude = true;\n                    break;\n                }\n            }\n        }\n\n        return isInclude;\n    }\n}\n\n/// <summary>\n/// 天选抽奖状态\n/// </summary>\npublic enum TianXuanStatus\n{\n    /// <summary>\n    /// 可参与抽奖\n    /// </summary>\n    Enable = 1,\n\n    /// <summary>\n    /// 已结束\n    /// </summary>\n    End = 2,\n}\n\n/// <summary>\n/// 天选抽奖条件\n/// </summary>\npublic enum RequireType\n{\n    /// <summary>\n    /// 无\n    /// </summary>\n    None = 0,\n\n    /// <summary>\n    /// 关注主播\n    /// </summary>\n    Follow = 1,\n\n    /// <summary>\n    /// 粉丝勋章级数要求\n    /// </summary>\n    FansLevel = 2,\n\n    /// <summary>\n    /// 至少成为提督舰长等\n    /// </summary>\n    TiDuOrJianZhang = 3,\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/EnterRoomRequest.cs",
    "content": "﻿using Newtonsoft.Json;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class EnterRoomRequest\n{\n    public EnterRoomRequest(\n        long roomId,\n        long parentId,\n        long areaID,\n        int seqNumber, // 心跳包编号\n        long timestamp,\n        string userAgent,\n        string csrf,\n        long ruid,\n        string device\n    )\n    {\n        Id = JsonConvert.SerializeObject(new[] { parentId, areaID, seqNumber, roomId });\n        Ts = timestamp;\n        Ua = userAgent;\n        Csrf = csrf;\n        Ruid = ruid;\n\n        Is_patch = 0;\n        Heart_beat = \"[]\";\n        Visit_id = \"\";\n        Device = device;\n    }\n\n    public string Id { get; set; }\n\n    public long Ruid { get; set; }\n\n    public long Ts { get; set; }\n\n    public int Is_patch { get; set; }\n\n    public string Heart_beat { get; set; }\n\n    public string Ua { get; set; }\n\n    public string Csrf_token => Csrf;\n\n    public string Csrf { get; set; }\n\n    public string Visit_id { get; set; }\n\n    public string Device { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/ExchangeSilverStatusResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class ExchangeSilverStatusResponse\n{\n    public int Silver { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/GetListRequest.cs",
    "content": "using Ray.BiliBiliTool.Agent.BiliBiliAgent.Services;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class GetListRequest : IWrid\n{\n    public string platform { get; set; } = \"web\";\n    public long parent_area_id { get; set; }\n    public long area_id { get; set; }\n    public string? sort_type { get; set; }\n    public int page { get; set; }\n    public long wts { get; set; }\n    public string? w_rid { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/GetLiveRoomInfoResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class GetLiveRoomInfoResponse\n{\n    public long Room_id { get; set; }\n\n    public long Area_id { get; set; }\n\n    public long Parent_area_id { get; set; }\n\n    public int Live_Status { get; set; }\n\n    public long Uid { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/HeartBeatRequest.cs",
    "content": "﻿using Newtonsoft.Json;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Utils;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class HeartBeatRequest\n{\n    public HeartBeatRequest(\n        long roomId,\n        long parentId,\n        long areaID,\n        int seqNumber, // 心跳包编号\n        string buvid, // cookie['LIVE_BUVID']\n        long timestamp,\n        long ets, // 由后端返回的 timestamp\n        string userAgent,\n        ICollection<int> secretRule,\n        string secretKey,\n        string csrf,\n        string uuid,\n        string device\n    )\n    {\n        Id = JsonConvert.SerializeObject(new[] { parentId, areaID, seqNumber, roomId });\n        Ets = ets;\n        Benchmark = secretKey;\n        Time = 60;\n        Ts = timestamp;\n        Ua = userAgent;\n        Csrf = csrf;\n        Device = device;\n\n        // 构造哈希值\n        var json = new\n        {\n            platform = \"web\",\n            parent_id = parentId,\n            area_id = areaID,\n            seq_id = seqNumber,\n            room_id = roomId,\n            buvid,\n            uuid,\n            ets,\n            time = 60,\n            ts = timestamp,\n        };\n        string jsonString = JsonConvert.SerializeObject(json);\n        S = LiveHeartBeatCrypto.Sypder(jsonString, secretRule, secretKey);\n\n        Visit_id = \"\";\n    }\n\n    public string S { get; set; }\n\n    public string Id { get; set; }\n\n    public long Ets { get; set; }\n\n    public string Benchmark { get; set; }\n\n    public long Time { get; set; }\n\n    public long Ts { get; set; }\n\n    public string Ua { get; set; }\n\n    public string Csrf_token => Csrf;\n\n    public string Csrf { get; set; }\n\n    public string Visit_id { get; set; }\n\n    public string Device { get; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/HeartBeatResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class HeartBeatResponse\n{\n    public int Heartbeat_interval { get; set; }\n\n    public string? Secret_key { get; set; }\n\n    public List<int> Secret_rule { get; set; } = [];\n\n    public long Timestamp { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/JoinTianXuanRequest.cs",
    "content": "﻿using Ray.BiliBiliTool.Infrastructure.Helpers;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class JoinTianXuanRequest\n{\n    /// <summary>\n    /// Id（从Check接口获取）\n    /// </summary>\n    public long Id { get; set; }\n\n    /// <summary>\n    /// 礼物Id（从Check接口获取）\n    /// </summary>\n    public long Gift_id { get; set; }\n\n    /// <summary>\n    /// 礼物数量（从Check接口获取）\n    /// </summary>\n    public int Gift_num { get; set; }\n\n    /// <summary>\n    /// bili_jct（取自Cookie）\n    /// </summary>\n    public required string Csrf { get; set; }\n\n    public string Csrf_token => Csrf;\n\n    /// <summary>\n    ///\n    /// </summary>\n    /// <sample>8u0w3cesz1o0</sample>\n    /// <sample>33moy4vugle0</sample>\n    /// <sample>9zys612vo0c0</sample>\n    /// <sample>3uu2mkxt21c0</sample>\n    /// <sample>8orqn5vf4i00</sample>\n    public string Visit_id { get; set; } = _visitId; //todo\n\n    public string Platform { get; set; } = \"pc\";\n\n    public static string GetRandomVisitId()\n    {\n        var ran = new Random();\n        int first = ran.Next(1, 10);\n        int last = 0;\n\n        var s = new RandomHelper().GenerateCode(10).ToLower();\n\n        return $\"{first}{s}{last}\";\n    }\n\n    private static string _visitId = GetRandomVisitId();\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/JoinTianXuanResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class JoinTianXuanResponse\n{\n    public long Discount_id { get; set; }\n\n    public long Gold { get; set; }\n\n    public long Silver { get; set; }\n\n    public long Cur_gift_num { get; set; }\n\n    public long Goods_id { get; set; }\n\n    public required string New_order_id { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/LikeLiveRoomRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class LikeLiveRoomRequest\n{\n    public LikeLiveRoomRequest(long roomid, string csrf, int clickTime, long anchorId, string uid)\n    {\n        Roomid = roomid;\n        Csrf = csrf;\n        Click_Time = clickTime;\n        Anchor_Id = anchorId;\n        Uid = uid;\n    }\n\n    public long Roomid { get; set; }\n\n    public string Csrf { get; set; }\n\n    public string Csrf_Token => Csrf;\n\n    public int Click_Time { get; set; }\n\n    public long Anchor_Id { get; set; }\n\n    public string Uid { get; set; }\n\n    public string RawTextBuild()\n    {\n        return $\"click_time={Click_Time.ToString()}&room_id={Roomid.ToString()}&uid={Uid}&anchor_id={Anchor_Id}&csrf_token={Csrf_Token}&csrf={Csrf}\";\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/ListItemDto.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class GetListResponse\n{\n    public List<LiveSortTag> New_tags { get; set; } = [];\n\n    public List<ListItemDto> List { get; set; } = [];\n\n    public int Has_more { get; set; }\n}\n\npublic class LiveSortTag\n{\n    public long Id { get; set; }\n\n    public required string Name { get; set; }\n\n    public string? Sort_type { get; set; }\n}\n\npublic class ListItemDto\n{\n    public long Roomid { get; set; }\n\n    public long Uid { get; set; }\n\n    public required string Title { get; set; }\n\n    public string ShortTitle\n    {\n        get\n        {\n            if (string.IsNullOrWhiteSpace(Title) || Title.Length <= 10)\n                return Title;\n\n            return Title.Substring(0, 7) + \"...\";\n        }\n    }\n\n    public required string Uname { get; set; }\n\n    public long Parent_id { get; set; }\n\n    public required string Parent_name { get; set; }\n\n    public long Area_id { get; set; }\n\n    public string? Area_name { get; set; }\n\n    /// <summary>\n    ///\n    /// </summary>\n    /// <sample>1：百人成就</sample>\n    /// <sample>2：天选时刻、新星主播</sample>\n    public Dictionary<string, PendantInfo>? Pendant_info { get; set; }\n}\n\npublic class PendantInfo\n{\n    /// <summary>\n    /// Id\n    /// </summary>\n    /// <sample>504：天选</sample>\n    /// <sample>426：百人成就</sample>\n    /// <sample>397：新星主播</sample>\n    public long Pendent_id { get; set; }\n\n    /// <summary>\n    /// 内容\n    /// </summary>\n    /// <sample>天选时刻</sample>\n    public string? Content { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/LiveSignResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class LiveSignResponse\n{\n    public string? Text { get; set; }\n\n    public string? SpecialText { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/LiveWalletStatusResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class LiveWalletStatusResponse\n{\n    /// <summary>\n    /// 硬币余额\n    /// </summary>\n    public decimal Coin { get; set; }\n\n    /// <summary>\n    /// 金瓜子余额\n    /// </summary>\n    public decimal Gold { get; set; }\n\n    /// <summary>\n    /// 银瓜子余额\n    /// </summary>\n    public decimal Silver { get; set; }\n\n    /// <summary>\n    /// 银瓜子兑换硬币剩余次数\n    /// </summary>\n    public int Silver_2_coin_left { get; set; }\n\n    /// <summary>\n    /// 硬币兑换银瓜子剩余次数\n    /// </summary>\n    public int Coin_2_silver_left { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/MedalWallDto.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class MedalWallResponse\n{\n    public List<MedalWallDto> List { get; set; } = [];\n}\n\npublic class MedalWallDto\n{\n    public int Live_status { get; set; }\n\n    public required string Target_name { get; set; }\n\n    public required string Link { get; set; }\n\n    public required MedalInfoDto Medal_info { get; set; }\n}\n\npublic class MedalInfoDto\n{\n    public required string Medal_name { get; set; }\n\n    public long Medal_id { get; set; }\n\n    public long Target_id { get; set; }\n\n    public int Level { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/SendLiveDanmukuRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class SendLiveDanmukuRequest\n{\n    public SendLiveDanmukuRequest(string csrf, long room_id, string message)\n    {\n        this.Csrf = csrf;\n        this.Msg = message;\n        this.Roomid = room_id;\n        this.Bubble = \"0\";\n        this.Mode = \"1\";\n        this.Fontsize = \"25\";\n        this.Rnd = \"1672305761\";\n        this.Color = \"16777215\";\n    }\n\n    public string Bubble { get; set; }\n\n    public string Msg { get; set; }\n\n    public string Color { get; set; }\n\n    public string Mode { get; set; }\n\n    public string Fontsize { get; set; }\n\n    public string Rnd { get; set; }\n\n    public long Roomid { get; set; }\n\n    public string Csrf { get; set; }\n\n    public string Csrf_token => Csrf;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/Silver2CoinRequest.cs",
    "content": "﻿using Ray.BiliBiliTool.Infrastructure.Helpers;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class Silver2CoinRequest\n{\n    public Silver2CoinRequest(string csrf)\n    {\n        Csrf = csrf;\n    }\n\n    public string Csrf { get; set; }\n\n    public string Csrf_token => Csrf;\n\n    /// <summary>\n    ///\n    /// </summary>\n    /// <sample>8u0w3cesz1o0</sample>\n    /// <sample>33moy4vugle0</sample>\n    /// <sample>9zys612vo0c0</sample>\n    /// <sample>3uu2mkxt21c0</sample>\n    /// <sample>8orqn5vf4i00</sample>\n    public string Visit_id { get; set; } = _visitId; //todo\n\n    public static string GetRandomVisitId()\n    {\n        var ran = new Random();\n        int first = ran.Next(1, 10);\n        int last = 0;\n\n        var s = new RandomHelper().GenerateCode(10).ToLower();\n\n        return $\"{first}{s}{last}\";\n    }\n\n    private static string _visitId = GetRandomVisitId();\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/Silver2CoinResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class Silver2CoinResponse\n{\n    public long Coin { get; set; }\n\n    public long Gold { get; set; }\n\n    public long Silver { get; set; }\n\n    public string? Tid { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/WearMedalWallRequest.cs",
    "content": "﻿using Ray.BiliBiliTool.Infrastructure.Helpers;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class WearMedalWallRequest\n{\n    public WearMedalWallRequest(string csrf, int medal_id)\n    {\n        Csrf = csrf;\n        Medal_id = medal_id;\n    }\n\n    public int Medal_id { get; set; }\n\n    public string Csrf { get; set; }\n\n    public string Csrf_token => Csrf;\n\n    /// <summary>\n    ///\n    /// </summary>\n    /// <sample>8u0w3cesz1o0</sample>\n    /// <sample>33moy4vugle0</sample>\n    /// <sample>9zys612vo0c0</sample>\n    /// <sample>3uu2mkxt21c0</sample>\n    /// <sample>8orqn5vf4i00</sample>\n    public string Visit_id { get; set; } = _visitId; //todo\n\n    public static string GetRandomVisitId()\n    {\n        var ran = new Random();\n        int first = ran.Next(1, 10);\n        int last = 0;\n\n        var s = new RandomHelper().GenerateCode(10).ToLower();\n\n        return $\"{first}{s}{last}\";\n    }\n\n    private static string _visitId = GetRandomVisitId();\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/WebHeartBeatRequest.cs",
    "content": "﻿using System.Text;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class WebHeartBeatRequest\n{\n    public WebHeartBeatRequest(int room_id, int next_interval)\n    {\n        this.RoomId = room_id;\n        this.NextInterval = next_interval;\n    }\n\n    public long RoomId { set; get; }\n\n    public int NextInterval { set; get; }\n\n    public override string ToString()\n    {\n        string arg = $\"{this.NextInterval}|{this.RoomId}|1|0\";\n        return Convert.ToBase64String(Encoding.UTF8.GetBytes(arg));\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Live/WebHeartBeatResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\npublic class WebHeartBeatResponse\n{\n    public int Next_interval { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/GetCombineRequest.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall;\n\npublic class GetCombineRequest : BaseAppRequest\n{\n    public required string buvid { get; set; }\n    public required string csrf { get; set; }\n\n    public string brand { get; set; } = \"Samsung\";\n    public string channel { get; set; } = \"bili\";\n    public string containerName { get; set; } = \"AbstractWebActivity\";\n    public string device { get; set; } = \"phone\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/PointInfo.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall;\n\npublic record PointInfo(int point, int expire_point, int expire_time, int expire_days);\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/Sign2Request.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall;\n\npublic class Sign2Request\n{\n    public string device { get; set; } = \"phone\";\n\n    /// <summary>\n    /// 当前时间（毫秒）\n    /// </summary>\n    /// <sample>1748445354567</sample>\n    public long t { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();\n\n    /// <summary>\n    /// 当前时间（秒）\n    /// </summary>\n    /// <sample>1748445354</sample>\n    public long ts => t / 1000;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/Sign2RequestPath.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall;\n\npublic class Sign2RequestPath(string csrf)\n{\n    public string mobi_app { get; set; } = \"android\";\n\n    public string csrf { get; set; } = csrf;\n\n    public string platform { get; set; } = \"android\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/Sign2Response.cs",
    "content": "using System.Text;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall;\n\npublic class Sign2Response\n{\n    public int count { get; set; }\n    public int countdown { get; set; }\n    public int duration { get; set; }\n    public bool hasCoupon { get; set; }\n    public int score { get; set; }\n    public int vipScore { get; set; }\n    public int vipStatus { get; set; }\n\n    public override string ToString()\n    {\n        var sb = new StringBuilder();\n        sb.AppendLine($\"获得经验：{score}\");\n        sb.AppendLine($\"累计签到：{count}/{duration} 天\");\n\n        return sb.ToString();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/TaskInfo.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall;\n\npublic class TaskInfo\n{\n    public int Score_month { get; set; }\n\n    public int Score_limit { get; set; }\n\n    public List<ModuleItem> Modules { get; set; } = [];\n\n    [Obsolete(\n        \"The sign result comes from combine API is not correct, use IVipBigPointApi.GetThreeDaySignAsync instead.\"\n    )]\n    public required SingTaskItem Sing_task_item { get; set; }\n}\n\npublic class SingTaskItem\n{\n    public int Count { get; set; }\n\n    public int Base_score { get; set; }\n\n    public List<Histtory> Histories { get; set; } = [];\n\n    public Histtory? TodayHistory => Histories.FirstOrDefault(x => x.Is_today);\n\n    public bool IsTodaySigned => TodayHistory?.Signed == true;\n}\n\npublic class ModuleItem\n{\n    public required string module_title { get; set; }\n\n    public List<CommonTaskItem> common_task_item { get; set; } = [];\n}\n\npublic class CommonTaskItem\n{\n    public required string title { get; set; }\n\n    public string? subtitle { get; set; }\n\n    public string? explain { get; set; }\n\n    public required string task_code { get; set; }\n\n    public int state { get; set; }\n\n    public int vip_limit { get; set; }\n\n    public int complete_times { get; set; }\n\n    public int max_times { get; set; }\n\n    public int recall_num { get; set; }\n}\n\npublic class Histtory\n{\n    public DateTime Day { get; set; }\n\n    public bool Signed { get; set; }\n\n    public int Score { get; set; }\n\n    public bool Is_today { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Mall/VipBigPointCombine.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall;\n\npublic class VipBigPointCombine\n{\n    public required PointInfo point_info { get; set; }\n    public required TaskInfo Task_info { get; set; }\n\n    public void LogFullInfo(ILogger logger)\n    {\n        logger.LogInformation(\"当前经验：{point}\", point_info.point);\n        // logger.LogInformation(\"打卡：{signed}\", Task_info.Sing_task_item.IsTodaySigned ? \"√\" : \"X\");\n        foreach (var moduleItem in Task_info.Modules)\n        {\n            logger.LogInformation(\"-{title}\", moduleItem.module_title);\n            foreach (var commonTaskItem in moduleItem.common_task_item)\n            {\n                logger.LogInformation(\n                    \"---{title}：{status}\",\n                    commonTaskItem.title,\n                    commonTaskItem.state == 3 ? \"√\" : \"X\"\n                );\n            }\n        }\n    }\n\n    public void LogPointInfo(ILogger logger)\n    {\n        logger.LogInformation(\"当前经验：{point}\", point_info.point);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/MangaVipRewardResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class MangaVipRewardResponse\n{\n    public int Amount { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Passport/GetSsoListResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Passport;\n\npublic class GetSsoListResponse\n{\n    public List<string> sso { get; set; } = [];\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Passport/QrCodeDto.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Passport;\n\npublic class QrCodeDto\n{\n    public required string Qrcode_key { get; set; }\n\n    public required string Url { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Passport/TokenDto.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Passport;\n\npublic class TokenDto\n{\n    public required string Url { get; set; }\n    public required string Refresh_token { get; set; }\n    public long Timestamp { get; set; }\n    public int Code { get; set; }\n    public string? Message { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/RankingInfo.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class Ranking\n{\n    public List<RankingInfo> List { get; set; } = [];\n}\n\n/// <summary>\n/// 排行榜信息\n/// </summary>\npublic class RankingInfo\n{\n    public long Aid { get; set; }\n\n    public required string Bvid { get; set; }\n\n    public long Cid { get; set; }\n\n    public required string Title { get; set; }\n\n    /// <summary>\n    /// 是否转载\n    /// <sample>1：原创</sample>\n    /// <sample>2：转载</sample>\n    /// </summary>\n    public int Copyright { get; set; }\n\n    public int Duration { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/CopyUserToGroupRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation;\n\npublic class CopyUserToGroupRequest\n{\n    public CopyUserToGroupRequest(List<long> fids, string tagid, string csrf)\n    {\n        Fids = string.Join(\",\", fids);\n        Tagids = tagid;\n        Csrf = csrf;\n    }\n\n    public string Fids { get; set; }\n\n    public string Tagids { get; set; }\n\n    public string Csrf { get; set; }\n\n    public string Jsonp { get; set; } = \"jsonp\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/CreateTagRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation;\n\npublic class CreateTagRequest\n{\n    public required string Tag { get; set; }\n\n    public required string Csrf { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/CreateTagResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation;\n\npublic class CreateTagResponse\n{\n    public long Tagid { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/GetFollowingsRequest.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation;\n\npublic class GetFollowingsRequest\n{\n    public GetFollowingsRequest(\n        long userId,\n        FollowingsOrderType followingsOrder = FollowingsOrderType.AttentionDesc\n    )\n    {\n        Vmid = userId;\n        Order_type = followingsOrder.DefaultValue();\n    }\n\n    public long Vmid { get; set; }\n\n    public string Order_type { get; set; }\n\n    public int Pn { get; set; } = 1;\n\n    public int Ps { get; set; } = 20;\n\n    public string Order { get; set; } = \"desc\";\n\n    public string Jsonp { get; set; } = \"jsonp\";\n\n    //public string Callback { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/GetFollowingsResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation;\n\npublic class GetFollowingsResponse\n{\n    public List<UpInfo> List { get; set; } = [];\n\n    public int Total { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/ModifyRelationRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation;\n\npublic class ModifyRelationRequest\n{\n    public ModifyRelationRequest(long fid, string csrf)\n    {\n        Fid = fid;\n        Csrf = csrf;\n    }\n\n    public long Fid { get; set; }\n\n    public string Csrf { get; set; }\n\n    /// <summary>\n    /// 动作\n    /// </summary>\n    /// <sample>2:取关</sample>\n    public int Act { get; set; } = 2;\n\n    public int Re_src { get; set; } = 11;\n\n    public string Jsonp { get; set; } = \"jsonp\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Relation/TagDto.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation;\n\npublic class TagDto\n{\n    public long Tagid { get; set; }\n\n    public required string Name { get; set; }\n\n    /// <summary>\n    /// 关注up个数\n    /// </summary>\n    public int Count { get; set; }\n\n    public string? Tip { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/SearchUpVideosResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class SearchUpVideosResponse\n{\n    public UpContent? List { get; set; }\n\n    public required BiliPageResult Page { get; set; }\n}\n\npublic class UpContent\n{\n    public List<UpVideoInfo> Vlist { get; set; } = [];\n}\n\npublic class UpVideoInfo\n{\n    public long Aid { get; set; }\n\n    public string? Author { get; set; }\n\n    public required string Bvid { get; set; }\n\n    public required string Title { get; set; }\n\n    /// <summary>\n    /// 视频时长\n    /// <sample>61:05</sample>\n    /// <sample>00:15</sample>\n    /// </summary>\n    public required string Length { get; set; }\n\n    /// <summary>\n    /// 视频时长的秒数\n    /// </summary>\n    public int? Duration\n    {\n        get\n        {\n            int? result = null;\n\n            try\n            {\n                var list = Length.Split(':');\n                var min = int.Parse(list[0]);\n                var sec = int.Parse(list[1]);\n                return min * 60 + sec;\n            }\n            catch (Exception)\n            {\n                //throw;\n            }\n            return result;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ShareVideoRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class ShareVideoRequest\n{\n    public ShareVideoRequest(long aid, string csrf)\n    {\n        Aid = aid;\n        Csrf = csrf;\n    }\n\n    public long Aid { get; set; }\n\n    public string Csrf { get; set; }\n\n    public string Eab_x { get; set; } = \"1\";\n\n    public string Ramval { get; set; } = $\"{new Random().Next(3, 20)}\";\n\n    public string Source { get; set; } = \"web_normal\";\n\n    public string Ga { get; set; } = \"1\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/UpInfo.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class UpInfo\n{\n    public long Mid { get; set; }\n\n    public required string Uname { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/UploadVideoHeartbeatRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class UploadVideoHeartbeatRequest\n{\n    public long Aid { get; set; }\n\n    /// <summary>\n    /// 视频CID，用于识别分P\n    /// </summary>\n    public long? Cid { get; set; }\n\n    public required string Bvid { get; set; }\n\n    public long? Epid { get; set; }\n\n    public long? Sid { get; set; }\n\n    /// <summary>\n    /// 当前用户UID\n    /// </summary>\n    public long Mid { get; set; }\n\n    public required string Csrf { get; set; }\n\n    /// <summary>\n    /// 视频播放进度（即视频进度条的当前秒数），单位为秒，默认为0\n    /// </summary>\n    public int Played_time { get; set; }\n\n    public int Real_played_time { get; set; }\n\n    /// <summary>\n    /// 总计播放时间，单位为秒\n    /// </summary>\n    public int Realtime { get; set; }\n\n    /// <summary>\n    /// 开始播放时刻，时间戳\n    /// </summary>\n    public long Start_ts { get; set; } = DateTime.Now.ToTimeStamp();\n\n    /// <summary>\n    /// 视频类型\n    /// <sample>3：投稿视频</sample>\n    /// <sample>4：剧集</sample>\n    /// <sample>10：课程</sample>\n    /// </summary>\n    public int Type { get; set; } = 3;\n\n    /// <summary>\n    /// 剧集副类型\n    /// </summary>\n    public int? Sub_type { get; set; }\n\n    /// <summary>\n    /// 2\n    /// </summary>\n    public int Dt { get; set; } = 2;\n\n    /// <summary>\n    /// 播放动作\n    /// <sample>0：播放中</sample>\n    /// <sample>1：开始播放</sample>\n    /// <sample>2：暂停</sample>\n    /// <sample>3：继续播放</sample>\n    /// </summary>\n    public int Play_type { get; set; } = 3;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/UserInfo.cs",
    "content": "﻿using System.Text;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\n/// <summary>\n/// 账户信息\n/// </summary>\npublic class UserInfo\n{\n    /// <summary>\n    /// 用户Id\n    /// </summary>\n    public long Mid { get; set; }\n\n    /// <summary>\n    /// 是否登录\n    /// </summary>\n    public bool IsLogin { get; set; }\n\n    /// <summary>\n    /// 等级信息\n    /// </summary>\n    public LevelInfo? Level_info { get; set; }\n\n    public decimal? Money { get; set; }\n\n    public string? Uname { get; set; }\n\n    public Wallet? Wallet { get; set; }\n\n    /// <summary>\n    /// 会员状态\n    /// <para>只有VipStatus为1的时候获取到VipType才是有效的</para>\n    /// </summary>\n    public VipStatus VipStatus { get; set; }\n\n    public VipType VipType { get; set; }\n\n    /// <summary>\n    /// 获取隐私处理后的用户名\n    /// </summary>\n    /// <returns></returns>\n    public string GetFuzzyUname()\n    {\n        if (Uname == null)\n        {\n            return \"\";\n        }\n\n        var sb = new StringBuilder();\n        int s1 = Uname.Length / 2;\n        int s2 = (s1 + 1) / 2;\n        for (int i = 0; i < Uname.Length; i++)\n        {\n            if (i >= s2 && i < s1 + s2)\n                sb.Append(\"x\");\n            else\n                sb.Append(Uname[i]);\n        }\n\n        return sb.ToString();\n    }\n\n    /// <summary>\n    /// 返回会员类型\n    /// </summary>\n    /// <returns>\n    /// <para> 0:无会员（会员过期、当前不是会员）</para>\n    /// <para>1:月会员</para>\n    /// <para>2:年会员</para>\n    /// </returns>\n    public VipType GetVipType()\n    {\n        if (VipStatus == VipStatus.Enable)\n        {\n            //只有VipStatus为1的时候获取到VipType才是有效的。\n            return VipType;\n        }\n        else\n        {\n            return VipType.None;\n        }\n    }\n\n    /// <summary>\n    /// 防爬加密用的\n    /// </summary>\n    public required WbiImg Wbi_img { get; set; }\n}\n\n/// <summary>\n/// 会员等级\n/// </summary>\npublic class LevelInfo\n{\n    /// <summary>\n    /// 当前等级\n    /// </summary>\n    public int Current_level { get; set; }\n\n    //public long Current_min { get; set; }\n\n    /// <summary>\n    /// 当前经验值\n    /// </summary>\n    public long Current_exp { get; set; }\n\n    private long _next_exp;\n\n    /// <summary>\n    /// 下一升级经验值（因为Lv6的带佬会返回字符串“--”，所以这里只能用string接收）\n    /// </summary>\n    public object Next_exp\n    {\n        get { return _next_exp; }\n        set\n        {\n            bool isLong = long.TryParse(value.ToString(), out long exp);\n            if (isLong)\n            {\n                _next_exp = exp;\n            }\n            else\n                _next_exp = long.MinValue;\n        }\n    }\n\n    /// <summary>\n    /// 获取下一升级经验值\n    /// </summary>\n    /// <returns></returns>\n    public long GetNext_expLong()\n    {\n        if (Current_level == 6)\n            return long.MaxValue;\n        else\n            return _next_exp;\n    }\n}\n\n/// <summary>\n/// 钱包\n/// </summary>\npublic class Wallet\n{\n    //public long Mid { get; set; }\n\n    //public int Bcoin_balance { get; set; }\n\n    public decimal Coupon_balance { get; set; }\n\n    //public int Coupon_due_time { get; set; }\n}\n\npublic class WbiImg\n{\n    /// <summary>\n    /// img url\n    /// </summary>\n    /// <sample>https://i0.hdslb.com/bfs/wbi/9cd4224d4fe74c7e9d6963e2ef891688.png</sample>\n    public required string img_url { get; set; }\n\n    /// <summary>\n    /// sub url\n    /// </summary>\n    /// <sample>https://i0.hdslb.com/bfs/wbi/263655ae2cad4cce95c9c401981b044a.png</sample>\n    public required string sub_url { get; set; }\n\n    public string ImgKey => img_url.Split(\"wbi/\").ToList().Last().Replace(\".png\", \"\");\n\n    public string SubKey => sub_url.Split(\"wbi/\").ToList().Last().Replace(\".png\", \"\");\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Video/GetBangumiBySsidResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Video;\n\npublic class GetBangumiBySsidResponse\n{\n    public int Code { get; set; } = int.MinValue;\n\n    public string? Message { get; set; }\n\n    public required Result Result { get; set; }\n}\n\npublic class Result\n{\n    public List<Episode> episodes { get; set; } = [];\n}\n\npublic class Episode\n{\n    public int aid { get; set; }\n\n    public required string bvid { get; set; }\n\n    public int cid { get; set; }\n\n    public int duration { get; set; }\n\n    public int ep_id { get; set; }\n\n    public int id { get; set; }\n\n    public required string long_title { get; set; }\n\n    public required string share_copy { get; set; }\n\n    public int status { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/Video/SearchVideosByUpIdFullDto.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Services;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Video;\n\npublic class SearchVideosByUpIdDto : IWrid\n{\n    /// <summary>\n    /// upId\n    /// </summary>\n    public long mid { get; set; }\n\n    /// <summary>\n    /// pageSize\n    /// </summary>\n    public int ps { get; set; } = 30;\n\n    /// <summary>\n    /// pageNumber\n    /// </summary>\n    public int pn { get; set; } = 1;\n\n    public int tid { get; set; } = 0;\n\n    public string keyword { get; set; } = \"\";\n\n    public string order { get; set; } = \"pubdate\";\n\n    public string platform { get; set; } = \"web\";\n\n    public int web_location { get; set; } = 1550101;\n\n    public string order_avoided { get; set; } = \"true\";\n\n    public string? w_rid { get; set; }\n\n    public long wts { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VideoDetail.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class VideoDetail\n{\n    public required string Bvid { get; set; }\n\n    public long Aid { get; set; }\n\n    /// <summary>\n    /// 稿件分P总数\n    /// </summary>\n    public int Videos { get; set; }\n\n    /// <summary>\n    /// 子分区名称\n    /// </summary>\n    public string? Tname { get; set; }\n\n    /// <summary>\n    /// 是否转载\n    /// <sample>1：原创</sample>\n    /// <sample>2：转载</sample>\n    /// </summary>\n    public int Copyright { get; set; }\n\n    /// <summary>\n    /// 稿件封面图片url\n    /// </summary>\n    public string? Pic { get; set; }\n\n    /// <summary>\n    /// 稿件标题\n    /// </summary>\n    public required string Title { get; set; }\n\n    /// <summary>\n    /// 稿件发布时间(时间戳)\n    /// </summary>\n    public long Pubdate { get; set; }\n\n    /// <summary>\n    /// 用户提交稿件的时间(时间戳)\n    /// </summary>\n    public long Ctime { get; set; }\n\n    /// <summary>\n    /// \t视频简介\n    /// </summary>\n    public string? Desc { get; set; }\n\n    /// <summary>\n    /// 视频状态\n    /// </summary>\n    public int State { get; set; }\n\n    /// <summary>\n    /// 稿件总时长（所有分P）(单位为秒)\n    /// </summary>\n    public long Duration { get; set; }\n}\n\n/*\n * 样例\n * {\"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\":\"\"}}}\n */\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VideoInfo.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic class VideoInfo\n{\n    public long Id { get; set; }\n\n    public required string Bv_id { get; set; }\n\n    public required string Title { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/ViewMall/ViewvipMallRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.ViewMall;\n\npublic class ViewVipMallRequest\n{\n    public required string Csrf { get; set; }\n    public string EventId { get; set; } = \"hevent_oy4b7h3epeb\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipPrivilegeType.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic enum VipPrivilegeType\n{\n    BCoinCoupon = 1,\n    MembershipBenefits = 2,\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipStatus.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic enum VipStatus\n{\n    [Description(\"无/过期\")]\n    Disable = 0,\n\n    [Description(\"正常\")]\n    Enable = 1,\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/CompleteOgvWatchRequest.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask;\n\npublic class CompleteOgvWatchRequest : BaseAppRequest\n{\n    public CompleteOgvWatchRequest(long taskId, string token)\n    {\n        task_id = taskId;\n        this.token = token;\n    }\n\n    public long task_id { get; set; }\n\n    public string token { get; set; }\n\n    public string? task_sign { get; set; }\n\n    public long timestamp { get; set; }\n\n    public string c_locale { get; } = \"zh_CN\";\n    public string channel { get; } = Constants.Channel;\n    public string s_locale { get; } = \"zh_CN\";\n    public string from_spmid { get; } = \"united.player-video-detail.player.continue\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ReceiveOrCompleteTaskRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask;\n\npublic class ReceiveOrCompleteTaskRequest\n{\n    public ReceiveOrCompleteTaskRequest(string taskCode)\n    {\n        TaskCode = taskCode;\n    }\n\n    public string TaskCode { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/SignRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask;\n\npublic class SignRequest\n{\n    public string? csrf { get; set; }\n\n    public string statistics { get; set; } =\n        \"{\\\"appId\\\":1,\\\"platform\\\":3,\\\"version\\\":\\\"6.85.0\\\",\\\"abtest\\\":\\\"\\\"}\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/StartOgvWatchRequest.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask;\n\npublic class StartOgvWatchRequest : BaseAppRequest\n{\n    public long ep_id { get; } = 328482;\n\n    public long season_id { get; } = 12548;\n\n    public string Activity_code { get; } = \"\";\n\n    public string spmid { get; } = \"united.player-video-detail.0.0\";\n\n    public string c_locale { get; } = \"zh_CN\";\n    public string channel { get; } = Constants.Channel;\n    public string s_locale { get; } = \"zh_CN\";\n    public string from_spmid { get; } = \"united.player-video-detail.player.continue\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/StartOgvWatchResponse.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask;\n\npublic class StartOgvWatchResponse\n{\n    public string? closeType { get; set; }\n\n    public string? showTime { get; set; }\n\n    public long task_id { get; set; }\n\n    public string? token { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/BigPointDto.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask.ThreeDaysSign;\n\npublic record BigPointDto(int point, int expire_point, int expire_time, int expire_days);\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/ThreeDaySignDto.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask.ThreeDaysSign;\n\n/// <summary>\n/// ThreeDaySignDto\n/// </summary>\n/// <param name=\"previous_vip_status\"></param>\n/// <param name=\"vip_status\"></param>\n/// <param name=\"day\">周期内的第几天</param>\n/// <param name=\"signed\">今日是否已签到</param>\n/// <param name=\"count\">已累计签到天数</param>\n/// <param name=\"has_coupon\"></param>\n/// <param name=\"countdown\"></param>\n/// <param name=\"score\"></param>\n/// <param name=\"vip_score\"></param>\n/// <param name=\"explain\"></param>\n/// <param name=\"exp_value\"></param>\n/// <param name=\"received_coupon\"></param>\n/// <param name=\"duration\">累计签到周期</param>\npublic record ThreeDaySignDto(\n    int previous_vip_status,\n    int vip_status,\n    int day,\n    bool signed,\n    int count,\n    bool has_coupon,\n    int countdown,\n    int score,\n    int vip_score,\n    string explain,\n    int exp_value,\n    bool received_coupon,\n    int duration\n);\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/ThreeDaySignRequest.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask.ThreeDaysSign;\n\npublic class ThreeDaySignRequest : BaseAppRequest\n{\n    public required string csrf { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ThreeDaysSign/ThreeDaySignResponse.cs",
    "content": "using System.Text;\nusing Microsoft.Extensions.Logging;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask.ThreeDaysSign;\n\npublic class ThreeDaySignResponse\n{\n    public required BigPointDto big_point { get; set; }\n\n    public required ThreeDaySignDto three_day_sign { get; set; }\n\n    public override string ToString()\n    {\n        var sb = new StringBuilder();\n        sb.AppendLine($\"今日获得签到积分: {three_day_sign.score}\");\n        sb.AppendLine($\"累计签到: {three_day_sign.count}/{three_day_sign.duration} 天\");\n\n        if (three_day_sign.count < 3)\n        {\n            sb.AppendLine(\n                $\"{three_day_sign.duration} 天内累计签到 3 天，可额外获取 {three_day_sign.exp_value} 经验\"\n            );\n        }\n        else\n        {\n            sb.AppendLine($\"满 3 天，已获得额外 {three_day_sign.vip_score} 经验\");\n        }\n\n        return sb.ToString();\n    }\n\n    public void LogPointInfo(ILogger logger)\n    {\n        logger.LogInformation(\"当前经验：{point}\", big_point.point);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/ViewRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask;\n\npublic class ViewRequest : BaseAppRequest\n{\n    public ViewRequest(string position)\n    {\n        this.position = position;\n    }\n\n    public string position { get; }\n\n    public string c_locale { get; } = \"zh_CN\";\n\n    public string channel { get; } = Constants.Channel;\n\n    public string s_locale { get; } = \"zh_CN\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/VipExperienceRequest.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask;\n\npublic class VipExperienceRequest\n{\n    public required string csrf { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipTask/VouchersInfoResponse.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask;\n\npublic class VouchersInfoResponse\n{\n    public List<List> List { get; set; } = [];\n    public bool IsShortVip { get; set; }\n    public bool IsFreightOpen { get; set; }\n    public int Level { get; set; }\n    public int CurExp { get; set; }\n    public int NextExp { get; set; }\n    public bool IsVip { get; set; }\n    public int IsSeniorMember { get; set; }\n    public int Format060102 { get; set; }\n}\n\npublic class List\n{\n    public int Type { get; set; }\n    public int State { get; set; }\n    public int ExpireTime { get; set; }\n    public int VipType { get; set; }\n    public int NextReceiveDays { get; set; }\n    public int PeriodEndUnix { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Dtos/VipType.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\npublic enum VipType\n{\n    [Description(\"无\")]\n    None = 0,\n\n    [Description(\"月度大会员\")]\n    Mensual = 1,\n\n    [Description(\"年度大会员\")]\n    Annual = 2,\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IAccountApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n[Header(\"Host\", \"account.bilibili.com\")]\npublic interface IAccountApi : IBiliBiliApi\n{\n    /// <summary>\n    /// 获取硬币余额\n    /// </summary>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://account.bilibili.com/account/coin\")]\n    [HttpGet(\"/site/getCoin\")]\n    Task<BiliApiResponse<CoinBalance>> GetCoinBalanceAsync([Header(\"Cookie\")] string ck);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IArticleApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Article;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n[Header(\"Host\", \"api.bilibili.com\")]\npublic interface IArticleApi : IBiliBiliApi\n{\n    [Header(\"Referer\", \"https://www.bilibili.com/\")]\n    [Header(\"Origin\", \"https://space.bilibili.com\")]\n    [HttpGet(\"/x/space/wbi/article\")]\n    Task<BiliApiResponse<SearchUpArticlesResponse>> SearchUpArticlesByUpIdAsync(\n        [PathQuery] SearchArticlesByUpIdDto request\n    );\n\n    /// <summary>\n    /// 获取专栏详情\n    /// </summary>\n    /// <param name=\"cvid\"></param>\n    /// <returns></returns>\n    [HttpGet(\"/x/article/viewinfo?id={cvid}\")]\n    Task<BiliApiResponse<SearchArticleInfoResponse>> SearchArticleInfoAsync(long cvid);\n\n    /// <summary>\n    /// 为专栏文章投币\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <param name=\"refer\"></param>\n    /// <returns></returns>\n    [Header(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n    [Header(\"Origin\", \"https://www.bilibili.com\")]\n    [HttpPost(\"/x/web-interface/coin/add\")]\n    Task<BiliApiResponse> AddCoinForArticleAsync(\n        [FormContent] AddCoinForArticleRequest request,\n        [Header(\"Cookie\")] string ck,\n        [Header(\"referer\")]\n            string refer =\n            \"https://www.bilibili.com/read/cv5806746/?from=search&spm_id_from=333.337.0.0\"\n    );\n\n    /// <summary>\n    /// 为专栏文章点赞\n    /// </summary>\n    /// <param name=\"cvid\"></param>\n    /// <param name=\"csrf\"></param>\n    /// <returns></returns>\n    [Header(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n    [Header(\n        \"Referer\",\n        \"https://www.bilibili.com/read/cv{cvid}/?from=search&spm_id_from=333.337.0.0\"\n    )]\n    [Header(\"Origin\", \"https://www.bilibili.com\")]\n    [HttpPost(\"/x/article/like?id={cvid}&type=1&csrf={csrf}\")]\n    Task<BiliApiResponse> LikeAsync(long cvid, string csrf, [Header(\"Cookie\")] string ck);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IBiliBiliApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n[AppendHeader(\"Accept\", \"application/json, text/plain, */*\", AppendHeaderType.AddIfNotExist)]\n//[Header(\"Accept-Encoding\", \"gzip, deflate, br\")]\n[AppendHeader(\n    \"Accept-Language\",\n    \"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\",\n    AppendHeaderType.AddIfNotExist\n)]\n[AppendHeader(\"Sec-Fetch-Dest\", \"empty\", AppendHeaderType.AddIfNotExist)]\n[AppendHeader(\"Sec-Fetch-Mode\", \"cors\", AppendHeaderType.AddIfNotExist)]\n[AppendHeader(\"Sec-Fetch-Site\", \"same-site\", AppendHeaderType.AddIfNotExist)]\n[AppendHeader(\"Connection\", \"keep-alive\", AppendHeaderType.AddIfNotExist)]\n[LogFilter]\npublic interface IBiliBiliApi;\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IChargeApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n/// <summary>\n/// 充电相关接口\n/// </summary>\n[Header(\"Host\", \"api.bilibili.com\")]\npublic interface IChargeApi : IBiliBiliApi\n{\n    /// <summary>\n    /// 充电\n    /// </summary>\n    /// <param name=\"elec_num\">充电电池数量（B币*10）,必须在20-99990之间</param>\n    /// <param name=\"up_mid\">充电对象用户UID</param>\n    /// <param name=\"oid\">充电来源代码(空间充电：充电对象用户UID;视频充电：稿件avID)</param>\n    /// <param name=\"csrf\"></param>\n    /// <returns></returns>\n    [HttpPost(\n        \"/x/ugcpay/trade/elec/pay/quick?elec_num={elec_num}&up_mid={up_mid}&otype=up&oid={oid}&csrf={csrf}\"\n    )]\n    [Obsolete]\n    Task<BiliApiResponse<ChargeResponse>> Charge(\n        int elec_num,\n        string up_mid,\n        string oid,\n        string csrf,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 充电V2\n    /// </summary>\n    /// <param name=\"bp_num\">B币个数</param>\n    /// <param name=\"up_mid\">对方Id</param>\n    /// <param name=\"oid\">对方来源代码(空间充电：充电对象用户UID;视频充电：稿件avID)</param>\n    /// <param name=\"csrf\">自己的bili_jct</param>\n    /// <returns></returns>\n    [Header(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n    [Header(\"Referer\", \"https://www.bilibili.com/\")]\n    [Header(\"Origin\", \"https://www.bilibili.com\")]\n    [HttpPost(\"/x/ugcpay/web/v2/trade/elec/pay/quick\")]\n    Task<BiliApiResponse<ChargeV2Response>> ChargeV2Async(\n        [FormContent] ChargeRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 充电后留言\n    /// </summary>\n    /// <param name=\"elec_num\"></param>\n    /// <param name=\"up_mid\"></param>\n    /// <param name=\"oid\"></param>\n    /// <param name=\"csrf\"></param>\n    /// <returns></returns>\n    [Header(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n    [Header(\"Referer\", \"https://www.bilibili.com/\")]\n    [Header(\"Origin\", \"https://www.bilibili.com\")]\n    [HttpPost(\"/x/ugcpay/trade/elec/message\")]\n    Task<BiliApiResponse<ChargeResponse>> ChargeCommentAsync(\n        [FormContent] ChargeCommentRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IDailyTaskApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n/// <summary>\n/// BiliBili每日任务相关接口\n/// </summary>\n[Header(\"Host\", \"api.bilibili.com\")]\npublic interface IDailyTaskApi : IBiliBiliApi\n{\n    /// <summary>\n    /// 获取每日任务的完成情况\n    /// </summary>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://account.bilibili.com/account/home\")]\n    [Header(\"Origin\", \"https://account.bilibili.com\")]\n    [HttpGet(\"/x/member/web/exp/reward\")]\n    Task<BiliApiResponse<DailyTaskInfo>> GetDailyTaskRewardInfoAsync([Header(\"Cookie\")] string ck);\n\n    /// <summary>\n    /// 获取通过投币已获取的经验值\n    /// </summary>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://www.bilibili.com/\")]\n    [Header(\"Origin\", \"https://www.bilibili.com\")]\n    [HttpGet(\"/x/web-interface/coin/today/exp\")]\n    Task<BiliApiResponse<int>> GetDonateCoinExpAsync([Header(\"Cookie\")] string ck);\n\n    /// <summary>\n    /// 获取VIP特权\n    /// </summary>\n    /// <param name=\"type\"></param>\n    /// <param name=\"csrf\"></param>\n    /// <returns></returns>\n    [HttpPost(\"/x/vip/privilege/receive?type={type}&csrf={csrf}\")]\n    Task<BiliApiResponse> ReceiveVipPrivilegeAsync(\n        int type,\n        string csrf,\n        [Header(\"Cookie\")] string ck\n    );\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IHomeApi.cs",
    "content": "﻿using WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n/// <summary>\n/// 主站首页接口API\n/// </summary>\npublic interface IHomeApi : IBiliBiliApi\n{\n    [HttpGet(\"\")]\n    Task<HttpResponseMessage> GetHomePageAsync([Header(\"Cookie\")] string ck);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/ILiveApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n/// <summary>\n/// 直播相关接口\n/// </summary>\n[Header(\"Host\", \"api.live.bilibili.com\")]\npublic interface ILiveApi : IBiliBiliApi\n{\n    /// <summary>\n    /// 直播签到\n    /// </summary>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://link.bilibili.com/\")]\n    [Header(\"Origin\", \"https://link.bilibili.com\")]\n    [HttpGet(\"/xlive/web-ucenter/v1/sign/DoSign\")]\n    Task<BiliApiResponse<LiveSignResponse>> Sign([Header(\"Cookie\")] string ck);\n\n    /// <summary>\n    /// 银瓜子兑换硬币\n    /// </summary>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://link.bilibili.com/\")]\n    [Header(\"Origin\", \"https://link.bilibili.com\")]\n    [Header(\"Content-Type\", \"application/x-www-form-urlencoded; charset=UTF-8\")]\n    [HttpGet(\"/pay/v1/Exchange/silver2coin\")]\n    [Obsolete]\n    Task<BiliApiResponse> ExchangeSilver2Coin([Header(\"Cookie\")] string ck);\n\n    /// <summary>\n    /// 获取银瓜子余额\n    /// </summary>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://link.bilibili.com/\")]\n    [Header(\"Origin\", \"https://link.bilibili.com\")]\n    [HttpGet(\"/pay/v1/Exchange/getStatus\")]\n    [Obsolete]\n    Task<BiliApiResponse<ExchangeSilverStatusResponse>> GetExchangeSilverStatus(\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 银瓜子兑换硬币\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    //[Header(\"Referer\", \"https://link.bilibili.com/p/center/index?visit_id=1ddo4yl01q00\")]\n    [Header(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n    [Header(\"Origin\", \"https://link.bilibili.com\")]\n    [HttpPost(\"/xlive/revenue/v1/wallet/silver2coin\")]\n    Task<BiliApiResponse<Silver2CoinResponse>> Silver2Coin(\n        [FormContent] Silver2CoinRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 获取直播中心钱包状态\n    /// </summary>\n    /// <returns></returns>\n    //[Header(\"Referer\", \"https://link.bilibili.com/p/center/index?visit_id=1ddo4yl01q00\")]\n    [Header(\"Origin\", \"https://link.bilibili.com\")]\n    [HttpGet(\"/xlive/revenue/v1/wallet/getStatus\")]\n    Task<BiliApiResponse<LiveWalletStatusResponse>> GetLiveWalletStatus(\n        [Header(\"Cookie\")] string ck\n    );\n\n    [HttpGet(\"/xlive/web-interface/v1/index/getWebAreaList?source_id=2\")]\n    Task<BiliApiResponse<GetArteaListResponse>> GetAreaList([Header(\"Cookie\")] string ck);\n\n    /// <summary>\n    /// 获取直播列表\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <param name=\"ck\"></param>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://live.bilibili.com/\")]\n    [Header(\"Origin\", \"https://live.bilibili.com\")]\n    [HttpGet(\"/xlive/web-interface/v1/second/getList\")]\n    Task<BiliApiResponse<GetListResponse>> GetList(\n        [PathQuery] GetListRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 检查天选时刻抽奖\n    /// </summary>\n    /// <param name=\"roomId\"></param>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://live.bilibili.com/\")]\n    [Header(\"Origin\", \"https://live.bilibili.com\")]\n    [HttpGet(\"/xlive/lottery-interface/v1/Anchor/Check?roomid={roomId}\")]\n    Task<BiliApiResponse<CheckTianXuanDto>> CheckTianXuan(\n        long roomId,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 参加天选时刻抽奖\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    [HttpPost(\"/xlive/lottery-interface/v1/Anchor/Join\")]\n    Task<BiliApiResponse<JoinTianXuanResponse>> Join(\n        [FormContent] JoinTianXuanRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 获取用户的粉丝勋章\n    /// </summary>\n    /// <param name=\"userId\">uid</param>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://live.bilibili.com/\")]\n    [Header(\"Origin\", \"https://live.bilibili.com\")]\n    [HttpGet(\"/xlive/web-ucenter/user/MedalWall?target_id={userId}\")]\n    Task<BiliApiResponse<MedalWallResponse>> GetMedalWall(\n        string userId,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 佩戴粉丝勋章\n    /// </summary>\n    /// <param name=\"userId\">uid</param>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://live.bilibili.com/\")]\n    [Header(\"Origin\", \"https://live.bilibili.com\")]\n    [HttpPost(\"/xlive/app-ucenter/v1/fansMedal/wear\")]\n    Task<BiliApiResponse> WearMedalWall(\n        [FormContent] WearMedalWallRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 发送弹幕\n    /// </summary>\n    /// <param name=\"request\">request</param>\n    /// <returns></returns>\n    [HttpPost(\"/msg/send\")]\n    Task<BiliApiResponse> SendLiveDanmuku(\n        [FormContent] SendLiveDanmukuRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 获取直播间信息\n    /// </summary>\n    /// <param name=\"roomId\">roomId</param>\n    /// <returns></returns>\n    [HttpGet(\"/room/v1/Room/get_info?room_id={roomId}&from=room\")]\n    Task<BiliApiResponse<GetLiveRoomInfoResponse>> GetLiveRoomInfo(long roomId);\n\n    /// <summary>\n    /// 请求直播主页用于配置直播相关 Cookie\n    /// </summary>\n    [HttpGet(\"/news/v1/notice/recom?product=live\")]\n    Task<HttpResponseMessage> GetLiveHome([Header(\"Cookie\")] string ck);\n\n    /// <summary>\n    /// 点赞直播间\n    /// </summary>\n    [HttpPost(\"/xlive/app-ucenter/v1/like_info_v3/like/likeReportV3\")]\n    [Header(\"Referer\", \"https://live.bilibili.com/\")]\n    [Header(\"Origin\", \"https://live.bilibili.com\")]\n    Task<BiliApiResponse> LikeLiveRoom(\n        [RawFormContent] string request,\n        [Header(\"Cookie\")] string ck\n    );\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/ILiveTraceApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n[Header(\"Host\", \"live-trace.bilibili.com\")]\npublic interface ILiveTraceApi : IBiliBiliApi\n{\n    [HttpGet(\"/xlive/rdata-interface/v1/heartbeat/webHeartBeat?hb={request}&pf=web\")]\n    Task<BiliApiResponse<WebHeartBeatResponse>> WebHeartBeat(\n        WebHeartBeatRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    [HttpPost(\"/xlive/data-interface/v1/x25Kn/E\")]\n    Task<BiliApiResponse<HeartBeatResponse>> EnterRoom(\n        [FormContent] EnterRoomRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    [HttpPost(\"/xlive/data-interface/v1/x25Kn/X\")]\n    Task<BiliApiResponse<HeartBeatResponse>> HeartBeat(\n        [FormContent] HeartBeatRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IMallApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.Attributes;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n/// <summary>\n/// 大会员大积分\n/// </summary>\n[LogFilter]\n[Header(\"Host\", \"api.bilibili.com\")]\npublic interface IMallApi\n{\n    /// <summary>\n    /// 签到任务\n    /// </summary>\n    /// <param name=\"requestPath\"></param>\n    /// <param name=\"request\"></param>\n    /// <param name=\"ck\"></param>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://big.bilibili.com/mobile/index\")]\n    [HttpPost(\"/pgc/activity/score/task/sign2\")]\n    Task<BiliApiResponse<Sign2Response>> Sign2Async(\n        [PathQuery] Sign2RequestPath requestPath,\n        [JsonContent] Sign2Request request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 获取任务 combine 信息\n    /// </summary>\n    /// <remarks>里面的登录信息是错误的，阿B特色</remarks>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://big.bilibili.com/mobile/bigPoint/task\")]\n    [HttpGet(\"/x/vip_point/task/combine\")]\n    Task<BiliApiResponse<VipBigPointCombine>> GetCombineAsync(\n        [PathQuery] GetCombineRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IMangaApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.Attributes;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n/// <summary>\n/// 漫画相关接口\n/// </summary>\n[Header(\"Origin\", \"https://manga.bilibili.com\")]\n[Header(\"Host\", \"manga.bilibili.com\")]\npublic interface IMangaApi : IBiliBiliApi\n{\n    /// <summary>\n    /// 漫画签到\n    /// </summary>\n    /// <param name=\"platform\"></param>\n    /// <returns></returns>\n    [LogFilter(false)]\n    [HttpPost(\"/twirp/activity.v1.Activity/ClockIn?platform={platform}\")]\n    Task<BiliApiResponse> ClockIn(string platform, [Header(\"Cookie\")] string ck);\n\n    /// <summary>\n    /// 漫画阅读\n    /// </summary>\n    /// <param name=\"platform\"></param>\n    /// <returns></returns>\n    [HttpPost(\n        \"/twirp/bookshelf.v1.Bookshelf/AddHistory?platform={platform}&comic_id={comic_id}&ep_id={ep_id}\"\n    )]\n    Task<BiliApiResponse> ReadManga(\n        string platform,\n        long comic_id,\n        long ep_id,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 获取会员漫画奖励\n    /// </summary>\n    /// <param name=\"reason_id\"></param>\n    /// <returns></returns>\n    [HttpPost(\"/twirp/user.v1.User/GetVipReward?reason_id={reason_id}\")]\n    Task<BiliApiResponse<MangaVipRewardResponse>> ReceiveMangaVipReward(\n        int reason_id,\n        [Header(\"Cookie\")] string ck\n    );\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IPassportApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Passport;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n[Header(\"Host\", \"passport.bilibili.com\")]\npublic interface IPassportApi : IBiliBiliApi\n{\n    [HttpGet(\"/x/passport-login/web/qrcode/generate\")]\n    Task<BiliApiResponse<QrCodeDto>> GenerateQrCode();\n\n    [HttpGet(\"/x/passport-login/web/qrcode/poll?qrcode_key={qrcode_key}&source=main_mini\")]\n    //Task<BiliApiResponse<TokenDto>> CheckQrCodeHasScaned(string qrcode_key);\n    Task<HttpResponseMessage> CheckQrCodeHasScaned(string qrcode_key);\n\n    [HttpGet(\"/x/passport-login/web/sso/list?biliCSRF={csrf}\")]\n    Task<BiliApiResponse<GetSsoListResponse>> GetSsoListAsync(string csrf);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IRelationApi.cs",
    "content": "﻿using System.ComponentModel;\nusing Ray.BiliBiliTool.Agent.Attributes;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n/// <summary>\n/// 关注相关接口\n/// </summary>\n[AppendHeader(\"Host\", \"api.bilibili.com\", AppendHeaderType.AddIfNotExist)]\n[AppendHeader(\"Referer\", \"https://space.bilibili.com/\", AppendHeaderType.AddIfNotExist)]\npublic interface IRelationApi : IBiliBiliApi\n{\n    /// <summary>\n    /// 获取关注列表\n    /// </summary>\n    /// <returns></returns>\n    [HttpGet(\"/x/relation/followings\")]\n    Task<BiliApiResponse<GetFollowingsResponse>> GetFollowings(\n        GetFollowingsRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 获取特别关注列表\n    /// </summary>\n    /// <returns></returns>\n    [Header(\"Cache-Control\", \"no-cache\")]\n    [Header(\"Pragma\", \"no-cache\")]\n    [JsonReturn(EnsureMatchAcceptContentType = false)]\n    [HttpGet(\"/x/relation/tag\")]\n    Task<BiliApiResponse<List<UpInfo>>> GetFollowingsByTag(\n        GetSpecialFollowingsRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 获取关注分组\n    /// </summary>\n    /// <returns></returns>\n    [AppendHeader(\"Sec-Fetch-Mode\", \"no-cors\")]\n    [AppendHeader(\"Sec-Fetch-Dest\", \"script\")]\n    [HttpGet(\"/x/relation/tags?jsonp=jsonp\")]\n    Task<BiliApiResponse<List<TagDto>>> GetTags(\n        [Header(\"Cookie\")] string ck,\n        [AppendHeader(\"Referer\")] string referer = RelationApiConstant.GetTagsReferer\n    );\n\n    /// <summary>\n    /// 添加关注分组（tag）\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    [AppendHeader(\"Origin\", \"https://space.bilibili.com\")]\n    [HttpPost(\"/x/relation/tag/create?cross_domain=true\")]\n    Task<BiliApiResponse<CreateTagResponse>> CreateTag(\n        [FormContent] CreateTagRequest request,\n        [Header(\"Cookie\")] string ck,\n        [AppendHeader(\"Referer\")] string referer = RelationApiConstant.GetTagsReferer\n    );\n\n    /// <summary>\n    /// 批量拷贝关注up到某指定分组\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    [AppendHeader(\"Origin\", \"https://space.bilibili.com\")]\n    [HttpPost(\"/x/relation/tags/copyUsers\")]\n    Task<BiliApiResponse> CopyUpsToGroup(\n        [FormContent] CopyUserToGroupRequest request,\n        [Header(\"Cookie\")] string ck,\n        [AppendHeader(\"Referer\")] string referer = RelationApiConstant.CopyReferer\n    );\n\n    /// <summary>\n    /// 修改关系\n    /// </summary>\n    /// <returns></returns>\n    [AppendHeader(\"Origin\", \"https://space.bilibili.com\")]\n    [HttpPost(\"/x/relation/modify\")]\n    Task<BiliApiResponse> ModifyRelation(\n        [FormContent] ModifyRelationRequest request,\n        [Header(\"Cookie\")] string ck,\n        [AppendHeader(\"Referer\")] string referer = RelationApiConstant.ModifyReferer\n    );\n}\n\npublic enum FollowingsOrderType\n{\n    /// <summary>\n    /// 最常访问频率倒序\n    /// </summary>\n    [DefaultValue(\"attention\")]\n    AttentionDesc,\n\n    /// <summary>\n    /// 关注时间倒序\n    /// </summary>\n    [DefaultValue(\"\")]\n    TimeDesc,\n}\n\npublic class RelationApiConstant\n{\n    /// <summary>\n    /// GetTags接口中的Referer\n    /// {0}为UserId\n    /// </summary>\n    public const string GetTagsReferer = \"https://space.bilibili.com/{0}/fans/follow\";\n\n    /// <summary>\n    /// CopyUpsToGroup接口中的Referer\n    /// {0}为UserId\n    /// </summary>\n    public const string CopyReferer = \"https://space.bilibili.com/{0}/fans/follow?tagid=-1\";\n\n    /// <summary>\n    /// ModifyRelation接口种的Referer\n    /// </summary>\n    public const string ModifyReferer = \"https://space.bilibili.com/{0}/fans/follow?tagid={1}\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IUpInfoApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n/// <summary>\n/// 用户信息接口API\n/// </summary>\n[Header(\"Referer\", \"https://www.bilibili.com/\")]\n[Header(\"Origin\", \"https://www.bilibili.com\")]\n[Header(\"Host\", \"api.bilibili.com\")]\npublic interface IUpInfoApi : IBiliBiliApi\n{\n    /// <summary>\n    /// 获取用户空间信息\n    /// </summary>\n    /// <param name=\"userId\">uid</param>\n    /// <returns></returns>\n    [HttpGet(\"/x/space/wbi/acc/info\")]\n    Task<BiliApiResponse<GetSpaceInfoResponse>> GetSpaceInfo(\n        [PathQuery] GetSpaceInfoDto request,\n        [Header(\"Cookie\")] string ck\n    );\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IUserInfoApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n/// <summary>\n/// 用户信息接口API\n/// </summary>\n[Header(\"Referer\", \"https://www.bilibili.com/\")]\n[Header(\"Origin\", \"https://www.bilibili.com\")]\n[Header(\"Host\", \"api.bilibili.com\")]\npublic interface IUserInfoApi : IBiliBiliApi\n{\n    /// <summary>\n    /// 登录\n    /// </summary>\n    /// <returns></returns>\n    [HttpGet(\"/x/web-interface/nav\")]\n    Task<BiliApiResponse<UserInfo>> LoginByCookie([Header(\"Cookie\")] string ck);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IVideoApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Video;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n/// <summary>\n/// 视频相关接口\n/// </summary>\n[Header(\"Host\", \"api.bilibili.com\")]\npublic interface IVideoApi : IBiliBiliApi\n{\n    /// <summary>\n    /// 分享视频\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <remarks>ck中必须要有buvid3，否则几率性-403</remarks>\n    /// <returns></returns>\n    [Header(\"Origin\", \"https://www.bilibili.com\")]\n    [HttpPost(\"/x/web-interface/share/add\")]\n    Task<BiliApiResponse> ShareVideo(\n        [FormContent] ShareVideoRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 上传视频观看进度\n    /// 每15秒上报一次\n    /// </summary>\n    /// <returns></returns>\n    //[Header(\"Content-Length\", \"186\")]\n    [Header(\"Content-Type\", \"application/x-www-form-urlencoded; charset=UTF-8\")]\n    [Header(\"Referer\", \"https://www.bilibili.com/\")]\n    [Header(\"Origin\", \"https://www.bilibili.com\")]\n    [HttpPost(\"/x/click-interface/web/heartbeat?aid={aid}&played_time={playedTime}\")]\n    Task<BiliApiResponse> UploadVideoHeartbeat(\n        [FormContent] UploadVideoHeartbeatRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    #region 投币相关\n    /// <summary>\n    /// 为视频投币\n    /// </summary>\n    /// <param name=\"aid\"></param>\n    /// <param name=\"multiply\"></param>\n    /// <param name=\"select_like\"></param>\n    /// <param name=\"csrf\"></param>\n    /// <returns></returns>\n    [Header(\"Content-Type\", \"application/x-www-form-urlencoded\")]\n    //[Header(\"Referer\", \"https://www.bilibili.com/\")]\n    [Header(\"Origin\", \"https://www.bilibili.com\")]\n    [HttpPost(\"/x/web-interface/coin/add\")]\n    Task<BiliApiResponse> AddCoinForVideo(\n        [FormContent] AddCoinRequest request,\n        [Header(\"Cookie\")] string ck,\n        [Header(\"referer\")]\n            string refer =\n            \"https://www.bilibili.com/video/BV123456/?spm_id_from=333.1007.tianma.1-1-1.click&vd_source=80c1601a7003934e7a90709c18dfcffd\"\n    );\n\n    /// <summary>\n    /// 获取当前用户对<paramref name=\"aid\"/>视频的投币信息\n    /// </summary>\n    /// <param name=\"aid\"></param>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://www.bilibili.com/\")]\n    [HttpGet(\"/x/web-interface/archive/coins\")]\n    Task<BiliApiResponse<DonatedCoinsForVideo>> GetDonatedCoinsForVideo(\n        GetAlreadyDonatedCoinsRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n    #endregion\n\n    /// <summary>\n    /// 搜索指定Up的视频列表\n    /// </summary>\n    /// <param name=\"upId\"></param>\n    /// <param name=\"pageSize\">[1,100]验证不通过接口会报异常</param>\n    /// <param name=\"pageNumber\"></param>\n    /// <param name=\"keyword\"></param>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://www.bilibili.com/\")]\n    [Header(\"Origin\", \"https://space.bilibili.com\")]\n    //[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\")]\n    [HttpGet(\"/x/space/wbi/arc/search\")]\n    Task<BiliApiResponse<SearchUpVideosResponse>> SearchVideosByUpId(\n        [PathQuery] SearchVideosByUpIdDto request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 通过ssid获取番剧的具体信息\n    /// </summary>\n    /// <param name=\"Ssid\"></param>\n    /// <returns></returns>\n    [HttpGet(\"/pgc/view/web/season?season_id={ssid}\")]\n    Task<GetBangumiBySsidResponse> GetBangumiBySsid(long ssid, [Header(\"Cookie\")] string ck);\n}\n\n/// <summary>\n/// 不需要传递Cookie的接口\n/// </summary>\npublic interface IVideoWithoutCookieApi : IVideoApi\n{\n    /// <summary>\n    /// 获取视频详情\n    /// </summary>\n    /// <param name=\"aid\"></param>\n    /// <returns></returns>\n    [HttpGet(\"/x/web-interface/view?aid={aid}\")]\n    Task<BiliApiResponse<VideoDetail>> GetVideoDetail(string aid);\n\n    /// <summary>\n    /// 获取某分区下X日内排行榜\n    /// </summary>\n    /// <param name=\"rid\"></param>\n    /// <param name=\"day\"></param>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://www.bilibili.com/\")]\n    [Header(\"Origin\", \"https://www.bilibili.com\")]\n    [HttpGet(\"/x/web-interface/ranking/region?rid={rid}&day={day}\")]\n    [Obsolete]\n    Task<BiliApiResponse<List<RankingInfo>>> GetRegionRankingVideos(int rid, int day);\n\n    /// <summary>\n    /// 获取排行榜\n    /// </summary>\n    /// <returns></returns>\n    [Header(\"Referer\", \"https://www.bilibili.com/\")]\n    [Header(\"Origin\", \"https://www.bilibili.com\")]\n    [Header(\"dnt\", \"1\")]\n    [HttpGet(\"/x/web-interface/ranking/v2?rid=0&type=all\")]\n    Task<BiliApiResponse<Ranking>> GetRegionRankingVideosV2();\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IVipBigPointApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.Attributes;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask.ThreeDaysSign;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n/// <summary>\n/// 大会员大积分\n/// </summary>\n[Header(\"Host\", \"api.bilibili.com\")]\n[Header(\"Referer\", \"https://big.bilibili.com/mobile/bigPoint/task\")]\n[LogFilter]\npublic interface IVipBigPointApi\n{\n    /// <summary>\n    /// 获取签到信息\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <param name=\"ck\"></param>\n    /// <returns></returns>\n    [HttpGet(\"/x/vip/vip_center/sign_in/three_days_sign\")]\n    Task<BiliApiResponse<ThreeDaySignResponse>> GetThreeDaySignAsync(\n        [PathQuery] ThreeDaySignRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 获取任务列表\n    /// </summary>\n    /// <remarks>里面的登录信息是错误的，阿B特色</remarks>\n    /// <returns></returns>\n    [Obsolete(\"Using IMallApi.GetCombineAsync instead.\")]\n    [HttpGet(\"/x/vip_point/task/combine\")]\n    Task<BiliApiResponse<VipBigPointCombine>> GetCombineAsync([Header(\"Cookie\")] string ck);\n\n    /// <summary>\n    /// 签到任务\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    [Obsolete(\"Using IMallApi.Sign2Async instead.\")]\n    [HttpPost(\"/pgc/activity/score/task/sign\")]\n    Task<BiliApiResponse> SignAsync(\n        [FormContent] SignRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 领取任务\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    [Obsolete]\n    [HttpPost(\"/pgc/activity/score/task/receive\")]\n    Task<BiliApiResponse> Receive(\n        [JsonContent] ReceiveOrCompleteTaskRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 领取任务\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    [HttpPost(\"/pgc/activity/score/task/receive/v2\")]\n    Task<BiliApiResponse> ReceiveV2(\n        [FormContent] ReceiveOrCompleteTaskRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 完成任务\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    [HttpPost(\"/pgc/activity/score/task/complete\")]\n    Task<BiliApiResponse> CompleteAsync(\n        [JsonContent] ReceiveOrCompleteTaskRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 完成任务\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    [HttpPost(\"/pgc/activity/score/task/complete/v2\")]\n    Task<BiliApiResponse> CompleteV2(\n        [FormContent] ReceiveOrCompleteTaskRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 完成浏览页面任务\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <param name=\"ck\"></param>\n    /// <returns></returns>\n    [HttpPost(\"/pgc/activity/deliver/task/complete\")]\n    Task<BiliApiResponse> ViewComplete(\n        [FormContent] ViewRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    [HttpGet(\"/x/vip/privilege/my\")]\n    Task<BiliApiResponse<VouchersInfoResponse>> GetVouchersInfoAsync([Header(\"Cookie\")] string ck);\n\n    /// <summary>\n    /// 兑换大会员经验\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    [HttpPost(\"/x/vip/experience/add\")]\n    Task<BiliApiResponse> ObtainVipExperienceAsync(\n        [FormContent] VipExperienceRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 开始观看剧集任务\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    Task<BiliApiResponse<StartOgvWatchResponse>> StartOgvWatchAsync(\n        StartOgvWatchRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n\n    /// <summary>\n    /// 完成观看剧集任务\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    Task<BiliApiResponse> CompleteOgvWatchAsync(\n        CompleteOgvWatchRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Interfaces/IVipMallApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.Attributes;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.ViewMall;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\n\n[Header(\"Host\", \"show.bilibili.com\")]\n[LogFilter]\npublic interface IVipMallApi\n{\n    [HttpPost(\"/api/activity/fire/common/event/dispatch\")]\n    Task<BiliApiResponse> ViewVipMallAsync(\n        [JsonContent] ViewVipMallRequest request,\n        [Header(\"Cookie\")] string ck\n    );\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Services/IWbiService.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Services;\n\n/// <summary>\n/// 防爬\n/// </summary>\npublic interface IWbiService\n{\n    Task<WridDto> GetWridAsync(Dictionary<string, string> parameters, BiliCookie ck);\n\n    /// <summary>\n    /// 获取WbiKey\n    /// </summary>\n    /// <returns></returns>\n    Task SetWridAsync<T>(T ob, BiliCookie ck)\n        where T : IWrid;\n\n    WridDto EncWbi(\n        Dictionary<string, string> parameters,\n        string imgKey,\n        string subKey,\n        long timespan = 0\n    );\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Services/WbiService.cs",
    "content": "﻿using System.Security.Cryptography;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing Microsoft.Extensions.Logging;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Helpers;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Services;\n\n/// <summary>\n/// 防爬\n/// </summary>\npublic class WbiService(ILogger<WbiService> logger, IUserInfoApi userInfoApi) : IWbiService\n{\n    private Dictionary<BiliCookie, WbiImg> _cache = new();\n\n    public async Task<WridDto> GetWridAsync(Dictionary<string, string> parameters, BiliCookie ck)\n    {\n        parameters.Remove(nameof(IWrid.wts));\n        parameters.Remove(nameof(IWrid.w_rid));\n\n        WbiImg wbi = await GetWbiKeysAsync(ck);\n\n        return EncWbi(parameters, wbi.ImgKey, wbi.SubKey);\n    }\n\n    public async Task SetWridAsync<T>(T request, BiliCookie ck)\n        where T : IWrid\n    {\n        //生成字典\n        Dictionary<string, string> parameters = ObjectHelper\n            .ObjectToDictionary(request)\n            .ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? \"\");\n\n        //生成\n        var re = await GetWridAsync(parameters, ck);\n\n        request.w_rid = re.w_rid;\n        request.wts = re.wts;\n    }\n\n    /// <summary>\n    /// 为请求参数进行 wbi 签名\n    /// </summary>\n    /// <param name=\"parameters\"></param>\n    /// <param name=\"imgKey\"></param>\n    /// <param name=\"subKey\"></param>\n    /// <param name=\"timespan\"></param>\n    /// <returns></returns>\n    public WridDto EncWbi(\n        Dictionary<string, string> parameters,\n        string imgKey,\n        string subKey,\n        long timespan = 0\n    )\n    {\n        var re = new WridDto();\n\n        var mixinKey = GetMixinKey(imgKey + subKey);\n\n        if (timespan == 0)\n        {\n            re.wts = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;\n        }\n        else\n        {\n            re.wts = timespan;\n        }\n\n        var chrFilter = new Regex(\"[!'()*]\");\n\n        var dic = new Dictionary<string, string> { { \"wts\", re.wts.ToString() } };\n\n        foreach (var entry in parameters)\n        {\n            var key = entry.Key;\n            var value = entry.Value;\n\n            var encodedValue = chrFilter.Replace(value, \"\");\n\n            dic.Add(Uri.EscapeDataString(key), Uri.EscapeDataString(encodedValue));\n        }\n\n        var keyList = dic.Keys.ToList();\n        keyList.Sort();\n\n        var queryList = (\n            from item in keyList\n            let value = dic[item]\n            select $\"{item}={value}\"\n        ).ToList();\n\n        var queryString = string.Join(\"&\", queryList);\n        var hashStr = queryString + mixinKey;\n        var hashedQueryString = MD5.HashData(Encoding.UTF8.GetBytes(hashStr));\n        var wbiSign = BitConverter.ToString(hashedQueryString).Replace(\"-\", \"\").ToLower();\n\n        re.w_rid = wbiSign;\n\n        return re;\n    }\n\n    private async Task<WbiImg> GetWbiKeysAsync(BiliCookie ck)\n    {\n        _cache.TryGetValue(ck, out var wbiImg);\n\n        if (wbiImg != null)\n            return wbiImg;\n\n        BiliApiResponse<UserInfo> apiResponse = await userInfoApi.LoginByCookie(ck.ToString());\n        UserInfo useInfo = apiResponse.Data!;\n        logger.LogDebug(\"【img_url】{0}\", useInfo.Wbi_img.img_url);\n        logger.LogDebug(\"【sub_url】{0}\", useInfo.Wbi_img.sub_url);\n\n        wbiImg = useInfo.Wbi_img;\n        _cache[ck] = wbiImg;\n        return wbiImg;\n    }\n\n    /// <summary>\n    /// 对 imgKey 和 subKey 进行字符顺序打乱编码\n    /// </summary>\n    /// <param name=\"orig\"></param>\n    /// <returns></returns>\n    private string GetMixinKey(string orig)\n    {\n        int[] mixinKeyEncTab = new int[]\n        {\n            46,\n            47,\n            18,\n            2,\n            53,\n            8,\n            23,\n            32,\n            15,\n            50,\n            10,\n            31,\n            58,\n            3,\n            45,\n            35,\n            27,\n            43,\n            5,\n            49,\n            33,\n            9,\n            42,\n            19,\n            29,\n            28,\n            14,\n            39,\n            12,\n            38,\n            41,\n            13,\n            37,\n            48,\n            7,\n            16,\n            24,\n            55,\n            40,\n            61,\n            26,\n            17,\n            0,\n            1,\n            60,\n            51,\n            30,\n            4,\n            22,\n            25,\n            54,\n            21,\n            56,\n            59,\n            6,\n            63,\n            57,\n            62,\n            11,\n            36,\n            20,\n            34,\n            44,\n            52,\n        };\n\n        var temp = new StringBuilder();\n        foreach (var index in mixinKeyEncTab)\n        {\n            temp.Append(orig[index]);\n        }\n        return temp.ToString().Substring(0, 32);\n    }\n}\n\npublic class WridDto : IWrid\n{\n    public long wts { get; set; }\n\n    public string? w_rid { get; set; }\n}\n\npublic interface IWrid\n{\n    public long wts { get; set; }\n\n    public string? w_rid { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/Utils/LiveHeartBeatCrypto.cs",
    "content": "﻿using System.Security.Cryptography;\nusing System.Text;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent.Utils;\n\npublic class LiveHeartBeatCrypto\n{\n    public static string Sypder(string text, ICollection<int> rules, string key)\n    {\n        string result = text;\n        foreach (var rule in rules)\n        {\n            switch (rule)\n            {\n                case 0:\n                    result = Hash(result, key, \"HMACMD5\");\n                    break;\n                case 1:\n                    result = Hash(result, key, \"HMACSHA1\");\n                    break;\n                case 2:\n                    result = Hash(result, key, \"HMACSHA256\");\n                    break;\n                case 3:\n                    result = Hash(result, key, \"HMACSHA224\");\n                    break;\n                case 4:\n                    result = Hash(result, key, \"HMACSHA512\");\n                    break;\n                case 5:\n                    result = Hash(result, key, \"HMACSHA384\");\n                    break;\n                default:\n                    break;\n            }\n        }\n        return result;\n    }\n\n    private static string Hash(string text, string key, string algorithmName)\n    {\n        HMAC hamc = algorithmName.ToUpperInvariant() switch\n        {\n            \"HMACSHA256\" => new HMACSHA256(Encoding.UTF8.GetBytes(key)),\n            \"HMACSHA1\" => new HMACSHA1(Encoding.UTF8.GetBytes(key)),\n            \"HMACMD5\" => new HMACMD5(Encoding.UTF8.GetBytes(key)),\n            _ => throw new ArgumentException($\"Unsupported algorithm: {algorithmName}\"),\n        };\n\n        using HMAC hmac = hamc;\n        byte[] hashBytes = hamc.ComputeHash(Encoding.UTF8.GetBytes(text));\n        return BitConverter.ToString(hashBytes).Replace(\"-\", \"\").ToLowerInvariant();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliBiliAgent/WridEncryptionDelegatingHandler.cs",
    "content": "using System.Collections.Specialized;\nusing System.Web;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Services;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\n\nnamespace Ray.BiliBiliTool.Agent.BiliBiliAgent;\n\npublic class WridEncryptionDelegatingHandler(IWbiService wbiService) : DelegatingHandler\n{\n    protected override async Task<HttpResponseMessage> SendAsync(\n        HttpRequestMessage request,\n        CancellationToken cancellationToken\n    )\n    {\n        if (request.Content is FormUrlEncodedContent originalFormContent)\n        {\n            var originalFormDataString = await originalFormContent.ReadAsStringAsync(\n                cancellationToken\n            );\n            var formData = HttpUtility.ParseQueryString(originalFormDataString);\n\n            await TrySetWridAync(request, formData, cancellationToken);\n\n            var newFormKeyValuePairs = formData\n                .AllKeys.Select(key => new KeyValuePair<string, string>(key!, formData[key] ?? \"\"))\n                .ToList();\n            request.Content = new FormUrlEncodedContent(newFormKeyValuePairs);\n        }\n\n        if (request.RequestUri?.Query != null)\n        {\n            var queryParameters = HttpUtility.ParseQueryString(request.RequestUri.Query);\n\n            await TrySetWridAync(request, queryParameters, cancellationToken);\n\n            var uriBuilder = new UriBuilder(request.RequestUri)\n            {\n                Query = queryParameters?.ToString() ?? \"\",\n            };\n            request.RequestUri = uriBuilder.Uri;\n        }\n\n        return await base.SendAsync(request, cancellationToken);\n    }\n\n    private async Task TrySetWridAync(\n        HttpRequestMessage request,\n        NameValueCollection formData,\n        CancellationToken cancellationToken\n    )\n    {\n        var paramsToSign = new Dictionary<string, string>();\n        foreach (var key in formData.AllKeys)\n        {\n            paramsToSign[key!] = formData[key] ?? \"\";\n        }\n\n        if (paramsToSign.All(x => x.Key != \"w_rid\"))\n        {\n            return;\n        }\n\n        var ckStr = request\n            .Headers.FirstOrDefault(x => x.Key == \"Cookie\")\n            .Value.FirstOrDefault()\n            ?.ToString();\n        var ck = CookieStrFactory<BiliCookie>.CreateNew(ckStr ?? \"\");\n\n        var wbi = await wbiService.GetWridAsync(paramsToSign, ck);\n\n        formData[\"w_rid\"] = wbi.w_rid;\n        formData[\"wts\"] = wbi.wts.ToString();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliCookie.cs",
    "content": "﻿using System.ComponentModel;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\nusing Ray.BiliBiliTool.Infrastructure.Extensions;\n\nnamespace Ray.BiliBiliTool.Agent;\n\npublic class BiliCookie(Dictionary<string, string> cookieDic) : CookieInfo(cookieDic)\n{\n    protected override string CkValueBuild(string value)\n    {\n        value = base.CkValueBuild(value);\n\n        if (value.Contains(','))\n        {\n            value = Uri.EscapeDataString(value);\n        }\n\n        return value;\n    }\n\n    #region 扩充属性\n\n    [Description(\"DedeUserID\")]\n    public string UserId =>\n        CookieItemDictionary.TryGetValue(GetPropertyDescription(nameof(UserId)), out string? userId)\n            ? userId\n            : \"\";\n\n    /// <summary>\n    /// SESSDATA\n    /// </summary>\n    [Description(\"SESSDATA\")]\n    public string SessData =>\n        CookieItemDictionary.TryGetValue(GetPropertyDescription(nameof(SessData)), out string? sess)\n            ? sess\n            : \"\";\n\n    [Description(\"bili_jct\")]\n    public string BiliJct =>\n        CookieItemDictionary.TryGetValue(GetPropertyDescription(nameof(BiliJct)), out string? jct)\n            ? jct\n            : \"\";\n\n    [Description(\"LIVE_BUVID\")]\n    public string LiveBuvid =>\n        CookieItemDictionary.TryGetValue(\n            GetPropertyDescription(nameof(LiveBuvid)),\n            out string? liveBuvid\n        )\n            ? liveBuvid\n            : \"\";\n\n    [Description(\"buvid3\")]\n    public string Buvid =>\n        CookieItemDictionary.TryGetValue(GetPropertyDescription(nameof(Buvid)), out string? buvid)\n            ? buvid\n            : \"\";\n\n    #endregion\n\n\n    /// <summary>\n    /// 检查是否已配置\n    /// </summary>\n    /// <returns></returns>\n    public override void Check()\n    {\n        base.Check();\n\n        if (CookieItemDictionary.Count == 0)\n            throw new Exception(\"Cookie字符串格式异常，内部无等号\");\n\n        bool result = true;\n        string msg = \"Cookie字符串异常，无[{1}]项\";\n\n        //UserId为空，抛异常\n        if (string.IsNullOrWhiteSpace(UserId))\n        {\n            throw new Exception(string.Format(msg, GetPropertyDescription(nameof(UserId))));\n        }\n        else if (!long.TryParse(UserId, out long uid)) //不为空，但不能转换为long，警告\n        {\n            throw new Exception(\n                string.Format(\n                    \"[{uidKey}]={uid} 不能转换为long型，请确认配置的是正确的Cookie值\",\n                    GetPropertyDescription(nameof(UserId)),\n                    UserId\n                )\n            );\n        }\n\n        //SessData为空，抛异常\n        if (string.IsNullOrWhiteSpace(SessData))\n        {\n            throw new Exception(string.Format(msg, GetPropertyDescription(nameof(SessData))));\n        }\n\n        //BiliJct为空，抛异常\n        if (string.IsNullOrWhiteSpace(BiliJct))\n        {\n            throw new Exception(string.Format(msg, GetPropertyDescription(nameof(BiliJct))));\n        }\n\n        if (!result)\n            throw new Exception(\n                $\"请正确配置Cookie后再运行，配置方式见 {Config.Constants.SourceCodeUrl}\"\n            );\n    }\n\n    private string GetPropertyDescription(string propertyName)\n    {\n        return GetType().GetPropertyDescription(propertyName);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/BiliHosts.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Agent;\n\npublic static class BiliHosts\n{\n    public const string Api = \"https://api.bilibili.com\";\n    public const string App = \"https://app.bilibili.com\";\n    public const string Show = \"https://show.bilibili.com\";\n    public const string Passport = \"http://passport.bilibili.com\";\n    public const string LiveTrace = \"https://live-trace.bilibili.com\";\n    public const string Www = \"https://www.bilibili.com\";\n    public const string Manga = \"https://manga.bilibili.com\";\n    public const string Account = \"https://account.bilibili.com\";\n    public const string Live = \"https://api.live.bilibili.com\";\n    public const string Mall = \"https://mall.bilibili.com\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/Constants.cs",
    "content": "namespace Ray.BiliBiliTool.Agent;\n\npublic static class Constants\n{\n    public const string Channel = \"bili\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/Extensions/ServiceCollectionExtension.cs",
    "content": "﻿using System.Net;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Options;\nusing Polly;\nusing Polly.Extensions.Http;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Services;\nusing Ray.BiliBiliTool.Agent.HttpClientDelegatingHandlers;\nusing Ray.BiliBiliTool.Agent.QingLong;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\n\nnamespace Ray.BiliBiliTool.Agent.Extensions;\n\npublic static class ServiceCollectionExtension\n{\n    /// <summary>\n    /// 注册强类型api客户端\n    /// </summary>\n    /// <param name=\"services\"></param>\n    /// <returns></returns>\n    public static IServiceCollection AddBiliBiliClientApi(\n        this IServiceCollection services,\n        IConfiguration configuration\n    )\n    {\n        //Cookie\n        services.AddSingleton<CookieStrFactory<BiliCookie>>();\n\n        //全局代理\n        services.SetGlobalProxy(configuration);\n\n        //DelegatingHandler\n        services.Scan(scan =>\n            scan.FromAssemblyOf<IBiliBiliApi>()\n                .AddClasses(classes => classes.AssignableTo<DelegatingHandler>())\n                .AsSelf()\n                .WithTransientLifetime()\n        );\n\n        //服务\n        services.AddScoped<IWbiService, WbiService>();\n\n        //bilibli\n        Action<IServiceProvider, HttpClient> config = (sp, c) =>\n        {\n            c.DefaultRequestHeaders.Add(\n                \"User-Agent\",\n                sp.GetRequiredService<IOptionsMonitor<SecurityOptions>>().CurrentValue.UserAgent\n            );\n        };\n        Action<IServiceProvider, HttpClient> configApp = (sp, c) =>\n        {\n            c.DefaultRequestHeaders.Add(\n                \"User-Agent\",\n                sp.GetRequiredService<IOptionsMonitor<SecurityOptions>>().CurrentValue.UserAgentApp\n            );\n        };\n\n        services.AddBiliBiliClientApi<IUserInfoApi>(BiliHosts.Api, config, true);\n\n        services.AddBiliBiliClientApi<IUpInfoApi>(BiliHosts.Api, config);\n        services.AddBiliBiliClientApi<IDailyTaskApi>(BiliHosts.Api, config);\n        services.AddBiliBiliClientApi<IRelationApi>(BiliHosts.Api, config);\n        services.AddBiliBiliClientApi<IChargeApi>(BiliHosts.Api, config);\n        services.AddBiliBiliClientApi<IVideoApi>(BiliHosts.Api, config);\n        services.AddBiliBiliClientApi<IVideoWithoutCookieApi>(BiliHosts.Api, config);\n        services.AddBiliBiliClientApi<IArticleApi>(BiliHosts.Api, config);\n\n        services.AddBiliBiliClientApi<IVipMallApi>(BiliHosts.Show, config);\n        services.AddBiliBiliClientApi<IPassportApi>(BiliHosts.Passport, config);\n        services.AddBiliBiliClientApi<ILiveTraceApi>(BiliHosts.LiveTrace, config);\n        services.AddBiliBiliClientApi<IHomeApi>(BiliHosts.Www, config);\n        services.AddBiliBiliClientApi<IMangaApi>(BiliHosts.Manga, config);\n        services.AddBiliBiliClientApi<IAccountApi>(BiliHosts.Account, config);\n        services.AddBiliBiliClientApi<ILiveApi>(BiliHosts.Live, config);\n\n        services.AddBiliBiliClientApi<IVipBigPointApi>(BiliHosts.App, configApp);\n        services.AddBiliBiliClientApi<IMallApi>(BiliHosts.Mall, configApp);\n\n        //qinglong\n        var qinglongHost = configuration[\"QL_URL\"] ?? \"http://localhost:5600\";\n        services\n            .AddHttpApi<IQingLongApi>(o =>\n            {\n                o.HttpHost = new Uri(qinglongHost);\n                o.UseDefaultUserAgent = false;\n            })\n            .ConfigureHttpClient(\n                (sp, c) =>\n                {\n                    c.DefaultRequestHeaders.Add(\n                        \"User-Agent\",\n                        sp.GetRequiredService<\n                            IOptionsMonitor<SecurityOptions>\n                        >().CurrentValue.UserAgent\n                    );\n                }\n            )\n            .AddPolicyHandler(GetRetryPolicy());\n\n        return services;\n    }\n\n    /// <summary>\n    /// 封装Refit，默认将Cookie添加到Header中\n    /// </summary>\n    /// <typeparam name=\"TInterface\"></typeparam>\n    /// <param name=\"services\"></param>\n    /// <param name=\"host\"></param>\n    /// <returns></returns>\n    private static IServiceCollection AddBiliBiliClientApi<TInterface>(\n        this IServiceCollection services,\n        string host,\n        Action<IServiceProvider, HttpClient> config,\n        bool ignorWrid = false\n    )\n        where TInterface : class\n    {\n        var uri = new Uri(host);\n        IHttpClientBuilder httpClientBuilder = services\n            .AddHttpApi<TInterface>(o =>\n            {\n                o.HttpHost = uri;\n                o.UseDefaultUserAgent = false;\n            })\n            .ConfigureHttpClient(config)\n            .AddHttpMessageHandler<IntervalDelegatingHandler>()\n            .AddPolicyHandler(GetRetryPolicy());\n\n        if (!ignorWrid)\n        {\n            httpClientBuilder.AddHttpMessageHandler<WridEncryptionDelegatingHandler>();\n        }\n\n        return services;\n    }\n\n    /// <summary>\n    /// 设置全局代理(如果配置了代理)\n    /// </summary>\n    /// <param name=\"services\"></param>\n    /// <returns></returns>\n    private static IServiceCollection SetGlobalProxy(\n        this IServiceCollection services,\n        IConfiguration configuration\n    )\n    {\n        var proxyAddress = configuration[\"Security:WebProxy\"];\n        if (!string.IsNullOrWhiteSpace(proxyAddress))\n        {\n            WebProxy webProxy = new WebProxy();\n\n            //user:password@host:port http proxy only .Tested with tinyproxy-1.11.0-rc1\n            if (proxyAddress!.Contains(\"@\"))\n            {\n                string userPass = proxyAddress.Split(\"@\")[0];\n                string address = proxyAddress.Split(\"@\")[1];\n\n                string proxyUser = \"\";\n                string proxyPass = \"\";\n                if (userPass.Contains(\":\"))\n                {\n                    proxyUser = userPass.Split(\":\")[0];\n                    proxyPass = userPass.Split(\":\")[1];\n                }\n\n                webProxy.Address = new Uri(\"http://\" + address);\n                webProxy.Credentials = new NetworkCredential(proxyUser, proxyPass);\n            }\n            else\n            {\n                webProxy.Address = new Uri(proxyAddress);\n            }\n\n            HttpClient.DefaultProxy = webProxy;\n        }\n\n        return services;\n    }\n\n    static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()\n    {\n        return HttpPolicyExtensions\n            .HandleTransientHttpError()\n            .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)\n            .WaitAndRetryAsync(1, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/HttpClientDelegatingHandlers/IntervalDelegatingHandler.cs",
    "content": "﻿using Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Config.Options;\n\nnamespace Ray.BiliBiliTool.Agent.HttpClientDelegatingHandlers;\n\npublic class IntervalDelegatingHandler(IOptionsMonitor<SecurityOptions> securityOptions)\n    : DelegatingHandler\n{\n    private readonly Dictionary<string, int> _special = new()\n    {\n        { \"/xlive/lottery-interface/v1/Anchor/Join\", 3 }, //天选抽奖，有时效，不能间隔过久，使用默认3秒\n        { \"/xlive/data-interface/v1/x25Kn/E\", 1 },\n        { \"/xlive/data-interface/v1/x25Kn/X\", 1 },\n    };\n\n    protected override async Task<HttpResponseMessage> SendAsync(\n        HttpRequestMessage request,\n        CancellationToken cancellationToken\n    )\n    {\n        await IntervalForSecurityAsync(request, cancellationToken);\n        return await base.SendAsync(request, cancellationToken);\n    }\n\n    private async Task IntervalForSecurityAsync(\n        HttpRequestMessage request,\n        CancellationToken cancellationToken\n    )\n    {\n        if (securityOptions.CurrentValue.IntervalSecondsBetweenRequestApi <= 0)\n            return;\n        if (!securityOptions.CurrentValue.GetIntervalMethods().Contains(request.Method))\n            return;\n\n        int seconds = 0;\n        //需要特殊处理的接口\n        if (_special.TryGetValue(request.RequestUri?.AbsolutePath ?? \"\", out int s))\n        {\n            seconds = s;\n        }\n        else\n        {\n            int maxSeconds = securityOptions.CurrentValue.IntervalSecondsBetweenRequestApi;\n            seconds = new Random().Next(maxSeconds / 2, maxSeconds + 1);\n        }\n\n        await Task.Delay(seconds * 1000, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/HttpClientDelegatingHandlers/LogDelegatingHandler.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\n\nnamespace Ray.BiliBiliTool.Agent.HttpClientDelegatingHandlers;\n\npublic class LogDelegatingHandler(ILogger<LogDelegatingHandler> logger) : DelegatingHandler\n{\n    protected override async Task<HttpResponseMessage> SendAsync(\n        HttpRequestMessage request,\n        CancellationToken cancellationToken\n    )\n    {\n        //记录请求内容\n        logger.LogDebug(\"发起请求：[{method}] {uri}\", request.Method, request.RequestUri);\n\n        if (request.Content != null)\n        {\n            var requestContent = await request.Content.ReadAsStringAsync(cancellationToken);\n            logger.LogDebug(\"请求Content： {content}\", requestContent);\n        }\n\n        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);\n\n        var content = await response.Content.ReadAsStringAsync(cancellationToken);\n        logger.LogDebug(\"返回Content：{content}\", content);\n\n        return response;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/QingLong/Dtos/AddQingLongEnv.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.QingLong.Dtos;\n\npublic class AddQingLongEnv\n{\n    public required string name { get; set; }\n    public required string value { get; set; }\n    public string? remarks { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/QingLong/Dtos/QingLongEnv.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.QingLong.Dtos;\n\npublic class QingLongEnv : UpdateQingLongEnv\n{\n    public required string timestamp { get; set; }\n    public int status { get; set; }\n\n    //public long position { get; set; }\n    public DateTime createdAt { get; set; }\n    public DateTime updatedAt { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/QingLong/Dtos/QingLongGenericResponse.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.QingLong.Dtos;\n\npublic class QingLongGenericResponse<T>\n{\n    public int Code { get; set; }\n\n    public required T Data { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/QingLong/Dtos/TokenResponse.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.QingLong.Dtos;\n\npublic class TokenResponse\n{\n    public required string token { get; set; }\n\n    public required string token_type { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/QingLong/Dtos/UpdateQingLongEnv.cs",
    "content": "namespace Ray.BiliBiliTool.Agent.QingLong.Dtos;\n\npublic class UpdateQingLongEnv : AddQingLongEnv\n{\n    public long id { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/QingLong/IQingLongApi.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.Attributes;\nusing Ray.BiliBiliTool.Agent.QingLong.Dtos;\nusing WebApiClientCore.Attributes;\n\nnamespace Ray.BiliBiliTool.Agent.QingLong;\n\n[LogFilter]\npublic interface IQingLongApi\n{\n    [HttpGet(\"/open/auth/token\")]\n    Task<QingLongGenericResponse<TokenResponse>> GetTokenAsync(\n        string client_id,\n        string client_secret\n    );\n\n    [HttpGet(\"/open/envs\")]\n    Task<QingLongGenericResponse<List<QingLongEnv>>> GetEnvsAsync(\n        string searchValue,\n        [Header(\"Authorization\")] string token\n    );\n\n    [HttpPost(\"/open/envs\")]\n    Task<QingLongGenericResponse<List<QingLongEnv>>> AddEnvsAsync(\n        [JsonContent] List<AddQingLongEnv> envs,\n        [Header(\"Authorization\")] string token\n    );\n\n    [HttpPut(\"/open/envs\")]\n    Task<QingLongGenericResponse<QingLongEnv>> UpdateEnvsAsync(\n        [JsonContent] UpdateQingLongEnv env,\n        [Header(\"Authorization\")] string token\n    );\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Agent/Ray.BiliBiliTool.Agent.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Extensions.Http.Polly\" />\n    <PackageReference Include=\"Scrutor\" />\n    <PackageReference Include=\"WebApiClientCore\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Config\\Ray.BiliBiliTool.Config.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Infrastructure\\Ray.BiliBiliTool.Infrastructure.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/AppService.cs",
    "content": "﻿using Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic abstract class AppService : IAppService\n{\n    public abstract Task DoTaskAsync(CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/Attributes/TaskInterceptorAttribute.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Rougamo;\nusing Rougamo.Context;\n\nnamespace Ray.BiliBiliTool.Application.Attributes;\n\n/// <summary>\n/// 任务拦截器\n/// </summary>\npublic class TaskInterceptorAttribute(\n    string? taskName = null,\n    TaskLevel taskLevel = TaskLevel.Two,\n    bool rethrowWhenException = true\n) : MoAttribute\n{\n    private readonly ILogger _logger = Global.ServiceProviderRoot!.GetRequiredService<\n        ILogger<TaskInterceptorAttribute>\n    >();\n\n    public override void OnEntry(MethodContext context)\n    {\n        if (taskName == null)\n            return;\n        string end = taskLevel == TaskLevel.One ? Environment.NewLine : \"\";\n        string delimiter = GetDelimiters();\n        _logger.LogInformation(delimiter + \"开始 {taskName} \" + delimiter + end, taskName);\n    }\n\n    public override void OnExit(MethodContext context)\n    {\n        if (taskName == null)\n            return;\n\n        string delimiter = GetDelimiters();\n        var append = new string(GetDelimiter(), taskName.Length);\n\n        _logger.LogInformation(\n            delimiter + append + \"结束\" + append + delimiter + Environment.NewLine\n        );\n    }\n\n    public override void OnException(MethodContext context)\n    {\n        if (rethrowWhenException)\n        {\n            _logger.LogError(\"程序发生异常：{msg}\", context.Exception?.Message ?? \"\");\n            base.OnException(context);\n            return;\n        }\n\n        _logger.LogError(\n            \"{task}失败，继续其他任务。失败信息:{msg}\" + Environment.NewLine,\n            taskName,\n            context.Exception?.Message ?? \"\"\n        );\n        context.HandledException(this, null);\n    }\n\n    private string GetDelimiters()\n    {\n        char delimiter = GetDelimiter();\n\n        int count = Convert.ToInt32(taskLevel.DefaultValue());\n        return new string(delimiter, count);\n    }\n\n    private char GetDelimiter()\n    {\n        return taskLevel switch\n        {\n            TaskLevel.One => '=',\n            TaskLevel.Two => '-',\n            TaskLevel.Three => '-',\n            _ => throw new ArgumentOutOfRangeException(nameof(taskLevel), taskLevel, null),\n        };\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/Attributes/TaskLevel.cs",
    "content": "using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Application.Attributes;\n\npublic enum TaskLevel\n{\n    [DefaultValue(5)]\n    One,\n\n    [DefaultValue(3)]\n    Two,\n\n    [DefaultValue(2)]\n    Three,\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/BaseMultiAccountsAppService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic abstract class BaseMultiAccountsAppService(\n    ILogger logger,\n    CookieStrFactory<BiliCookie> cookieStrFactory\n) : AppService\n{\n    public override async Task DoTaskAsync(CancellationToken cancellationToken = default)\n    {\n        logger.LogInformation(\n            \"【账号个数】{count}个\" + Environment.NewLine,\n            cookieStrFactory.Count\n        );\n        for (int i = 0; i < cookieStrFactory.Count; i++)\n        {\n            logger.LogInformation(\"######### 账号 {num} #########\" + Environment.NewLine, i);\n            var ck = cookieStrFactory.GetCookie(i);\n            try\n            {\n                await DoTaskAccountAsync(ck, cancellationToken);\n            }\n            catch (Exception e)\n            {\n                //ignore\n                logger.LogWarning(\"异常：{msg}\", e);\n            }\n        }\n    }\n\n    protected abstract Task DoTaskAccountAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    );\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/ChargeTaskAppService.cs",
    "content": "﻿using Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Application.Attributes;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\nusing Ray.BiliBiliTool.Infrastructure.Enums;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic class ChargeTaskAppService(\n    ILogger<ChargeTaskAppService> logger,\n    IOptionsMonitor<ChargeTaskOptions> chargeTaskOptions,\n    IAccountDomainService accountDomainService,\n    IChargeDomainService chargeDomainService,\n    ILoginDomainService loginDomainService,\n    IConfiguration configuration,\n    CookieStrFactory<BiliCookie> cookieStrFactory\n) : BaseMultiAccountsAppService(logger, cookieStrFactory), IChargeTaskAppService\n{\n    [TaskInterceptor(\"免费B币券充电任务\", TaskLevel.One)]\n    protected override async Task DoTaskAccountAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!chargeTaskOptions.CurrentValue.IsEnable)\n        {\n            logger.LogInformation(\"已配置为关闭，跳过\");\n            return;\n        }\n\n        await SetCookiesAsync(ck, cancellationToken);\n        UserInfo userInfo = await Login(ck);\n        await Charge(userInfo, ck);\n    }\n\n    [TaskInterceptor(\"Set Cookie\")]\n    private async Task SetCookiesAsync(BiliCookie biliCookie, CancellationToken cancellationToken)\n    {\n        //判断cookie是否完整\n        if (!string.IsNullOrWhiteSpace(biliCookie.Buvid))\n        {\n            logger.LogInformation(\"Cookie完整，不需要Set Cookie\");\n            return;\n        }\n\n        //Set\n        logger.LogInformation(\"开始Set Cookie\");\n        var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken);\n\n        //持久化\n        logger.LogInformation(\"持久化Cookie\");\n        await SaveCookieAsync(ck, cancellationToken);\n    }\n\n    /// <summary>\n    /// 登录\n    /// </summary>\n    /// <returns></returns>\n    [TaskInterceptor(\"登录\")]\n    private async Task<UserInfo> Login(BiliCookie ck)\n    {\n        UserInfo userInfo = await accountDomainService.LoginByCookie(ck);\n        return userInfo;\n    }\n\n    /// <summary>\n    /// 每月为自己充电\n    /// </summary>\n    [TaskInterceptor(\"B币券充电\", rethrowWhenException: false)]\n    private async Task Charge(UserInfo userInfo, BiliCookie ck)\n    {\n        await chargeDomainService.Charge(userInfo, ck);\n    }\n\n    private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken)\n    {\n        var platformType = configuration.GetSection(\"PlatformType\").Get<PlatformType>();\n        logger.LogInformation(\"当前运行平台：{platform}\", platformType);\n\n        //更新cookie到青龙env\n        if (platformType == PlatformType.QingLong)\n        {\n            await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken);\n            return;\n        }\n\n        //更新cookie到json\n        await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/DailyTaskAppService.cs",
    "content": "﻿using Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Application.Attributes;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\nusing Ray.BiliBiliTool.Infrastructure.Enums;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic class DailyTaskAppService(\n    ILogger<DailyTaskAppService> logger,\n    IAccountDomainService accountDomainService,\n    IVideoDomainService videoDomainService,\n    IArticleDomainService articleDomainService,\n    IDonateCoinDomainService donateCoinDomainService,\n    IVipPrivilegeDomainService vipPrivilegeDomainService,\n    IOptionsMonitor<DailyTaskOptions> dailyTaskOptions,\n    ILoginDomainService loginDomainService,\n    IConfiguration configuration,\n    CookieStrFactory<BiliCookie> cookieStrFactory\n) : BaseMultiAccountsAppService(logger, cookieStrFactory), IDailyTaskAppService\n{\n    private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue;\n    private readonly Dictionary<string, int> _expDic = Config.Constants.ExpDic;\n\n    [TaskInterceptor(\"每日任务\", TaskLevel.One)]\n    protected override async Task DoTaskAccountAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!_dailyTaskOptions.IsEnable)\n        {\n            logger.LogInformation(\"已配置为关闭，跳过\");\n            return;\n        }\n\n        await SetCookiesAsync(ck, cancellationToken);\n\n        //每日任务赚经验：\n        UserInfo userInfo = await Login(ck);\n\n        DailyTaskInfo dailyTaskInfo = await GetDailyTaskStatus(ck);\n        await WatchAndShareVideo(dailyTaskInfo, ck);\n\n        await AddCoins(userInfo, ck);\n\n        await ReceiveVipPrivilege(userInfo, ck);\n    }\n\n    [TaskInterceptor(\"Set Cookie\")]\n    private async Task SetCookiesAsync(BiliCookie biliCookie, CancellationToken cancellationToken)\n    {\n        //判断cookie是否完整\n        if (!string.IsNullOrWhiteSpace(biliCookie.Buvid))\n        {\n            logger.LogInformation(\"Cookie完整，不需要Set Cookie\");\n            return;\n        }\n\n        //Set\n        logger.LogInformation(\"开始Set Cookie\");\n        var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken);\n\n        //持久化\n        logger.LogInformation(\"持久化Cookie\");\n        await SaveCookieAsync(ck, cancellationToken);\n    }\n\n    /// <summary>\n    /// 登录\n    /// </summary>\n    /// <returns></returns>\n    [TaskInterceptor(\"登录\")]\n    private async Task<UserInfo> Login(BiliCookie ck)\n    {\n        UserInfo userInfo = await accountDomainService.LoginByCookie(ck);\n\n        _expDic.TryGetValue(\"每日登录\", out int exp);\n        logger.LogInformation(\"登录成功，经验+{exp} √\", exp);\n\n        return userInfo;\n    }\n\n    /// <summary>\n    /// 获取任务完成情况\n    /// </summary>\n    /// <returns></returns>\n    [TaskInterceptor(rethrowWhenException: false)]\n    private async Task<DailyTaskInfo> GetDailyTaskStatus(BiliCookie ck)\n    {\n        return await accountDomainService.GetDailyTaskStatus(ck);\n    }\n\n    /// <summary>\n    /// 观看、分享视频\n    /// </summary>\n    [TaskInterceptor(\"观看、分享视频\", rethrowWhenException: false)]\n    private async Task WatchAndShareVideo(DailyTaskInfo dailyTaskInfo, BiliCookie ck)\n    {\n        if (!_dailyTaskOptions.IsWatchVideo && !_dailyTaskOptions.IsShareVideo)\n        {\n            logger.LogInformation(\"已配置为关闭，跳过任务\");\n            return;\n        }\n\n        await videoDomainService.WatchAndShareVideo(dailyTaskInfo, ck);\n    }\n\n    /// <summary>\n    /// 投币任务\n    /// </summary>\n    [TaskInterceptor(\"投币\", rethrowWhenException: false)]\n    private async Task AddCoins(UserInfo userInfo, BiliCookie ck)\n    {\n        if (_dailyTaskOptions.SaveCoinsWhenLv6 && userInfo.Level_info?.Current_level >= 6)\n        {\n            logger.LogInformation(\"已经为LV6大佬，开始白嫖\");\n            return;\n        }\n\n        if (_dailyTaskOptions.IsDonateCoinForArticle)\n        {\n            logger.LogInformation(\"专栏投币已开启\");\n\n            if (!await articleDomainService.AddCoinForArticles(ck))\n            {\n                logger.LogInformation(\"专栏投币结束，转入视频投币\");\n                await donateCoinDomainService.AddCoinsForVideos(ck);\n            }\n        }\n        else\n        {\n            await donateCoinDomainService.AddCoinsForVideos(ck);\n        }\n    }\n\n    /// <summary>\n    /// 每月领取大会员福利\n    /// </summary>\n    [TaskInterceptor(\"领取大会员福利\", rethrowWhenException: false)]\n    private async Task ReceiveVipPrivilege(UserInfo userInfo, BiliCookie ck)\n    {\n        var suc = await vipPrivilegeDomainService.ReceiveVipPrivilege(userInfo, ck);\n\n        //如果领取成功，需要刷新账户信息（比如B币余额）\n        if (suc)\n        {\n            try\n            {\n                await accountDomainService.LoginByCookie(ck);\n            }\n            catch (Exception ex)\n            {\n                logger.LogError(\"领取福利成功，但之后刷新用户信息时异常，信息：{msg}\", ex.Message);\n            }\n        }\n    }\n\n    private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken)\n    {\n        var platformType = configuration.GetSection(\"PlatformType\").Get<PlatformType>();\n        logger.LogInformation(\"当前运行平台：{platform}\", platformType);\n\n        //更新cookie到青龙env\n        if (platformType == PlatformType.QingLong)\n        {\n            await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken);\n            return;\n        }\n\n        //更新cookie到json\n        await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/Extensions/ServiceCollectionExtension.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Application.Extensions;\n\npublic static class ServiceCollectionExtension\n{\n    public static IServiceCollection AddAppServices(this IServiceCollection services)\n    {\n        services.Scan(scan =>\n            scan.FromAssemblyOf<DailyTaskAppService>()\n                .AddClasses(classes => classes.AssignableTo<IAppService>())\n                .AsImplementedInterfaces()\n                .WithTransientLifetime()\n        );\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/FodyWeavers.xml",
    "content": "﻿<Weavers\n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xsi:noNamespaceSchemaLocation=\"FodyWeavers.xsd\"\n>\n    <Rougamo />\n</Weavers>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/FodyWeavers.xsd",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n  <!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->\n  <xs:element name=\"Weavers\">\n    <xs:complexType>\n      <xs:all>\n        <xs:element name=\"Rougamo\" minOccurs=\"0\" maxOccurs=\"1\">\n          <xs:complexType>\n            <xs:all>\n              <xs:element name=\"Mos\" minOccurs=\"0\" maxOccurs=\"1\">\n                <xs:complexType>\n                  <xs:sequence>\n                    <xs:element name=\"Mo\" minOccurs=\"0\" maxOccurs=\"unbounded\">\n                      <xs:complexType>\n                        <xs:attribute name=\"assembly\" type=\"xs:string\">\n                          <xs:annotation>\n                            <xs:documentation>The assembly name of the aspect type, which does not contain the '.dll' suffix.</xs:documentation>\n                          </xs:annotation>\n                        </xs:attribute>\n                        <xs:attribute name=\"type\" type=\"xs:string\">\n                          <xs:annotation>\n                            <xs:documentation>The aspect type full name.</xs:documentation>\n                          </xs:annotation>\n                        </xs:attribute>\n                        <xs:attribute name=\"pattern\" type=\"xs:string\">\n                          <xs:annotation>\n                            <xs:documentation>An AspectN pattern. Apply the aspect type to methods matched by the pattern. This pattern will override the pointcut settings of the aspect type.</xs:documentation>\n                          </xs:annotation>\n                        </xs:attribute>\n                      </xs:complexType>\n                    </xs:element>\n                  </xs:sequence>\n                </xs:complexType>\n              </xs:element>\n            </xs:all>\n            <xs:attribute name=\"enabled\" type=\"xs:boolean\">\n              <xs:annotation>\n                <xs:documentation>Set to false to disable Rougamo. The default is true.</xs:documentation>\n              </xs:annotation>\n            </xs:attribute>\n            <xs:attribute name=\"composite-accessibility\" type=\"xs:boolean\">\n              <xs:annotation>\n                <xs:documentation>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.</xs:documentation>\n              </xs:annotation>\n            </xs:attribute>\n            <xs:attribute name=\"skip-ref-struct\" type=\"xs:boolean\">\n              <xs:annotation>\n                <xs:documentation>Set to true to skip saving ref struct parameters and return value into MethodContext. The default is false.</xs:documentation>\n              </xs:annotation>\n            </xs:attribute>\n            <xs:attribute name=\"pure-stacktrace\" type=\"xs:boolean\">\n              <xs:annotation>\n                <xs:documentation>Set to false to prevent generating the StackTraceHiddenAttribute for the proxy method. The default is true.</xs:documentation>\n              </xs:annotation>\n            </xs:attribute>\n            <xs:attribute name=\"iterator-returns\" type=\"xs:boolean\">\n              <xs:annotation>\n                <xs:documentation>Set to true to save the items that the iterator returns. This will take up additional memory space. The default is false.</xs:documentation>\n              </xs:annotation>\n            </xs:attribute>\n            <xs:attribute name=\"reverse-call-nonentry\" type=\"xs:boolean\">\n              <xs:annotation>\n                <xs:documentation>Set to false to make the execution order of the OnSuccess, OnException, and OnExit methods the same as OnEntry. The default is true.</xs:documentation>\n              </xs:annotation>\n            </xs:attribute>\n            <xs:attribute name=\"except-type-patterns\" type=\"xs:string\">\n              <xs:annotation>\n                <xs:documentation>Regex expressions for the type's full name, separated by ',' or ';'. All types matching any of these regex expressions will be ignored by Rougamo.</xs:documentation>\n              </xs:annotation>\n            </xs:attribute>\n          </xs:complexType>\n        </xs:element>\n      </xs:all>\n      <xs:attribute name=\"VerifyAssembly\" type=\"xs:boolean\">\n        <xs:annotation>\n          <xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>\n        </xs:annotation>\n      </xs:attribute>\n      <xs:attribute name=\"VerifyIgnoreCodes\" type=\"xs:string\">\n        <xs:annotation>\n          <xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>\n        </xs:annotation>\n      </xs:attribute>\n      <xs:attribute name=\"GenerateXsd\" type=\"xs:boolean\">\n        <xs:annotation>\n          <xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>\n        </xs:annotation>\n      </xs:attribute>\n    </xs:complexType>\n  </xs:element>\n</xs:schema>"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/LiveFansMedalAppService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Application.Attributes;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic class LiveFansMedalAppService(\n    ILogger<LiveFansMedalAppService> logger,\n    IOptionsMonitor<LiveFansMedalTaskOptions> liveFansMedalTaskOptions,\n    ILiveDomainService liveDomainService,\n    CookieStrFactory<BiliCookie> cookieStrFactory\n) : BaseMultiAccountsAppService(logger, cookieStrFactory), ILiveFansMedalAppService\n{\n    [TaskInterceptor(\"直播间互动\", TaskLevel.One)]\n    protected override async Task DoTaskAccountAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!liveFansMedalTaskOptions.CurrentValue.IsEnable)\n        {\n            logger.LogInformation(\"已配置为关闭，跳过\");\n            return;\n        }\n\n        await SendDanmaku(ck);\n        await Like(ck);\n        await HeartBeat(ck);\n    }\n\n    [TaskInterceptor(\"发送弹幕\", TaskLevel.Two, false)]\n    private async Task SendDanmaku(BiliCookie ck)\n    {\n        await liveDomainService.SendDanmakuToFansMedalLive(ck);\n    }\n\n    [TaskInterceptor(\"点赞直播间\", TaskLevel.Two, false)]\n    private async Task Like(BiliCookie ck)\n    {\n        await liveDomainService.LikeFansMedalLive(ck);\n    }\n\n    [TaskInterceptor(\"直播时长挂机\", TaskLevel.Two, false)]\n    private async Task HeartBeat(BiliCookie ck)\n    {\n        await liveDomainService.SendHeartBeatToFansMedalLive(ck);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/LiveLotteryTaskAppService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Application.Attributes;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic class LiveLotteryTaskAppService(\n    ILiveDomainService liveDomainService,\n    IOptionsMonitor<LiveLotteryTaskOptions> liveLotteryTaskOptions,\n    ILogger<LiveLotteryTaskAppService> logger,\n    IAccountDomainService accountDomainService,\n    CookieStrFactory<BiliCookie> cookieStrFactory\n) : BaseMultiAccountsAppService(logger, cookieStrFactory), ILiveLotteryTaskAppService\n{\n    private readonly LiveLotteryTaskOptions _liveLotteryTaskOptions =\n        liveLotteryTaskOptions.CurrentValue;\n\n    [TaskInterceptor(\"天选时刻抽奖\", TaskLevel.One)]\n    protected override async Task DoTaskAccountAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!liveLotteryTaskOptions.CurrentValue.IsEnable)\n        {\n            logger.LogInformation(\"已配置为关闭，跳过\");\n            return;\n        }\n\n        await LogUserInfo(ck);\n        await LotteryTianXuan(ck);\n        await AutoGroupFollowings(ck);\n    }\n\n    [TaskInterceptor(\"打印用户信息\")]\n    private async Task LogUserInfo(BiliCookie ck)\n    {\n        await accountDomainService.LoginByCookie(ck);\n    }\n\n    [TaskInterceptor(\"抽奖\")]\n    private async Task LotteryTianXuan(BiliCookie ck)\n    {\n        await liveDomainService.TianXuan(ck);\n    }\n\n    [TaskInterceptor(\"自动分组关注的主播\")]\n    private async Task AutoGroupFollowings(BiliCookie ck)\n    {\n        if (_liveLotteryTaskOptions.AutoGroupFollowings)\n        {\n            await liveDomainService.GroupFollowing(ck);\n        }\n        else\n        {\n            logger.LogInformation(\"配置未开启，跳过\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/LoginTaskAppService.cs",
    "content": "using Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Logging;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Application.Attributes;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Enums;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic class LoginTaskAppService(\n    IConfiguration configuration,\n    ILogger<LoginTaskAppService> logger,\n    ILoginDomainService loginDomainService\n) : AppService, ILoginTaskAppService\n{\n    [TaskInterceptor(\"扫码登录\", TaskLevel.One)]\n    public override async Task DoTaskAsync(CancellationToken cancellationToken = default)\n    {\n        //扫码登录\n        var cookieInfo = await QrCodeLoginAsync(cancellationToken);\n        if (cookieInfo == null)\n            return;\n\n        //set cookie\n        cookieInfo = await SetCookiesAsync(cookieInfo, cancellationToken);\n\n        //持久化cookie\n        await SaveCookieAsync(cookieInfo, cancellationToken);\n    }\n\n    [TaskInterceptor(\"获取二维码\")]\n    private async Task<BiliCookie> QrCodeLoginAsync(CancellationToken cancellationToken)\n    {\n        var biliCookie = await loginDomainService.LoginByQrCodeAsync(cancellationToken);\n        return biliCookie;\n    }\n\n    [TaskInterceptor(\"Set Cookie\")]\n    private async Task<BiliCookie> SetCookiesAsync(\n        BiliCookie biliCookie,\n        CancellationToken cancellationToken\n    )\n    {\n        var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken);\n        return ck;\n    }\n\n    [TaskInterceptor(\"持久化Cookie\")]\n    private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken)\n    {\n        var platformType = configuration.GetSection(\"PlatformType\").Get<PlatformType>();\n        logger.LogInformation(\"当前运行平台：{platform}\", platformType);\n\n        //更新cookie到青龙env\n        if (platformType == PlatformType.QingLong)\n        {\n            await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken);\n            return;\n        }\n\n        //更新cookie到json\n        await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/MangaPrivilegeTaskAppService.cs",
    "content": "﻿using Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Application.Attributes;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\nusing Ray.BiliBiliTool.Infrastructure.Enums;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic class MangaPrivilegeTaskAppService(\n    ILogger<MangaPrivilegeTaskAppService> logger,\n    IOptionsMonitor<MangaPrivilegeTaskOptions> mangaPrivilegeTaskOptions,\n    IAccountDomainService accountDomainService,\n    IMangaDomainService mangaDomainService,\n    ILoginDomainService loginDomainService,\n    IConfiguration configuration,\n    CookieStrFactory<BiliCookie> cookieStrFactory\n) : BaseMultiAccountsAppService(logger, cookieStrFactory), IMangaPrivilegeTaskAppService\n{\n    [TaskInterceptor(\"每月领取大会员漫画权益任务\", TaskLevel.One)]\n    protected override async Task DoTaskAccountAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!mangaPrivilegeTaskOptions.CurrentValue.IsEnable)\n        {\n            logger.LogInformation(\"已配置为关闭，跳过\");\n            return;\n        }\n\n        await SetCookiesAsync(ck, cancellationToken);\n        UserInfo userInfo = await Login(ck);\n        await ReceiveMangaVipReward(userInfo, ck);\n    }\n\n    [TaskInterceptor(\"Set Cookie\")]\n    private async Task SetCookiesAsync(BiliCookie biliCookie, CancellationToken cancellationToken)\n    {\n        //判断cookie是否完整\n        if (!string.IsNullOrWhiteSpace(biliCookie.Buvid))\n        {\n            logger.LogInformation(\"Cookie完整，不需要Set Cookie\");\n            return;\n        }\n\n        //Set\n        logger.LogInformation(\"开始Set Cookie\");\n        var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken);\n\n        //持久化\n        logger.LogInformation(\"持久化Cookie\");\n        await SaveCookieAsync(ck, cancellationToken);\n    }\n\n    /// <summary>\n    /// 登录\n    /// </summary>\n    /// <returns></returns>\n    [TaskInterceptor(\"登录\")]\n    private async Task<UserInfo> Login(BiliCookie ck)\n    {\n        UserInfo userInfo = await accountDomainService.LoginByCookie(ck);\n\n        return userInfo;\n    }\n\n    /// <summary>\n    /// 每月获取大会员漫画权益\n    /// </summary>\n    [TaskInterceptor(\"领取大会员漫画权益\", rethrowWhenException: false)]\n    private async Task ReceiveMangaVipReward(UserInfo userInfo, BiliCookie ck)\n    {\n        await mangaDomainService.ReceiveMangaVipReward(1, userInfo, ck);\n    }\n\n    private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken)\n    {\n        var platformType = configuration.GetSection(\"PlatformType\").Get<PlatformType>();\n        logger.LogInformation(\"当前运行平台：{platform}\", platformType);\n\n        //更新cookie到青龙env\n        if (platformType == PlatformType.QingLong)\n        {\n            await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken);\n            return;\n        }\n\n        //更新cookie到json\n        await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/MangaTaskAppService.cs",
    "content": "﻿using Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Application.Attributes;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\nusing Ray.BiliBiliTool.Infrastructure.Enums;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic class MangaTaskAppService(\n    ILogger<MangaTaskAppService> logger,\n    IOptionsMonitor<MangaTaskOptions> mangaTaskOptions,\n    IAccountDomainService accountDomainService,\n    IMangaDomainService mangaDomainService,\n    ILoginDomainService loginDomainService,\n    IConfiguration configuration,\n    CookieStrFactory<BiliCookie> cookieStrFactory\n) : BaseMultiAccountsAppService(logger, cookieStrFactory), IMangaTaskAppService\n{\n    [TaskInterceptor(\"漫画任务\", TaskLevel.One)]\n    protected override async Task DoTaskAccountAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!mangaTaskOptions.CurrentValue.IsEnable)\n        {\n            logger.LogInformation(\"已配置为关闭，跳过\");\n            return;\n        }\n\n        await SetCookiesAsync(ck, cancellationToken);\n        await Login(ck);\n\n        await MangaSign(ck);\n        await MangaRead(ck);\n    }\n\n    [TaskInterceptor(\"Set Cookie\")]\n    private async Task SetCookiesAsync(BiliCookie biliCookie, CancellationToken cancellationToken)\n    {\n        //判断cookie是否完整\n        if (!string.IsNullOrWhiteSpace(biliCookie.Buvid))\n        {\n            logger.LogInformation(\"Cookie完整，不需要Set Cookie\");\n            return;\n        }\n\n        //Set\n        logger.LogInformation(\"开始Set Cookie\");\n        var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken);\n\n        //持久化\n        logger.LogInformation(\"持久化Cookie\");\n        await SaveCookieAsync(ck, cancellationToken);\n    }\n\n    /// <summary>\n    /// 登录\n    /// </summary>\n    /// <returns></returns>\n    [TaskInterceptor(\"登录\")]\n    private async Task Login(BiliCookie ck)\n    {\n        await accountDomainService.LoginByCookie(ck);\n    }\n\n    /// <summary>\n    /// 漫画签到\n    /// </summary>\n    [TaskInterceptor(\"漫画签到\", rethrowWhenException: false)]\n    private async Task MangaSign(BiliCookie ck)\n    {\n        await mangaDomainService.MangaSign(ck);\n    }\n\n    /// <summary>\n    /// 漫画阅读\n    /// </summary>\n    [TaskInterceptor(\"漫画阅读\", rethrowWhenException: false)]\n    private async Task MangaRead(BiliCookie ck)\n    {\n        await mangaDomainService.MangaRead(ck);\n    }\n\n    private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken)\n    {\n        var platformType = configuration.GetSection(\"PlatformType\").Get<PlatformType>();\n        logger.LogInformation(\"当前运行平台：{platform}\", platformType);\n\n        //更新cookie到青龙env\n        if (platformType == PlatformType.QingLong)\n        {\n            await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken);\n            return;\n        }\n\n        //更新cookie到json\n        await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/Ray.BiliBiliTool.Application.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <NoWarn>$(NoWarn);CS8600;CS8601</NoWarn>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Extensions.FileProviders.Physical\" />\n    <PackageReference Include=\"Microsoft.Extensions.Http\" />\n    <PackageReference Include=\"QRCoder\" />\n    <PackageReference Include=\"Rougamo.Fody\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Agent\\Ray.BiliBiliTool.Agent.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Application.Contracts\\Ray.BiliBiliTool.Application.Contracts.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Config\\Ray.BiliBiliTool.Config.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.DomainService\\Ray.BiliBiliTool.DomainService.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Infrastructure\\Ray.BiliBiliTool.Infrastructure.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/Silver2CoinTaskAppService.cs",
    "content": "﻿using Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Application.Attributes;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\nusing Ray.BiliBiliTool.Infrastructure.Enums;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic class Silver2CoinTaskAppService(\n    ILogger<Silver2CoinTaskAppService> logger,\n    IOptionsMonitor<Silver2CoinTaskOptions> silver2CoinTaskOptions,\n    IAccountDomainService accountDomainService,\n    ILoginDomainService loginDomainService,\n    IConfiguration configuration,\n    ILiveDomainService liveDomainService,\n    ICoinDomainService coinDomainService,\n    CookieStrFactory<BiliCookie> cookieStrFactory\n) : BaseMultiAccountsAppService(logger, cookieStrFactory), ISilver2CoinTaskAppService\n{\n    [TaskInterceptor(\"银瓜子兑换硬币任务\", TaskLevel.One)]\n    protected override async Task DoTaskAccountAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!silver2CoinTaskOptions.CurrentValue.IsEnable)\n        {\n            logger.LogInformation(\"已配置为关闭，跳过\");\n            return;\n        }\n\n        await SetCookiesAsync(ck, cancellationToken);\n        await Login(ck);\n\n        await ExchangeSilver2Coin(ck);\n    }\n\n    [TaskInterceptor(\"Set Cookie\")]\n    private async Task SetCookiesAsync(BiliCookie biliCookie, CancellationToken cancellationToken)\n    {\n        //判断cookie是否完整\n        if (!string.IsNullOrWhiteSpace(biliCookie.Buvid))\n        {\n            logger.LogInformation(\"Cookie完整，不需要Set Cookie\");\n            return;\n        }\n\n        //Set\n        logger.LogInformation(\"开始Set Cookie\");\n        var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken);\n\n        //持久化\n        logger.LogInformation(\"持久化Cookie\");\n        await SaveCookieAsync(ck, cancellationToken);\n    }\n\n    /// <summary>\n    /// 登录\n    /// </summary>\n    /// <returns></returns>\n    [TaskInterceptor(\"登录\")]\n    private async Task Login(BiliCookie ck)\n    {\n        await accountDomainService.LoginByCookie(ck);\n    }\n\n    /// <summary>\n    /// 直播中心的银瓜子兑换硬币\n    /// </summary>\n    [TaskInterceptor(\"银瓜子兑换硬币\", rethrowWhenException: false)]\n    private async Task ExchangeSilver2Coin(BiliCookie ck)\n    {\n        var success = await liveDomainService.ExchangeSilver2Coin(ck);\n        if (!success)\n            return;\n\n        //如果兑换成功，则打印硬币余额\n        var coinBalance = coinDomainService.GetCoinBalance(ck);\n        logger.LogInformation(\"【硬币余额】 {coin}\", coinBalance);\n    }\n\n    private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken)\n    {\n        var platformType = configuration.GetSection(\"PlatformType\").Get<PlatformType>();\n        logger.LogInformation(\"当前运行平台：{platform}\", platformType);\n\n        //更新cookie到青龙env\n        if (platformType == PlatformType.QingLong)\n        {\n            await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken);\n            return;\n        }\n\n        //更新cookie到json\n        await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/TestAppService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Application.Attributes;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic class TestAppService(\n    ILogger<TestAppService> logger,\n    IAccountDomainService accountDomainService,\n    CookieStrFactory<BiliCookie> cookieStrFactory\n) : BaseMultiAccountsAppService(logger, cookieStrFactory), ITestAppService\n{\n    [TaskInterceptor(\"测试Cookie\")]\n    protected override async Task DoTaskAccountAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await accountDomainService.LoginByCookie(ck);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/UnfollowBatchedTaskAppService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Application.Attributes;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic class UnfollowBatchedTaskAppService(\n    ILogger<UnfollowBatchedTaskAppService> logger,\n    IOptionsMonitor<UnfollowBatchedTaskOptions> unfollowBatchedTaskOptions,\n    IAccountDomainService accountDomainService,\n    CookieStrFactory<BiliCookie> cookieStrFactory\n) : BaseMultiAccountsAppService(logger, cookieStrFactory), IUnfollowBatchedTaskAppService\n{\n    [TaskInterceptor(\"批量取关\", TaskLevel.One)]\n    protected override async Task DoTaskAccountAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!unfollowBatchedTaskOptions.CurrentValue.IsEnable)\n        {\n            logger.LogInformation(\"已配置为关闭，跳过\");\n            return;\n        }\n\n        await accountDomainService.UnfollowBatched(ck);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/VipBigPointAppService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall;\nusing Ray.BiliBiliTool.Application.Attributes;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic class VipBigPointAppService(\n    ILogger<VipBigPointAppService> logger,\n    IOptionsMonitor<VipBigPointOptions> vipBigPointOptions,\n    IAccountDomainService loginDomainService,\n    IVipBigPointDomainService vipBigPointDomainService,\n    CookieStrFactory<BiliCookie> cookieFactory\n) : BaseMultiAccountsAppService(logger, cookieFactory), IVipBigPointAppService\n{\n    [TaskInterceptor(\"大会员大积分\", TaskLevel.One)]\n    protected override async Task DoTaskAccountAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!vipBigPointOptions.CurrentValue.IsEnable)\n        {\n            logger.LogInformation(\"已配置为关闭，跳过\");\n            return;\n        }\n\n        bool isVip = await LoginAndCheckVipStatusAsync(ck, cancellationToken);\n        if (!isVip)\n        {\n            return;\n        }\n\n        await ExpressAsync(ck, cancellationToken);\n        await SignAsync(ck, cancellationToken);\n        var combine = await CheckCombineAsync(ck, cancellationToken);\n\n        // 2 个一次性任务\n        await BonusMissionAsync(combine, ck, cancellationToken);\n        await PrivilegeMissionAsync(combine, ck, cancellationToken);\n\n        // 日常任务\n        await ReceiveMissionsAsync(combine, ck, cancellationToken);\n        await DailyMissionsAsync(combine, ck, cancellationToken);\n\n        await CheckCombineAsync(ck, cancellationToken);\n    }\n\n    [TaskInterceptor(\"登录并检测会员状态\")]\n    private async Task<bool> LoginAndCheckVipStatusAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        UserInfo userInfo = await loginDomainService.LoginByCookie(ck);\n        if (userInfo.GetVipType() == VipType.None)\n        {\n            logger.LogInformation(\"当前不是大会员，跳过任务\");\n            return false;\n        }\n\n        return true;\n    }\n\n    [TaskInterceptor(\"查看大会员大积分状态\")]\n    private async Task<VipBigPointCombine> CheckCombineAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        VipBigPointCombine combine = await vipBigPointDomainService.GetCombineAsync(ck);\n        combine.LogFullInfo(logger);\n        return combine;\n    }\n\n    /// <summary>\n    /// 领经验（专属等级加速包），观看视频 1 分钟领取 10 经验\n    /// </summary>\n    /// <param name=\"ck\"></param>\n    /// <param name=\"cancellationToken\"></param>\n    [TaskInterceptor(\"大会员经验观看任务\", rethrowWhenException: false)]\n    private async Task ExpressAsync(BiliCookie ck, CancellationToken cancellationToken = default)\n    {\n        await vipBigPointDomainService.VipExpressAsync(ck);\n    }\n\n    [TaskInterceptor(\"签到任务\", rethrowWhenException: false)]\n    private async Task SignAsync(BiliCookie ck, CancellationToken cancellationToken = default)\n    {\n        await vipBigPointDomainService.SignAsync(ck);\n    }\n\n    [TaskInterceptor(\"领取日常任务\", rethrowWhenException: false)]\n    private async Task ReceiveMissionsAsync(\n        VipBigPointCombine combine,\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await vipBigPointDomainService.ReceiveDailyMissionsAsync(combine, ck);\n    }\n\n    [TaskInterceptor(\"福利任务\", rethrowWhenException: false)]\n    private async Task BonusMissionAsync(\n        VipBigPointCombine combine,\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await vipBigPointDomainService.ReceiveAndCompleteAsync(\n            combine,\n            \"福利任务\",\n            \"bonus\",\n            ck,\n            async (_, _) => await vipBigPointDomainService.CompleteAsync(\"bonus\", ck)\n        );\n    }\n\n    [TaskInterceptor(\"体验任务\", rethrowWhenException: false)]\n    private async Task PrivilegeMissionAsync(\n        VipBigPointCombine combine,\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await vipBigPointDomainService.ReceiveAndCompleteAsync(\n            combine,\n            \"体验任务\",\n            \"privilege\",\n            ck,\n            async (_, _) => await vipBigPointDomainService.CompleteAsync(\"privilege\", ck)\n        );\n    }\n\n    [TaskInterceptor(\"日常任务\", rethrowWhenException: false)]\n    private async Task DailyMissionsAsync(\n        VipBigPointCombine combine,\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await DailyDressViewMissionAsync(combine, ck, cancellationToken);\n        await DailyVipMallViewMissionAsync(combine, ck, cancellationToken);\n        await DailyVipMallBuyMissionAsync(cancellationToken);\n        await DailyAnimateTabMissionAsync(combine, ck, cancellationToken);\n        await DailyFilmTabMissionAsync(combine, ck, cancellationToken);\n        await DailyOgvWatchMissionAsync(combine, ck, cancellationToken);\n        await DailyTvOdBuyMissionAsync(cancellationToken);\n        await DailyDressBuyAmountMissionAsync(cancellationToken);\n    }\n\n    [TaskInterceptor(\"日常1：浏览装扮商城\", TaskLevel.Three, rethrowWhenException: false)]\n    private async Task DailyDressViewMissionAsync(\n        VipBigPointCombine combine,\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await vipBigPointDomainService.ReceiveAndCompleteAsync(\n            combine,\n            \"日常任务\",\n            \"dress-view\",\n            ck,\n            async (_, _) => await vipBigPointDomainService.CompleteV2Async(\"dress-view\", ck)\n        );\n    }\n\n    [TaskInterceptor(\"日常2：浏览会员购\", TaskLevel.Three, rethrowWhenException: false)]\n    private async Task DailyVipMallViewMissionAsync(\n        VipBigPointCombine combine,\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await vipBigPointDomainService.ReceiveAndCompleteAsync(\n            combine,\n            \"日常任务\",\n            \"vipmallview\",\n            ck,\n            async (_, _) =>\n                await vipBigPointDomainService.CompleteViewVipMallAsync(\"vipmallview\", ck)\n        );\n    }\n\n    [TaskInterceptor(\"日常3：购买会员购\", TaskLevel.Three, rethrowWhenException: false)]\n    private Task DailyVipMallBuyMissionAsync(CancellationToken cancellationToken = default)\n    {\n        logger.LogInformation(\"需购买，跳过\");\n        return Task.CompletedTask;\n    }\n\n    [TaskInterceptor(\"日常4：浏览追番频道\", TaskLevel.Three, rethrowWhenException: false)]\n    private async Task DailyAnimateTabMissionAsync(\n        VipBigPointCombine combine,\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await vipBigPointDomainService.ReceiveAndCompleteAsync(\n            combine,\n            \"日常任务\",\n            \"animatetab\",\n            ck,\n            async (_, _) => await vipBigPointDomainService.CompleteViewAsync(\"animatetab\", ck)\n        );\n    }\n\n    [TaskInterceptor(\"日常5：浏览影视频道\", TaskLevel.Three, rethrowWhenException: false)]\n    private async Task DailyFilmTabMissionAsync(\n        VipBigPointCombine combine,\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await vipBigPointDomainService.ReceiveAndCompleteAsync(\n            combine,\n            \"日常任务\",\n            \"filmtab\",\n            ck,\n            async (_, _) => await vipBigPointDomainService.CompleteViewAsync(\"filmtab\", ck)\n        );\n    }\n\n    [TaskInterceptor(\"日常6：观看剧集\", TaskLevel.Three, rethrowWhenException: false)]\n    private async Task DailyOgvWatchMissionAsync(\n        VipBigPointCombine combine,\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        await vipBigPointDomainService.ReceiveAndCompleteAsync(\n            combine,\n            \"日常任务\",\n            \"ogvwatchnew\",\n            ck,\n            async (_, _) => await vipBigPointDomainService.CompleteV2Async(\"ogvwatchnew\", ck)\n        );\n    }\n\n    [TaskInterceptor(\"日常7：购买影片\", TaskLevel.Three, rethrowWhenException: false)]\n    private Task DailyTvOdBuyMissionAsync(CancellationToken cancellationToken = default)\n    {\n        logger.LogInformation(\"需购买，跳过\");\n        return Task.CompletedTask;\n    }\n\n    [TaskInterceptor(\"日常8：购买装扮\", TaskLevel.Three, rethrowWhenException: false)]\n    private Task DailyDressBuyAmountMissionAsync(CancellationToken cancellationToken = default)\n    {\n        logger.LogInformation(\"需购买，跳过\");\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application/VipPrivilegeTaskAppService.cs",
    "content": "﻿using Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Application.Attributes;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\nusing Ray.BiliBiliTool.Infrastructure.Enums;\n\nnamespace Ray.BiliBiliTool.Application;\n\npublic class VipPrivilegeTaskAppService(\n    ILogger<VipPrivilegeTaskAppService> logger,\n    IOptionsMonitor<VipPrivilegeOptions> vipPrivilegeOptions,\n    IAccountDomainService accountDomainService,\n    IVipPrivilegeDomainService vipPrivilegeDomainService,\n    ILoginDomainService loginDomainService,\n    IConfiguration configuration,\n    CookieStrFactory<BiliCookie> cookieStrFactory\n) : BaseMultiAccountsAppService(logger, cookieStrFactory), IVipPrivilegeTaskAppService\n{\n    [TaskInterceptor(\"领取大会员福利任务\", TaskLevel.One)]\n    protected override async Task DoTaskAccountAsync(\n        BiliCookie ck,\n        CancellationToken cancellationToken = default\n    )\n    {\n        if (!vipPrivilegeOptions.CurrentValue.IsEnable)\n        {\n            logger.LogInformation(\"已配置为关闭，跳过\");\n            return;\n        }\n\n        await SetCookiesAsync(ck, cancellationToken);\n        UserInfo userInfo = await Login(ck);\n\n        await ReceiveVipPrivilege(userInfo, ck);\n    }\n\n    [TaskInterceptor(\"Set Cookie\")]\n    private async Task SetCookiesAsync(BiliCookie biliCookie, CancellationToken cancellationToken)\n    {\n        //判断cookie是否完整\n        if (!string.IsNullOrWhiteSpace(biliCookie.Buvid))\n        {\n            logger.LogInformation(\"Cookie完整，不需要Set Cookie\");\n            return;\n        }\n\n        //Set\n        logger.LogInformation(\"开始Set Cookie\");\n        var ck = await loginDomainService.SetCookieAsync(biliCookie, cancellationToken);\n\n        //持久化\n        logger.LogInformation(\"持久化Cookie\");\n        await SaveCookieAsync(ck, cancellationToken);\n    }\n\n    /// <summary>\n    /// 登录\n    /// </summary>\n    /// <returns></returns>\n    [TaskInterceptor(\"登录\")]\n    private async Task<UserInfo> Login(BiliCookie ck)\n    {\n        UserInfo userInfo = await accountDomainService.LoginByCookie(ck);\n        return userInfo;\n    }\n\n    /// <summary>\n    /// 每月领取大会员福利\n    /// </summary>\n    [TaskInterceptor(\"领取\", rethrowWhenException: false)]\n    private async Task ReceiveVipPrivilege(UserInfo userInfo, BiliCookie ck)\n    {\n        var suc = await vipPrivilegeDomainService.ReceiveVipPrivilege(userInfo, ck);\n\n        //如果领取成功，需要刷新账户信息（比如B币余额）\n        if (suc)\n        {\n            try\n            {\n                await accountDomainService.LoginByCookie(ck);\n            }\n            catch (Exception ex)\n            {\n                logger.LogError(\"领取福利成功，但之后刷新用户信息时异常，信息：{msg}\", ex.Message);\n            }\n        }\n    }\n\n    private async Task SaveCookieAsync(BiliCookie ckInfo, CancellationToken cancellationToken)\n    {\n        var platformType = configuration.GetSection(\"PlatformType\").Get<PlatformType>();\n        logger.LogInformation(\"当前运行平台：{platform}\", platformType);\n\n        //更新cookie到青龙env\n        if (platformType == PlatformType.QingLong)\n        {\n            await loginDomainService.SaveCookieToQinLongAsync(ckInfo, cancellationToken);\n            return;\n        }\n\n        //更新cookie到json\n        await loginDomainService.SaveCookieToJsonFileAsync(ckInfo, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/IAppService.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Application.Contracts;\n\npublic interface IAppService\n{\n    Task DoTaskAsync(CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/IChargeTaskAppService.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Application.Contracts;\n\n/// <summary>\n/// 免费B币券充电任务\n/// </summary>\n[Description(\"Charge\")]\npublic interface IChargeTaskAppService : IAppService;\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/IDailyTaskAppService.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Application.Contracts;\n\n/// <summary>\n/// 每日自动任务\n/// </summary>\n[Description(\"Daily\")]\npublic interface IDailyTaskAppService : IAppService;\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/ILiveFansMedalAppService.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Application.Contracts;\n\n/// <summary>\n/// 直播粉丝牌任务\n/// </summary>\n[Description(\"LiveFansMedal\")]\npublic interface ILiveFansMedalAppService : IAppService;\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/ILiveLotteryTaskAppService.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Application.Contracts;\n\n/// <summary>\n/// 每日自动任务\n/// </summary>\n[Description(\"LiveLottery\")]\npublic interface ILiveLotteryTaskAppService : IAppService;\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/ILoginTaskAppService.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Application.Contracts;\n\n/// <summary>\n/// 登录任务\n/// </summary>\n[Description(\"Login\")]\npublic interface ILoginTaskAppService : IAppService;\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/IMangaPrivilegeTaskAppService.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Application.Contracts;\n\n/// <summary>\n/// 领取大会员漫画权益任务\n/// </summary>\n[Description(\"MangaPrivilege\")]\npublic interface IMangaPrivilegeTaskAppService : IAppService;\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/IMangaTaskAppService.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Application.Contracts;\n\n/// <summary>\n/// 漫画任务\n/// </summary>\n[Description(\"Manga\")]\npublic interface IMangaTaskAppService : IAppService;\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/ISilver2CoinTaskAppService.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Application.Contracts;\n\n/// <summary>\n/// 银瓜子兑换硬币\n/// </summary>\n[Description(\"Silver2Coin\")]\npublic interface ISilver2CoinTaskAppService : IAppService;\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/ITestAppService.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Application.Contracts;\n\n/// <summary>\n/// 每日自动任务\n/// </summary>\n[Description(\"Test\")]\npublic interface ITestAppService : IAppService;\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/IUnfollowBatchedTaskAppService.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Application.Contracts;\n\n/// <summary>\n/// 每日自动任务\n/// </summary>\n[Description(\"UnfollowBatched\")]\npublic interface IUnfollowBatchedTaskAppService : IAppService;\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/IVipBigPointAppService.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Application.Contracts;\n\n/// <summary>\n/// 每日自动任务\n/// </summary>\n[Description(\"VipBigPoint\")]\npublic interface IVipBigPointAppService : IAppService;\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/IVipPrivilegeTaskAppService.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Application.Contracts;\n\n[Description(\"VipPrivilege\")]\npublic interface IVipPrivilegeTaskAppService : IAppService;\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/Ray.BiliBiliTool.Application.Contracts.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.DomainService\\Ray.BiliBiliTool.DomainService.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Application.Contracts/TaskTypeFactory.cs",
    "content": "﻿using System.ComponentModel;\nusing System.Reflection;\nusing Microsoft.Extensions.Logging;\n\nnamespace Ray.BiliBiliTool.Application.Contracts;\n\npublic static class TaskTypeFactory\n{\n    private static readonly List<Type> TypeList =\n    [\n        typeof(ILoginTaskAppService),\n        typeof(ITestAppService),\n        typeof(IDailyTaskAppService),\n        typeof(IMangaTaskAppService),\n        typeof(IMangaPrivilegeTaskAppService),\n        typeof(IVipPrivilegeTaskAppService),\n        typeof(ISilver2CoinTaskAppService),\n        typeof(IChargeTaskAppService),\n        typeof(ILiveFansMedalAppService),\n        typeof(ILiveLotteryTaskAppService),\n        typeof(IVipBigPointAppService),\n        typeof(IUnfollowBatchedTaskAppService),\n    ];\n\n    private static readonly List<TaskTypeItem> All = [];\n\n    static TaskTypeFactory()\n    {\n        for (int i = 0; i < TypeList.Count; i++)\n        {\n            All.Add(\n                new TaskTypeItem(\n                    i + 1,\n                    TypeList[i].GetCustomAttribute<DescriptionAttribute>()?.Description\n                        ?? \"Unknown\",\n                    TypeList[i]\n                )\n            );\n        }\n    }\n\n    public static Type Get(string code)\n    {\n        return All.First(x => x.Code == code).Type;\n    }\n\n    public static void Show(ILogger logger)\n    {\n        foreach (var item in All)\n        {\n            logger.LogInformation(\"{id}):{code}\", item.Id, item.Code);\n        }\n    }\n\n    public static string GetCodeByIndex(int index)\n    {\n        return All.First(x => x.Id == index).Code;\n    }\n}\n\npublic class TaskTypeItem(int id, string code, Type type)\n{\n    public int Id { get; } = id;\n\n    public string Code { get; } = code;\n\n    public Type Type { get; } = type;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Constants.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Config;\n\npublic static class Constants\n{\n    /// <summary>\n    /// 每天的最大投币数，优先级最高，默认每天最多投5个币（包含已投过的数量）\n    /// </summary>\n    public static int MaxNumberOfDonateCoins = 5;\n\n    /// <summary>\n    /// 每天可获取的满额经验值\n    /// </summary>\n    public static int EveryDayExp = 65;\n\n    /// <summary>\n    /// 开源地址\n    /// </summary>\n    public static string SourceCodeUrl = \"https://github.com/RayWangQvQ/BiliBiliToolPro\";\n\n    public static string FallbackAutoChargeUpId = \"220893216\";\n\n    /// <summary>\n    /// 每日任务exp\n    /// </summary>\n    /// <returns></returns>\n    public static readonly Dictionary<string, int> ExpDic = new()\n    {\n        { \"每日登录\", 5 },\n        { \"每日观看视频\", 5 },\n        { \"每日分享视频\", 5 },\n        { \"每日投币\", 10 },\n    };\n\n    /// <summary>\n    /// 投币接口的data.code返回以下这些状态码，则可以继续尝试投币<para></para>\n    /// 如返回除这些之外的状态码，则终止投币流程，不进行无意义的尝试<para></para>\n    /// （比如返回-101：账号未登录；-102：账号被封停；-111：csrf校验失败等）\n    /// </summary>\n    /// <returns></returns>\n    public static readonly Dictionary<string, string> DonateCoinCanContinueStatusDic = new()\n    {\n        { \"0\", \"成功\" },\n        { \"-400\", \"请求错误\" },\n        { \"10003\", \"不存在该稿件\" },\n        { \"34002\", \"不能给自己投币\" },\n        { \"34003\", \"非法的投币数量\" },\n        { \"34004\", \"投币间隔太短\" },\n        { \"34005\", \"超过投币上限\" },\n    };\n\n    public static readonly Dictionary<string, string> CommandLineMappingsDic = new()\n    {\n        { \"--cookieStr1\", \"BiliBiliCookies:1\" },\n        { \"--runTasks\", \"RunTasks\" },\n        { \"--randomSleep\", \"Security:RandomSleepMaxMin\" },\n        { \"--numberOfCoins\", \"DailyTaskConfig:NumberOfCoins\" },\n        { \"--numberOfProtectedCoins\", \"DailyTaskConfig:NumberOfProtectedCoins\" },\n        { \"--saveCoinsWhenLv6\", \"DailyTaskConfig:SaveCoinsWhenLv6\" },\n        { \"--selectLike\", \"DailyTaskConfig:SelectLike\" },\n        { \"--supportUpIds\", \"DailyTaskConfig:SupportUpIds\" },\n        { \"--dayOfAutoCharge\", \"DailyTaskConfig:DayOfAutoCharge\" },\n        { \"--autoChargeUpId\", \"DailyTaskConfig:AutoChargeUpId\" },\n        { \"--dayOfReceiveVipPrivilege\", \"DailyTaskConfig:DayOfReceiveVipPrivilege\" },\n        { \"--isExchangeSilver2Coin\", \"DailyTaskConfig:IsExchangeSilver2Coin\" },\n        { \"--devicePlatform\", \"DailyTaskConfig:DevicePlatform\" },\n        { \"--excludeAwardNames\", \"LiveLotteryTaskConfig:ExcludeAwardNames\" },\n        { \"--includeAwardNames\", \"LiveLotteryTaskConfig:INCLUDEAWARDNAMES\" },\n        { \"--unfollowGroup\", \"UnfollowBatchedTaskConfig:GroupName\" },\n        { \"--unfollowCount\", \"UnfollowBatchedTaskConfig:Count\" },\n        { \"--intervalSecondsBetweenRequestApi\", \"Security:IntervalSecondsBetweenRequestApi\" },\n        { \"--intervalMethodTypes\", \"Security:IntervalMethodTypes\" },\n        { \"--pushScKey\", \"Serilog:WriteTo:6:Args:scKey\" },\n        { \"--proxy\", \"WebProxy\" },\n    };\n\n    public const string SqliteTableName = \"bili_appsettings\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Extensions/ServiceCollectionExtension.cs",
    "content": "﻿using System.Text.Json;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Infrastructure;\n\nnamespace Ray.BiliBiliTool.Config.Extensions;\n\npublic static class ServiceCollectionExtension\n{\n    /// <summary>\n    /// 注册配置\n    /// </summary>\n    /// <param name=\"services\"></param>\n    /// <returns></returns>\n    public static IServiceCollection AddBiliBiliConfigs(\n        this IServiceCollection services,\n        IConfiguration configuration\n    )\n    {\n        //Options\n        services\n            .AddOptions()\n            .Configure<JsonSerializerOptions>(o => o = JsonSerializerOptionsBuilder.DefaultOptions)\n            .Configure<BiliBiliCookieOptions>(configuration.GetSection(\"BiliBiliCookie\"))\n            .Configure<DailyTaskOptions>(configuration.GetSection(\"DailyTaskConfig\"))\n            .Configure<MangaTaskOptions>(configuration.GetSection(\"MangaTaskConfig\"))\n            .Configure<MangaPrivilegeTaskOptions>(\n                configuration.GetSection(\"MangaPrivilegeTaskConfig\")\n            )\n            .Configure<Silver2CoinTaskOptions>(configuration.GetSection(\"Silver2CoinTaskConfig\"))\n            .Configure<ChargeTaskOptions>(configuration.GetSection(\"ChargeTaskConfig\"))\n            .Configure<LiveLotteryTaskOptions>(configuration.GetSection(\"LiveLotteryTaskConfig\"))\n            .Configure<UnfollowBatchedTaskOptions>(\n                configuration.GetSection(\"UnfollowBatchedTaskConfig\")\n            )\n            .Configure<VipBigPointOptions>(configuration.GetSection(\"VipBigPointConfig\"))\n            .Configure<SecurityOptions>(configuration.GetSection(\"Security\"))\n            .Configure<VipPrivilegeOptions>(configuration.GetSection(\"VipPrivilegeConfig\"))\n            .Configure<LiveFansMedalTaskOptions>(\n                configuration.GetSection(\"LiveFansMedalTaskConfig\")\n            )\n            .Configure<QingLongOptions>(configuration.GetSection(\"QingLongConfig\"));\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/IHasCron.cs",
    "content": "namespace Ray.BiliBiliTool.Config;\n\npublic interface IHasCron\n{\n    public string? Cron { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/BaseConfigOptions.cs",
    "content": "namespace Ray.BiliBiliTool.Config.Options;\n\n/// <summary>\n/// 基础配置选项类，包含所有配置共有的属性\n/// </summary>\npublic abstract class BaseConfigOptions : IHasCron, IConfigOptions\n{\n    /// <summary>\n    /// 定时任务Cron表达式\n    /// </summary>\n    public string? Cron { get; set; }\n\n    /// <summary>\n    /// 是否启用该任务\n    /// </summary>\n    public bool IsEnable { get; set; } = true;\n\n    /// <summary>\n    /// 配置节名称，由子类实现\n    /// </summary>\n    public abstract string SectionName { get; }\n\n    /// <summary>\n    /// 转换为配置字典，子类可以重写以添加更多配置项\n    /// </summary>\n    public virtual Dictionary<string, string> ToConfigDictionary()\n    {\n        return GetBaseConfigDictionary();\n    }\n\n    /// <summary>\n    /// 获取基础配置字典，避免循环调用\n    /// </summary>\n    protected Dictionary<string, string> GetBaseConfigDictionary()\n    {\n        return new Dictionary<string, string>\n        {\n            { $\"{SectionName}:{nameof(Cron)}\", Cron ?? \"\" },\n            { $\"{SectionName}:{nameof(IsEnable)}\", IsEnable.ToString().ToLower() },\n        };\n    }\n\n    /// <summary>\n    /// 合并配置字典，用于子类添加额外配置项\n    /// </summary>\n    protected Dictionary<string, string> MergeConfigDictionary(\n        Dictionary<string, string> additionalConfig\n    )\n    {\n        var baseConfig = GetBaseConfigDictionary();\n        foreach (var kvp in additionalConfig)\n        {\n            baseConfig[kvp.Key] = kvp.Value;\n        }\n        return baseConfig;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/BiliBiliCookieOptions.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Config.Options;\n\n/// <summary>\n/// Cookie信息\n/// </summary>\npublic class BiliBiliCookieOptions\n{\n    public string? CookieStr { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/ChargeTaskOptions.cs",
    "content": "namespace Ray.BiliBiliTool.Config.Options;\n\npublic class ChargeTaskOptions : BaseConfigOptions\n{\n    public override string SectionName => \"ChargeTaskConfig\";\n\n    /// <summary>\n    /// 充电Up主Id\n    /// </summary>\n    public string? AutoChargeUpId { get; set; }\n\n    private string? _chargeComment;\n\n    /// <summary>\n    /// 充电后留言\n    /// </summary>\n    public string ChargeComment\n    {\n        get =>\n            string.IsNullOrWhiteSpace(_chargeComment)\n                ? DefaultComments[new Random().Next(0, DefaultComments.Count)]\n                : _chargeComment;\n        set => _chargeComment = value;\n    }\n\n    private static readonly List<string> DefaultComments =\n    [\n        \"棒\",\n        \"棒唉\",\n        \"棒耶\",\n        \"加油~\",\n        \"UP加油!\",\n        \"支持~\",\n        \"支持支持！\",\n        \"催更啦\",\n        \"顶顶\",\n        \"留下脚印~\",\n        \"干杯\",\n        \"bilibili干杯\",\n        \"o(*￣▽￣*)o\",\n        \"(｡･∀･)ﾉﾞ嗨\",\n        \"(●ˇ∀ˇ●)\",\n        \"( •̀ ω •́ )y\",\n        \"(ง •_•)ง\",\n        \">.<\",\n        \"^_~\",\n    ];\n\n    public override Dictionary<string, string> ToConfigDictionary()\n    {\n        return MergeConfigDictionary(\n            new Dictionary<string, string>\n            {\n                { $\"{SectionName}:{nameof(AutoChargeUpId)}\", AutoChargeUpId ?? \"\" },\n                { $\"{SectionName}:{nameof(ChargeComment)}\", _chargeComment ?? \"\" },\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/DailyTaskOptions.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Config.Options;\n\n/// <summary>\n/// 程序自定义个性化配置\n/// </summary>\npublic class DailyTaskOptions : BaseConfigOptions\n{\n    public override string SectionName => \"DailyTaskConfig\";\n\n    /// <summary>\n    /// 是否观看视频\n    /// </summary>\n    public bool IsWatchVideo { get; set; }\n\n    /// <summary>\n    /// 是否分享视频\n    /// </summary>\n    public bool IsShareVideo { get; set; }\n\n    /// <summary>\n    /// 是否开启专栏投币模式\n    /// </summary>\n    public bool IsDonateCoinForArticle { get; set; }\n\n    /// <summary>\n    /// 每日设定的投币数 [0,5]\n    /// </summary>\n    public int NumberOfCoins { get; set; } = 5;\n\n    /// <summary>\n    /// 要保留的硬币数量 [0,int_max]\n    /// </summary>\n    public int NumberOfProtectedCoins { get; set; } = 0;\n\n    /// <summary>\n    /// 达到六级后是否开始白嫖\n    /// </summary>\n    public bool SaveCoinsWhenLv6 { get; set; } = false;\n\n    /// <summary>\n    /// 投币时是否点赞[false,true]\n    /// </summary>\n    public bool SelectLike { get; set; } = false;\n\n    /// <summary>\n    /// 优先选择支持的up主Id集合，配置后会优先从指定的up主下挑选视频进行观看、分享和投币，不配置则从排行耪随机获取支持视频\n    /// </summary>\n    public string? SupportUpIds { get; set; }\n\n    /// <summary>\n    /// 执行客户端操作时的平台 [ios,android]\n    /// </summary>\n    public string DevicePlatform { get; set; } = \"android\";\n\n    public List<long> SupportUpIdList\n    {\n        get\n        {\n            List<long> re = [];\n            if (string.IsNullOrWhiteSpace(SupportUpIds) | SupportUpIds == \"-1\")\n                return re;\n\n            string[] array = SupportUpIds?.Split(',') ?? [];\n            foreach (string item in array)\n            {\n                re.Add(long.TryParse(item.Trim(), out long upId) ? upId : long.MinValue);\n            }\n            return re;\n        }\n    }\n\n    private static readonly List<string> DefaultComments =\n    [\n        \"棒\",\n        \"棒唉\",\n        \"棒耶\",\n        \"加油~\",\n        \"UP加油!\",\n        \"支持~\",\n        \"支持支持！\",\n        \"催更啦\",\n        \"顶顶\",\n        \"留下脚印~\",\n        \"干杯\",\n        \"bilibili干杯\",\n        \"o(*￣▽￣*)o\",\n        \"(｡･∀･)ﾉﾞ嗨\",\n        \"(●ˇ∀ˇ●)\",\n        \"( •̀ ω •́ )y\",\n        \"(ง •_•)ง\",\n        \">.<\",\n        \"^_~\",\n    ];\n\n    public override Dictionary<string, string> ToConfigDictionary()\n    {\n        return MergeConfigDictionary(\n            new Dictionary<string, string>\n            {\n                { $\"{SectionName}:{nameof(IsWatchVideo)}\", IsWatchVideo.ToString().ToLower() },\n                { $\"{SectionName}:{nameof(IsShareVideo)}\", IsShareVideo.ToString().ToLower() },\n                {\n                    $\"{SectionName}:{nameof(IsDonateCoinForArticle)}\",\n                    IsDonateCoinForArticle.ToString().ToLower()\n                },\n                { $\"{SectionName}:{nameof(NumberOfCoins)}\", NumberOfCoins.ToString() },\n                {\n                    $\"{SectionName}:{nameof(NumberOfProtectedCoins)}\",\n                    NumberOfProtectedCoins.ToString()\n                },\n                {\n                    $\"{SectionName}:{nameof(SaveCoinsWhenLv6)}\",\n                    SaveCoinsWhenLv6.ToString().ToLower()\n                },\n                { $\"{SectionName}:{nameof(SelectLike)}\", SelectLike.ToString().ToLower() },\n                { $\"{SectionName}:{nameof(SupportUpIds)}\", SupportUpIds ?? \"\" },\n                { $\"{SectionName}:{nameof(DevicePlatform)}\", DevicePlatform },\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/IConfigOptions.cs",
    "content": "namespace Ray.BiliBiliTool.Config.Options;\n\npublic interface IConfigOptions\n{\n    Dictionary<string, string> ToConfigDictionary();\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/LiveFansMedalTaskOptions.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Config.Options;\n\n/// <summary>\n/// 粉丝牌等级任务相关配置\n/// </summary>\npublic class LiveFansMedalTaskOptions : BaseConfigOptions\n{\n    public override string SectionName => \"LiveFansMedalTaskConfig\";\n\n    /// <summary>\n    /// 自定义发送弹幕内容，如 \"打卡\" 等来触发直播间内机器人关键词\n    /// </summary>\n    public string DanmakuContent { get; set; } = \"OvO\";\n\n    /// <summary>\n    /// 心跳包发送的个数 / 挂机的时间，单位为分钟\n    /// </summary>\n    public int HeartBeatNumber { get; set; } = 70;\n\n    /// <summary>\n    /// 当心跳包发送连续失败多少次时放弃\n    /// </summary>\n    public int HeartBeatSendGiveUpThreshold { get; set; } = 5;\n\n    /// <summary>\n    /// 对于直播时长任务是否跳过粉丝牌等级大于等于 20 的\n    /// </summary>\n    public bool IsSkipLevel20Medal { get; set; } = true;\n\n    public const int HeartBeatInterval = 60;\n\n    /// <summary>\n    /// 点赞次数，默认值为30（用于点亮粉丝勋章）\n    /// </summary>\n    public int LikeNumber { get; set; } = 30;\n\n    /// <summary>\n    /// 发送弹幕次数\n    /// </summary>\n    public int SendDanmakuNumber { get; set; } = 1;\n\n    /// <summary>\n    /// 弹幕发送失败多少次时放弃\n    /// </summary>\n    public int SendDanmakugiveUpThreshold { get; set; } = 3;\n\n    public override Dictionary<string, string> ToConfigDictionary()\n    {\n        return MergeConfigDictionary(\n            new Dictionary<string, string>\n            {\n                { $\"{SectionName}:{nameof(DanmakuContent)}\", DanmakuContent },\n                { $\"{SectionName}:{nameof(HeartBeatNumber)}\", HeartBeatNumber.ToString() },\n                {\n                    $\"{SectionName}:{nameof(HeartBeatSendGiveUpThreshold)}\",\n                    HeartBeatSendGiveUpThreshold.ToString()\n                },\n                {\n                    $\"{SectionName}:{nameof(IsSkipLevel20Medal)}\",\n                    IsSkipLevel20Medal.ToString().ToLower()\n                },\n                { $\"{SectionName}:{nameof(LikeNumber)}\", LikeNumber.ToString() },\n                { $\"{SectionName}:{nameof(SendDanmakuNumber)}\", SendDanmakuNumber.ToString() },\n                {\n                    $\"{SectionName}:{nameof(SendDanmakugiveUpThreshold)}\",\n                    SendDanmakugiveUpThreshold.ToString()\n                },\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/LiveLotteryTaskOptions.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Config.Options;\n\npublic class LiveLotteryTaskOptions : BaseConfigOptions\n{\n    public override string SectionName => \"LiveLotteryTaskConfig\";\n\n    public string? IncludeAwardNames { get; set; }\n\n    public string? ExcludeAwardNames { get; set; }\n\n    public List<string> IncludeAwardNameList =>\n        IncludeAwardNames\n            ?.Split(\"|\", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)\n            .ToList() ?? new List<string>();\n\n    public List<string> ExcludeAwardNameList =>\n        ExcludeAwardNames\n            ?.Split(\"|\", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)\n            .ToList() ?? new List<string>();\n\n    public bool AutoGroupFollowings { get; set; } = true;\n\n    public string? DenyUids { get; set; }\n\n    public List<string> DenyUidList =>\n        DenyUids\n            ?.Split(\",\", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)\n            .ToList() ?? new List<string>();\n\n    public override Dictionary<string, string> ToConfigDictionary()\n    {\n        return MergeConfigDictionary(\n            new Dictionary<string, string>\n            {\n                { $\"{SectionName}:{nameof(IncludeAwardNames)}\", IncludeAwardNames ?? \"\" },\n                { $\"{SectionName}:{nameof(ExcludeAwardNames)}\", ExcludeAwardNames ?? \"\" },\n                {\n                    $\"{SectionName}:{nameof(AutoGroupFollowings)}\",\n                    AutoGroupFollowings.ToString().ToLower()\n                },\n                { $\"{SectionName}:{nameof(DenyUids)}\", DenyUids ?? \"\" },\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/MangaPrivilegeTaskOptions.cs",
    "content": "namespace Ray.BiliBiliTool.Config.Options;\n\npublic class MangaPrivilegeTaskOptions : BaseConfigOptions\n{\n    public override string SectionName => \"MangaPrivilegeTaskConfig\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/MangaTaskOptions.cs",
    "content": "namespace Ray.BiliBiliTool.Config.Options;\n\npublic class MangaTaskOptions : BaseConfigOptions\n{\n    public override string SectionName => \"MangaTaskConfig\";\n\n    /// <summary>\n    /// 自定义漫画阅读 comic_id\n    /// </summary>\n    public long CustomComicId { get; set; } = 27355;\n\n    /// <summary>\n    /// 自定义漫画阅读 ep_id\n    /// </summary>\n    public long CustomEpId { get; set; } = 381662;\n\n    public override Dictionary<string, string> ToConfigDictionary()\n    {\n        return MergeConfigDictionary(\n            new Dictionary<string, string>\n            {\n                { $\"{SectionName}:{nameof(CustomComicId)}\", CustomComicId.ToString() },\n                { $\"{SectionName}:{nameof(CustomEpId)}\", CustomEpId.ToString() },\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/QingLongOptions.cs",
    "content": "namespace Ray.BiliBiliTool.Config.Options;\n\npublic class QingLongOptions\n{\n    public string? ClientId { get; set; }\n\n    public string? ClientSecret { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/SecurityOptions.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Config.Options;\n\n/// <summary>\n/// 安全相关配置\n/// </summary>\npublic class SecurityOptions\n{\n    /// <summary>\n    /// 是否跳过执行任务，用于特殊情况下，通过配置灵活的开启和关闭任务\n    /// </summary>\n    public bool IsSkipDailyTask { get; set; } = false;\n\n    /// <summary>\n    /// 随机睡眠的最大时长，用于使每天运行时间在范围内相对随机\n    /// </summary>\n    public int RandomSleepMaxMin { get; set; } = 10;\n\n    /// <summary>\n    /// 两次调用api之间间隔的秒数[0,+]\n    /// 有人担心在几秒内连续调用api会被b站安全机制发现，所以为不放心的朋友添加了间隔秒数配置，两次调用Api之间会大于该秒数\n    /// </summary>\n    public int IntervalSecondsBetweenRequestApi { get; set; } = 3;\n\n    /// <summary>\n    /// 间隔秒数所针对的HttpMethod，多个用英文逗号隔开，当前有GET和POST两种，可配置如“GET,POST”\n    /// 服务器一般对GET请求不是很敏感，建议只针对POST请求做间隔就可以了\n    /// </summary>\n    public string IntervalMethodTypes { get; set; } = \"GET,POST\";\n\n    public List<HttpMethod> GetIntervalMethods()\n    {\n        List<HttpMethod> result = new List<HttpMethod>();\n        if (string.IsNullOrWhiteSpace(IntervalMethodTypes))\n            return result;\n\n        foreach (var item in IntervalMethodTypes.Split(','))\n        {\n            try\n            {\n                HttpMethod method = new HttpMethod(item);\n                if (method != null && !result.Contains(method))\n                    result.Add(method);\n            }\n            catch (Exception)\n            {\n                //ignore\n            }\n        }\n\n        return result;\n    }\n\n    /// <summary>\n    /// 请求B站接口时头部传递的User-Agent\n    /// </summary>\n    public string UserAgent { get; set; } =\n        \"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\";\n\n    /// <summary>\n    /// App请求B站接口时头部传递的User-Agent\n    /// </summary>\n    public string UserAgentApp { get; set; } =\n        \"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\";\n\n    /// <summary>\n    /// 代理\n    /// </summary>\n    public string? WebProxy { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/Silver2CoinTaskOptions.cs",
    "content": "namespace Ray.BiliBiliTool.Config.Options;\n\npublic class Silver2CoinTaskOptions : BaseConfigOptions\n{\n    public override string SectionName => \"Silver2CoinTaskConfig\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/UnfollowBatchedTaskOptions.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Config.Options;\n\npublic class UnfollowBatchedTaskOptions : BaseConfigOptions\n{\n    public override string SectionName => \"UnfollowBatchedTaskConfig\";\n    private const string DefaultGroupName = \"天选时刻\";\n\n    public string GroupName { get; set; } = DefaultGroupName;\n\n    public int Count { get; set; }\n\n    public string? RetainUids { get; set; }\n\n    public List<string> RetainUidList =>\n        RetainUids\n            ?.Split(\",\", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)\n            .ToList() ?? new List<string>();\n\n    public override Dictionary<string, string> ToConfigDictionary()\n    {\n        return MergeConfigDictionary(\n            new Dictionary<string, string>\n            {\n                { $\"{SectionName}:{nameof(GroupName)}\", GroupName },\n                { $\"{SectionName}:{nameof(Count)}\", Count.ToString() },\n                { $\"{SectionName}:{nameof(RetainUids)}\", RetainUids ?? \"\" },\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/VipBigPointOptions.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Config.Options;\n\npublic class VipBigPointOptions : BaseConfigOptions\n{\n    public override string SectionName => \"VipBigPointConfig\";\n\n    public string? ViewBangumis { get; set; }\n\n    public List<long> ViewBangumiList\n    {\n        get\n        {\n            List<long> re = [];\n            if (string.IsNullOrWhiteSpace(ViewBangumis) | ViewBangumis == \"-1\")\n                return re;\n\n            string[] array = ViewBangumis?.Split(',') ?? [];\n            foreach (string item in array)\n            {\n                if (long.TryParse(item.Trim(), out long upId))\n                    re.Add(upId);\n                else\n                    re.Add(long.MinValue);\n            }\n            return re;\n        }\n    }\n\n    public override Dictionary<string, string> ToConfigDictionary()\n    {\n        return MergeConfigDictionary(\n            new Dictionary<string, string>\n            {\n                { $\"{SectionName}:{nameof(ViewBangumis)}\", ViewBangumis ?? \"\" },\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Options/VipPrivilegeOptions.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Config.Options;\n\npublic class VipPrivilegeOptions : BaseConfigOptions\n{\n    public override string SectionName => \"VipPrivilegeConfig\";\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/Ray.BiliBiliTool.Config.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Data.Sqlite\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.EnvironmentVariables\" />\n    <PackageReference Include=\"Microsoft.Extensions.Options\" />\n    <PackageReference Include=\"Microsoft.Extensions.Options.ConfigurationExtensions\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Infrastructure\\Ray.BiliBiliTool.Infrastructure.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/SQLite/SqliteConfigurationExtensions.cs",
    "content": "using Microsoft.Extensions.Configuration;\n\nnamespace Ray.BiliBiliTool.Config.SQLite;\n\npublic static class SqliteConfigurationExtensions\n{\n    public static IConfigurationBuilder AddSqlite(\n        this IConfigurationBuilder builder,\n        string connectionString,\n        string tableName = \"AppSettings\",\n        string keyColumnName = \"Key\",\n        string valueColumnName = \"Value\"\n    )\n    {\n        return builder.Add(\n            new SqliteConfigurationSource\n            {\n                ConnectionString = connectionString,\n                TableName = tableName,\n                KeyColumnName = keyColumnName,\n                ValueColumnName = valueColumnName,\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/SQLite/SqliteConfigurationProvider.cs",
    "content": "using Microsoft.Data.Sqlite;\nusing Microsoft.Extensions.Configuration;\n\nnamespace Ray.BiliBiliTool.Config.SQLite;\n\npublic class SqliteConfigurationProvider(SqliteConfigurationSource source) : ConfigurationProvider\n{\n    private readonly string _connectionString =\n        source.ConnectionString ?? throw new ArgumentNullException(nameof(source.ConnectionString));\n    private readonly string _tableName = source.TableName ?? \"AppSettings\";\n    private readonly string _keyColumnName = source.KeyColumnName ?? \"Key\";\n    private readonly string _valueColumnName = source.ValueColumnName ?? \"Value\";\n\n    public override void Load()\n    {\n        Data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);\n\n        using var connection = new SqliteConnection(_connectionString);\n        connection.Open();\n\n        EnsureTableExists(connection);\n\n        using var command = connection.CreateCommand();\n        command.CommandText =\n            $\"SELECT [{_keyColumnName}], [{_valueColumnName}] FROM [{_tableName}]\";\n\n        using var reader = command.ExecuteReader();\n        while (reader.Read())\n        {\n            string key = reader.GetString(0);\n            string value = reader.GetString(1);\n            Data[key] = value;\n        }\n    }\n\n    private void EnsureTableExists(SqliteConnection connection)\n    {\n        using var command = connection.CreateCommand();\n        command.CommandText =\n            $@\"\n            CREATE TABLE IF NOT EXISTS [{_tableName}] (\n                [{_keyColumnName}] TEXT PRIMARY KEY,\n                [{_valueColumnName}] TEXT NOT NULL\n            )\";\n        command.ExecuteNonQuery();\n    }\n\n    public override void Set(string key, string? value)\n    {\n        using var connection = new SqliteConnection(_connectionString);\n        connection.Open();\n\n        using var command = connection.CreateCommand();\n        command.CommandText =\n            $@\"\n            INSERT OR REPLACE INTO [{_tableName}] ([{_keyColumnName}], [{_valueColumnName}])\n            VALUES (@key, @value)\";\n        command.Parameters.AddWithValue(\"@key\", key);\n        command.Parameters.AddWithValue(\"@value\", value);\n        command.ExecuteNonQuery();\n\n        Data[key] = value;\n    }\n\n    public void BatchSet(Dictionary<string, string> configValues)\n    {\n        using var connection = new SqliteConnection(_connectionString);\n        connection.Open();\n\n        using var transaction = connection.BeginTransaction();\n        using var command = connection.CreateCommand();\n        command.Transaction = transaction;\n\n        try\n        {\n            foreach (var kvp in configValues)\n            {\n                command.CommandText =\n                    $@\"\n                    INSERT OR REPLACE INTO [{_tableName}] ([{_keyColumnName}], [{_valueColumnName}])\n                    VALUES (@key, @value)\";\n                command.Parameters.Clear();\n                command.Parameters.AddWithValue(\"@key\", kvp.Key);\n                command.Parameters.AddWithValue(\"@value\", kvp.Value ?? (object)DBNull.Value);\n                command.ExecuteNonQuery();\n\n                Data[kvp.Key] = kvp.Value;\n            }\n\n            transaction.Commit();\n        }\n        catch\n        {\n            transaction.Rollback();\n            throw;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Config/SQLite/SqliteConfigurationSource.cs",
    "content": "using Microsoft.Extensions.Configuration;\n\nnamespace Ray.BiliBiliTool.Config.SQLite;\n\npublic class SqliteConfigurationSource : IConfigurationSource\n{\n    public string? ConnectionString { get; set; }\n    public string? TableName { get; set; }\n    public string? KeyColumnName { get; set; }\n    public string? ValueColumnName { get; set; }\n\n    public IConfigurationProvider Build(IConfigurationBuilder builder)\n    {\n        return new SqliteConfigurationProvider(this);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Console/BiliBiliToolHostedService.cs",
    "content": "﻿using System.Reflection;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.Serilog.Sinks.Batched;\nusing Constants = Ray.BiliBiliTool.Config.Constants;\n\nnamespace Ray.BiliBiliTool.Console;\n\npublic class BiliBiliToolHostedService(\n    IHostApplicationLifetime applicationLifetime,\n    IServiceProvider serviceProvider,\n    IHostEnvironment environment,\n    IConfiguration configuration,\n    ILogger<BiliBiliToolHostedService> logger,\n    IOptionsMonitor<SecurityOptions> securityOptions\n) : IHostedService\n{\n    private readonly SecurityOptions _securityOptions = securityOptions.CurrentValue;\n\n    public async Task StartAsync(CancellationToken cancellationToken)\n    {\n        try\n        {\n            logger.LogInformation(\"BiliBiliToolPro 开始运行...\" + Environment.NewLine);\n\n            bool pass = await PreCheckAsync(cancellationToken);\n            if (!pass)\n                return;\n\n            await RandomSleepAsync(cancellationToken);\n\n            string[] tasks = await ReadTargetTasksAsync(cancellationToken);\n            logger.LogInformation(\"【目标任务】{tasks}\", string.Join(\",\", tasks));\n            await DoTasksAsync(tasks, cancellationToken);\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(\"程序异常终止，原因：{msg}\", ex.Message);\n            throw;\n        }\n        finally\n        {\n            LogAppInfo();\n\n            //环境\n            logger.LogInformation(\"运行环境：{env}\", environment.EnvironmentName);\n            logger.LogInformation(\n                \"应用目录：{path}\" + Environment.NewLine,\n                environment.ContentRootPath\n            );\n            logger.LogInformation(\"运行结束\");\n\n            //自动退出\n            applicationLifetime.StopApplication();\n        }\n    }\n\n    public Task StopAsync(CancellationToken cancellationToken)\n    {\n        return Task.CompletedTask;\n    }\n\n    private Task<bool> PreCheckAsync(CancellationToken cancellationToken)\n    {\n        //是否跳过\n        if (_securityOptions.IsSkipDailyTask)\n        {\n            logger.LogWarning(\"已配置为跳过任务\" + Environment.NewLine);\n            return Task.FromResult(false);\n        }\n\n        return Task.FromResult(true);\n    }\n\n    private async Task RandomSleepAsync(CancellationToken cancellationToken)\n    {\n        if (\n            configuration[\"RunTasks\"]!.Contains(\"Login\")\n            || configuration[\"RunTasks\"]!.Contains(\"Test\")\n        )\n            return;\n\n        if (_securityOptions.RandomSleepMaxMin > 0)\n        {\n            int randomMin = new Random().Next(1, ++_securityOptions.RandomSleepMaxMin);\n            logger.LogInformation(\"随机休眠{min}分钟\" + Environment.NewLine, randomMin);\n            await Task.Delay(randomMin * 1000 * 60, cancellationToken);\n        }\n    }\n\n    /// <summary>\n    /// 读取目标任务\n    /// </summary>\n    /// <param name=\"cancellationToken\"></param>\n    /// <returns></returns>\n    private Task<string[]> ReadTargetTasksAsync(CancellationToken cancellationToken)\n    {\n        string[] tasks = configuration[\"RunTasks\"]!.Split(\n            \"&\",\n            options: StringSplitOptions.RemoveEmptyEntries\n        );\n        if (tasks.Any())\n        {\n            return Task.FromResult(tasks);\n        }\n\n        logger.LogInformation(\"未指定目标任务，请选择要运行的任务：\");\n        TaskTypeFactory.Show(logger);\n        logger.LogInformation(\"请输入：\");\n\n        while (true)\n        {\n            var index = System.Console.ReadLine();\n            bool suc = int.TryParse(index, out int num);\n            if (suc)\n            {\n                string code = TaskTypeFactory.GetCodeByIndex(num);\n                configuration[\"RunTasks\"] = code;\n                return Task.FromResult(new[] { code });\n            }\n\n            logger.LogWarning(\"输入异常，请输入序号\");\n        }\n    }\n\n    private async Task DoTasksAsync(string[] tasks, CancellationToken cancellationToken)\n    {\n        using IServiceScope scope = serviceProvider.CreateScope();\n        foreach (string task in tasks)\n        {\n            var type = TaskTypeFactory.Get(task);\n\n            IAppService appService = (IAppService)scope.ServiceProvider.GetRequiredService(type);\n            await appService.DoTaskAsync(cancellationToken);\n        }\n    }\n\n    private void LogAppInfo()\n    {\n        logger.LogInformation(Environment.NewLine + \"========================\");\n        logger.LogInformation(\n            \"v{version} 开源 by {url}\",\n            typeof(Program).Assembly.GetName().Version?.ToString(),\n            Constants.SourceCodeUrl + Environment.NewLine\n        );\n        //_logger.LogInformation(\"【当前IP】{ip} \", IpHelper.GetIp());\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Console/Program.cs",
    "content": "﻿using System.Reflection;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Ray.BiliBiliTool.Agent.Extensions;\nusing Ray.BiliBiliTool.Application.Extensions;\nusing Ray.BiliBiliTool.Config.Extensions;\nusing Ray.BiliBiliTool.DomainService.Extensions;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Serilog;\nusing Serilog.Debugging;\n\nnamespace Ray.BiliBiliTool.Console;\n\npublic class Program\n{\n    public static async Task<int> Main(string[] args)\n    {\n        System.Console.CancelKeyPress += (sender, eventArgs) =>\n        {\n            eventArgs.Cancel = true;\n            Environment.Exit(0);\n        };\n\n        PrintLogo();\n\n        IHost host = CreateHost(args);\n\n        try\n        {\n            await host.RunAsync();\n            return 0;\n        }\n        catch (Exception ex)\n        {\n            Log.Fatal(ex, \"Host terminated unexpectedly!\");\n            return 1;\n        }\n        finally\n        {\n            await Log.CloseAndFlushAsync();\n        }\n    }\n\n    public static IHost CreateHost(string[] args)\n    {\n        IHost host = CreateHostBuilder(args).UseConsoleLifetime().Build();\n        Global.ServiceProviderRoot = host.Services;\n        return host;\n    }\n\n    private static HostBuilder CreateHostBuilder(string[] args)\n    {\n        //IHostBuilder hostBuilder = Host.CreateDefaultBuilder();\n        var hostBuilder = new HostBuilder();\n\n        //hostBuilder.UseContentRoot(Directory.GetCurrentDirectory());\n\n        hostBuilder.ConfigureHostConfiguration(hostConfigurationBuilder =>\n        {\n            hostConfigurationBuilder.AddEnvironmentVariables(prefix: \"DOTNET_\");\n\n            if (args is { Length: > 0 })\n            {\n                hostConfigurationBuilder.AddCommandLine(args);\n            }\n        });\n\n        hostBuilder.ConfigureAppConfiguration(\n            (hostBuilderContext, configurationBuilder) =>\n            {\n                IHostEnvironment env = hostBuilderContext.HostingEnvironment;\n\n                //json文件：\n                string envName = hostBuilderContext.HostingEnvironment.EnvironmentName;\n                configurationBuilder\n                    .AddJsonFile(\"appsettings.json\", true, true)\n                    .AddJsonFile($\"appsettings.{envName}.json\", true, true);\n\n                //用户机密：\n                if (env.IsDevelopment() && env.ApplicationName?.Length > 0)\n                {\n                    //var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));\n                    var appAssembly = Assembly.GetAssembly(typeof(Program));\n                    configurationBuilder.AddUserSecrets(\n                        appAssembly!,\n                        optional: true,\n                        reloadOnChange: true\n                    );\n                }\n\n                //环境变量：\n                configurationBuilder.AddEnvironmentVariables(\"Ray_\");\n                configurationBuilder.AddEnvironmentVariables();\n\n                //命令行：\n                if (args is { Length: > 0 })\n                {\n                    configurationBuilder.AddCommandLine(\n                        args,\n                        Config.Constants.CommandLineMappingsDic\n                    );\n                }\n\n                //本地cookie存储文件\n                configurationBuilder.AddJsonFile(\"cookies.json\", true, true);\n            }\n        );\n\n        SelfLog.Enable(x => System.Console.WriteLine(x ?? \"\"));\n        hostBuilder.UseSerilog(\n            (context, services, configuration) =>\n                configuration.ReadFrom.Configuration(context.Configuration)\n        );\n\n        hostBuilder.ConfigureServices(\n            (hostContext, services) =>\n            {\n                services.AddHostedService<BiliBiliToolHostedService>();\n\n                services.AddBiliBiliConfigs(hostContext.Configuration);\n                services.AddBiliBiliClientApi(hostContext.Configuration);\n                services.AddDomainServices();\n                services.AddAppServices();\n            }\n        );\n\n        return hostBuilder;\n    }\n\n    /// <summary>\n    /// 输出本工具启动logo\n    /// </summary>\n    private static void PrintLogo()\n    {\n        System.Console.WriteLine(@\"  ____    _   _____           _  \");\n        System.Console.WriteLine(@\" | __ ) _| |_|_   _|__   ___ | | \");\n        System.Console.WriteLine(@\" |  _ \\(_) (_) | |/ _ \\ / _ \\| | \");\n        System.Console.WriteLine(@\" | |_) | | | | | | (_) | (_) | | \");\n        System.Console.WriteLine(@\" |____/|_|_|_| |_|\\___/ \\___/|_| \");\n        System.Console.WriteLine();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Console/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"Ray.BiliBiliTool.Console\": {\n      \"commandName\": \"Project\",\n      \"environmentVariables\": {\n        \"DOTNET_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Docker\": {\n      \"commandName\": \"Docker\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Console/Ray.BiliBiliTool.Console.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <Import Project=\"..\\..\\common.props\" />\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <UserSecretsId>3cc5407e-fe0e-4df6-a127-7385c75abd8a</UserSecretsId>\n    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>\n    <DockerfileContext>..\\..</DockerfileContext>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Remove=\"appsettings.json\" />\n    <None Remove=\"appsettings.Production.json\" />\n  </ItemGroup>\n  <ItemGroup>\n    <Content Include=\"appsettings.Production.json\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n      <ExcludeFromSingleFile>true</ExcludeFromSingleFile>\n      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>\n    </Content>\n    <Content Include=\"appsettings.json\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n      <ExcludeFromSingleFile>true</ExcludeFromSingleFile>\n      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>\n    </Content>\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.UserSecrets\" />\n    <PackageReference Include=\"Microsoft.Extensions.DependencyInjection\" />\n    <PackageReference Include=\"Microsoft.Extensions.Hosting\" />\n    <PackageReference Include=\"Microsoft.Extensions.Http\" />\n    <PackageReference Include=\"Serilog\" />\n    <PackageReference Include=\"Serilog.Extensions.Hosting\" />\n    <PackageReference Include=\"Serilog.Settings.Configuration\" />\n    <PackageReference Include=\"Serilog.Sinks.Console\" />\n    <PackageReference Include=\"Serilog.Sinks.Debug\" />\n    <PackageReference Include=\"Serilog.Sinks.File\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.CoolPushBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.DingTalkBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.GotifyBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.MicrosoftTeamsBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.OtherApiBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.PushPlusBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.ServerChanBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.TelegramBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.WorkWeiXinAppBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.WorkWeiXinBatched\" />\n    <PackageReference Include=\"Microsoft.VisualStudio.Azure.Containers.Tools.Targets\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Agent\\Ray.BiliBiliTool.Agent.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Application.Contracts\\Ray.BiliBiliTool.Application.Contracts.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Application\\Ray.BiliBiliTool.Application.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Config\\Ray.BiliBiliTool.Config.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.DomainService\\Ray.BiliBiliTool.DomainService.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Infrastructure\\Ray.BiliBiliTool.Infrastructure.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <None Update=\"appsettings.Development.json\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"cookies.json\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n  <ProjectExtensions>\n    <VisualStudio>\n      <UserProperties\n        appsettings_1development_1json__JsonSchema=\"https://json.schemastore.org/appsettings\"\n        appsettings_1json__JsonSchema=\"\"\n      />\n    </VisualStudio>\n  </ProjectExtensions>\n  <Target Name=\"Husky\" BeforeTargets=\"Restore;CollectPackageReferences\" Condition=\"'$(HUSKY)' != 0\">\n    <Exec\n      Command=\"dotnet tool restore\"\n      StandardOutputImportance=\"Low\"\n      StandardErrorImportance=\"High\"\n    />\n    <Exec\n      Command=\"dotnet husky install\"\n      StandardOutputImportance=\"Low\"\n      StandardErrorImportance=\"High\"\n      WorkingDirectory=\"..\\..\"\n    />\n  </Target>\n</Project>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Console/appsettings.Development.json",
    "content": "{\n  \"DailyTaskConfig\": {\n    \"SupportUpIds\": \"\",\n    \"AutoChargeUpId\": \"341688380\"\n  },\n  \"Security\": {\n    \"IsSkipDailyTask\": false,\n    \"RandomSleepMaxMin\": 0,\n    \"IntervalSecondsBetweenRequestApi\": 7\n  }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Console/appsettings.Production.json",
    "content": "{\n  \"DailyTaskConfig\": {\n    \"SupportUpIds\": \"\",\n    \"AutoChargeUpId\": \"341688380\"\n  },\n  //用于UT验证\n  \"IsPrd\": true\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Console/appsettings.json",
    "content": "{\n  \"RunTasks\": \"\", //要运行的任务名称[Daily,LiveLottery,UnfollowBatched,VipBigPoint,Test]，多个使用&分隔，如“Daily&LiveLottery”,建议使用命令行参数指定\n\n\n  \"DailyTaskConfig\": {\n    \"Cron\": \"0 0 15 * * ?\",\n    \"IsEnable\": true,\n    \"IsWatchVideo\": true, //是否观看视频\n    \"IsShareVideo\": true, //是否分享视频\n    \"IsDonateCoinForArticle\": false,\n    \"NumberOfCoins\": 5, //每日设定的投币数 [0,5]\n    \"NumberOfProtectedCoins\": 0, // 要保留的硬币数量 [0,int_max]，0 为不保留，int_max 通常取 (2^31)-1\n    \"SaveCoinsWhenLv6\": false, //达到六级后是否开始白嫖[false,true]\n    \"SelectLike\": true, //投币时是否同时点赞[false,true]\n    \"SupportUpIds\": \"\", //优先选择支持的up主Id集合，多个以英文逗号分隔，如：\"123,456\"。配置后会优先从指定的up主下挑选视频进行观看、分享和投币，不配置或配置为-1则表示没有特别支持的up，会从关注和排行耪中随机获取支持视频\n    \"DevicePlatform\": \"android\", //执行客户端操作时的平台 [ios,android]\n  },\n\n  \"MangaTaskConfig\": {\n    \"Cron\": \"0 0 14 * * ?\",\n    \"IsEnable\": true,\n    \"CustomComicId\": 27355, //自定义漫画阅读 comic_id，若不清楚含义请勿修改\n    \"CustomEpId\": 381662 //自定义漫画阅读 ep_id，若不清楚含义请勿修改\n  },\n\n  \"MangaPrivilegeTaskConfig\": {\n    \"Cron\": \"0 0 15 * * ?\",\n    \"IsEnable\": true\n  },\n\n  \"Silver2CoinTaskConfig\": {\n    \"Cron\": \"0 0 8 * * ?\",\n    \"IsEnable\": true\n  },\n\n  \"ChargeTaskConfig\": {\n    \"Cron\": \"0 0 12 28 * ?\",\n    \"IsEnable\": true,\n    \"AutoChargeUpId\": \"-1\", //指定支持的UP主Id，-1表示自己\n    \"ChargeComment\": \"\" //充电后留言\n  },\n\n  \"VipPrivilegeConfig\": {\n    \"Cron\": \"0 0 1 * * ?\",\n    \"IsEnable\": true\n  },\n\n  \"VipBigPointConfig\": {\n    \"Cron\": \"0 7 1 * * ?\",\n    \"IsEnable\": true,\n    \"ViewBangumis\": \"33378\" // 自定义番剧的ssid，若不清楚含义请勿修改（默认为名侦探柯南）\n  },\n\n  \"LiveLotteryTaskConfig\": {\n    \"Cron\": \"0 0 22 * * ?\",\n    \"IsEnable\": true,\n    \"ExcludeAwardNames\": \"舰|船|航海|代金券|自拍|照|写真|图|提督\", //根据关键字排除包含这些文字的奖品名称，多个用“|”分隔，如“照|舰|船|航海|代金券|自拍”\n    \"IncludeAwardNames\": \"\", //根据关键字指定奖品名称必须包含的文字，多个用“|”分隔，如“红包|现金|块|元”\n    \"AutoGroupFollowings\": true, //抽奖结束后是否自动将关注的主播分组到“天选时刻”分组，值域[true,false]\n    \"DenyUids\": \"65566781,1277481241,1643654862,603676925\" //主播Uid黑名单（一般是中奖后的老赖），多个用英文逗号分隔，配置后不会参加黑名单中的主播的抽奖活动\n  },\n\n  \"LiveFansMedalTaskConfig\": {\n    \"Cron\": \"0 5 0 * * ?\",\n    \"IsEnable\": true,\n    \"DanmakuContent\": \"OvO\",\n    \"HeartBeatNumber\": 70, //直播间观看的时长，单位为分钟\",\n    \"HeartBeatSendGiveUpThreshold\": 5, //当心跳包发送连续失败多少次时放弃\n    \"IsSkipLevel20Medal\": true // 是否跳过粉丝牌等级 >=0 的\n  },\n\n  \"UnfollowBatchedTaskConfig\": {\n    \"Cron\": \"0 0 6 1 * ?\",\n    \"IsEnable\": true,\n    \"GroupName\": \"天选时刻\", //取关的分组名称\n    \"Count\": 20, //本次取关个数（倒序，从后往前取关）\n    \"RetainUids\": \"\" //白名单（保留的UpId），多个用英文都好分隔，配置后，批量取关时不会取关配置的Up\n  },\n\n  //安全相关配置\n  \"Security\": {\n    \"IsSkipDailyTask\": false, //是否跳过执行任务，用于特殊情况下，通过配置灵活的开启和关闭任务\n    \"RandomSleepMaxMin\": 0, //随机睡眠的最大时长(单位为分钟)，用于使每天运行时间在范围内相对随机，值域[0,+]；配置为0表示不进行休眠\n    \"IntervalSecondsBetweenRequestApi\": 20, //两次调用api之间的间隔[0,+](单位为秒)。因为有人担心在几秒内连续调用api会被b站安全机制发现，所以为不放心的朋友添加了间隔秒数配置，两次连续调用Api之间会大于该秒数\n    \"IntervalMethodTypes\": \"GET,POST\", //间隔秒数所针对的HttpMethod，多个用英文逗号隔开，当前有GET和POST两种，可配置如“GET,POST”\n    \"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\n    \"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\n    \"WebProxy\": \"\" //代理，格式为http://host:port，如果有鉴权则为user:password@http://host:port\n  },\n\n  \"QingLongConfig\": {\n    \"ClientId\": \"\",\n    \"ClientSecret\": \"\",\n  },\n\n  //日志\n  \"Serilog\": {\n    \"Using\": [\n      \"Serilog.Sinks.Console\",\n      \"Serilog.Sinks.Debug\",\n      \"Serilog.Sinks.File\",\n      \"Ray.Serilog.Sinks.TelegramBatched\",\n      \"Ray.Serilog.Sinks.WorkWeiXinBatched\",\n      \"Ray.Serilog.Sinks.DingTalkBatched\",\n      \"Ray.Serilog.Sinks.ServerChanBatched\",\n      \"Ray.Serilog.Sinks.CoolPushBatched\",\n      \"Ray.Serilog.Sinks.OtherApiBatched\",\n      \"Ray.Serilog.Sinks.PushPlusBatched\",\n      \"Ray.Serilog.Sinks.MicrosoftTeamsBatched\",\n      \"Ray.Serilog.Sinks.WorkWeiXinAppBatched\",\n      \"Ray.Serilog.Sinks.GotifyBatched\"\n    ],\n    \"MinimumLevel\": {\n      \"Default\": \"Debug\",\n      \"Override\": {\n        \"Microsoft\": \"Warning\",\n        \"System\": \"Warning\",\n        \"Microsoft.AspNetCore\": \"Warning\"\n      }\n    },\n    \"WriteTo\": [\n      //0.Console\n      {\n        \"Name\": \"Console\",\n        \"Args\": {\n          \"restrictedToMinimumLevel\": \"Information\",\n          \"outputTemplate\": \"[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}\"\n        }\n      },\n      //1.Debug\n      { \"Name\": \"Debug\" },\n      //2.File\n      {\n        \"Name\": \"File\",\n        \"Args\": {\n          \"path\": \"Logs/log.txt\",\n          \"restrictedToMinimumLevel\": \"Verbose\",\n          \"rollingInterval\": 3\n        }\n      },\n\n      //3.Telegram机器人（https://core.telegram.org/bots/api#available-methods）\n      {\n        \"Name\": \"TelegramBatched\",\n        \"Args\": {\n          \"botToken\": \"\",\n          \"chatId\": \"\",\n          \"restrictedToMinimumLevel\": \"Information\",\n          \"proxy\": \"\", //代理，user:password@host:port\n          \"apiHost\": \"https://api.telegram.org\" //可以替换成自己搭建的反代host（https://hostloc.com/thread-805441-1-1.html）\n        }\n      },\n      //4.企业微信机器人（https://work.weixin.qq.com/api/doc/90000/90136/91770）\n      {\n        \"Name\": \"WorkWeiXinBatched\",\n        \"Args\": {\n          \"webHookUrl\": \"\", //群机器人生成\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //5.钉钉机器人（https://developers.dingtalk.com/document/app/overview-of-group-robots）\n      {\n        \"Name\": \"DingTalkBatched\",\n        \"Args\": {\n          \"webHookUrl\": \"\", //群机器人生成\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //6.Server酱（http://sc.ftqq.com/9.version）\n      {\n        \"Name\": \"ServerChanBatched\",\n        \"Args\": {\n          \"scKey\": \"\", //已过时，待删除\n          \"turboScKey\": \"\", //平台生成的ScKey\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //7.酷推\n      {\n        \"Name\": \"CoolPushBatched\",\n        \"Args\": {\n          \"sKey\": \"\",\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //8.自定义Api\n      {\n        \"Name\": \"OtherApiBatched\",\n        \"Args\": {\n          \"api\": \"\",\n          \"placeholder\": \"#msg#\", //占位符\n          \"bodyJsonTemplate\": \"{\\\"msgtype\\\":\\\"markdown\\\",\\\"markdown\\\":{\\\"content\\\":#msg#}}\", //json模板，会当作post的body，占位符会被日志内容替换（日志文本为json字符串，已经带有引号，所有模板中占位符不用使用引号包裹，如例子所示为企业微信的标准推送格式）\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //9.PushPlus（http://www.pushplus.plus/doc/）\n      {\n        \"Name\": \"PushPlusBatched\",\n        \"Args\": {\n          \"token\": \"\",\n          \"channel\": \"\", //渠道,值域[wechat,webhook,cp,sms,mail]，分别对应[微信公众号，指定第三方webhook，企业微信应用，短信，邮件]\n          \"topic\": \"\", //群组编码,用于群发，没有就不填(不填仅发送给自己)；channel为webhook时无效\n          \"webhook\": \"\", //webhook编码(不是地址)，仅在channel使用webhook渠道和CP渠道时需要填写\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //10.MicrosoftTeams\n      {\n        \"Name\": \"MicrosoftTeamsBatched\",\n        \"Args\": {\n          \"webhook\": \"\", //webhook完整地址\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //11.企业微信应用推送\n      {\n        \"Name\": \"WorkWeiXinAppBatched\",\n        \"Args\": {\n          \"corpId\": \"\", //必填\n          \"agentId\": \"\", //必填\n          \"secret\": \"\", //必填\n          \"toUser\": \"@all\",\n          \"toParty\": \"\",\n          \"toTag\": \"\",\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //12.gotify推送\n      {\n        \"Name\": \"GotifyBatched\",\n        \"Args\": {\n          \"host\": \"\", //必填，如https://www.mygotify.com\n          \"token\": \"\", //必填，应用（app）的token\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      }\n    ],\n    \"Enrich\": [ \"FromLogContext\" ]\n  },\n\n  \"PlatformType\": \"Unknown\",\n  \"IsPrd\": false\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Domain/BiliLogs.cs",
    "content": "using System.ComponentModel.DataAnnotations;\nusing System.ComponentModel.DataAnnotations.Schema;\n\nnamespace Ray.BiliBiliTool.Domain;\n\n[Table(\"bili_logs\")]\npublic class BiliLogs\n{\n    [Key]\n    [Column(\"id\")]\n    public long Id { get; set; }\n\n    [Column(\"timeStamp\")]\n    public required DateTime Timestamp { get; set; }\n\n    [Column(\"level\")]\n    public required string Level { get; set; }\n\n    [Column(\"exception\")]\n    public string? Exception { get; set; }\n\n    [Column(\"renderedMessage\")]\n    public string? RenderedMessage { get; set; }\n\n    [Column(\"properties\")]\n    public string? Properties { get; set; }\n\n    [Column(\"fireInstanceIdComputed\")]\n    public string? FireInstanceIdComputed { get; set; }\n\n    public string FormattedLogLevel =>\n        Level.ToLower() switch\n        {\n            \"verbose\" => \"VERB\",\n            \"debug\" => \"DBG\",\n            \"information\" => \"INFO\",\n            \"warning\" => \"WARN\",\n            \"error\" => \"ERR\",\n            \"fatal\" => \"FATAL\",\n            _ => Level.ToUpper(),\n        };\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Domain/ExecutionLog.cs",
    "content": "using System.ComponentModel.DataAnnotations;\nusing System.ComponentModel.DataAnnotations.Schema;\n\nnamespace Ray.BiliBiliTool.Domain;\n\n[Table(\"bili_execution_logs\")]\npublic class ExecutionLog\n{\n    [Key]\n    public long LogId { get; set; }\n\n    [MaxLength(256)]\n    public string? RunInstanceId { get; set; }\n\n    [Column(TypeName = \"varchar(20)\")]\n    public LogType LogType { get; set; }\n\n    [MaxLength(256)]\n    public string? JobName { get; set; }\n\n    [MaxLength(256)]\n    public string? JobGroup { get; set; }\n\n    [MaxLength(256)]\n    public string? TriggerName { get; set; }\n\n    [MaxLength(256)]\n    public string? TriggerGroup { get; set; }\n\n    /// <summary>\n    /// Expected time the job should get triggered\n    /// </summary>\n    public DateTimeOffset? ScheduleFireTimeUtc { get; set; }\n\n    /// <summary>\n    /// Actual time the job got triggered\n    /// </summary>\n    public DateTimeOffset? FireTimeUtc { get; set; }\n\n    public TimeSpan? JobRunTime { get; set; }\n    public int? RetryCount { get; set; }\n\n    [MaxLength(8000)]\n    public string? Result { get; set; }\n\n    [MaxLength(8000)]\n    public string? ErrorMessage { get; set; }\n    public bool? IsVetoed { get; set; }\n    public bool? IsException { get; set; }\n\n    /// <summary>\n    /// Indicate whether the execution is successful or not.\n    /// If <see cref=\"LogType\"/> is <see cref=\"LogType.ScheduleJob\"/>, it may have value:\n    /// <para>true - If job does not return IsSuccess or when execution completed successfully</para>\n    /// <para>false - Execution completed but return code is error or job throw an exception</para>\n    /// <para>null - Job still running or terminated unexpectedly.</para>\n    /// If <see cref=\"LogType\"/> is not <see cref=\"LogType.ScheduleJob\"/> value will be null.\n    /// </summary>\n    public bool? IsSuccess { get; set; }\n\n    /// <summary>\n    /// Return code of execution.\n    /// <para>Ex.</para>\n    /// <para>for HTTP call - 200, 404, 500 etc.</para>\n    /// <para>for command line - 0 = success, -1 = failed</para>\n    /// </summary>\n    [MaxLength(28)]\n    public string? ReturnCode { get; set; }\n\n    public DateTimeOffset DateAddedUtc { get; set; }\n    public ExecutionLogDetail? ExecutionLogDetail { get; set; }\n\n    public ExecutionLog()\n    {\n        DateAddedUtc = DateTimeOffset.UtcNow;\n    }\n\n    public DateTimeOffset? GetFinishTimeUtc() => FireTimeUtc?.Add(JobRunTime ?? TimeSpan.Zero);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Domain/ExecutionLogDetail.cs",
    "content": "using System.ComponentModel.DataAnnotations;\n\nnamespace Ray.BiliBiliTool.Domain;\n\npublic class ExecutionLogDetail\n{\n    public string? ExecutionDetails { get; set; }\n    public string? ErrorStackTrace { get; set; }\n    public int? ErrorCode { get; set; }\n\n    [MaxLength(1000)]\n    public string? ErrorHelpLink { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Domain/LogType.cs",
    "content": "namespace Ray.BiliBiliTool.Domain;\n\npublic enum LogType\n{\n    ScheduleJob,\n    Trigger,\n    System,\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Domain/Ray.BiliBiliTool.Domain.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Domain/User.cs",
    "content": "using System.ComponentModel.DataAnnotations;\nusing System.ComponentModel.DataAnnotations.Schema;\n\nnamespace Ray.BiliBiliTool.Domain;\n\n[Table(\"bili_user\")]\npublic class User\n{\n    [Key]\n    public long Id { get; set; }\n    public required string Username { get; set; }\n    public required string PasswordHash { get; set; }\n    public required string Salt { get; set; }\n    public List<string> Roles { get; set; } = [];\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/AccountDomainService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\n\nnamespace Ray.BiliBiliTool.DomainService;\n\n/// <summary>\n/// 账户\n/// </summary>\npublic class AccountDomainService(\n    ILogger<AccountDomainService> logger,\n    IDailyTaskApi dailyTaskApi,\n    IUserInfoApi userInfoApi,\n    IRelationApi relationApi,\n    IOptionsMonitor<UnfollowBatchedTaskOptions> unfollowBatchedTaskOptions,\n    IOptionsMonitor<DailyTaskOptions> dailyTaskOptions\n) : IAccountDomainService\n{\n    private readonly UnfollowBatchedTaskOptions _unfollowBatchedTaskOptions =\n        unfollowBatchedTaskOptions.CurrentValue;\n    private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue;\n\n    /// <summary>\n    /// 登录\n    /// </summary>\n    /// <returns></returns>\n    public async Task<UserInfo> LoginByCookie(BiliCookie cookie)\n    {\n        BiliApiResponse<UserInfo> apiResponse = await userInfoApi.LoginByCookie(cookie.ToString());\n\n        if (apiResponse.Code != 0 || !apiResponse.Data!.IsLogin)\n        {\n            throw new Exception(\"登录失败，请检查Cookie\");\n            ;\n        }\n\n        UserInfo useInfo = apiResponse.Data;\n\n        logger.LogInformation(\"【用户名】{0}\", useInfo.GetFuzzyUname());\n        logger.LogInformation(\"【会员类型】{0}\", useInfo.VipType.Description());\n        logger.LogInformation(\"【会员状态】{0}\", useInfo.VipStatus.Description());\n        logger.LogInformation(\"【硬币余额】{0}\", useInfo.Money ?? 0);\n\n        if (useInfo.Level_info?.Current_level < 6)\n        {\n            logger.LogInformation(\n                \"【距升级Lv{0}】预计{1}天\",\n                useInfo.Level_info.Current_level + 1,\n                CalculateUpgradeTime(useInfo)\n            );\n        }\n        else\n        {\n            logger.LogInformation(\"【当前经验】{0}\", useInfo.Level_info?.Current_exp);\n            logger.LogInformation(\"您已是 Lv6 的大佬了，无敌是多么寂寞~\");\n        }\n\n        return useInfo;\n    }\n\n    /// <summary>\n    /// 获取每日任务完成情况\n    /// </summary>\n    /// <returns></returns>\n    public async Task<DailyTaskInfo> GetDailyTaskStatus(BiliCookie ck)\n    {\n        DailyTaskInfo result = new();\n        BiliApiResponse<DailyTaskInfo> apiResponse = await dailyTaskApi.GetDailyTaskRewardInfoAsync(\n            ck.ToString()\n        );\n        if (apiResponse.Code == 0)\n        {\n            logger.LogDebug(\"请求本日任务完成状态成功\");\n            result = apiResponse.Data;\n        }\n        else\n        {\n            logger.LogWarning(\"获取今日任务完成状态失败：{result}\", apiResponse.ToJsonStr());\n            result = (await dailyTaskApi.GetDailyTaskRewardInfoAsync(ck.ToString())).Data;\n            //todo:偶发性请求失败，再请求一次，这么写很丑陋，待用polly再框架层面实现\n        }\n\n        return result!;\n    }\n\n    /// <summary>\n    /// 取关\n    /// </summary>\n    /// <param name=\"groupName\"></param>\n    /// <param name=\"count\"></param>\n    public async Task UnfollowBatched(BiliCookie ck)\n    {\n        logger.LogInformation(\"【分组名】{group}\", _unfollowBatchedTaskOptions.GroupName);\n\n        //根据分组名称获取tag\n        TagDto? tag = await GetTag(_unfollowBatchedTaskOptions.GroupName, ck);\n        var tagId = tag?.Tagid;\n        int total = tag?.Count ?? 0;\n\n        if (!tagId.HasValue)\n        {\n            logger.LogWarning(\"分组名称不存在\");\n            return;\n        }\n\n        if (total == 0)\n        {\n            logger.LogWarning(\"分组下不存在up\");\n            return;\n        }\n        int count = _unfollowBatchedTaskOptions.Count;\n        if (count == -1)\n            count = total;\n\n        logger.LogInformation(\"【分组下共有】{count}人\", total);\n        logger.LogInformation(\"【目标取关】{count}人\" + Environment.NewLine, count);\n\n        //计算共几页\n        int totalPage = (int)Math.Ceiling(total / (double)20);\n\n        //从最后一页开始获取\n        var req = new GetSpecialFollowingsRequest(long.Parse(ck.UserId), tagId.Value)\n        {\n            Pn = totalPage,\n        };\n        List<UpInfo> followings = (await relationApi.GetFollowingsByTag(req, ck.ToString())).Data;\n        followings.Reverse();\n\n        var targetList = new List<UpInfo>();\n\n        if (count <= followings.Count)\n        {\n            targetList = followings.Take(count).ToList();\n        }\n        else\n        {\n            int pn = totalPage;\n            while (targetList.Count < count)\n            {\n                targetList.AddRange(followings);\n\n                //获取前一页\n                pn -= 1;\n                if (pn <= 0)\n                    break;\n                req.Pn = pn;\n                followings = (await relationApi.GetFollowingsByTag(req, ck.ToString())).Data;\n                followings.Reverse();\n            }\n        }\n\n        logger.LogInformation(\"开始取关...\" + Environment.NewLine);\n        int success = 0;\n        for (int i = 1; i <= targetList.Count && i <= count; i++)\n        {\n            UpInfo info = targetList[i - 1];\n\n            logger.LogInformation(\"【序号】{num}\", i);\n            logger.LogInformation(\"【UP】{up}\", info.Uname);\n\n            if (_unfollowBatchedTaskOptions.RetainUidList.Contains(info.Mid.ToString()))\n            {\n                logger.LogInformation(\"【取关结果】白名单，跳过\" + Environment.NewLine);\n                continue;\n            }\n\n            string modifyReferer = string.Format(\n                RelationApiConstant.ModifyReferer,\n                ck.UserId,\n                tagId\n            );\n            var modifyReq = new ModifyRelationRequest(info.Mid, ck.BiliJct);\n            var re = await relationApi.ModifyRelation(modifyReq, ck.ToString(), modifyReferer);\n\n            if (re.Code == 0)\n            {\n                logger.LogInformation(\"【取关结果】成功\" + Environment.NewLine);\n                success++;\n            }\n            else\n            {\n                logger.LogInformation(\"【取关结果】失败\");\n                logger.LogInformation(\"【原因】{msg}\" + Environment.NewLine, re.Message);\n            }\n        }\n\n        logger.LogInformation(\"【本次共取关】{count}人\", success);\n\n        //计算剩余\n        tag = await GetTag(_unfollowBatchedTaskOptions.GroupName, ck);\n        logger.LogInformation(\"【分组下剩余】{count}人\", tag?.Count);\n    }\n\n    /// <summary>\n    /// 获取分组（标签）\n    /// </summary>\n    /// <param name=\"groupName\"></param>\n    /// <param name=\"ck\"></param>\n    /// <returns></returns>\n    private async Task<TagDto?> GetTag(string groupName, BiliCookie ck)\n    {\n        string getTagsReferer = string.Format(RelationApiConstant.GetTagsReferer, ck.UserId);\n        List<TagDto> tagList = (await relationApi.GetTags(ck.ToString(), getTagsReferer)).Data!;\n        var tag = tagList.FirstOrDefault(x => x.Name == groupName);\n        return tag;\n    }\n\n    /// <summary>\n    /// 计算升级时间\n    /// </summary>\n    /// <param name=\"useInfo\"></param>\n    /// <returns>升级时间</returns>\n    public int CalculateUpgradeTime(UserInfo useInfo)\n    {\n        double availableCoins =\n            decimal.ToDouble(useInfo.Money ?? 0) - _dailyTaskOptions.NumberOfProtectedCoins;\n        long needExp =\n            useInfo.Level_info != null\n                ? useInfo.Level_info.GetNext_expLong() - useInfo.Level_info.Current_exp\n                : 0;\n        int needDay;\n\n        if (availableCoins < 0)\n            needDay = (int)(\n                (double)needExp / 25\n                + _dailyTaskOptions.NumberOfProtectedCoins\n                - Math.Abs(availableCoins)\n            );\n\n        switch (_dailyTaskOptions.NumberOfCoins)\n        {\n            case 0:\n                needDay = (int)(needExp / 15);\n                break;\n            case 1:\n                needDay = (int)(needExp / 25);\n                break;\n            default:\n                int dailyExpAvailable = 15 + _dailyTaskOptions.NumberOfCoins * 10;\n                double needFrontDay = availableCoins / (_dailyTaskOptions.NumberOfCoins - 1);\n\n                if ((double)needExp / dailyExpAvailable > needFrontDay)\n                    needDay = (int)(\n                        needFrontDay + (needExp - dailyExpAvailable * needFrontDay) / 25\n                    );\n                else\n                    needDay = (int)(needExp / dailyExpAvailable);\n                break;\n        }\n\n        return needDay;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/ArticleDomainService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Article;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\n\nnamespace Ray.BiliBiliTool.DomainService;\n\npublic class ArticleDomainService(\n    IArticleApi articleApi,\n    ILogger<ArticleDomainService> logger,\n    IOptionsMonitor<DailyTaskOptions> dailyTaskOptions,\n    ICoinDomainService coinDomainService,\n    IAccountApi accountApi\n) : IArticleDomainService\n{\n    private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue;\n\n    /// <summary>\n    /// up的专栏总数缓存\n    /// </summary>\n    private readonly Dictionary<long, int> _upArticleCountDicCatch = new();\n\n    /// <summary>\n    /// 已对投币数量缓存\n    /// </summary>\n    private readonly Dictionary<string, int> _alreadyDonatedCoinCountCatch = new();\n\n    public async Task LikeArticle(long cvid, BiliCookie ck)\n    {\n        await articleApi.LikeAsync(cvid, ck.BiliJct, ck.ToString());\n    }\n\n    /// <summary>\n    /// 投币专栏任务\n    /// </summary>\n    /// <returns></returns>\n    public async Task<bool> AddCoinForArticles(BiliCookie ck)\n    {\n        var donateCoinsCounts = await CalculateDonateCoinsCounts(ck);\n\n        if (donateCoinsCounts == 0)\n        {\n            // 没有可投的币相当于投币任务全部完成\n            return true;\n        }\n\n        int success = 0;\n        int tryCount = 10;\n\n        for (int i = 0; i <= tryCount && success < donateCoinsCounts; i++)\n        {\n            logger.LogDebug(\"开始尝试第{num}次\", i);\n\n            var upId = GetUpFromConfigUps(ck);\n            if (upId == 0)\n            {\n                logger.LogDebug(\"未能成功选择支持的Up主\");\n                continue;\n            }\n            // 当upId不符合时，会直接报错，需要将两者的判断分隔开\n            var cvid = await GetRandomArticleFromUp(upId, ck);\n            if (cvid == 0)\n            {\n                logger.LogDebug(\"第{num}次尝试，未能成功选择合适的专栏\", i);\n                continue;\n            }\n\n            if (await AddCoinForArticle(cvid, upId, ck))\n            {\n                // 点赞\n                if (_dailyTaskOptions.SelectLike)\n                {\n                    await LikeArticle(cvid, ck);\n                    logger.LogInformation(\"专栏点赞成功\");\n                }\n\n                success++;\n            }\n        }\n\n        if (success == donateCoinsCounts)\n            logger.LogInformation(\"专栏投币任务完成\");\n        else\n        {\n            logger.LogInformation(\"投币尝试超过10次，已终止\");\n            return false;\n        }\n\n        logger.LogInformation(\n            \"【硬币余额】{coin}\",\n            (await accountApi.GetCoinBalanceAsync(ck.ToString())).Data!.Money ?? 0\n        );\n\n        return true;\n    }\n\n    /// <summary>\n    /// 给某一篇专栏投币\n    /// </summary>\n    /// <param name=\"cvid\">文章cvid</param>\n    /// <param name=\"mid\">文章作者mid</param>\n    /// <returns>投币是否成功（false 投币失败，true 投币成功）</returns>\n    public async Task<bool> AddCoinForArticle(long cvid, long mid, BiliCookie ck)\n    {\n        BiliApiResponse result;\n        try\n        {\n            var refer =\n                $\"https://www.bilibili.com/read/cv{cvid}/?from=search&spm_id_from=333.337.0.0\";\n            result = await articleApi.AddCoinForArticleAsync(\n                new AddCoinForArticleRequest(cvid, mid, ck.BiliJct),\n                ck.ToString(),\n                refer\n            );\n        }\n        catch (Exception)\n        {\n            return false;\n        }\n\n        if (result.Code == 0)\n        {\n            logger.LogInformation(\"投币成功，经验+10 √\");\n            return true;\n        }\n        else\n        {\n            logger.LogError(\"投币错误 {message}\", result.Message);\n            return false;\n        }\n    }\n\n    #region private\n\n    /// <summary>\n    /// 从某个up主中随机挑选一个专栏\n    /// </summary>\n    /// <param name=\"mid\"></param>\n    /// <returns>专栏的cvid</returns>\n    private async Task<long> GetRandomArticleFromUp(long mid, BiliCookie ck)\n    {\n        if (!_upArticleCountDicCatch.TryGetValue(mid, out int articleCount))\n        {\n            articleCount = await GetArticleCountOfUp(mid, ck);\n            _upArticleCountDicCatch.Add(mid, articleCount);\n        }\n\n        // 专栏数为0时\n        if (articleCount == 0)\n        {\n            return 0;\n        }\n\n        var req = new SearchArticlesByUpIdDto()\n        {\n            mid = mid,\n            ps = 1,\n            pn = new Random().Next(1, articleCount + 1),\n        };\n\n        BiliApiResponse<SearchUpArticlesResponse> re = await articleApi.SearchUpArticlesByUpIdAsync(\n            req\n        );\n\n        if (re.Code != 0)\n        {\n            throw new Exception(re.Message);\n        }\n\n        var articleInfo = re.Data.Articles.FirstOrDefault();\n\n        logger.LogInformation(\"获取到的专栏{cvid}({title})\", articleInfo?.Id, articleInfo?.Title);\n\n        // 检查是否可投\n        if (articleInfo == null || !await IsCanDonate(articleInfo.Id))\n        {\n            return 0;\n        }\n\n        return articleInfo.Id;\n    }\n\n    // TODO 转变为异步代码\n    /// <summary>\n    /// 从支持UP主列表中随机挑选一位\n    /// </summary>\n    /// <returns>被挑选up主的mid</returns>\n    private long GetUpFromConfigUps(BiliCookie ck)\n    {\n        if (\n            _dailyTaskOptions.SupportUpIdList == null\n            || _dailyTaskOptions.SupportUpIdList.Count == 0\n        )\n        {\n            return 0;\n        }\n\n        try\n        {\n            long randomUpId = _dailyTaskOptions.SupportUpIdList[\n                new Random().Next(0, _dailyTaskOptions.SupportUpIdList.Count)\n            ];\n\n            if (randomUpId is 0 or long.MinValue)\n                return 0;\n\n            if (randomUpId.ToString() == ck.UserId)\n            {\n                logger.LogDebug(\"不能为自己投币\");\n                return 0;\n            }\n\n            logger.LogDebug(\"挑选出的up主为{UpId}\", randomUpId);\n            return randomUpId;\n        }\n        catch (Exception e)\n        {\n            logger.LogWarning(\"异常：{msg}\", e);\n        }\n\n        return 0;\n    }\n\n    /// <summary>\n    /// 获取Up主专栏总数\n    /// </summary>\n    /// <param name=\"mid\">up主mid</param>\n    /// <returns>专栏总数</returns>\n    /// <exception cref=\"Exception\"></exception>\n    private async Task<int> GetArticleCountOfUp(long mid, BiliCookie ck)\n    {\n        var req = new SearchArticlesByUpIdDto() { mid = mid };\n\n        BiliApiResponse<SearchUpArticlesResponse> re = await articleApi.SearchUpArticlesByUpIdAsync(\n            req\n        );\n\n        if (re.Code != 0)\n        {\n            throw new Exception(re.Message);\n        }\n\n        return re.Data.Count;\n    }\n\n    /// <summary>\n    /// 计算所需要投的硬币数量\n    /// </summary>\n    /// <returns>硬币数量</returns>\n    private async Task<int> CalculateDonateCoinsCounts(BiliCookie ck)\n    {\n        int needCoins = await GetNeedDonateCoinCounts(ck);\n\n        int protectedCoins = _dailyTaskOptions.NumberOfProtectedCoins;\n        if (needCoins <= 0)\n            return 0;\n\n        //投币前硬币余额\n        decimal coinBalance = await coinDomainService.GetCoinBalance(ck);\n        logger.LogInformation(\"【投币前余额】 : {coinBalance}\", coinBalance);\n        _ = int.TryParse(\n            decimal.Truncate(coinBalance - protectedCoins).ToString(),\n            out int unprotectedCoins\n        );\n\n        if (coinBalance <= 0)\n        {\n            logger.LogInformation(\"因硬币余额不足，今日暂不执行投币任务\");\n            return 0;\n        }\n\n        if (coinBalance <= protectedCoins)\n        {\n            logger.LogInformation(\"因硬币余额达到或低于保留值，今日暂不执行投币任务\");\n            return 0;\n        }\n\n        //余额小于目标投币数，按余额投\n        if (coinBalance < needCoins)\n        {\n            _ = int.TryParse(decimal.Truncate(coinBalance).ToString(), out needCoins);\n            logger.LogInformation(\"因硬币余额不足，目标投币数调整为: {needCoins}\", needCoins);\n            return needCoins;\n        }\n\n        //投币后余额小于等于保护值，按保护值允许投\n        if (coinBalance - needCoins <= protectedCoins)\n        {\n            //排除需投等于保护后可投数量相等时的情况\n            if (unprotectedCoins != needCoins)\n            {\n                needCoins = unprotectedCoins;\n                logger.LogInformation(\n                    \"因硬币余额投币后将达到或低于保留值，目标投币数调整为: {needCoins}\",\n                    needCoins\n                );\n                return needCoins;\n            }\n        }\n\n        return needCoins;\n    }\n\n    private async Task<int> GetNeedDonateCoinCounts(BiliCookie ck)\n    {\n        int configCoins = _dailyTaskOptions.NumberOfCoins;\n\n        if (configCoins <= 0)\n        {\n            logger.LogInformation(\"已配置为跳过投币任务\");\n            return configCoins;\n        }\n\n        //已投的硬币\n        int alreadyCoins = await coinDomainService.GetDonatedCoins(ck);\n\n        int targetCoins = configCoins;\n\n        logger.LogInformation(\"【今日已投】{already}枚\", alreadyCoins);\n        logger.LogInformation(\"【目标欲投】{already}枚\", targetCoins);\n\n        if (targetCoins > alreadyCoins)\n        {\n            int needCoins = targetCoins - alreadyCoins;\n            logger.LogInformation(\"【还需再投】{need}枚\", needCoins);\n            return needCoins;\n        }\n\n        logger.LogInformation(\"已完成投币任务，不需要再投啦~\");\n        return 0;\n    }\n\n    private async Task<bool> IsCanDonate(long cvid)\n    {\n        try\n        {\n            if (_alreadyDonatedCoinCountCatch.Any(x => x.Key == cvid.ToString()))\n            {\n                logger.LogDebug(\"重复专栏，丢弃处理\");\n                return false;\n            }\n\n            if (!_alreadyDonatedCoinCountCatch.TryGetValue(cvid.ToString(), out int multiply))\n            {\n                multiply = (await articleApi.SearchArticleInfoAsync(cvid)).Data.Coin;\n                _alreadyDonatedCoinCountCatch.TryAdd(cvid.ToString(), multiply);\n            }\n\n            // 在网页端我测试时只能投一枚硬币，暂时设置最多投一枚\n            if (multiply >= 1)\n            {\n                return false;\n            }\n\n            return true;\n        }\n        catch (Exception e)\n        {\n            logger.LogWarning(\"异常：{mag}\", e);\n            return false;\n        }\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/ChargeDomainService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\n\nnamespace Ray.BiliBiliTool.DomainService;\n\n/// <summary>\n/// 充电\n/// </summary>\npublic class ChargeDomainService(\n    ILogger<ChargeDomainService> logger,\n    IOptionsMonitor<DailyTaskOptions> dailyTaskOptions,\n    IOptionsMonitor<ChargeTaskOptions> chargeTaskOptions,\n    IDailyTaskApi dailyTaskApi,\n    IChargeApi chargeApi\n) : IChargeDomainService\n{\n    private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue;\n    private readonly ChargeTaskOptions _chargeTaskOptions = chargeTaskOptions.CurrentValue;\n    private readonly IDailyTaskApi _dailyTaskApi = dailyTaskApi;\n\n    /// <summary>\n    /// 月底自动己充电\n    /// 仅充会到期的B币券，低于2的时候不会充\n    /// </summary>\n    public async Task Charge(UserInfo userInfo, BiliCookie ck)\n    {\n        //大会员类型\n        VipType vipType = userInfo.GetVipType();\n        if (vipType != VipType.Annual)\n        {\n            logger.LogInformation(\"不是年度大会员，跳过\");\n            return;\n        }\n\n        logger.LogInformation(\"【今天】{today}号\", DateTime.Today.Day);\n\n        //B币券余额\n        decimal couponBalance = userInfo.Wallet?.Coupon_balance ?? 0;\n        logger.LogInformation(\"【B币券】{couponBalance}\", couponBalance);\n        if (couponBalance < 2)\n        {\n            logger.LogInformation(\"余额小于2，无法充电\");\n            return;\n        }\n\n        //如果没有配置或配了-1，则使用fallback值（B站最新策略已不允许为自己充电）\n        string targetUpId =\n            string.IsNullOrWhiteSpace(_chargeTaskOptions.AutoChargeUpId)\n            || _chargeTaskOptions.AutoChargeUpId == \"-1\"\n                ? Config.Constants.FallbackAutoChargeUpId\n                : _chargeTaskOptions.AutoChargeUpId!;\n\n        logger.LogDebug(\"【目标Up】{up}\", targetUpId);\n\n        var request = new ChargeRequest(couponBalance, long.Parse(targetUpId), ck.BiliJct);\n\n        //BiliApiResponse<ChargeResponse> response = await _chargeApi.Charge(decimal.ToInt32(couponBalance * 10), _dailyTaskOptions.AutoChargeUpId, _cookieOptions.UserId, _cookieOptions.BiliJct);\n        BiliApiResponse<ChargeV2Response> response = await chargeApi.ChargeV2Async(\n            request,\n            ck.ToString()\n        );\n\n        if (response.Code == 0)\n        {\n            if (response.Data?.Status == 4)\n            {\n                logger.LogInformation(\"【充电结果】成功\");\n                logger.LogInformation(\"【充值个数】 {num}个B币\", couponBalance);\n                logger.LogInformation(\"经验+{exp} √\", couponBalance);\n                logger.LogInformation(\"在过期前使用成功，赠送的B币券没有浪费哦~\");\n\n                //充电留言\n                await ChargeComments(response.Data.Order_no, ck);\n            }\n            else\n            {\n                logger.LogInformation(\"【充电结果】失败\");\n                logger.LogError(\"【原因】{reason}\", response.ToJsonStr());\n            }\n        }\n        else\n        {\n            logger.LogInformation(\"【充电结果】失败\");\n            logger.LogError(\"【原因】{reason}\", response.Message);\n        }\n    }\n\n    /// <summary>\n    /// 充电后留言\n    /// </summary>\n    /// <param name=\"token\"></param>\n    public async Task ChargeComments(string orderNum, BiliCookie ck)\n    {\n        var comment = _chargeTaskOptions.ChargeComment ?? \"\";\n        var request = new ChargeCommentRequest(orderNum, comment, ck.BiliJct);\n        await chargeApi.ChargeCommentAsync(request, ck.ToString());\n\n        logger.LogInformation(\"【留言】{comment}\", comment);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/CoinDomainService.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\n\nnamespace Ray.BiliBiliTool.DomainService;\n\n/// <summary>\n/// 硬币\n/// </summary>\npublic class CoinDomainService(IAccountApi accountApi, IDailyTaskApi dailyTaskApi)\n    : ICoinDomainService\n{\n    /// <summary>\n    /// 获取账户硬币余额\n    /// </summary>\n    /// <returns></returns>\n    public async Task<decimal> GetCoinBalance(BiliCookie ck)\n    {\n        var response = await accountApi.GetCoinBalanceAsync(ck.ToString());\n        return response.Data!.Money ?? 0;\n    }\n\n    /// <summary>\n    /// 获取今日已投币数\n    /// </summary>\n    /// <returns></returns>\n    public async Task<int> GetDonatedCoins(BiliCookie ck)\n    {\n        return (await GetDonateCoinExp(ck)) / 10;\n    }\n\n    #region private\n    /// <summary>\n    /// 获取今日通过投币已获取的经验值\n    /// </summary>\n    /// <returns></returns>\n    private async Task<int> GetDonateCoinExp(BiliCookie ck)\n    {\n        return (await dailyTaskApi.GetDonateCoinExpAsync(ck.ToString())).Data;\n    }\n    #endregion\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/DonateCoinDomainService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\n\nnamespace Ray.BiliBiliTool.DomainService;\n\n/// <summary>\n/// 投币\n/// </summary>\npublic class DonateCoinDomainService(\n    ILogger<DonateCoinDomainService> logger,\n    IOptionsMonitor<DailyTaskOptions> dailyTaskOptions,\n    IAccountApi accountApi,\n    ICoinDomainService coinDomainService,\n    IVideoDomainService videoDomainService,\n    IRelationApi relationApi,\n    IVideoApi videoApi\n) : IDonateCoinDomainService\n{\n    private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue;\n    private readonly Dictionary<string, int> _expDic = Config.Constants.ExpDic;\n    private readonly Dictionary<string, string> _donateContinueStatusDic = Config\n        .Constants\n        .DonateCoinCanContinueStatusDic;\n\n    /// <summary>\n    /// up的视频稿件总数缓存\n    /// </summary>\n    private readonly Dictionary<long, int> _upVideoCountDicCatch = new();\n\n    /// <summary>\n    /// 已对视频投币数量缓存\n    /// </summary>\n    private readonly Dictionary<string, int> _alreadyDonatedCoinCountCatch = new();\n\n    /// <summary>\n    /// 完成投币任务\n    /// </summary>\n    public async Task AddCoinsForVideos(BiliCookie ck)\n    {\n        int needCoins = await GetNeedDonateCoinNum(ck);\n        int protectedCoins = _dailyTaskOptions.NumberOfProtectedCoins;\n        if (needCoins <= 0)\n            return;\n\n        //投币前硬币余额\n        decimal coinBalance = await coinDomainService.GetCoinBalance(ck);\n        logger.LogInformation(\"【投币前余额】 : {coinBalance}\", coinBalance);\n        _ = int.TryParse(\n            decimal.Truncate(coinBalance - protectedCoins).ToString(),\n            out int unprotectedCoins\n        );\n\n        if (coinBalance <= 0)\n        {\n            logger.LogInformation(\"因硬币余额不足，今日暂不执行投币任务\");\n            return;\n        }\n\n        if (coinBalance <= protectedCoins)\n        {\n            logger.LogInformation(\"因硬币余额达到或低于保留值，今日暂不执行投币任务\");\n            return;\n        }\n\n        //余额小于目标投币数，按余额投\n        if (coinBalance < needCoins)\n        {\n            _ = int.TryParse(decimal.Truncate(coinBalance).ToString(), out needCoins);\n            logger.LogInformation(\"因硬币余额不足，目标投币数调整为: {needCoins}\", needCoins);\n        }\n\n        //投币后余额小于等于保护值，按保护值允许投\n        if (coinBalance - needCoins <= protectedCoins)\n        {\n            //排除需投等于保护后可投数量相等时的情况\n            if (unprotectedCoins != needCoins)\n            {\n                needCoins = unprotectedCoins;\n                logger.LogInformation(\n                    \"因硬币余额投币后将达到或低于保留值，目标投币数调整为: {needCoins}\",\n                    needCoins\n                );\n            }\n        }\n\n        int success = 0;\n        int tryCount = 10;\n        for (int i = 1; i <= tryCount && success < needCoins; i++)\n        {\n            logger.LogDebug(\"开始尝试第{num}次\", i);\n\n            var video = await TryGetCanDonatedVideo(ck);\n            if (video == null)\n                continue;\n\n            logger.LogInformation(\"【视频】{title}\", video.Title);\n\n            bool re = await DoAddCoinForVideo(video, _dailyTaskOptions.SelectLike, ck);\n            if (re)\n                success++;\n        }\n\n        if (success == needCoins)\n            logger.LogInformation(\"视频投币任务完成\");\n        else\n            logger.LogInformation(\"投币尝试超过10次，已终止\");\n\n        logger.LogInformation(\n            \"【硬币余额】{coin}\",\n            (await accountApi.GetCoinBalanceAsync(ck.ToString())).Data?.Money ?? 0\n        );\n    }\n\n    /// <summary>\n    /// 尝试获取一个可以投币的视频\n    /// </summary>\n    /// <returns></returns>\n    public async Task<UpVideoInfo?> TryGetCanDonatedVideo(BiliCookie ck)\n    {\n        UpVideoInfo? result;\n\n        //从配置的up中随机尝试获取1次\n        result = await TryGetCanDonateVideoByConfigUps(1, ck);\n        if (result != null)\n            return result;\n\n        //然后从特别关注列表尝试获取1次\n        result = await TryGetCanDonateVideoBySpecialUps(1, ck);\n        if (result != null)\n            return result;\n\n        //然后从普通关注列表获取1次\n        result = await TryGetCanDonateVideoByFollowingUps(1, ck);\n        if (result != null)\n            return result;\n\n        //最后从排行榜尝试5次\n        result = await TryGetCanDonateVideoByRegion(5, ck);\n\n        return result;\n    }\n\n    /// <summary>\n    /// 为视频投币\n    /// </summary>\n    /// <param name=\"aid\">av号</param>\n    /// <param name=\"multiply\">投币数量</param>\n    /// <param name=\"select_like\">是否同时点赞 1是0否</param>\n    /// <returns>是否投币成功</returns>\n    public async Task<bool> DoAddCoinForVideo(UpVideoInfo video, bool select_like, BiliCookie ck)\n    {\n        BiliApiResponse result;\n        try\n        {\n            var request = new AddCoinRequest(video.Aid, ck.BiliJct)\n            {\n                Select_like = select_like ? 1 : 0,\n            };\n            var referer =\n                $\"https://www.bilibili.com/video/{video.Bvid}/?spm_id_from=333.1007.tianma.1-1-1.click&vd_source=80c1601a7003934e7a90709c18dfcffd\";\n            result = await videoApi.AddCoinForVideo(request, ck.ToString(), referer);\n        }\n        catch (Exception)\n        {\n            return false;\n        }\n\n        if (result.Code == 0)\n        {\n            _expDic.TryGetValue(\"每日投币\", out int exp);\n            logger.LogInformation(\"投币成功，经验+{exp} √\", exp);\n            return true;\n        }\n\n        if (_donateContinueStatusDic.Any(x => x.Key == result.Code.ToString()))\n        {\n            logger.LogError(\"投币失败，原因：{msg}\", result.Message);\n            return false;\n        }\n        else\n        {\n            string errorMsg = $\"投币发生未预计异常：{result.Message}\";\n            logger.LogError(errorMsg);\n            throw new Exception(errorMsg);\n        }\n    }\n\n    #region private\n\n    /// <summary>\n    /// 获取今日的目标投币数\n    /// </summary>\n    /// <returns></returns>\n    private async Task<int> GetNeedDonateCoinNum(BiliCookie ck)\n    {\n        //获取自定义配置投币数\n        int configCoins = _dailyTaskOptions.NumberOfCoins;\n\n        if (configCoins <= 0)\n        {\n            logger.LogInformation(\"已配置为跳过投币任务\");\n            return configCoins;\n        }\n\n        //已投的硬币\n        int alreadyCoins = await coinDomainService.GetDonatedCoins(ck);\n        //目标\n        //int targetCoins = configCoins > Constants.MaxNumberOfDonateCoins\n        //    ? Constants.MaxNumberOfDonateCoins\n        //    : configCoins;\n        int targetCoins = configCoins;\n\n        logger.LogInformation(\"【今日已投】{already}枚\", alreadyCoins);\n        logger.LogInformation(\"【目标欲投】{already}枚\", targetCoins);\n\n        if (targetCoins > alreadyCoins)\n        {\n            int needCoins = targetCoins - alreadyCoins;\n            logger.LogInformation(\"【还需再投】{need}枚\", needCoins);\n            return needCoins;\n        }\n\n        logger.LogInformation(\"已完成投币任务，不需要再投啦~\");\n        return 0;\n    }\n\n    /// <summary>\n    /// 尝试从配置的up主里随机获取一个可以投币的视频\n    /// </summary>\n    /// <param name=\"tryCount\"></param>\n    /// <returns></returns>\n    private async Task<UpVideoInfo?> TryGetCanDonateVideoByConfigUps(int tryCount, BiliCookie ck)\n    {\n        //是否配置了up主\n        if (_dailyTaskOptions.SupportUpIdList.Count == 0)\n            return null;\n\n        return await TryCanDonateVideoByUps(_dailyTaskOptions.SupportUpIdList, tryCount, ck);\n        ;\n    }\n\n    /// <summary>\n    /// 尝试从特别关注的Up主中随机获取一个可以投币的视频\n    /// </summary>\n    /// <param name=\"tryCount\"></param>\n    /// <returns></returns>\n    private async Task<UpVideoInfo?> TryGetCanDonateVideoBySpecialUps(int tryCount, BiliCookie ck)\n    {\n        //获取特别关注列表\n        var request = new GetSpecialFollowingsRequest(long.Parse(ck.UserId));\n        BiliApiResponse<List<UpInfo>> specials = await relationApi.GetFollowingsByTag(\n            request,\n            ck.ToString()\n        );\n        if (specials.Data == null || specials.Data.Count == 0)\n            return null;\n\n        return await TryCanDonateVideoByUps(\n            specials.Data.Select(x => x.Mid).ToList(),\n            tryCount,\n            ck\n        );\n    }\n\n    /// <summary>\n    /// 尝试从普通关注的Up主中随机获取一个可以投币的视频\n    /// </summary>\n    /// <param name=\"tryCount\"></param>\n    /// <returns></returns>\n    private async Task<UpVideoInfo?> TryGetCanDonateVideoByFollowingUps(int tryCount, BiliCookie ck)\n    {\n        //获取特别关注列表\n        var request = new GetFollowingsRequest(long.Parse(ck.UserId));\n        BiliApiResponse<GetFollowingsResponse> result = await relationApi.GetFollowings(\n            request,\n            ck.ToString()\n        );\n        if (result.Data.Total == 0)\n            return null;\n\n        return await TryCanDonateVideoByUps(\n            result.Data.List.Select(x => x.Mid).ToList(),\n            tryCount,\n            ck\n        );\n    }\n\n    /// <summary>\n    /// 尝试从排行榜中获取一个没有看过的视频\n    /// </summary>\n    /// <param name=\"tryCount\"></param>\n    /// <returns></returns>\n    private async Task<UpVideoInfo?> TryGetCanDonateVideoByRegion(int tryCount, BiliCookie ck)\n    {\n        try\n        {\n            for (int i = 0; i < tryCount; i++)\n            {\n                RankingInfo video = await videoDomainService.GetRandomVideoOfRanking();\n                if (!await IsCanDonate(video.Aid.ToString(), ck))\n                    continue;\n                return new UpVideoInfo()\n                {\n                    Aid = video.Aid,\n                    Bvid = video.Bvid,\n                    Title = video.Title,\n                    Length = \"00:15\",\n                };\n            }\n        }\n        catch (Exception e)\n        {\n            //ignore\n            logger.LogWarning(\"异常：{msg}\", e);\n        }\n        return null;\n    }\n\n    /// <summary>\n    /// 尝试从指定的up主集合中随机获取一个可以尝试投币的视频\n    /// </summary>\n    /// <param name=\"upIds\"></param>\n    /// <param name=\"tryCount\"></param>\n    /// <returns></returns>\n    private async Task<UpVideoInfo?> TryCanDonateVideoByUps(\n        List<long> upIds,\n        int tryCount,\n        BiliCookie ck\n    )\n    {\n        if (upIds.Count == 0)\n            return null;\n\n        try\n        {\n            //尝试tryCount次\n            for (int i = 1; i <= tryCount; i++)\n            {\n                //获取随机Up主Id\n                long randomUpId = upIds[new Random().Next(0, upIds.Count)];\n\n                if (randomUpId == 0 || randomUpId == long.MinValue)\n                    continue;\n\n                if (randomUpId.ToString() == ck.UserId)\n                {\n                    logger.LogDebug(\"不能为自己投币\");\n                    continue;\n                }\n\n                //该up的视频总数\n                if (!_upVideoCountDicCatch.TryGetValue(randomUpId, out int videoCount))\n                {\n                    videoCount = await videoDomainService.GetVideoCountOfUp(randomUpId, ck);\n                    _upVideoCountDicCatch.Add(randomUpId, videoCount);\n                }\n                if (videoCount == 0)\n                    continue;\n\n                var videoInfo = await videoDomainService.GetRandomVideoOfUp(\n                    randomUpId,\n                    videoCount,\n                    ck\n                );\n                logger.LogDebug(\"获取到视频{aid}({title})\", videoInfo?.Aid, videoInfo?.Title);\n\n                //检查是否可以投\n                if (videoInfo == null || !await IsCanDonate(videoInfo.Aid.ToString(), ck))\n                    continue;\n\n                return videoInfo;\n            }\n        }\n        catch (Exception e)\n        {\n            //ignore\n            logger.LogWarning(\"异常：{msg}\", e);\n        }\n\n        return null;\n    }\n\n    /// <summary>\n    /// 已为视频投币个数是否小于最大限制\n    /// </summary>\n    /// <param name=\"aid\">av号</param>\n    /// <returns></returns>\n    private async Task<bool> IsDonatedLessThenLimitCoinsForVideo(string aid, BiliCookie ck)\n    {\n        try\n        {\n            //获取已投币数量\n            if (!_alreadyDonatedCoinCountCatch.TryGetValue(aid, out int multiply))\n            {\n                multiply = (\n                    await videoApi.GetDonatedCoinsForVideo(\n                        new GetAlreadyDonatedCoinsRequest(long.Parse(aid)),\n                        ck.ToString()\n                    )\n                )\n                    .Data\n                    .Multiply;\n                _alreadyDonatedCoinCountCatch.TryAdd(aid, multiply);\n            }\n\n            logger.LogDebug(\"已为Av{aid}投过{num}枚硬币\", aid, multiply);\n\n            if (multiply >= 2)\n                return false;\n\n            //获取该视频可投币数量\n            int limitCoinNum =\n                (await videoDomainService.GetVideoDetail(aid)).Copyright == 1\n                    ? 2 //原创，最多可投2枚\n                    : 1; //转载，最多可投1枚\n            logger.LogDebug(\"该视频的最大投币数为{num}\", limitCoinNum);\n\n            return multiply < limitCoinNum;\n        }\n        catch (Exception e)\n        {\n            //ignore\n            logger.LogWarning(\"异常：{mag}\", e);\n            return false;\n        }\n    }\n\n    /// <summary>\n    /// 检查获取到的视频是否可以投币\n    /// </summary>\n    /// <param name=\"aid\"></param>\n    /// <returns></returns>\n    private async Task<bool> IsCanDonate(string aid, BiliCookie ck)\n    {\n        //本次运行已经尝试投过的,不进行重复投（不管成功还是失败，凡取过尝试过的，不重复尝试）\n        if (_alreadyDonatedCoinCountCatch.Any(x => x.Key == aid))\n        {\n            logger.LogDebug(\"重复视频，丢弃处理\");\n            return false;\n        }\n\n        //已经投满2个币的，不能再投\n        if (!await IsDonatedLessThenLimitCoinsForVideo(aid, ck))\n        {\n            logger.LogDebug(\"超出单个视频投币数量限制，丢弃处理\");\n            return false;\n        }\n\n        return true;\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Dtos/FansMedalInfoDto.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\nnamespace Ray.BiliBiliTool.DomainService.Dtos;\n\npublic class FansMedalInfoDto\n{\n    public FansMedalInfoDto(\n        long roomId,\n        MedalWallDto medalInfo,\n        GetLiveRoomInfoResponse liveRoomInfo\n    )\n    {\n        RoomId = roomId;\n        MedalInfo = medalInfo;\n        LiveRoomInfo = liveRoomInfo;\n    }\n\n    // 直播间 id\n    public long RoomId { get; set; }\n\n    // 粉丝牌信息\n    public MedalWallDto MedalInfo { get; set; }\n\n    // 直播间信息\n    public GetLiveRoomInfoResponse LiveRoomInfo { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Dtos/HeartBeatIterationInfoDto.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\nnamespace Ray.BiliBiliTool.DomainService.Dtos;\n\npublic class HeartBeatIterationInfoDto(\n    long roomId,\n    GetLiveRoomInfoResponse roomInfo,\n    HeartBeatResponse heartBeatInfo,\n    int heartBeatCount,\n    long lastBeatTime\n)\n{\n    public long RoomId { get; set; } = roomId;\n\n    public GetLiveRoomInfoResponse RoomInfo { get; set; } = roomInfo;\n\n    public HeartBeatResponse HeartBeatInfo { get; set; } = heartBeatInfo;\n\n    // 成功发送的心跳包个数\n    public int HeartBeatCount { get; set; } = heartBeatCount;\n\n    public long LastBeatTime { get; set; } = lastBeatTime;\n\n    // 连续失败的次数\n    public int FailedTimes { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Dtos/VideoInfoDto.cs",
    "content": "﻿namespace Ray.BiliBiliTool.DomainService.Dtos;\n\npublic class VideoInfoDto\n{\n    public required string Aid { get; set; }\n\n    public required string Bvid { get; set; }\n\n    public long Cid { get; set; }\n\n    public required string Title { get; set; }\n\n    public int? Duration { get; set; }\n\n    /// <summary>\n    /// 是否转载\n    /// <sample>1：原创</sample>\n    /// <sample>2：转载</sample>\n    /// </summary>\n    public int Copyright { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Extensions/ServiceCollectionExtensions.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\n\nnamespace Ray.BiliBiliTool.DomainService.Extensions;\n\npublic static class ServiceCollectionExtensions\n{\n    public static IServiceCollection AddDomainServices(this IServiceCollection services)\n    {\n        services.Scan(scan =>\n            scan.FromAssemblyOf<IAccountDomainService>()\n                .AddClasses(classes => classes.AssignableTo<IDomainService>())\n                .AsImplementedInterfaces()\n                .WithTransientLifetime()\n        );\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Interfaces/IAccountDomainService.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\nnamespace Ray.BiliBiliTool.DomainService.Interfaces;\n\n/// <summary>\n/// 账户\n/// </summary>\npublic interface IAccountDomainService : IDomainService\n{\n    /// <summary>\n    /// 使用Cookie登录\n    /// </summary>\n    /// <returns></returns>\n    Task<UserInfo> LoginByCookie(BiliCookie cookie);\n\n    /// <summary>\n    /// 获取每日任务完成情况\n    /// </summary>\n    /// <returns></returns>\n    Task<DailyTaskInfo> GetDailyTaskStatus(BiliCookie ck);\n\n    /// <summary>\n    /// 批量取关\n    /// </summary>\n    Task UnfollowBatched(BiliCookie ck);\n\n    /// <summary>\n    /// 计算升级时间\n    /// </summary>\n    /// <param name=\"useInfo\"></param>\n    /// <returns>升级时间</returns>\n    int CalculateUpgradeTime(UserInfo useInfo);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Interfaces/IArticleDomainService.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent;\n\nnamespace Ray.BiliBiliTool.DomainService.Interfaces;\n\npublic interface IArticleDomainService : IDomainService\n{\n    Task<bool> AddCoinForArticle(long cvid, long mid, BiliCookie ck);\n\n    Task<bool> AddCoinForArticles(BiliCookie ck);\n\n    Task LikeArticle(long cvid, BiliCookie ck);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Interfaces/IChargeDomainService.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\nnamespace Ray.BiliBiliTool.DomainService.Interfaces;\n\n/// <summary>\n/// 充电\n/// </summary>\npublic interface IChargeDomainService : IDomainService\n{\n    /// <summary>\n    /// 充电\n    /// </summary>\n    /// <param name=\"userInfo\"></param>\n    Task Charge(UserInfo userInfo, BiliCookie ck);\n\n    /// <summary>\n    /// 充电后留言\n    /// </summary>\n    /// <param name=\"token\"></param>\n    Task ChargeComments(string token, BiliCookie ck);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Interfaces/ICoinDomainService.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent;\n\nnamespace Ray.BiliBiliTool.DomainService.Interfaces;\n\n/// <summary>\n/// B币\n/// </summary>\npublic interface ICoinDomainService : IDomainService\n{\n    /// <summary>\n    /// 获取账户硬币余额\n    /// </summary>\n    /// <returns></returns>\n    Task<decimal> GetCoinBalance(BiliCookie ck);\n\n    /// <summary>\n    /// 获取今日已投币数量\n    /// </summary>\n    /// <returns></returns>\n    Task<int> GetDonatedCoins(BiliCookie ck);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Interfaces/IDomainService.cs",
    "content": "﻿namespace Ray.BiliBiliTool.DomainService.Interfaces;\n\n/// <summary>\n/// 定义一个领域服务\n/// </summary>\npublic interface IDomainService { }\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Interfaces/IDonateCoinDomainService.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\nnamespace Ray.BiliBiliTool.DomainService.Interfaces;\n\n/// <summary>\n/// 投币\n/// </summary>\npublic interface IDonateCoinDomainService : IDomainService\n{\n    Task AddCoinsForVideos(BiliCookie ck);\n\n    Task<UpVideoInfo?> TryGetCanDonatedVideo(BiliCookie ck);\n\n    Task<bool> DoAddCoinForVideo(UpVideoInfo video, bool select_like, BiliCookie ck);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Interfaces/ILiveDomainService.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\n\nnamespace Ray.BiliBiliTool.DomainService.Interfaces;\n\n/// <summary>\n/// 直播中心\n/// </summary>\npublic interface ILiveDomainService : IDomainService\n{\n    /// <summary>\n    /// 签到\n    /// </summary>\n    Task LiveSign(BiliCookie ck);\n\n    /// <summary>\n    /// 银瓜子兑换硬币\n    /// </summary>\n    /// <returns></returns>\n    Task<bool> ExchangeSilver2Coin(BiliCookie ck);\n\n    /// <summary>\n    /// 天选抽奖\n    /// </summary>\n    Task TianXuan(BiliCookie ck);\n\n    Task TryJoinTianXuan(ListItemDto target, BiliCookie ck);\n\n    Task GroupFollowing(BiliCookie ck);\n\n    /// <summary>\n    /// 发送弹幕\n    /// </summary>\n    Task SendDanmakuToFansMedalLive(BiliCookie ck);\n\n    /// <summary>\n    /// 直播时长挂机\n    /// </summary>\n    Task SendHeartBeatToFansMedalLive(BiliCookie ck);\n\n    /// <summary>\n    /// 点赞直播间\n    /// </summary>\n    Task LikeFansMedalLive(BiliCookie ck);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Interfaces/ILoginDomainService.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent;\n\nnamespace Ray.BiliBiliTool.DomainService.Interfaces;\n\n/// <summary>\n/// 账户\n/// </summary>\npublic interface ILoginDomainService : IDomainService\n{\n    /// <summary>\n    /// 扫描二维码登录\n    /// </summary>\n    /// <returns></returns>\n    Task<BiliCookie> LoginByQrCodeAsync(CancellationToken cancellationToken);\n\n    /// <summary>\n    /// Set Cookie\n    /// </summary>\n    /// <param name=\"cookie\"></param>\n    /// <returns></returns>\n    Task<BiliCookie> SetCookieAsync(BiliCookie cookie, CancellationToken cancellationToken);\n\n    /// <summary>\n    /// 持久化Cookie到配置文件\n    /// </summary>\n    /// <returns></returns>\n    Task SaveCookieToJsonFileAsync(BiliCookie ckInfo, CancellationToken cancellationToken);\n\n    /// <summary>\n    /// 持久化Cookie到青龙环境变量\n    /// </summary>\n    /// <param name=\"ckInfo\"></param>\n    /// <param name=\"cancellationToken\"></param>\n    /// <returns></returns>\n    Task<bool> SaveCookieToQinLongAsync(BiliCookie ckInfo, CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Interfaces/IMangaDomainService.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\nnamespace Ray.BiliBiliTool.DomainService.Interfaces;\n\n/// <summary>\n/// 漫画\n/// </summary>\npublic interface IMangaDomainService : IDomainService\n{\n    /// <summary>\n    /// 签到\n    /// </summary>\n    Task MangaSign(BiliCookie ck);\n\n    /// <summary>\n    /// 阅读\n    /// </summary>\n    Task MangaRead(BiliCookie ck);\n\n    /// <summary>\n    /// 获取大会员权益\n    /// </summary>\n    /// <param name=\"reason_id\"></param>\n    /// <param name=\"userIfo\"></param>\n    Task ReceiveMangaVipReward(int reason_id, UserInfo userIfo, BiliCookie ck);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Interfaces/IVideoDomainService.cs",
    "content": "﻿using System.Threading.Tasks;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.DomainService.Dtos;\n\nnamespace Ray.BiliBiliTool.DomainService.Interfaces;\n\n/// <summary>\n/// 视频\n/// </summary>\npublic interface IVideoDomainService : IDomainService\n{\n    /// <summary>\n    /// 获取视频详情\n    /// </summary>\n    /// <param name=\"aid\"></param>\n    /// <returns></returns>\n    Task<VideoDetail> GetVideoDetail(string aid);\n\n    /// <summary>\n    /// 从排行榜获取一个随机视频\n    /// </summary>\n    /// <returns></returns>\n    Task<RankingInfo> GetRandomVideoOfRanking();\n\n    /// <summary>\n    /// 从某个指定UP下获取随机视频\n    /// </summary>\n    /// <param name=\"upId\"></param>\n    /// <param name=\"total\"></param>\n    /// <returns></returns>\n    Task<UpVideoInfo?> GetRandomVideoOfUp(long upId, int total, BiliCookie ck);\n\n    Task<int> GetVideoCountOfUp(long upId, BiliCookie ck);\n\n    /// <summary>\n    /// 观看并分享视频\n    /// </summary>\n    /// <param name=\"dailyTaskStatus\"></param>\n    Task WatchAndShareVideo(DailyTaskInfo dailyTaskStatus, BiliCookie ck);\n\n    /// <summary>\n    /// 观看\n    /// </summary>\n    /// <param name=\"aid\"></param>\n    /// <param name=\"dailyTaskStatus\"></param>\n    Task WatchVideo(VideoInfoDto videoInfo, BiliCookie ck);\n\n    /// <summary>\n    /// 分享\n    /// </summary>\n    /// <param name=\"aid\"></param>\n    /// <param name=\"dailyTaskStatus\"></param>\n    Task ShareVideo(VideoInfoDto videoInfo, BiliCookie ck);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Interfaces/IVipBigPointDomainService.cs",
    "content": "using Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask;\n\nnamespace Ray.BiliBiliTool.DomainService.Interfaces;\n\npublic interface IVipBigPointDomainService : IDomainService\n{\n    Task<VipBigPointCombine> GetCombineAsync(BiliCookie ck);\n\n    Task VipExpressAsync(BiliCookie ck);\n\n    Task SignAsync(BiliCookie ck);\n\n    Task ReceiveDailyMissionsAsync(VipBigPointCombine combine, BiliCookie ck);\n\n    Task ReceiveAndCompleteAsync(\n        VipBigPointCombine info,\n        string moduleCode,\n        string taskCode,\n        BiliCookie ck,\n        Func<string, BiliCookie, Task<bool>> completeFunc\n    );\n\n    Task<bool> CompleteAsync(string taskCode, BiliCookie ck);\n\n    Task<bool> CompleteViewAsync(string taskCode, BiliCookie ck);\n\n    Task<bool> CompleteViewVipMallAsync(string taskCode, BiliCookie ck);\n\n    Task<bool> CompleteV2Async(string taskCode, BiliCookie ck);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Interfaces/IVipPrivilegeDomainService.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\nnamespace Ray.BiliBiliTool.DomainService.Interfaces;\n\n/// <summary>\n/// 大会员权益\n/// </summary>\npublic interface IVipPrivilegeDomainService : IDomainService\n{\n    /// <summary>\n    /// 获取大会员权益\n    /// </summary>\n    /// <param name=\"useInfo\"></param>\n    Task<bool> ReceiveVipPrivilege(UserInfo userInfo, BiliCookie ck);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/LiveDomainService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Newtonsoft.Json;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Dtos;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Extensions;\n\nnamespace Ray.BiliBiliTool.DomainService;\n\n/// <summary>\n/// 直播\n/// </summary>\npublic class LiveDomainService(\n    ILogger<LiveDomainService> logger,\n    ILiveApi liveApi,\n    IRelationApi relationApi,\n    ILiveTraceApi liveTraceApi,\n    IOptionsMonitor<DailyTaskOptions> dailyTaskOptions,\n    IOptionsMonitor<LiveLotteryTaskOptions> liveLotteryTaskOptions,\n    IOptionsMonitor<LiveFansMedalTaskOptions> liveFansMedalTaskOptions,\n    IOptionsMonitor<SecurityOptions> securityOptions,\n    IOptionsMonitor<Silver2CoinTaskOptions> silver2CoinTaskOptions,\n    IUpInfoApi upInfoApi\n) : ILiveDomainService\n{\n    private readonly LiveLotteryTaskOptions _liveLotteryTaskOptions =\n        liveLotteryTaskOptions.CurrentValue;\n    private readonly LiveFansMedalTaskOptions _liveFansMedalTaskOptions =\n        liveFansMedalTaskOptions.CurrentValue;\n    private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue;\n    private readonly SecurityOptions _securityOptions = securityOptions.CurrentValue;\n    private readonly Silver2CoinTaskOptions _silver2CoinTaskOptions =\n        silver2CoinTaskOptions.CurrentValue;\n\n    /// <summary>\n    /// 本次通过天选关注的主播\n    /// </summary>\n    private List<ListItemDto> _tianXuanFollowed = new();\n\n    /// <summary>\n    /// 开始抽奖前最后一个关注的up\n    /// </summary>\n    private long _lastFollowUpId;\n\n    /// <summary>\n    /// 直播签到\n    /// </summary>\n    public async Task LiveSign(BiliCookie ck)\n    {\n        var response = await liveApi.Sign(ck.ToString());\n\n        if (response.Code == 0)\n        {\n            logger.LogInformation(\"【签到结果】成功\");\n            logger.LogInformation(\n                \"【本次获取】{text},{special}\",\n                response.Data!.Text,\n                response.Data.SpecialText\n            );\n        }\n        else\n        {\n            logger.LogInformation(\"【签到结果】失败\");\n            logger.LogInformation(\"【原因】{msg}\", response.Message);\n        }\n    }\n\n    /// <summary>\n    /// 直播中心银瓜子兑换B币\n    /// </summary>\n    /// <returns>兑换银瓜子后硬币余额</returns>\n    public async Task<bool> ExchangeSilver2Coin(BiliCookie ck)\n    {\n        var result = false;\n\n        if (!_silver2CoinTaskOptions.IsEnable)\n        {\n            logger.LogInformation(\"已配置为关闭，跳过\");\n            return false;\n        }\n\n        logger.LogInformation(\"【今天】{day}号\", DateTime.Today.Day);\n\n        BiliApiResponse<LiveWalletStatusResponse> queryStatus = await liveApi.GetLiveWalletStatus(\n            ck.ToString()\n        );\n        logger.LogInformation(\"【银瓜子余额】 {silver}\", queryStatus.Data.Silver);\n        logger.LogInformation(\"【硬币余额】 {coin}\", queryStatus.Data.Coin);\n        logger.LogInformation(\"【今日剩余兑换次数】 {left}\", queryStatus.Data.Silver_2_coin_left);\n\n        if (queryStatus.Data.Silver_2_coin_left <= 0)\n            return false;\n\n        logger.LogInformation(\"开始尝试兑换...\");\n        Silver2CoinRequest request = new(ck.BiliJct);\n        var response = await liveApi.Silver2Coin(request, ck.ToString());\n        if (response.Code == 0)\n        {\n            result = true;\n            logger.LogInformation(\"【兑换结果】成功兑换 {coin} 枚硬币\", response.Data?.Coin);\n            logger.LogInformation(\"【银瓜子余额】 {silver}\", response.Data?.Silver);\n        }\n        else\n        {\n            logger.LogInformation(\"【兑换结果】失败\");\n            logger.LogInformation(\"【原因】{reason}\", response.Message);\n        }\n\n        return result;\n    }\n\n    #region 天选时刻抽奖\n\n    /// <summary>\n    /// 天选抽奖\n    /// </summary>\n    public async Task TianXuan(BiliCookie ck)\n    {\n        _tianXuanFollowed = new List<ListItemDto>();\n\n        if (_liveLotteryTaskOptions.AutoGroupFollowings)\n        {\n            //获取此时最后一个关注的up，此后再新增的关注，与参与成功的抽奖，取交集，就是本地新增的天选关注\n            _lastFollowUpId = await GetLastFollowUpId(ck);\n        }\n\n        //获取直播的分区\n        List<AreaDto> areaList = (await liveApi.GetAreaList(ck.ToString())).Data.Data;\n\n        //遍历分区\n        int count = 0;\n        foreach (var area in areaList)\n        {\n            logger.LogInformation(\"【扫描分区】{area}...\" + Environment.NewLine, area.Name);\n\n            string defaultSort = \"\";\n            //每个分区下搜索5页\n            for (int i = 1; i < 6; i++)\n            {\n                var request = new GetListRequest\n                {\n                    platform = \"web\",\n                    parent_area_id = area.Id,\n                    area_id = 0,\n                    sort_type = defaultSort,\n                    page = i,\n                    wts = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),\n                };\n                var reData = (await liveApi.GetList(request, ck.ToString())).Data;\n\n                foreach (var item in reData.List)\n                {\n                    if (item.Pendant_info == null || item.Pendant_info.Count == 0)\n                        continue;\n                    var suc = item.Pendant_info.TryGetValue(\"2\", out var pendant);\n                    if (!suc)\n                        continue;\n                    if (pendant?.Pendent_id != 504)\n                        continue;\n                    count++;\n\n                    await TryJoinTianXuan(item, ck);\n                }\n\n                if (reData.Has_more != 1)\n                    break;\n                defaultSort = reData.New_tags.FirstOrDefault()?.Sort_type ?? \"\";\n            }\n\n            defaultSort = \"\";\n        }\n\n        if (count == 0)\n        {\n            logger.LogInformation(\"未搜索到直播间\");\n            return;\n        }\n    }\n\n    public async Task TryJoinTianXuan(ListItemDto target, BiliCookie ck)\n    {\n        logger.LogDebug(\"【房间】{name}\", target.Title);\n        try\n        {\n            //黑名单\n            if (_liveLotteryTaskOptions.DenyUidList.Contains(target.Uid.ToString()))\n            {\n                logger.LogDebug(\"黑名单，跳过\");\n                return;\n            }\n\n            CheckTianXuanDto check = (\n                await liveApi.CheckTianXuan(target.Roomid, ck.ToString())\n            ).Data;\n\n            if (check == null)\n            {\n                logger.LogDebug(\"数据异常，跳过\");\n                return;\n            }\n\n            if (check.Status != TianXuanStatus.Enable)\n            {\n                logger.LogDebug(\"已开奖，跳过\" + Environment.NewLine);\n                return;\n            }\n\n            //根据配置过滤\n            if (\n                !check.AwardNameIsSatisfied(\n                    _liveLotteryTaskOptions.IncludeAwardNameList,\n                    _liveLotteryTaskOptions.ExcludeAwardNameList\n                )\n            )\n            {\n                logger.LogDebug(\"不满足配置的筛选条件，跳过\" + Environment.NewLine);\n                return;\n            }\n\n            //是否需要赠礼\n            if (check.Gift_price > 0)\n            {\n                logger.LogDebug(\"【赠礼】{gift}\", check.GiftDesc);\n                logger.LogDebug(\"需赠送礼物，跳过\" + Environment.NewLine);\n                return;\n            }\n\n            //条件\n            if (check.Require_type != RequireType.None && check.Require_type != RequireType.Follow)\n            {\n                logger.LogDebug(\"【条件】{text}\", check.Require_text);\n                logger.LogDebug(\"要求粉丝勋章，跳过\");\n                return;\n            }\n\n            logger.LogInformation(\"【房间】{name}\", target.ShortTitle);\n            logger.LogInformation(\"【主播】{name}({id})\", target.Uname, target.Uid);\n            logger.LogInformation(\n                \"【奖品】{name}【条件】{text}\",\n                check.Award_name,\n                check.Require_text\n            );\n\n            var request = new JoinTianXuanRequest\n            {\n                Id = check.Id,\n                Gift_id = check.Gift_id,\n                Gift_num = check.Gift_num,\n                Csrf = ck.BiliJct,\n            };\n            var re = await liveApi.Join(request, ck.ToString());\n            if (re.Code == 0)\n            {\n                logger.LogInformation(\"【抽奖】成功 √\" + Environment.NewLine);\n                if (check.Require_type == RequireType.Follow)\n                    _tianXuanFollowed.AddIfNotExist(target, x => x.Uid == target.Uid);\n                return;\n            }\n\n            logger.LogInformation(\"【抽奖】失败\");\n            logger.LogInformation(\"【原因】{msg}\" + Environment.NewLine, re.Message);\n        }\n        catch (Exception ex)\n        {\n            logger.LogWarning(\"【异常】{msg}，{detail}\" + Environment.NewLine, ex.Message, ex);\n            //ignore\n        }\n    }\n\n    /// <summary>\n    /// 将本次抽奖新增的关注统一转移到指定分组中\n    /// </summary>\n    public async Task GroupFollowing(BiliCookie ck)\n    {\n        if (!_tianXuanFollowed.Any())\n        {\n            logger.LogInformation(\"未关注主播\");\n            return;\n        }\n\n        logger.LogInformation(\n            \"【抽奖的主播】{ups}\",\n            string.Join(\"，\", _tianXuanFollowed.Select(x => x.Uname))\n        );\n\n        //目标分组up集合\n        List<ListItemDto> targetUps = await GetNeedGroup(ck);\n        logger.LogInformation(\n            \"【将自动分组】{ups}\",\n            string.Join(\"，\", targetUps.Select(x => x.Uname))\n        );\n\n        if (!targetUps.Any())\n        {\n            return;\n        }\n\n        //目标分组Id\n        long targetGroupId = await GetOrCreateTianXuanGroupId(ck);\n\n        //执行批量分组\n        var referer = string.Format(RelationApiConstant.CopyReferer, ck.UserId);\n        var req = new CopyUserToGroupRequest(\n            targetUps.Select(x => x.Uid).ToList(),\n            targetGroupId.ToString(),\n            ck.BiliJct\n        );\n        var re = await relationApi.CopyUpsToGroup(req, ck.ToString(), referer);\n\n        if (re.Code == 0)\n        {\n            logger.LogInformation(\"【分组结果】全部成功\");\n        }\n        else\n        {\n            logger.LogWarning(\"【分组结果】失败\");\n            logger.LogWarning(\"【原因】{msg}\", re.Message);\n        }\n    }\n\n    /// <summary>\n    /// 获取抽奖前最后一个关注的up\n    /// </summary>\n    /// <returns></returns>\n    private async Task<long> GetLastFollowUpId(BiliCookie ck)\n    {\n        var followings = await relationApi.GetFollowings(\n            new GetFollowingsRequest(long.Parse(ck.UserId), FollowingsOrderType.TimeDesc),\n            ck.ToString()\n        );\n        return followings.Data.List.FirstOrDefault()?.Mid ?? 0;\n    }\n\n    /// <summary>\n    /// 获取本次需要自动分组的主播\n    /// </summary>\n    /// <returns></returns>\n    private async Task<List<ListItemDto>> GetNeedGroup(BiliCookie ck)\n    {\n        List<long> addUpIds = new();\n\n        //获取最后一个upId之后关注的所有upId\n        var followings = await relationApi.GetFollowings(\n            new GetFollowingsRequest(long.Parse(ck.UserId), FollowingsOrderType.TimeDesc),\n            ck.ToString()\n        );\n\n        foreach (UpInfo item in followings.Data.List)\n        {\n            if (item.Mid == _lastFollowUpId)\n            {\n                break;\n            }\n\n            addUpIds.Add(item.Mid);\n        }\n\n        //和成功抽奖的主播取交集\n        List<ListItemDto> target = new();\n        foreach (var listItemDto in _tianXuanFollowed)\n        {\n            if (addUpIds.Contains(listItemDto.Uid))\n                target.Add(listItemDto);\n        }\n\n        return target;\n    }\n\n    /// <summary>\n    /// 获取或创建天选时刻分组\n    /// </summary>\n    /// <returns></returns>\n    private async Task<long> GetOrCreateTianXuanGroupId(BiliCookie ck)\n    {\n        //获取天选分组Id，没有就创建\n        long groupId = 0;\n        string referer = string.Format(RelationApiConstant.GetTagsReferer, ck.UserId);\n        var groups = await relationApi.GetTags(referer);\n        var tianXuanGroup = groups.Data!.FirstOrDefault(x => x.Name == \"天选时刻\");\n        if (tianXuanGroup == null)\n        {\n            logger.LogInformation(\"“天选时刻”分组不存在，尝试创建...\");\n            //创建一个\n            var createRe = await relationApi.CreateTag(\n                new CreateTagRequest { Tag = \"天选时刻\", Csrf = ck.BiliJct },\n                ck.ToString()\n            );\n            groupId = createRe.Data.Tagid;\n            logger.LogInformation(\"创建成功\");\n        }\n        else\n        {\n            logger.LogInformation(\"“天选时刻”分组已存在\");\n            groupId = tianXuanGroup.Tagid;\n        }\n\n        return groupId;\n    }\n\n    #endregion\n\n    public async Task SendDanmakuToFansMedalLive(BiliCookie ck)\n    {\n        if (!await CheckLiveCookie(ck))\n            return;\n\n        var infoList = await GetFansMedalInfoList(ck);\n\n        foreach (var info in infoList)\n        {\n            var medal = info.MedalInfo;\n\n            logger.LogInformation(\"【直播间】{liveRoomName}\", medal.Target_name);\n            logger.LogInformation(\"【粉丝牌】{medalName}\", medal.Medal_info.Medal_name);\n            logger.LogInformation(\"正在发送弹幕...\");\n\n            // 通过空间主页信息获取直播间 id\n            var liveHostUserId = medal.Medal_info.Target_id;\n            var req = new GetSpaceInfoDto() { mid = liveHostUserId };\n\n            var spaceInfo = await upInfoApi.GetSpaceInfo(req, ck.ToString());\n            if (spaceInfo.Code != 0)\n            {\n                logger.LogError(\"【获取直播间信息】失败\");\n                logger.LogError(\"【原因】{message}\", spaceInfo.Message);\n                return;\n            }\n\n            var successCount = 0;\n            var failedCount = 0;\n\n            // 发送弹幕\n\n            while (\n                successCount < _liveFansMedalTaskOptions.SendDanmakuNumber\n                && failedCount < _liveFansMedalTaskOptions.SendDanmakugiveUpThreshold\n            )\n            {\n                var sendResult = await liveApi.SendLiveDanmuku(\n                    new SendLiveDanmukuRequest(\n                        ck.BiliJct,\n                        spaceInfo.Data.Live_room.Roomid,\n                        _liveFansMedalTaskOptions.DanmakuContent\n                    ),\n                    ck.ToString()\n                );\n\n                if (sendResult.Code != 0)\n                {\n                    logger.LogError(\"【弹幕发送】失败\");\n                    logger.LogError(\"【原因】{message}\", sendResult.Message);\n                    failedCount++;\n                }\n                else\n                    successCount++;\n\n                var delay = new Random().Next(2000, 4000);\n                await Task.Delay(delay);\n            }\n\n            logger.LogInformation(\n                \"【弹幕发送】发送情况：你向主播 {name} 发送弹幕{success}/{total}\",\n                spaceInfo.Data.Name,\n                successCount,\n                successCount + failedCount\n            );\n        }\n    }\n\n    public async Task SendHeartBeatToFansMedalLive(BiliCookie ck)\n    {\n        if (!await CheckLiveCookie(ck))\n            return;\n\n        var infoList = new List<HeartBeatIterationInfoDto>();\n        (await GetFansMedalInfoList(ck))\n            .FindAll(info => info.LiveRoomInfo.Live_Status != 0)\n            .ForEach(medal => infoList.Add(new(medal.RoomId, medal.LiveRoomInfo, new(), 0, 0)));\n\n        if (infoList.Count == 0)\n        {\n            logger.LogInformation(\"【直播观看时长】跳过，未检测到符合条件的主播\");\n            return;\n        }\n\n        var Now = () => new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds();\n\n        while (\n            infoList.Min(info =>\n                info.FailedTimes >= _liveFansMedalTaskOptions.HeartBeatSendGiveUpThreshold\n                    ? int.MaxValue\n                    : info.HeartBeatCount\n            ) < _liveFansMedalTaskOptions.HeartBeatNumber\n        )\n        {\n            foreach (var info in infoList)\n            {\n                // 忽略连续失败超过上限的直播间\n                if (info.FailedTimes >= _liveFansMedalTaskOptions.HeartBeatSendGiveUpThreshold)\n                    continue;\n\n                string uuid = Guid.NewGuid().ToString();\n                var current = Now();\n                if (\n                    current - info.LastBeatTime\n                    <= (LiveFansMedalTaskOptions.HeartBeatInterval + 5) * 1000\n                )\n                {\n                    int sleepTime = (int)(\n                        (LiveFansMedalTaskOptions.HeartBeatInterval + 5) * 1000\n                        - (current - info.LastBeatTime)\n                    );\n                    logger.LogDebug(\"【休眠】{time} 毫秒\", sleepTime);\n                    Thread.Sleep(sleepTime);\n                }\n\n                // Heart Beat 接口\n                var timestamp = Now();\n                BiliApiResponse<HeartBeatResponse>? heartBeatResult = null;\n                if (info.HeartBeatCount == 0)\n                {\n                    heartBeatResult = await liveTraceApi.EnterRoom(\n                        new EnterRoomRequest(\n                            info.RoomId,\n                            info.RoomInfo.Parent_area_id,\n                            info.RoomInfo.Area_id,\n                            info.HeartBeatCount,\n                            timestamp,\n                            _securityOptions.UserAgent,\n                            ck.BiliJct,\n                            info.RoomInfo.Uid,\n                            $\"[\\\"{ck.LiveBuvid}\\\",\\\"{uuid}\\\"]\"\n                        ),\n                        ck.ToString()\n                    );\n                }\n                else\n                {\n                    heartBeatResult = await liveTraceApi.HeartBeat(\n                        new HeartBeatRequest(\n                            info.RoomId,\n                            info.RoomInfo.Parent_area_id,\n                            info.RoomInfo.Area_id,\n                            info.HeartBeatCount,\n                            ck.LiveBuvid,\n                            timestamp,\n                            info.HeartBeatInfo.Timestamp,\n                            _securityOptions.UserAgent,\n                            info.HeartBeatInfo.Secret_rule,\n                            info.HeartBeatInfo.Secret_key!,\n                            ck.BiliJct,\n                            uuid,\n                            $\"[\\\"{ck.LiveBuvid}\\\",\\\"{uuid}\\\"]\"\n                        ),\n                        ck.ToString()\n                    );\n                }\n\n                info.LastBeatTime = Now();\n\n                if (heartBeatResult != null && heartBeatResult.Data != null)\n                {\n                    info.HeartBeatInfo.Secret_key = heartBeatResult.Data.Secret_key;\n                    info.HeartBeatInfo.Secret_rule = heartBeatResult.Data.Secret_rule;\n                    info.HeartBeatInfo.Timestamp = heartBeatResult.Data.Timestamp;\n                }\n\n                if (heartBeatResult == null || heartBeatResult.Code != 0)\n                {\n                    logger.LogError(\"【心跳包】直播间 {room} 发送失败\", info.RoomId);\n                    logger.LogError(\n                        \"【原因】{message}\",\n                        heartBeatResult != null ? heartBeatResult.Message : \"\"\n                    );\n                    info.FailedTimes += 1;\n                    continue;\n                }\n\n                info.HeartBeatCount += 1;\n                info.FailedTimes = 0;\n\n                logger.LogInformation(\n                    \"【直播间】{roomId} 的第 {index} 个心跳包发送成功\",\n                    info.RoomId,\n                    info.HeartBeatCount\n                );\n            }\n        }\n\n        var successCount = infoList.Count(info =>\n            info.HeartBeatCount >= _liveFansMedalTaskOptions.HeartBeatNumber\n        );\n        logger.LogInformation(\n            \"【直播观看时长】完成情况：{success}/{total} \",\n            successCount,\n            infoList.Count\n        );\n    }\n\n    /// <summary>\n    /// 点赞直播间\n    /// </summary>\n    public async Task LikeFansMedalLive(BiliCookie ck)\n    {\n        if (!await CheckLiveCookie(ck))\n            return;\n\n        var infoList = await GetFansMedalInfoList(ck);\n        infoList = infoList.FindAll(info => info.LiveRoomInfo.Live_Status != 0);\n        logger.LogInformation(\"当前开播直播间数量：{num}\", infoList.Count);\n        foreach (var info in infoList)\n        {\n            // Clike_Time 暂时设置为等于设置的LikeNumber，不清楚是否会被风控，我自己抓包最大值为10\n            var request = new LikeLiveRoomRequest(\n                info.RoomId,\n                ck.BiliJct,\n                _liveFansMedalTaskOptions.LikeNumber,\n                info.LiveRoomInfo.Uid,\n                ck.UserId\n            );\n\n            var result = await liveApi.LikeLiveRoom(request.RawTextBuild(), ck.ToString());\n            if (result.Code == 0)\n            {\n                logger.LogInformation(\"【点赞直播间】{roomId} 完成\", info.RoomId);\n            }\n            else\n            {\n                logger.LogError(\"【点赞直播间】{roomId} 时候出现错误\", info.RoomId);\n                logger.LogError(\"【原因】{message}\", result.Message);\n            }\n\n            var delay = new Random().Next(5000, 8000);\n            await Task.Delay(delay);\n        }\n    }\n\n    private async Task<List<FansMedalInfoDto>> GetFansMedalInfoList(BiliCookie ck)\n    {\n        logger.LogInformation(\"【获取直播列表】获取拥有粉丝牌的直播列表\");\n        var medalWallInfo = await liveApi.GetMedalWall(ck.UserId, ck.ToString());\n\n        if (medalWallInfo.Code != 0)\n        {\n            logger.LogError(\"【获取直播列表】失败\");\n            logger.LogError(\"【原因】{message}\", medalWallInfo.Message);\n            return new List<FansMedalInfoDto>();\n        }\n\n        var infoList = new List<FansMedalInfoDto>();\n        foreach (var medal in medalWallInfo.Data.List)\n        {\n            logger.LogInformation(\"【主播】{name} \", medal.Target_name);\n            if (_liveFansMedalTaskOptions.IsSkipLevel20Medal && medal.Medal_info.Level >= 20)\n            {\n                logger.LogInformation(\n                    \"粉丝牌等级为 {level}，观看将不再增长亲密度，跳过\",\n                    medal.Medal_info.Level\n                );\n                continue;\n            }\n\n            // 通过空间主页信息获取直播间 id\n            var liveHostUserId = medal.Medal_info.Target_id;\n            var req = new GetSpaceInfoDto() { mid = liveHostUserId };\n\n            var spaceInfo = await upInfoApi.GetSpaceInfo(req, ck.ToString());\n            if (spaceInfo.Code != 0)\n            {\n                logger.LogError(\"【获取空间信息】失败\");\n                logger.LogError(\"【原因】{message}\", spaceInfo.Message);\n                continue;\n            }\n\n            // 用以排除有牌子无直播间的up主\n            if (spaceInfo.Data.Live_room is null)\n            {\n                logger.LogInformation(\"【主播】{name} 直播间id获取失败，已跳过\", medal.Target_name);\n                continue;\n            }\n\n            var roomId = spaceInfo.Data.Live_room.Roomid;\n\n            // 获取直播间详细信息\n            var liveRoomInfo = await liveApi.GetLiveRoomInfo(roomId);\n            if (liveRoomInfo.Code != 0)\n            {\n                logger.LogError(\"【获取直播间信息】失败\");\n                logger.LogError(\"【原因】{message}\", liveRoomInfo.Message);\n                continue;\n            }\n\n            infoList.Add(new FansMedalInfoDto(roomId, medal, liveRoomInfo.Data!));\n        }\n\n        return infoList;\n    }\n\n    /// <summary>\n    /// 自动配置直播相关 Cookie，来兼容较低版本中保存的 Cookie 配置\n    /// </summary>\n    /// <returns>\n    /// bool 成功配置 or not\n    /// </returns>\n    private async Task<bool> CheckLiveCookie(BiliCookie ck)\n    {\n        // 检测 _biliCookie 是否正确配置\n        if (!string.IsNullOrWhiteSpace(ck.LiveBuvid))\n            return true;\n\n        try\n        {\n            logger.LogInformation(\"检测到直播 Cookie 未正确配置，尝试自动配置中...\");\n\n            // 请求主播主页来正确配置 cookie\n            var liveHome = await liveApi.GetLiveHome(ck.ToString());\n            var liveHomeContent = JsonConvert.DeserializeObject<BiliApiResponse>(\n                await liveHome.Content.ReadAsStringAsync()\n            );\n            if (liveHomeContent?.Code != 0)\n            {\n                throw new Exception(liveHomeContent?.Message);\n            }\n\n            var setHeader = liveHome.Headers.FirstOrDefault(header => header.Key == \"Set-Cookie\");\n            ck.MergeCurrentCookie(setHeader.Value.ToList());\n\n            logger.LogDebug(\"LiveBuvid {value}\", ck.LiveBuvid);\n            logger.LogInformation(\"直播 Cookie 配置成功！\");\n        }\n        catch (Exception exception)\n        {\n            logger.LogError(\"【配置直播Cookie】失败，放弃执行后续任务...\");\n            logger.LogError(\"【原因】{message}\", exception.Message);\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/LoginDomainService.cs",
    "content": "﻿using Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.FileProviders;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Newtonsoft.Json;\nusing QRCoder;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Passport;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Agent.QingLong;\nusing Ray.BiliBiliTool.Agent.QingLong.Dtos;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\n\nnamespace Ray.BiliBiliTool.DomainService;\n\n/// <summary>\n/// 账户\n/// </summary>\npublic class LoginDomainService(\n    ILogger<LoginDomainService> logger,\n    IPassportApi passportApi,\n    IHostEnvironment hostingEnvironment,\n    IQingLongApi qingLongApi,\n    IHomeApi homeApi,\n    IConfiguration configuration,\n    IOptions<QingLongOptions> qingLongOptions\n) : ILoginDomainService\n{\n    public async Task<BiliCookie> LoginByQrCodeAsync(CancellationToken cancellationToken)\n    {\n        BiliCookie? cookieInfo = null;\n\n        var re = await passportApi.GenerateQrCode();\n        if (re.Code != 0)\n        {\n            throw new Exception($\"获取二维码失败：{re.ToJsonStr()}\");\n        }\n\n        var url = re.Data.Url;\n        GenerateQrCode(url);\n\n        var online = GetOnlinePic(url);\n        logger.LogInformation(Environment.NewLine + Environment.NewLine);\n        logger.LogInformation(\n            \"如果上方二维码显示异常，或扫描失败，请使用浏览器访问如下链接，查看高清二维码：\"\n        );\n        logger.LogInformation(online + Environment.NewLine + Environment.NewLine);\n\n        var waitTimes = 10;\n        logger.LogInformation(\"我数到{num}，动作快点\", waitTimes);\n        for (int i = 0; i < waitTimes; i++)\n        {\n            logger.LogInformation(\"[{num}]等待扫描...\", i + 1);\n\n            await Task.Delay(5 * 1000, cancellationToken);\n\n            var check = await passportApi.CheckQrCodeHasScaned(re.Data.Qrcode_key);\n            if (!check.IsSuccessStatusCode)\n            {\n                logger.LogWarning(\"调用检测接口异常\");\n                continue;\n            }\n\n            var contentStr = await check.Content.ReadAsStringAsync(cancellationToken);\n            var content = JsonConvert.DeserializeObject<BiliApiResponse<TokenDto>>(contentStr);\n            if (content?.Code != 0)\n            {\n                logger.LogWarning(\"调用检测接口异常：{msg}\", check.ToJsonStr());\n                break;\n            }\n\n            if (content.Data.Code == 86038) //已失效\n            {\n                logger.LogInformation(content.Data.Message);\n                break;\n            }\n\n            if (content.Data.Code == 0)\n            {\n                logger.LogInformation(\"扫描成功！\");\n                IEnumerable<string> cookies = check\n                    .Headers.SingleOrDefault(header => header.Key == \"Set-Cookie\")\n                    .Value;\n\n                var cookieStr = CookieInfo.ConvertSetCkHeadersToCkStr(cookies);\n\n                cookieInfo = CookieStrFactory<BiliCookie>.CreateNew(cookieStr);\n                cookieInfo.Check();\n\n                break;\n            }\n\n            logger.LogInformation(\"{msg}\", content.Data.Message + Environment.NewLine);\n        }\n\n        if (cookieInfo == null)\n        {\n            throw new Exception(\"登录超时\");\n        }\n\n        return cookieInfo;\n    }\n\n    public async Task<BiliCookie> SetCookieAsync(\n        BiliCookie biliCookie,\n        CancellationToken cancellationToken\n    )\n    {\n        try\n        {\n            var homePage = await homeApi.GetHomePageAsync(biliCookie.ToString());\n            if (homePage.IsSuccessStatusCode)\n            {\n                logger.LogInformation(\"访问主站成功\");\n                IEnumerable<string> setCookieHeaders = homePage\n                    .Headers.SingleOrDefault(header => header.Key == \"Set-Cookie\")\n                    .Value;\n                if (setCookieHeaders != null)\n                {\n                    biliCookie.MergeCurrentCookieBySetCookieHeaders(setCookieHeaders);\n                    logger.LogInformation(\"SetCookie成功\");\n                }\n                else\n                {\n                    logger.LogInformation(\"无需set\");\n                }\n\n                return biliCookie;\n            }\n            logger.LogError(\"访问主站失败：{msg}\", homePage.ToJsonStr());\n        }\n        catch (Exception e)\n        {\n            //buvid只影响分享和投币，可以吞掉异常\n            logger.LogError(e.ToJsonStr());\n        }\n\n        return biliCookie;\n    }\n\n    public async Task SaveCookieToJsonFileAsync(\n        BiliCookie ckInfo,\n        CancellationToken cancellationToken\n    )\n    {\n        //读取json\n        var path = hostingEnvironment.ContentRootPath;\n        var indexOfBin = path.LastIndexOf(\"bin\");\n        if (indexOfBin != -1)\n        {\n            path = path[..indexOfBin];\n        }\n        if (string.Equals(configuration[\"PlatformType\"], \"Web\", StringComparison.OrdinalIgnoreCase))\n        {\n            path = Path.Combine(path, \"config\");\n        }\n        var fileProvider = new PhysicalFileProvider(path);\n        IFileInfo fileInfo = fileProvider.GetFileInfo(\"cookies.json\");\n        logger.LogInformation(\"目标json地址：{path}\", fileInfo.PhysicalPath);\n\n        if (!fileInfo.Exists)\n        {\n            await using var stream = File.Create(fileInfo.PhysicalPath!);\n            await using var sw = new StreamWriter(stream);\n            await sw.WriteAsync($\"{{{Environment.NewLine}}}\");\n        }\n\n        string json;\n        await using (var stream = new FileStream(fileInfo.PhysicalPath!, FileMode.Open))\n        {\n            using var reader = new StreamReader(stream);\n            json = await reader.ReadToEndAsync();\n        }\n        var lines = json.Split(Environment.NewLine).ToList();\n\n        var indexOfCkConfigKey = lines.FindIndex(x =>\n            x.TrimStart().StartsWith(\"\\\"BiliBiliCookies\\\"\")\n        );\n        if (indexOfCkConfigKey == -1)\n        {\n            logger.LogInformation(\"未配置过cookie，初始化并新增\");\n\n            var indexOfInsert = lines.FindIndex(x => x.TrimStart().StartsWith(\"{\"));\n            lines.InsertRange(\n                indexOfInsert + 1,\n                new List<string>()\n                {\n                    \"  \\\"BiliBiliCookies\\\":[\",\n                    $@\"    \"\"{ckInfo.CookieStr}\"\",\",\n                    \"  ],\",\n                }\n            );\n\n            await SaveJson(lines, fileInfo);\n            logger.LogInformation(\"新增成功！\");\n            return;\n        }\n\n        ckInfo.CookieItemDictionary.TryGetValue(\"DedeUserID\", out var userId);\n        userId ??= ckInfo.CookieStr;\n        var indexOfCkConfigEnd = lines.FindIndex(\n            indexOfCkConfigKey,\n            x => x.TrimStart().StartsWith(\"]\")\n        );\n        var indexOfTargetCk = lines.FindIndex(\n            indexOfCkConfigKey,\n            indexOfCkConfigEnd - indexOfCkConfigKey,\n            x => x.Contains(userId) && !x.TrimStart().StartsWith(\"//\")\n        );\n\n        if (indexOfTargetCk == -1)\n        {\n            logger.LogInformation(\"不存在该用户，新增cookie\");\n            lines.Insert(indexOfCkConfigEnd, $@\"    \"\"{ckInfo.CookieStr}\"\",\");\n            await SaveJson(lines, fileInfo);\n            logger.LogInformation(\"新增成功！\");\n            return;\n        }\n\n        logger.LogInformation(\"已存在该用户，更新cookie\");\n        lines[indexOfTargetCk] = $@\"    \"\"{ckInfo.CookieStr}\"\",\";\n        await SaveJson(lines, fileInfo);\n        logger.LogInformation(\"更新成功！\");\n    }\n\n    public async Task<bool> SaveCookieToQinLongAsync(\n        BiliCookie ckInfo,\n        CancellationToken cancellationToken\n    )\n    {\n        try\n        {\n            var token = await GetQingLongAuthTokenAsync();\n            if (string.IsNullOrEmpty(token))\n            {\n                throw new Exception(\"获取青龙token失败\");\n            }\n\n            var qlEnvList = await qingLongApi.GetEnvsAsync(\"Ray_BiliBiliCookies__\", token);\n            if (qlEnvList.Code != 200)\n            {\n                throw new Exception($\"查询环境变量失败：{qlEnvList.ToJsonStr()}\");\n            }\n\n            logger.LogDebug(qlEnvList.Data.ToJsonStr());\n            logger.LogDebug(ckInfo.ToString());\n\n            var list = qlEnvList\n                .Data.Where(x => x.name.StartsWith(\"Ray_BiliBiliCookies__\"))\n                .ToList();\n            var oldEnv = list.FirstOrDefault(x => x.value.Contains(ckInfo.UserId));\n\n            if (oldEnv != null)\n            {\n                logger.LogInformation(\"用户已存在，更新cookie\");\n                logger.LogInformation(\"Key：{key}\", oldEnv.name);\n                var update = new UpdateQingLongEnv\n                {\n                    id = oldEnv.id,\n                    name = oldEnv.name,\n                    value = ckInfo.CookieStr,\n                    remarks = string.IsNullOrEmpty(oldEnv.remarks)\n                        ? $\"bili-{ckInfo.UserId}\"\n                        : oldEnv.remarks,\n                };\n\n                var updateRe = await qingLongApi.UpdateEnvsAsync(update, token);\n                logger.LogInformation(updateRe.Code == 200 ? \"更新成功！\" : updateRe.ToJsonStr());\n\n                return true;\n            }\n\n            logger.LogInformation(\"用户不存在，新增cookie\");\n            var maxNum = -1;\n            if (list.Any())\n            {\n                maxNum = list.Select(x =>\n                    {\n                        var num = x.name.Replace(\"Ray_BiliBiliCookies__\", \"\");\n                        var parseSuc = int.TryParse(num, out int envNum);\n                        return parseSuc ? envNum : 0;\n                    })\n                    .Max();\n            }\n\n            var name = $\"Ray_BiliBiliCookies__{maxNum + 1}\";\n            logger.LogInformation(\"Key：{key}\", name);\n\n            var add = new AddQingLongEnv\n            {\n                name = name,\n                value = ckInfo.CookieStr,\n                remarks = $\"bili-{ckInfo.UserId}\",\n            };\n            var addRe = await qingLongApi.AddEnvsAsync([add], token);\n            logger.LogInformation(addRe.Code == 200 ? \"新增成功！\" : addRe.ToJsonStr());\n            return true;\n        }\n        catch\n        {\n            await PrintIfSaveCookieFailAsync(ckInfo, cancellationToken);\n            return false;\n        }\n    }\n\n    #region private\n\n    private void GenerateQrCode(string str)\n    {\n        var qrGenerator = new QRCodeGenerator();\n        QRCodeData qrCodeData = qrGenerator.CreateQrCode(str, QRCodeGenerator.ECCLevel.L);\n\n        logger.LogInformation(\"AsciiQRCode：\");\n        //var qrCode = new AsciiQRCode(qrCodeData);\n        //var qrCodeStr = qrCode.GetGraphic(1, drawQuietZones: false);\n        //_logger.LogInformation(Environment.NewLine + qrCodeStr);\n\n        //Console.WriteLine(\"Console：\");\n        //Print(qrCodeData);\n        PrintSmall(qrCodeData);\n    }\n\n    private void Print(QRCodeData qrCodeData)\n    {\n        Console.BackgroundColor = ConsoleColor.White;\n        for (int i = 0; i < qrCodeData.ModuleMatrix.Count + 2; i++)\n            Console.Write(\"　\"); //中文全角的空格符\n        Console.WriteLine();\n        for (int j = 0; j < qrCodeData.ModuleMatrix.Count; j++)\n        {\n            for (int i = 0; i < qrCodeData.ModuleMatrix.Count; i++)\n            {\n                //char charToPoint = qrCode.Matrix[i, j] ? '█' : '　';\n                Console.Write(i == 0 ? \"　\" : \"\"); //中文全角的空格符\n                Console.BackgroundColor = qrCodeData.ModuleMatrix[i][j]\n                    ? ConsoleColor.Black\n                    : ConsoleColor.White;\n                Console.Write('　'); //中文全角的空格符\n                Console.BackgroundColor = ConsoleColor.White;\n                Console.Write(i == qrCodeData.ModuleMatrix.Count - 1 ? \"　\" : \"\"); //中文全角的空格符\n            }\n            Console.WriteLine();\n        }\n        for (int i = 0; i < qrCodeData.ModuleMatrix.Count + 2; i++)\n            Console.Write(\"　\"); //中文全角的空格符\n\n        Console.WriteLine();\n    }\n\n    private void PrintSmall(QRCodeData qrCodeData)\n    {\n        //黑黑（\" \"）\n        //白白（\"█\"）\n        //黑白（\"▄\"）\n        //白黑（\"▀\"）\n        var dic = new Dictionary<string, char>()\n        {\n            { \"11\", ' ' },\n            { \"00\", '█' },\n            { \"10\", '▄' },\n            { \"01\", '▀' }, //todo:win平台的cmd会显示？,是已知问题，待想办法解决\n            //{\"01\", '^'},//▼▔\n        };\n\n        var count = qrCodeData.ModuleMatrix.Count;\n\n        var list = new List<string>();\n        for (int rowNum = 0; rowNum < count; rowNum++)\n        {\n            var rowStr = \"\";\n            for (int colNum = 0; colNum < count; colNum++)\n            {\n                var num = qrCodeData.ModuleMatrix[colNum][rowNum] ? \"1\" : \"0\";\n                var numDown = \"0\";\n                if (rowNum + 1 < count)\n                    numDown = qrCodeData.ModuleMatrix[colNum][rowNum + 1] ? \"1\" : \"0\";\n\n                rowStr += dic[num + numDown];\n            }\n            list.Add(rowStr);\n            rowNum++;\n        }\n\n        logger.LogInformation(Environment.NewLine + string.Join(Environment.NewLine, list));\n    }\n\n    private string GetOnlinePic(string str)\n    {\n        var encode = System.Web.HttpUtility.UrlEncode(str);\n        return $\"https://tool.lu/qrcode/basic.html?text={encode}\";\n    }\n\n    private async Task SaveJson(List<string> lines, IFileInfo fileInfo)\n    {\n        var newJson = string.Join(Environment.NewLine, lines);\n\n        await using var sw = new StreamWriter(fileInfo.PhysicalPath!);\n        await sw.WriteAsync(newJson);\n    }\n\n    #region qinglong\n\n    private async Task<string> GetQingLongAuthTokenAsync()\n    {\n        logger.LogWarning(\"使用OpenAPI鉴权\");\n        if (\n            string.IsNullOrWhiteSpace(qingLongOptions.Value.ClientId)\n            || string.IsNullOrWhiteSpace(qingLongOptions.Value.ClientSecret)\n        )\n        {\n            logger.LogWarning(\"未配置青龙的ClientId和ClientSecret，无法自动获取token\");\n            logger.LogWarning(\n                \"教程：{qingDoc}\",\n                \"https://github.com/RayWangQvQ/BiliBiliToolPro/blob/main/qinglong/README.md\"\n            );\n            return \"\";\n        }\n\n        var token = await qingLongApi.GetTokenAsync(\n            qingLongOptions.Value.ClientId!,\n            qingLongOptions.Value.ClientSecret!\n        );\n\n        return $\"{token.Data.token_type} {token.Data.token}\";\n    }\n\n    private Task PrintIfSaveCookieFailAsync(BiliCookie ckInfo, CancellationToken cancellationToken)\n    {\n        logger.LogError(\"持久化失败，青龙版本高于2.18，请手动添加环境变量到青龙\");\n        logger.LogWarning(\"变量Key：{key}\", \"Ray_BiliBiliCookies__0\");\n        logger.LogWarning(\"变量值：{value}\", ckInfo.CookieStr);\n        logger.LogWarning(\n            \"如果Key已存在，请自行+1，如Ray_BiliBiliCookies__1，Ray_BiliBiliCookies__2...\"\n        );\n        return Task.CompletedTask;\n    }\n\n    #endregion\n\n    #endregion\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/MangaDomainService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\n\nnamespace Ray.BiliBiliTool.DomainService;\n\n/// <summary>\n/// 漫画\n/// </summary>\npublic class MangaDomainService(\n    ILogger<MangaDomainService> logger,\n    IMangaApi mangaApi,\n    IOptionsMonitor<MangaTaskOptions> mangaTaskOptions,\n    IOptionsMonitor<DailyTaskOptions> dailyTaskOptions,\n    IOptionsMonitor<VipPrivilegeOptions> vipPrivilegeOptions\n) : IMangaDomainService\n{\n    private readonly MangaTaskOptions _mangaTaskOptions = mangaTaskOptions.CurrentValue;\n    private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue;\n    private readonly VipPrivilegeOptions _vipPrivilegeOptions = vipPrivilegeOptions.CurrentValue;\n\n    /// <summary>\n    /// 漫画签到\n    /// </summary>\n    public async Task MangaSign(BiliCookie ck)\n    {\n        BiliApiResponse response;\n        try\n        {\n            response = await mangaApi.ClockIn(_dailyTaskOptions.DevicePlatform, ck.ToString());\n        }\n        catch (Exception)\n        {\n            //ignore\n            //重复签到会报400异常,这里忽略掉\n            logger.LogInformation(\"【签到结果】失败\");\n            logger.LogInformation(\"【原因】今日已签到过，无法重复签到\");\n            return;\n        }\n\n        if (response.Code == 0)\n        {\n            logger.LogInformation(\"【签到结果】成功\");\n        }\n        else\n        {\n            logger.LogInformation(\"【签到结果】失败\");\n            logger.LogInformation(\"【原因】{msg}\", response.Message);\n        }\n    }\n\n    /// <summary>\n    /// 漫画阅读\n    /// </summary>\n    public async Task MangaRead(BiliCookie ck)\n    {\n        if (_mangaTaskOptions.CustomComicId <= 0)\n            return;\n        BiliApiResponse response = await mangaApi.ReadManga(\n            _dailyTaskOptions.DevicePlatform,\n            _mangaTaskOptions.CustomComicId,\n            _mangaTaskOptions.CustomEpId,\n            ck.ToString()\n        );\n\n        if (response.Code == 0)\n        {\n            logger.LogInformation(\"【漫画阅读】成功\");\n        }\n        else\n        {\n            logger.LogInformation(\"【漫画阅读】失败\");\n            logger.LogInformation(\"【原因】{msg}\", response.Message);\n        }\n    }\n\n    /// <summary>\n    /// 获取大会员漫画权益\n    /// </summary>\n    /// <param name=\"reason_id\">权益号，由https://api.bilibili.com/x/vip/privilege/my得到权益号数组，取值范围为数组中的整数\n    /// 这里为方便直接取1，为领取漫读劵，暂时不取其他的值</param>\n    public async Task ReceiveMangaVipReward(int reason_id, UserInfo userInfo, BiliCookie ck)\n    {\n        if (userInfo.GetVipType() == 0)\n        {\n            logger.LogInformation(\"不是会员，跳过\");\n            return;\n        }\n\n        int day = DateTime.Today.Day;\n        logger.LogInformation(\"【今天】{day}号\", day);\n\n        var response = await mangaApi.ReceiveMangaVipReward(reason_id, ck.ToString());\n        if (response.Code == 0)\n        {\n            logger.LogInformation(\"【领取结果】成功\");\n            logger.LogInformation($\"【获取】{response.Data.Amount}张漫读劵\");\n        }\n        else\n        {\n            logger.LogInformation(\"【领取结果】失败\");\n            logger.LogInformation(\"【原因】{msg}\", response.Message);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/Ray.BiliBiliTool.DomainService.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Extensions.FileProviders.Physical\" />\n    <PackageReference Include=\"Microsoft.Extensions.Hosting.Abstractions\" />\n    <PackageReference Include=\"Microsoft.Extensions.Http\" />\n    <PackageReference Include=\"QRCoder\" />\n    <PackageReference Include=\"Scrutor\" />\n    <PackageReference Include=\"Serilog\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Agent\\Ray.BiliBiliTool.Agent.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Config\\Ray.BiliBiliTool.Config.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Infrastructure\\Ray.BiliBiliTool.Infrastructure.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/VideoDomainService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Relation;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Video;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Dtos;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\n\nnamespace Ray.BiliBiliTool.DomainService;\n\n/// <summary>\n/// 视频\n/// </summary>\npublic class VideoDomainService(\n    ILogger<VideoDomainService> logger,\n    IOptionsMonitor<DailyTaskOptions> dailyTaskOptions,\n    IRelationApi relationApi,\n    IVideoApi videoApi,\n    IVideoWithoutCookieApi videoWithoutCookieApi\n) : IVideoDomainService\n{\n    private readonly DailyTaskOptions _dailyTaskOptions = dailyTaskOptions.CurrentValue;\n    private readonly Dictionary<string, int> _expDic = Config.Constants.ExpDic;\n\n    /// <summary>\n    /// 获取视频详情\n    /// </summary>\n    /// <param name=\"aid\"></param>\n    /// <returns></returns>\n    public async Task<VideoDetail> GetVideoDetail(string aid)\n    {\n        var re = await videoWithoutCookieApi.GetVideoDetail(aid);\n        return re.Data!;\n    }\n\n    /// <summary>\n    /// 从排行榜获取随机视频\n    /// </summary>\n    /// <returns></returns>\n    public async Task<RankingInfo> GetRandomVideoOfRanking()\n    {\n        var apiResponse = await videoWithoutCookieApi.GetRegionRankingVideosV2();\n        logger.LogDebug(\"获取排行榜成功\");\n        var data = apiResponse.Data.List[new Random().Next(apiResponse.Data.List.Count)];\n        return data;\n    }\n\n    public async Task<UpVideoInfo?> GetRandomVideoOfUp(long upId, int total, BiliCookie ck)\n    {\n        if (total <= 0)\n            return null;\n\n        var req = new SearchVideosByUpIdDto()\n        {\n            mid = upId,\n            ps = 1,\n            pn = new Random().Next(1, total + 1),\n        };\n\n        BiliApiResponse<SearchUpVideosResponse> re = await videoApi.SearchVideosByUpId(\n            req,\n            ck.ToString()\n        );\n\n        if (re.Code != 0)\n        {\n            throw new Exception(re.Message);\n        }\n\n        return re.Data?.List?.Vlist.FirstOrDefault();\n    }\n\n    /// <summary>\n    /// 获取UP主的视频总数量\n    /// </summary>\n    /// <param name=\"upId\"></param>\n    /// <returns></returns>\n    public async Task<int> GetVideoCountOfUp(long upId, BiliCookie ck)\n    {\n        var req = new SearchVideosByUpIdDto() { mid = upId };\n\n        BiliApiResponse<SearchUpVideosResponse> re = await videoApi.SearchVideosByUpId(\n            req,\n            ck.ToString()\n        );\n        if (re.Code != 0)\n        {\n            throw new Exception(re.Message);\n        }\n\n        return re.Data!.Page.Count;\n    }\n\n    public async Task WatchAndShareVideo(DailyTaskInfo dailyTaskStatus, BiliCookie ck)\n    {\n        VideoInfoDto? targetVideo = null;\n\n        //至少有一项未完成，获取视频\n        if (!dailyTaskStatus.Watch || !dailyTaskStatus.Share)\n        {\n            targetVideo = await GetRandomVideoForWatchAndShare(ck);\n            logger.LogInformation(\"【随机视频】{title}\", targetVideo.Title);\n        }\n\n        bool watched = false;\n        //观看\n        if (!dailyTaskStatus.Watch && _dailyTaskOptions.IsWatchVideo)\n        {\n            await WatchVideo(targetVideo!, ck);\n            watched = true;\n        }\n        else\n            logger.LogInformation(\"今天已经观看过了，不需要再看啦\");\n\n        //分享\n        if (!dailyTaskStatus.Share && _dailyTaskOptions.IsShareVideo)\n        {\n            //如果没有打开观看过，则分享前先打开视频\n            if (!watched)\n            {\n                try\n                {\n                    await OpenVideo(targetVideo!, ck);\n                }\n                catch (Exception e)\n                {\n                    //ignore\n                    logger.LogError(\"打开视频异常：{msg}\", e.Message);\n                }\n            }\n            await ShareVideo(targetVideo!, ck);\n        }\n        else\n            logger.LogInformation(\"今天已经分享过了，不用再分享啦\");\n    }\n\n    /// <summary>\n    /// 观看视频\n    /// </summary>\n    public async Task WatchVideo(VideoInfoDto videoInfo, BiliCookie ck)\n    {\n        //开始上报一次\n        await OpenVideo(videoInfo, ck);\n\n        //结束上报一次\n        videoInfo.Duration = videoInfo.Duration ?? 15;\n        int max = videoInfo.Duration < 15 ? videoInfo.Duration.Value : 15;\n        int playedTime = new Random().Next(1, max);\n\n        var request = new UploadVideoHeartbeatRequest\n        {\n            Aid = long.Parse(videoInfo.Aid),\n            Bvid = videoInfo.Bvid,\n            Cid = videoInfo.Cid,\n            Mid = long.Parse(ck.UserId),\n            Csrf = ck.BiliJct,\n\n            Played_time = playedTime,\n            Realtime = playedTime,\n            Real_played_time = playedTime,\n        };\n        BiliApiResponse apiResponse = await videoApi.UploadVideoHeartbeat(request, ck.ToString());\n\n        if (apiResponse.Code == 0)\n        {\n            _expDic.TryGetValue(\"每日观看视频\", out int exp);\n            logger.LogInformation(\n                \"视频播放成功，已观看到第{playedTime}秒，经验+{exp} √\",\n                playedTime,\n                exp\n            );\n        }\n        else\n        {\n            logger.LogError(\"视频播放失败，原因：{msg}\", apiResponse.Message);\n        }\n    }\n\n    /// <summary>\n    /// 分享视频\n    /// </summary>\n    /// <param name=\"videoInfo\">视频</param>\n    public async Task ShareVideo(VideoInfoDto videoInfo, BiliCookie ck)\n    {\n        var request = new ShareVideoRequest(long.Parse(videoInfo.Aid), ck.BiliJct);\n        BiliApiResponse apiResponse = await videoApi.ShareVideo(request, ck.ToString());\n\n        if (apiResponse.Code == 0)\n        {\n            _expDic.TryGetValue(\"每日观看视频\", out int exp);\n            logger.LogInformation(\"视频分享成功，经验+{exp} √\", exp);\n        }\n        else\n        {\n            logger.LogError(\"视频分享失败，原因: {msg}\", apiResponse.Message);\n        }\n    }\n\n    /// <summary>\n    /// 模拟打开视频播放（初始上报一次进度）\n    /// </summary>\n    /// <param name=\"videoInfo\"></param>\n    /// <returns></returns>\n    private async Task<bool> OpenVideo(VideoInfoDto videoInfo, BiliCookie ck)\n    {\n        var request = new UploadVideoHeartbeatRequest\n        {\n            Aid = long.Parse(videoInfo.Aid),\n            Bvid = videoInfo.Bvid,\n            Cid = videoInfo.Cid,\n\n            Mid = long.Parse(ck.UserId),\n            Csrf = ck.BiliJct,\n        };\n\n        //开始上报一次\n        BiliApiResponse apiResponse = await videoApi.UploadVideoHeartbeat(request, ck.ToString());\n\n        if (apiResponse.Code == 0)\n        {\n            logger.LogDebug(\"打开视频成功\");\n            return true;\n        }\n        else\n        {\n            logger.LogError(\"视频打开失败，原因：{msg}\", apiResponse.Message);\n            return false;\n        }\n    }\n\n    #region private\n    /// <summary>\n    /// 获取一个视频用来观看并分享\n    /// </summary>\n    /// <returns></returns>\n    private async Task<VideoInfoDto> GetRandomVideoForWatchAndShare(BiliCookie ck)\n    {\n        //先从配置的或关注的up中取\n        var video = await GetRandomVideoOfFollowingUps(ck);\n        if (video != null)\n            return video;\n\n        //然后从排行榜中取\n        var t = await GetRandomVideoOfRanking();\n        return new VideoInfoDto\n        {\n            Aid = t.Aid.ToString(),\n            Bvid = t.Bvid,\n            Cid = t.Cid,\n            Copyright = t.Copyright,\n            Duration = t.Duration,\n            Title = t.Title,\n        };\n    }\n\n    private async Task<VideoInfoDto?> GetRandomVideoOfFollowingUps(BiliCookie ck)\n    {\n        //配置的UpId\n        int configUpsCount = _dailyTaskOptions.SupportUpIdList.Count;\n        if (configUpsCount > 0)\n        {\n            var video = await GetRandomVideoOfUps(_dailyTaskOptions.SupportUpIdList, ck);\n            if (video != null)\n                return video;\n        }\n\n        //关注列表\n        var request = new GetFollowingsRequest(long.Parse(ck.UserId));\n        BiliApiResponse<GetFollowingsResponse> result = await relationApi.GetFollowings(\n            request,\n            ck.ToString()\n        );\n        if (result.Data.Total > 0)\n        {\n            var video = await GetRandomVideoOfUps(result.Data.List.Select(x => x.Mid).ToList(), ck);\n            if (video != null)\n                return video;\n        }\n\n        return null;\n    }\n\n    /// <summary>\n    /// 从up集合中获取一个随机视频\n    /// </summary>\n    /// <param name=\"upIds\"></param>\n    /// <returns></returns>\n    private async Task<VideoInfoDto?> GetRandomVideoOfUps(List<long> upIds, BiliCookie ck)\n    {\n        long upId = upIds[new Random().Next(0, upIds.Count)];\n\n        if (upId == 0 || upId == long.MinValue)\n            return null;\n\n        int count = await GetVideoCountOfUp(upId, ck);\n\n        if (count > 0)\n        {\n            var video = await GetRandomVideoOfUp(upId, count, ck);\n            if (video == null)\n                return null;\n            return new VideoInfoDto\n            {\n                Aid = video.Aid.ToString(),\n                Bvid = video.Bvid,\n                //Cid=,\n                //Copyright=\n                Title = video.Title,\n                Duration = video.Duration,\n            };\n        }\n\n        return null;\n    }\n    #endregion private\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/VipBigPointDomainService.cs",
    "content": "using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.ViewMall;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask.ThreeDaysSign;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Dtos;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\n\nnamespace Ray.BiliBiliTool.DomainService;\n\npublic class VipBigPointDomainService(\n    ILogger<VipBigPointDomainService> logger,\n    IOptionsMonitor<VipBigPointOptions> vipBigPointOptions,\n    IVipBigPointApi vipApi,\n    IMallApi mallApi,\n    IVipMallApi vipMallApi,\n    IVideoApi videoApi,\n    IAccountDomainService accountDomainService,\n    IVideoDomainService videoDomainService\n) : IVipBigPointDomainService\n{\n    private readonly VipBigPointOptions _vipBigPointOptions = vipBigPointOptions.CurrentValue;\n\n    public async Task<VipBigPointCombine> GetCombineAsync(BiliCookie ck)\n    {\n        var allTasks = await mallApi.GetCombineAsync(\n            new GetCombineRequest { csrf = ck.BiliJct, buvid = ck.Buvid },\n            ck.ToString()\n        );\n        if (allTasks.Code != 0)\n            throw new Exception(allTasks.ToJsonStr());\n        return allTasks.Data;\n    }\n\n    /// <summary>\n    /// 领取大会员专属等级加速包\n    /// </summary>\n    public async Task VipExpressAsync(BiliCookie ck)\n    {\n        var re = await vipApi.GetVouchersInfoAsync(ck.ToString());\n        if (re.Code == 0)\n        {\n            var state = re.Data.List.Find(x => x.Type == 9)?.State;\n\n            switch (state)\n            {\n                case 2:\n                    logger.LogInformation(\"大会员经验观看任务未完成\");\n                    logger.LogInformation(\"开始观看视频\");\n                    // 观看视频，暂时没有好办法解决，先这样使着\n                    DailyTaskInfo dailyTaskInfo = await accountDomainService.GetDailyTaskStatus(ck);\n                    await videoDomainService.WatchAndShareVideo(dailyTaskInfo, ck);\n                    // 跳转到未兑换，执行兑换任务\n                    goto case 0;\n\n                case 1:\n                    logger.LogInformation(\"大会员经验已兑换\");\n                    break;\n\n                case 0:\n                    logger.LogInformation(\"大会员经验未兑换\");\n                    //兑换api\n                    var response = await vipApi.ObtainVipExperienceAsync(\n                        new VipExperienceRequest { csrf = ck.BiliJct },\n                        ck.ToString()\n                    );\n                    if (response.Code != 0)\n                    {\n                        logger.LogInformation(\n                            \"大会员经验领取失败，错误信息：{message}\",\n                            response.Message\n                        );\n                        break;\n                    }\n\n                    logger.LogInformation(\"领取成功，经验+10 √\");\n                    var combine = await GetCombineAsync(ck);\n                    combine.LogPointInfo(logger);\n                    break;\n\n                default:\n                    logger.LogDebug(\"大会员经验领取失败，未知错误\");\n                    break;\n            }\n        }\n    }\n\n    /// <summary>\n    /// 签到\n    /// </summary>\n    /// <param name=\"ck\"></param>\n    /// <exception cref=\"Exception\"></exception>\n    public async Task SignAsync(BiliCookie ck)\n    {\n        var signInfo = await vipApi.GetThreeDaySignAsync(\n            new ThreeDaySignRequest { csrf = ck.BiliJct },\n            ck.ToString()\n        );\n        if (signInfo.Data.three_day_sign.signed)\n        {\n            logger.LogInformation(\"已完成，跳过\");\n            logger.LogInformation(signInfo.Data.ToString());\n            return;\n        }\n\n        BiliApiResponse<Sign2Response> re = await mallApi.Sign2Async(\n            new Sign2RequestPath(ck.BiliJct),\n            new Sign2Request(),\n            ck.ToString()\n        );\n        if (re.Code != 0)\n            throw new Exception(re.ToJsonStr());\n\n        logger.LogInformation(\"签到成功\");\n        logger.LogInformation(re.Data.ToString());\n\n        signInfo = await vipApi.GetThreeDaySignAsync(\n            new ThreeDaySignRequest { csrf = ck.BiliJct },\n            ck.ToString()\n        );\n        signInfo.Data.LogPointInfo(logger);\n    }\n\n    /// <summary>\n    /// 领取任务\n    /// </summary>\n    /// <param name=\"combine\"></param>\n    /// <param name=\"ck\"></param>\n    public async Task ReceiveDailyMissionsAsync(VipBigPointCombine combine, BiliCookie ck)\n    {\n        const string moduleCode = \"日常任务\";\n\n        var module = combine.Task_info.Modules.FirstOrDefault(x => x.module_title == moduleCode);\n        var missionsNeedReceive = module?.common_task_item.Where(x => x.state == 0).ToList();\n        if (missionsNeedReceive == null || missionsNeedReceive.Count == 0)\n        {\n            logger.LogInformation(\"均已领取，跳过\");\n            return;\n        }\n\n        foreach (var targetTask in missionsNeedReceive)\n        {\n            logger.LogInformation(\"开始领取任务：{task}\", targetTask.title);\n            await TryReceive(targetTask.task_code, ck);\n        }\n    }\n\n    public async Task ReceiveAndCompleteAsync(\n        VipBigPointCombine info,\n        string moduleCode,\n        string taskCode,\n        BiliCookie ck,\n        Func<string, BiliCookie, Task<bool>> completeFunc\n    )\n    {\n        var module = info.Task_info.Modules.FirstOrDefault(x => x.module_title == moduleCode);\n        var bonusTask = module?.common_task_item.FirstOrDefault(x => x.task_code == taskCode);\n\n        if (bonusTask == null)\n        {\n            logger.LogInformation(\"任务失效\");\n            return;\n        }\n\n        if (bonusTask.state == 3)\n        {\n            logger.LogInformation(\"已完成，跳过\");\n            return;\n        }\n\n        if (bonusTask.state == 0)\n        {\n            logger.LogInformation(\"开始领取任务\");\n            await TryReceive(bonusTask.task_code, ck);\n        }\n\n        logger.LogInformation(\"开始完成任务\");\n        var re = await completeFunc(taskCode, ck);\n\n        //确认\n        if (re)\n        {\n            var combine = await GetCombineAsync(ck);\n            module = combine.Task_info.Modules.FirstOrDefault(x => x.module_title == moduleCode);\n            bonusTask = module?.common_task_item.FirstOrDefault(x => x.task_code == taskCode);\n            var success = bonusTask is { state: 3, complete_times: >= 1 };\n            logger.LogInformation(\"确认：{re}\", success ? \"成功，经验 +10\" : \"失败\");\n        }\n    }\n\n    public async Task<bool> CompleteAsync(string taskCode, BiliCookie ck)\n    {\n        var request = new ReceiveOrCompleteTaskRequest(taskCode);\n        var re = await vipApi.CompleteAsync(request, ck.ToString());\n        if (re.Code == 0)\n        {\n            logger.LogInformation(\"已完成\");\n            return true;\n        }\n\n        logger.LogInformation(\"失败：{msg}\", re.ToJsonStr());\n        return false;\n    }\n\n    public async Task<bool> CompleteViewAsync(string taskCode, BiliCookie ck)\n    {\n        var channel = taskCode switch\n        {\n            \"animatetab\" => \"jp_channel\",\n            \"filmtab\" => \"tv_channel\",\n            _ => throw new ArgumentOutOfRangeException(\n                nameof(taskCode),\n                $\"Invalid taskCode: {taskCode}\"\n            ),\n        };\n\n        logger.LogInformation(\"开始浏览\");\n        await Task.Delay(10 * 1000);\n\n        var request = new ViewRequest(channel);\n        var re = await vipApi.ViewComplete(request, ck.ToString());\n        if (re.Code == 0)\n        {\n            logger.LogInformation(\"浏览完成\");\n            return true;\n        }\n\n        logger.LogInformation(\"浏览失败：{msg}\", re.ToJsonStr());\n        return false;\n    }\n\n    public async Task<bool> CompleteViewVipMallAsync(string taskCode, BiliCookie ck)\n    {\n        var re = await vipMallApi.ViewVipMallAsync(\n            new ViewVipMallRequest { Csrf = ck.BiliJct },\n            ck.ToString()\n        );\n        if (re.Code != 0)\n            throw new Exception(re.ToJsonStr());\n        return true;\n    }\n\n    public async Task<bool> CompleteV2Async(string taskCode, BiliCookie ck)\n    {\n        var request = new ReceiveOrCompleteTaskRequest(taskCode);\n        var re = await vipApi.CompleteV2(request, ck.ToString());\n        if (re.Code == 0)\n        {\n            logger.LogInformation(\"已完成\");\n            return true;\n        }\n\n        logger.LogInformation(\"失败：{msg}\", re.ToJsonStr());\n        return false;\n    }\n\n    #region private\n\n    /// <summary>\n    /// 领取任务\n    /// </summary>\n    private async Task TryReceive(string taskCode, BiliCookie ck)\n    {\n        BiliApiResponse? re = null;\n        try\n        {\n            var request = new ReceiveOrCompleteTaskRequest(taskCode);\n            re = await vipApi.ReceiveV2(request, ck.ToString());\n            if (re.Code == 0)\n                logger.LogInformation(\"领取任务成功\");\n            else\n                logger.LogInformation(\"领取任务失败：{msg}\", re.ToJsonStr());\n        }\n        catch (Exception e)\n        {\n            logger.LogError(\"领取任务异常\");\n            logger.LogError(e.Message + re?.ToJsonStr());\n        }\n    }\n\n    private async Task<bool> WatchBangumi(BiliCookie ck)\n    {\n        if (_vipBigPointOptions.ViewBangumiList.Count == 0)\n            return false;\n\n        long randomSsid = _vipBigPointOptions.ViewBangumiList[\n            new Random().Next(0, _vipBigPointOptions.ViewBangumiList.Count)\n        ];\n\n        var res = await GetBangumi(randomSsid, ck);\n        if (res is null)\n        {\n            return false;\n        }\n\n        var videoInfo = res.Value.Item1;\n\n        // 随机播放时间\n        int playedTime = new Random().Next(905, 1800);\n        // 观看该视频\n        var request = new UploadVideoHeartbeatRequest()\n        {\n            Aid = long.Parse(videoInfo.Aid),\n            Bvid = videoInfo.Bvid,\n            Cid = videoInfo.Cid,\n            Mid = long.Parse(ck.UserId),\n            Sid = randomSsid,\n            Epid = res.Value.Item2,\n            Csrf = ck.BiliJct,\n            Type = 4,\n            Sub_type = 1,\n            Start_ts = DateTime.Now.ToTimeStamp() - playedTime,\n            Played_time = playedTime,\n            Realtime = playedTime,\n            Real_played_time = playedTime,\n        };\n        BiliApiResponse apiResponse = await videoApi.UploadVideoHeartbeat(request, ck.ToString());\n        if (apiResponse.Code == 0)\n        {\n            return true;\n        }\n\n        return false;\n    }\n\n    /// <summary>\n    /// 从自定义的番剧ssid中选择其中的一部中的一集\n    /// </summary>\n    /// <param name=\"randomSsid\">番剧ssid</param>\n    /// <returns></returns>\n    private async Task<(VideoInfoDto, long)?> GetBangumi(long randomSsid, BiliCookie ck)\n    {\n        try\n        {\n            if (randomSsid is 0 or long.MinValue)\n                return null;\n            var bangumiInfo = await videoApi.GetBangumiBySsid(randomSsid, ck.ToString());\n\n            // 从获取的剧集中随机获得其中的一集\n\n            var bangumi = bangumiInfo.Result.episodes[\n                new Random().Next(0, bangumiInfo.Result.episodes.Count)\n            ];\n            var videoInfo = new VideoInfoDto()\n            {\n                Bvid = bangumi.bvid,\n                Aid = bangumi.aid.ToString(),\n                Cid = bangumi.cid,\n                Copyright = 1,\n                Duration = bangumi.duration,\n                Title = bangumi.share_copy,\n            };\n            logger.LogInformation(\"本次播放的正片为：{title}\", bangumi.share_copy);\n            return (videoInfo, bangumi.ep_id);\n        }\n        catch (Exception e)\n        {\n            logger.LogError(e.Message);\n        }\n\n        return null;\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.DomainService/VipPrivilegeDomainService.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.DomainService.Interfaces;\n\nnamespace Ray.BiliBiliTool.DomainService;\n\n/// <summary>\n/// 会员权益\n/// </summary>\npublic class VipPrivilegeDomainService(\n    ILogger<VipPrivilegeDomainService> logger,\n    IDailyTaskApi dailyTaskApi,\n    IOptionsMonitor<VipPrivilegeOptions> receiveVipPrivilegeOptions\n) : IVipPrivilegeDomainService\n{\n    private readonly VipPrivilegeOptions _vipPrivilegeOptions =\n        receiveVipPrivilegeOptions.CurrentValue;\n\n    /// <summary>\n    /// 每月领取大会员福利（B币券、大会员权益）\n    /// </summary>\n    /// <param name=\"userInfo\"></param>\n    /// <param name=\"ck\"></param>\n    public async Task<bool> ReceiveVipPrivilege(UserInfo userInfo, BiliCookie ck)\n    {\n        if (!_vipPrivilegeOptions.IsEnable)\n        {\n            logger.LogInformation(\"已配置为关闭，跳过\");\n            return false;\n        }\n\n        //大会员类型\n        VipType vipType = userInfo.GetVipType();\n        if (vipType != VipType.Annual)\n        {\n            logger.LogInformation(\"普通会员和月度大会员每月不赠送B币券，不需要领取权益喽\");\n            return false;\n        }\n\n        /*\n        int targetDay = _dailyTaskOptions.DayOfReceiveVipPrivilege == -1\n            ? 1\n            : _dailyTaskOptions.DayOfReceiveVipPrivilege;\n\n        _logger.LogInformation(\"【目标领取日期】{targetDay}号\", targetDay);\n        _logger.LogInformation(\"【今天】{day}号\", DateTime.Today.Day);\n\n        if (DateTime.Today.Day != targetDay\n            && DateTime.Today.Day != DateTime.Today.LastDayOfMonth().Day)\n        {\n            _logger.LogInformation(\"跳过\");\n            return false;\n        }\n        */\n\n        var suc1 = await ReceiveVipPrivilege(VipPrivilegeType.BCoinCoupon, ck);\n        var suc2 = await ReceiveVipPrivilege(VipPrivilegeType.MembershipBenefits, ck);\n\n        if (suc1 | suc2)\n            return true;\n        return false;\n    }\n\n    #region private\n\n    /// <summary>\n    /// 领取大会员每月赠送福利\n    /// </summary>\n    /// <param name=\"type\">1.大会员B币券；2.大会员福利</param>\n    /// <param name=\"ck\"></param>\n    private async Task<bool> ReceiveVipPrivilege(VipPrivilegeType type, BiliCookie ck)\n    {\n        var response = await dailyTaskApi.ReceiveVipPrivilegeAsync(\n            (int)type,\n            ck.BiliJct,\n            ck.ToString()\n        );\n\n        var name = GetPrivilegeName(type);\n        logger.LogInformation(\"【领取】{name}\", name);\n\n        if (response.Code == 0)\n        {\n            logger.LogInformation(\"【结果】成功\");\n            return true;\n        }\n        else\n        {\n            logger.LogInformation(\"【结果】失败\");\n            logger.LogInformation(\"【原因】 {msg}\", response.Message);\n            return false;\n        }\n    }\n\n    /// <summary>\n    /// 获取权益名称\n    /// </summary>\n    /// <param name=\"type\"></param>\n    /// <returns></returns>\n    private string GetPrivilegeName(VipPrivilegeType type)\n    {\n        switch (type)\n        {\n            case VipPrivilegeType.BCoinCoupon:\n                return \"年度大会员每月赠送的B币券\";\n\n            case VipPrivilegeType.MembershipBenefits:\n                return \"大会员福利/权益\";\n        }\n\n        return \"\";\n    }\n\n    #endregion private\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Cookie/CookieInfo.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Infrastructure.Cookie;\n\npublic class CookieInfo(Dictionary<string, string> cookieDic)\n{\n    public Dictionary<string, string> CookieItemDictionary { get; private set; } = cookieDic;\n\n    public string CookieStr =>\n        string.Join(\n            \"; \",\n            CookieItemDictionary\n                .Select(item => $\"{CkNameBuild(item.Key)}={CkValueBuild(item.Value)}\")\n                .ToList()\n        );\n\n    public virtual void Check()\n    {\n        if (CookieItemDictionary == null || CookieItemDictionary.Count == 0)\n            throw new Exception(\"Cookie字符串为空\");\n    }\n\n    protected virtual string CkNameBuild(string name)\n    {\n        return name;\n    }\n\n    protected virtual string CkValueBuild(string value)\n    {\n        return value;\n    }\n\n    public override string ToString()\n    {\n        var list = CookieItemDictionary.Select(d =>\n            $\"{CkNameBuild(d.Key)}={CkValueBuild(d.Value)}\"\n        );\n        return string.Join(\"; \", list);\n    }\n\n    #region merge\n\n    public void MergeCurrentCookieBySetCookieHeaders(IEnumerable<string> setCookieList)\n    {\n        MergeCurrentCookie(ConvertSetCkHeadersToCkItemList(setCookieList));\n    }\n\n    public void MergeCurrentCookie(string ckStr)\n    {\n        MergeCurrentCookie(ConvertCkStrToCkItemList(ckStr));\n    }\n\n    public void MergeCurrentCookie(List<string> ckItemList)\n    {\n        MergeCurrentCookie(ConvertCkItemListToCkDic(ckItemList));\n    }\n\n    private void MergeCurrentCookie(Dictionary<string, string> ckDic)\n    {\n        foreach (var item in ckDic)\n        {\n            CookieItemDictionary[item.Key] = item.Value;\n        }\n    }\n\n    #endregion\n\n    #region convert\n\n    /// <summary>\n    /// List of setCkHeader —> list of ckItem\n    /// </summary>\n    /// <param name=\"setCookieList\"></param>\n    /// <returns></returns>\n    private static List<string> ConvertSetCkHeadersToCkItemList(IEnumerable<string> setCookieList)\n    {\n        return setCookieList\n            .Select(item => item.Split(';').FirstOrDefault()?.Trim() ?? \"\")\n            .Where(x => !string.IsNullOrWhiteSpace(x))\n            .ToList();\n    }\n\n    /// <summary>\n    /// List of setCkHeader —> ckStr\n    /// </summary>\n    /// <param name=\"setCookieList\"></param>\n    /// <returns></returns>\n    public static string ConvertSetCkHeadersToCkStr(IEnumerable<string> setCookieList)\n    {\n        var ckItemList = ConvertSetCkHeadersToCkItemList(setCookieList);\n        return ConvertCkItemListToCkStr(ckItemList);\n    }\n\n    /// <summary>\n    /// ckStr—>List of ckItem\n    /// </summary>\n    /// <param name=\"ckStr\"></param>\n    /// <returns></returns>\n    private static List<string> ConvertCkStrToCkItemList(string ckStr)\n    {\n        return ckStr.Split(\";\", StringSplitOptions.TrimEntries).ToList();\n    }\n\n    /// <summary>\n    /// List of ckItem —> ckStr\n    /// </summary>\n    /// <param name=\"ckItemList\"></param>\n    /// <returns></returns>\n    private static string ConvertCkItemListToCkStr(IEnumerable<string> ckItemList)\n    {\n        return string.Join(\"; \", ckItemList);\n    }\n\n    /// <summary>\n    /// List of ckItem —> Dictionary\n    /// </summary>\n    /// <param name=\"ckItemList\"></param>\n    /// <returns></returns>\n    private static Dictionary<string, string> ConvertCkItemListToCkDic(\n        IEnumerable<string> ckItemList\n    )\n    {\n        return ckItemList.ToDictionary(\n            k => k[..k.IndexOf(\"=\", StringComparison.Ordinal)].Trim(),\n            v => v[(v.IndexOf(\"=\", StringComparison.Ordinal) + 1)..].Trim().TrimEnd(';')\n        );\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Cookie/CookieStrFactory.cs",
    "content": "﻿using Microsoft.Extensions.Configuration;\nusing Ray.BiliBiliTool.Infrastructure.Extensions;\n\nnamespace Ray.BiliBiliTool.Infrastructure.Cookie;\n\npublic class CookieStrFactory<TCookieInfo>(IConfiguration configuration)\n    where TCookieInfo : CookieInfo\n{\n    private Dictionary<int, Dictionary<string, string>> CookieDictionary => GetCookieDictionary();\n\n    public int Count => CookieDictionary.Count;\n\n    public TCookieInfo GetCookie(int index)\n    {\n        var dic = GetCookieDictionary()[index];\n        return (TCookieInfo)Activator.CreateInstance(typeof(TCookieInfo), dic)!\n            ?? throw new InvalidOperationException();\n    }\n\n    public static TCookieInfo CreateNew(string cookie)\n    {\n        Dictionary<string, string> dic = CkStrToDictionary(cookie);\n        return (TCookieInfo)Activator.CreateInstance(typeof(TCookieInfo), dic)!\n            ?? throw new InvalidOperationException();\n    }\n\n    #region private\n\n    private Dictionary<int, Dictionary<string, string>> GetCookieDictionary()\n    {\n        var list = configuration.GetSection(\"BiliBiliCookies\").Get<List<string>>() ?? [];\n        return CookeStrListToCookieDic(list);\n    }\n\n    private Dictionary<int, Dictionary<string, string>> CookeStrListToCookieDic(List<string> ckList)\n    {\n        var dic = new Dictionary<int, Dictionary<string, string>>();\n        ckList ??= [];\n\n        for (int i = 0; i < ckList?.Count; i++)\n        {\n            dic.Add(i, CkStrToDictionary(ckList[i]));\n        }\n\n        return dic;\n    }\n\n    private static Dictionary<string, string> CkStrToDictionary(string ckStr)\n    {\n        var dic = new Dictionary<string, string>();\n        var ckItemList = ckStr.Split(\";\", StringSplitOptions.TrimEntries).Distinct();\n        foreach (var item in ckItemList)\n        {\n            var key = item[..item.IndexOf(\"=\", StringComparison.Ordinal)].Trim();\n            var value = item[(item.IndexOf(\"=\", StringComparison.Ordinal) + 1)..].Trim();\n            dic.AddIfNotExist(new KeyValuePair<string, string>(key, value), p => p.Key == key);\n        }\n        return dic;\n    }\n\n    private string DictionaryToCkStr(Dictionary<string, string> dic)\n    {\n        var list = dic.Select(item => $\"{item.Key}={item.Value}\").ToList();\n        return string.Join(\"; \", list);\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Enums/PlatformType.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Infrastructure.Enums;\n\npublic enum PlatformType\n{\n    Unknown,\n    GitHubActions,\n    Docker,\n    QingLong,\n    Web,\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Extensions/ICollectionExtensions.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Infrastructure.Extensions;\n\npublic static class ICollectionExtensions\n{\n    public static void AddIfNotExist<T>(this ICollection<T> source, T add)\n    {\n        if (!source.Any(x => x != null && x.Equals(add)))\n            source.Add(add);\n    }\n\n    public static void AddIfNotExist<T>(this ICollection<T> source, T add, Func<T, bool> exist)\n    {\n        if (!source.Any(exist))\n            source.Add(add);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Extensions/KeyValuePairExtensions.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Infrastructure.Extensions;\n\npublic static class KeyValuePairExtensions\n{\n    /// <summary>\n    /// 获得一个新的<see cref=\"KeyValuePair{TNewKey, TNewValue}\"/>\n    /// </summary>\n    /// <typeparam name=\"TKey\">旧Key类型</typeparam>\n    /// <typeparam name=\"TValue\">旧Value类型</typeparam>\n    /// <typeparam name=\"TNewKey\">新Key类型</typeparam>\n    /// <typeparam name=\"TNewValue\">新Value类型</typeparam>\n    /// <param name=\"oldKv\"></param>\n    /// <param name=\"key\">设置新Key的委托</param>\n    /// <param name=\"value\">设置新Value的委托</param>\n    /// <returns></returns>\n    public static KeyValuePair<TNewKey, TNewValue> New<TKey, TValue, TNewKey, TNewValue>(\n        this KeyValuePair<TKey, TValue> oldKv,\n        Func<TKey, TNewKey> key,\n        Func<TValue, TNewValue> value\n    )\n    {\n        return KeyValuePair.Create(key(oldKv.Key), value(oldKv.Value));\n    }\n\n    /// <summary>\n    /// 获得一个新的<see cref=\"KeyValuePair{TNewKey, TValue}\"/>\n    /// </summary>\n    /// <typeparam name=\"TKey\">旧Key类型</typeparam>\n    /// <typeparam name=\"TValue\">旧Value类型</typeparam>\n    /// <typeparam name=\"TNewKey\">新Key类型</typeparam>\n    /// <param name=\"oldKv\"></param>\n    /// <param name=\"key\">设置新Key的委托</param>\n    /// <returns></returns>\n    public static KeyValuePair<TNewKey, TValue> NewKey<TKey, TValue, TNewKey>(\n        this KeyValuePair<TKey, TValue> oldKv,\n        Func<TKey, TNewKey> key\n    )\n    {\n        return KeyValuePairExtensions.New(oldKv, key, t => t);\n    }\n\n    /// <summary>\n    /// 获得一个新的<see cref=\"KeyValuePair{TNewKey, TValue}\"/>\n    /// </summary>\n    /// <typeparam name=\"TKey\">旧Key类型</typeparam>\n    /// <typeparam name=\"TValue\">旧Value类型</typeparam>\n    /// <typeparam name=\"TNewKey\">新Key类型</typeparam>\n    /// <param name=\"oldKv\"></param>\n    /// <param name=\"key\">新Key</param>\n    /// <returns></returns>\n    public static KeyValuePair<TNewKey, TValue> NewKey<TKey, TValue, TNewKey>(\n        this KeyValuePair<TKey, TValue> oldKv,\n        TNewKey key\n    )\n    {\n        return KeyValuePairExtensions.New(oldKv, t => key, t => t);\n    }\n\n    /// <summary>\n    /// 获得一个新的<see cref=\"KeyValuePair{TKey, TNewValue}\"/>\n    /// </summary>\n    /// <typeparam name=\"TKey\">旧Key类型</typeparam>\n    /// <typeparam name=\"TValue\">旧Value类型</typeparam>\n    /// <typeparam name=\"TNewValue\">新Value类型</typeparam>\n    /// <param name=\"oldKv\"></param>\n    /// <param name=\"value\">设置新Value的委托</param>\n    /// <returns></returns>\n    public static KeyValuePair<TKey, TNewValue> NewValue<TKey, TValue, TNewValue>(\n        this KeyValuePair<TKey, TValue> oldKv,\n        Func<TValue, TNewValue> value\n    )\n    {\n        return KeyValuePairExtensions.New(oldKv, t => t, value);\n    }\n\n    /// <summary>\n    /// 获得一个新的<see cref=\"KeyValuePair{TKey, TNewValue}\"/>\n    /// </summary>\n    /// <typeparam name=\"TKey\">旧Key类型</typeparam>\n    /// <typeparam name=\"TValue\">旧Value类型</typeparam>\n    /// <typeparam name=\"TNewValue\">新Value类型</typeparam>\n    /// <param name=\"oldKv\"></param>\n    /// <param name=\"value\">新Value/param>\n    /// <returns></returns>\n    public static KeyValuePair<TKey, TNewValue> NewValue<TKey, TValue, TNewValue>(\n        this KeyValuePair<TKey, TValue> oldKv,\n        TNewValue value\n    )\n    {\n        return KeyValuePairExtensions.New(oldKv, t => t, t => value);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Extensions/TypeExtensions.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace Ray.BiliBiliTool.Infrastructure.Extensions;\n\npublic static class TypeExtensions\n{\n    /// <summary>\n    /// 获取属性的Description\n    /// </summary>\n    /// <param name=\"type\"></param>\n    /// <param name=\"propertyName\"></param>\n    /// <returns></returns>\n    public static string GetPropertyDescription(this Type type, string propertyName)\n    {\n        var desc = (DescriptionAttribute?)\n            type.GetProperty(propertyName)\n                ?.GetCustomAttributes(typeof(DescriptionAttribute), false)\n                .FirstOrDefault();\n\n        return desc?.Description ?? \"\";\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Global.cs",
    "content": "﻿using Microsoft.Extensions.Configuration;\n\nnamespace Ray.BiliBiliTool.Infrastructure;\n\npublic class Global\n{\n    /// <summary>\n    /// 根配置\n    /// </summary>\n    public static IConfigurationRoot? ConfigurationRoot { get; set; }\n\n    /// <summary>\n    /// 根容器\n    /// </summary>\n    public static IServiceProvider? ServiceProviderRoot { get; set; }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Helpers/IpHelper.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Infrastructure.Helpers;\n\npublic class IpHelper\n{\n    public static string? GetIp()\n    {\n        try\n        {\n            var re = new HttpClient().GetAsync(\"http://api.ipify.org/\").Result;\n            return re.IsSuccessStatusCode ? re.Content.ReadAsStringAsync().Result : null;\n        }\n        catch (Exception)\n        {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Helpers/ObjectHelper.cs",
    "content": "﻿using System.Reflection;\n\nnamespace Ray.BiliBiliTool.Infrastructure.Helpers;\n\npublic static class ObjectHelper\n{\n    public static Dictionary<string, object?> ObjectToDictionary(object obj)\n    {\n        // 获取对象的所有属性\n        PropertyInfo[] properties = obj.GetType().GetProperties();\n\n        // 遍历所有属性并将其添加到字典中\n        return properties.ToDictionary(\n            property => property.Name,\n            property => property.GetValue(obj)\n        );\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Helpers/PasswordHelper.cs",
    "content": "using System.Security.Cryptography;\n\nnamespace Ray.BiliBiliTool.Infrastructure.Helpers;\n\npublic class PasswordHelper\n{\n    public static (string hash, string salt) HashPassword(string password)\n    {\n        byte[] saltBytes = RandomNumberGenerator.GetBytes(16);\n        string salt = Convert.ToBase64String(saltBytes);\n        string hash = ComputeHash(password, salt);\n        return (hash, salt);\n    }\n\n    public static bool VerifyPassword(string password, string salt, string hash)\n    {\n        string computedHash = ComputeHash(password, salt);\n        return computedHash == hash;\n    }\n\n    private static string ComputeHash(string password, string salt)\n    {\n        byte[] saltBytes = Convert.FromBase64String(salt);\n        using var pbkdf2 = new Rfc2898DeriveBytes(\n            password,\n            saltBytes,\n            100_000,\n            HashAlgorithmName.SHA256\n        );\n        byte[] hashBytes = pbkdf2.GetBytes(32);\n\n        return Convert.ToBase64String(hashBytes);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Helpers/RandomHelper.cs",
    "content": "﻿namespace Ray.BiliBiliTool.Infrastructure.Helpers;\n\npublic class RandomHelper\n{\n    private int rep = 0;\n\n    public string GenerateCode(int codeCount)\n    {\n        string str = string.Empty;\n        long num2 = DateTime.Now.Ticks + this.rep;\n        rep++;\n        Random random = new Random((int)((ulong)num2 & 0xffffffL) | (int)(num2 >> this.rep));\n        for (int i = 0; i < codeCount; i++)\n        {\n            char ch;\n            int num = random.Next();\n            if ((num % 2) == 0)\n            {\n                ch = (char)(0x30 + ((ushort)(num % 10)));\n            }\n            else\n            {\n                ch = (char)(0x41 + ((ushort)(num % 0x1a)));\n            }\n\n            str += ch.ToString();\n        }\n\n        return str;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Helpers/RegexHelper.cs",
    "content": "using System.Text.RegularExpressions;\n\nnamespace Ray.BiliBiliTool.Infrastructure.Helpers;\n\npublic class RegexHelper\n{\n    public static string QuerySingle(string source, string pattern)\n    {\n        Regex rg = new Regex(pattern, RegexOptions.Multiline | RegexOptions.Singleline);\n        return rg.Match(source).Value;\n    }\n\n    public static List<string> QueryMultiple(string source, string pattern)\n    {\n        Regex rg = new Regex(pattern, RegexOptions.Multiline | RegexOptions.Singleline);\n        //Regex rg = new Regex(pattern, RegexOptions.Singleline);\n\n        MatchCollection matches = rg.Matches(source);\n\n        List<string> resList = new List<string>();\n\n        foreach (Match item in matches)\n            resList.Add(item.Value);\n\n        return resList;\n    }\n\n    /// <summary>\n    /// 截取字符串中开始和结束字符串中间的字符串\n    /// </summary>\n    /// <param name=\"source\">源字符串</param>\n    /// <param name=\"startStr\">开始字符串</param>\n    /// <param name=\"endStr\">结束字符串</param>\n    /// <returns>中间字符串</returns>\n    public static string SubstringSingle(string source, string startStr, string endStr)\n    {\n        var regexStr = $\"(?<=({startStr}))[.\\\\s\\\\S]*?(?=({endStr}))\";\n        Regex rg = new Regex(regexStr, RegexOptions.Multiline | RegexOptions.Singleline);\n        return rg.Match(source).Value;\n    }\n\n    /// <summary>\n    /// （批量）截取字符串中开始和结束字符串中间的字符串\n    /// </summary>\n    /// <param name=\"source\">源字符串</param>\n    /// <param name=\"startStr\">开始字符串</param>\n    /// <param name=\"endStr\">结束字符串</param>\n    /// <returns>中间字符串</returns>\n    public static List<string> SubstringMultiple(string source, string startStr, string endStr)\n    {\n        var regexStr = $\"(?<=({startStr}))[.\\\\s\\\\S]*?(?=({endStr}))\";\n        // Regex rg = new Regex(regexStr, RegexOptions.Multiline | RegexOptions.Singleline);\n        Regex rg = new Regex(regexStr, RegexOptions.Singleline);\n\n        MatchCollection matches = rg.Matches(source);\n\n        List<string> resList = new List<string>();\n\n        foreach (Match item in matches)\n            resList.Add(item.Value);\n\n        return resList;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Helpers/ZipHelper.cs",
    "content": "﻿using System.IO.Compression;\nusing System.Text;\n\nnamespace Ray.BiliBiliTool.Infrastructure.Helpers;\n\n/// <summary>\n/// 解压缩Helper\n/// </summary>\npublic class ZipHelper\n{\n    /// <summary>\n    /// 将Gzip的byte数组读取为字符串\n    /// </summary>\n    /// <param name=\"bytes\"></param>\n    /// <param name=\"encoding\"></param>\n    /// <returns></returns>\n    public static string ReadGzip(byte[] bytes, string encoding = \"UTF-8\")\n    {\n        string result = string.Empty;\n        using (MemoryStream ms = new MemoryStream(bytes))\n        {\n            using (GZipStream decompressedStream = new GZipStream(ms, CompressionMode.Decompress))\n            {\n                using (\n                    StreamReader sr = new StreamReader(\n                        decompressedStream,\n                        Encoding.GetEncoding(encoding)\n                    )\n                )\n                {\n                    result = sr.ReadToEnd();\n                }\n            }\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/JsonSerializerOptionsBuilder.cs",
    "content": "﻿using System.Text.Json;\n\nnamespace Ray.BiliBiliTool.Infrastructure;\n\n/// <summary>\n/// System.Text.Json的序列化OptionsBuilder\n/// </summary>\npublic sealed class JsonSerializerOptionsBuilder\n{\n    static JsonSerializerOptionsBuilder()\n    {\n        DefaultOptions = Create().GetOrBuildDefaultOptions();\n    }\n\n    /// <summary>\n    /// 默认配置\n    /// </summary>\n    public static readonly JsonSerializerOptions DefaultOptions;\n\n    public List<Action<JsonSerializerOptions>> BuildActionList { get; }\n\n    private JsonSerializerOptionsBuilder()\n    {\n        BuildActionList = new List<Action<JsonSerializerOptions>>();\n    }\n\n    public static JsonSerializerOptionsBuilder Create()\n    {\n        return new JsonSerializerOptionsBuilder();\n    }\n\n    public JsonSerializerOptions Build()\n    {\n        JsonSerializerOptions options = new(); //这里没有使用 JsonSerializerDefaults.General 避免后续版本更新后设置改变\n\n        foreach (Action<JsonSerializerOptions> item in BuildActionList)\n        {\n            item?.Invoke(options);\n        }\n\n        return options;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/JsonSerializerOptionsBuilderExtensions.cs",
    "content": "﻿using System.Text.Encodings.Web;\nusing System.Text.Json;\nusing System.Text.Unicode;\n\nnamespace Ray.BiliBiliTool.Infrastructure;\n\npublic static class JsonSerializerOptionsBuilderExtensions\n{\n    private static JsonSerializerOptionsBuilder SetActionBase(\n        JsonSerializerOptionsBuilder builder,\n        Action<JsonSerializerOptions> action\n    )\n    {\n        builder.BuildActionList.Add(action);\n        return builder;\n    }\n\n    #region 设置区\n\n    public static JsonSerializerOptionsBuilder SetCamelCase(\n        this JsonSerializerOptionsBuilder builder\n    )\n    {\n        return SetActionBase(builder, t => t.PropertyNamingPolicy = JsonNamingPolicy.CamelCase);\n    }\n\n    public static JsonSerializerOptionsBuilder SetEncoderToUnicodeRangeAll(\n        this JsonSerializerOptionsBuilder builder\n    )\n    {\n        return SetActionBase(builder, t => t.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All));\n    }\n\n    public static JsonSerializerOptionsBuilder Configure(\n        this JsonSerializerOptionsBuilder builder,\n        Action<JsonSerializerOptions> action\n    )\n    {\n        return SetActionBase(builder, action);\n    }\n\n    #endregion 设置区\n\n    private static JsonSerializerOptions? _defaultOptions;\n\n    private static JsonSerializerOptions BuildAndSaveToDefault(\n        this JsonSerializerOptionsBuilder builder\n    )\n    {\n        JsonSerializerOptions option = builder.Build();\n        _defaultOptions = option;\n        return option;\n    }\n\n    public static JsonSerializerOptions GetOrBuildDefaultOptions(\n        this JsonSerializerOptionsBuilder builder\n    )\n    {\n        return _defaultOptions == null\n            ? builder.SetCamelCase().SetEncoderToUnicodeRangeAll().BuildAndSaveToDefault()\n            : _defaultOptions!;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure/Ray.BiliBiliTool.Infrastructure.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Extensions.Configuration\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.Binder\" />\n    <PackageReference Include=\"Ray.Infrastructure\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/BiliDbContext.cs",
    "content": "using AppAny.Quartz.EntityFrameworkCore.Migrations;\nusing AppAny.Quartz.EntityFrameworkCore.Migrations.SQLite;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing Microsoft.Extensions.Configuration;\nusing Ray.BiliBiliTool.Domain;\n\nnamespace Ray.BiliBiliTool.Infrastructure.EF;\n\npublic class BiliDbContext(IConfiguration config) : DbContext\n{\n    public DbSet<ExecutionLog> ExecutionLogs { get; set; }\n    public DbSet<BiliLogs> BiliLogs { get; set; }\n    public DbSet<User> Users { get; set; }\n\n    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)\n    {\n        optionsBuilder.UseSqlite(config.GetConnectionString(\"Sqlite\"));\n        base.OnConfiguring(optionsBuilder);\n    }\n\n    protected override void OnModelCreating(ModelBuilder modelBuilder)\n    {\n        base.OnModelCreating(modelBuilder);\n\n        modelBuilder.AddQuartz(builder => builder.UseSqlite(\"QRTZ_\"));\n\n        AddSqliteDateTimeOffsetSupport(modelBuilder);\n\n        modelBuilder\n            .Entity<ExecutionLog>()\n            .OwnsOne(\n                l => l.ExecutionLogDetail,\n                e =>\n                {\n                    e.ToTable(\"bili_execution_log_details\");\n                    e.WithOwner().HasForeignKey(\"LogId\");\n                }\n            );\n\n        modelBuilder.Entity<ExecutionLog>().HasIndex(l => l.RunInstanceId).IsUnique();\n\n        // for housekeeping or system log display\n        modelBuilder.Entity<ExecutionLog>().HasIndex(l => new { l.DateAddedUtc, l.LogType });\n\n        // joining with job\n        modelBuilder\n            .Entity<ExecutionLog>()\n            .HasIndex(l => new\n            {\n                l.TriggerName,\n                l.TriggerGroup,\n                l.JobName,\n                l.JobGroup,\n                l.DateAddedUtc,\n            });\n\n        modelBuilder.Entity<ExecutionLog>().Property(e => e.LogType).HasConversion<string>();\n\n        modelBuilder.Entity<BiliLogs>(entity =>\n        {\n            entity\n                .Property<string?>(x => x.FireInstanceIdComputed) // 定义一个影子属性\n                .HasComputedColumnSql(\n                    \"json_extract(Properties, '$.FireInstanceId')\",\n                    stored: false\n                ); // stored: true 表示持久化存储，利于索引；false (或省略) 为虚拟列\n\n            entity\n                .HasIndex(x => x.FireInstanceIdComputed) // 在计算列上创建索引\n                .HasDatabaseName(\"IX_Logs_FireInstanceIdComputed\");\n        });\n\n        modelBuilder.Entity<User>(entity =>\n        {\n            entity.HasKey(e => e.Id);\n            entity.Property(e => e.Username).IsRequired().HasMaxLength(50);\n            entity.Property(e => e.PasswordHash).IsRequired();\n            entity.Property(e => e.Salt).IsRequired();\n            entity\n                .Property(e => e.Roles)\n                .HasConversion(\n                    v => string.Join(',', v),\n                    v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()\n                );\n\n            entity.HasIndex(e => e.Username).IsUnique();\n        });\n    }\n\n    private void AddSqliteDateTimeOffsetSupport(ModelBuilder builder)\n    {\n        if (Database.ProviderName == \"Microsoft.EntityFrameworkCore.Sqlite\")\n        {\n            // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations\n            // here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations\n            // To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset\n            // use the DateTimeOffsetToBinaryConverter\n            // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754\n            // This only supports millisecond precision, but should be sufficient for most use cases.\n            foreach (var entityType in builder.Model.GetEntityTypes())\n            {\n                var properties = entityType\n                    .ClrType.GetProperties()\n                    .Where(p =>\n                        p.PropertyType == typeof(DateTimeOffset)\n                        || p.PropertyType == typeof(DateTimeOffset?)\n                    );\n                foreach (var property in properties)\n                {\n                    builder\n                        .Entity(entityType.Name)\n                        .Property(property.Name)\n                        .HasConversion(new DateTimeOffsetToBinaryConverter());\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/DbInitializer.cs",
    "content": "using Microsoft.EntityFrameworkCore;\nusing Ray.BiliBiliTool.Domain;\nusing Ray.BiliBiliTool.Infrastructure.Helpers;\n\nnamespace Ray.BiliBiliTool.Infrastructure.EF;\n\npublic class DbInitializer(BiliDbContext context)\n{\n    private const string DefaultUserName = \"admin\";\n    private const string DefaultPassword = \"BiliTool@2233\";\n\n    public async Task InitializeAsync()\n    {\n        await context.Database.MigrateAsync();\n\n        await InitUserAsync();\n    }\n\n    private async Task InitUserAsync()\n    {\n        if (await context.Users.AnyAsync())\n        {\n            return;\n        }\n\n        var (hash, salt) = PasswordHelper.HashPassword(DefaultPassword);\n        var adminUser = new User\n        {\n            Username = DefaultUserName,\n            PasswordHash = hash,\n            Salt = salt,\n            Roles = [\"Administrator\"],\n        };\n\n        context.Users.Add(adminUser);\n        await context.SaveChangesAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/Extensions/ServiceCollectionExtension.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\n\nnamespace Ray.BiliBiliTool.Infrastructure.EF.Extensions;\n\npublic static class ServiceCollectionExtension\n{\n    public static IServiceCollection AddEF(this IServiceCollection services)\n    {\n        services.AddDbContextFactory<BiliDbContext>();\n        services.AddScoped<DbInitializer>();\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503105406_InitQuartz.Designer.cs",
    "content": "﻿// <auto-generated />\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing Ray.BiliBiliTool.Web;\nusing Ray.BiliBiliTool.Domain;\nusing Ray.BiliBiliTool.Infrastructure.EF;\n\n#nullable disable\n\nnamespace Ray.BiliBiliTool.Web.Migrations\n{\n    [DbContext(typeof(BiliDbContext))]\n    [Migration(\"20250503105406_InitQuartz\")]\n    partial class InitQuartz\n    {\n        /// <inheritdoc />\n        protected override void BuildTargetModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder.HasAnnotation(\"ProductVersion\", \"9.0.4\");\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<byte[]>(\"BlobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"BLOB_DATA\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_BLOB_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"CalendarName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CALENDAR_NAME\");\n\n                    b.Property<byte[]>(\"Calendar\")\n                        .IsRequired()\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"CALENDAR\");\n\n                    b.HasKey(\"SchedulerName\", \"CalendarName\");\n\n                    b.ToTable(\"QRTZ_CALENDARS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"CronExpression\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CRON_EXPRESSION\");\n\n                    b.Property<string>(\"TimeZoneId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TIME_ZONE_ID\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_CRON_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"EntryId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"ENTRY_ID\");\n\n                    b.Property<long>(\"FiredTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"FIRED_TIME\");\n\n                    b.Property<string>(\"InstanceName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"INSTANCE_NAME\");\n\n                    b.Property<bool>(\"IsNonConcurrent\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_NONCONCURRENT\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<int>(\"Priority\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PRIORITY\");\n\n                    b.Property<bool?>(\"RequestsRecovery\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"REQUESTS_RECOVERY\");\n\n                    b.Property<long>(\"ScheduledTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"SCHED_TIME\");\n\n                    b.Property<string>(\"State\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STATE\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.HasKey(\"SchedulerName\", \"EntryId\");\n\n                    b.HasIndex(\"InstanceName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_INST_NAME\");\n\n                    b.HasIndex(\"JobGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_GROUP\");\n\n                    b.HasIndex(\"JobName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_NAME\");\n\n                    b.HasIndex(\"RequestsRecovery\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_REQ_RECOVERY\");\n\n                    b.HasIndex(\"TriggerGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_GROUP\");\n\n                    b.HasIndex(\"TriggerName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_NAME\");\n\n                    b.HasIndex(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_NM_GP\");\n\n                    b.ToTable(\"QRTZ_FIRED_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"DESCRIPTION\");\n\n                    b.Property<bool>(\"IsDurable\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_DURABLE\");\n\n                    b.Property<bool>(\"IsNonConcurrent\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_NONCONCURRENT\");\n\n                    b.Property<bool>(\"IsUpdateData\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_UPDATE_DATA\");\n\n                    b.Property<string>(\"JobClassName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_CLASS_NAME\");\n\n                    b.Property<byte[]>(\"JobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"JOB_DATA\");\n\n                    b.Property<bool>(\"RequestsRecovery\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"REQUESTS_RECOVERY\");\n\n                    b.HasKey(\"SchedulerName\", \"JobName\", \"JobGroup\");\n\n                    b.HasIndex(\"RequestsRecovery\")\n                        .HasDatabaseName(\"IDX_QRTZ_J_REQ_RECOVERY\");\n\n                    b.ToTable(\"QRTZ_JOB_DETAILS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"LockName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"LOCK_NAME\");\n\n                    b.HasKey(\"SchedulerName\", \"LockName\");\n\n                    b.ToTable(\"QRTZ_LOCKS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_PAUSED_TRIGGER_GRPS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"InstanceName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"INSTANCE_NAME\");\n\n                    b.Property<long>(\"CheckInInterval\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"CHECKIN_INTERVAL\");\n\n                    b.Property<long>(\"LastCheckInTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LAST_CHECKIN_TIME\");\n\n                    b.HasKey(\"SchedulerName\", \"InstanceName\");\n\n                    b.ToTable(\"QRTZ_SCHEDULER_STATE\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<bool?>(\"BooleanProperty1\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"BOOL_PROP_1\");\n\n                    b.Property<bool?>(\"BooleanProperty2\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"BOOL_PROP_2\");\n\n                    b.Property<decimal?>(\"DecimalProperty1\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"DEC_PROP_1\");\n\n                    b.Property<decimal?>(\"DecimalProperty2\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"DEC_PROP_2\");\n\n                    b.Property<int?>(\"IntegerProperty1\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"INT_PROP_1\");\n\n                    b.Property<int?>(\"IntegerProperty2\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"INT_PROP_2\");\n\n                    b.Property<long?>(\"LongProperty1\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LONG_PROP_1\");\n\n                    b.Property<long?>(\"LongProperty2\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LONG_PROP_2\");\n\n                    b.Property<string>(\"StringProperty1\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_1\");\n\n                    b.Property<string>(\"StringProperty2\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_2\");\n\n                    b.Property<string>(\"StringProperty3\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_3\");\n\n                    b.Property<string>(\"TimeZoneId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TIME_ZONE_ID\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_SIMPROP_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<long>(\"RepeatCount\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"REPEAT_COUNT\");\n\n                    b.Property<long>(\"RepeatInterval\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"REPEAT_INTERVAL\");\n\n                    b.Property<long>(\"TimesTriggered\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"TIMES_TRIGGERED\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_SIMPLE_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"CalendarName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CALENDAR_NAME\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"DESCRIPTION\");\n\n                    b.Property<long?>(\"EndTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"END_TIME\");\n\n                    b.Property<byte[]>(\"JobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"JOB_DATA\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"JobName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<short?>(\"MisfireInstruction\")\n                        .HasColumnType(\"smallint\")\n                        .HasColumnName(\"MISFIRE_INSTR\");\n\n                    b.Property<long?>(\"NextFireTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"NEXT_FIRE_TIME\");\n\n                    b.Property<long?>(\"PreviousFireTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"PREV_FIRE_TIME\");\n\n                    b.Property<int?>(\"Priority\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PRIORITY\");\n\n                    b.Property<long>(\"StartTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"START_TIME\");\n\n                    b.Property<string>(\"TriggerState\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_STATE\");\n\n                    b.Property<string>(\"TriggerType\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_TYPE\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.HasIndex(\"NextFireTime\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_NEXT_FIRE_TIME\");\n\n                    b.HasIndex(\"TriggerState\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_STATE\");\n\n                    b.HasIndex(\"NextFireTime\", \"TriggerState\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_NFT_ST\");\n\n                    b.HasIndex(\"SchedulerName\", \"JobName\", \"JobGroup\");\n\n                    b.ToTable(\"QRTZ_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"BlobTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"CronTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"SimplePropertyTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"SimpleTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", \"JobDetail\")\n                        .WithMany(\"Triggers\")\n                        .HasForeignKey(\"SchedulerName\", \"JobName\", \"JobGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"JobDetail\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", b =>\n                {\n                    b.Navigation(\"Triggers\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.Navigation(\"BlobTriggers\");\n\n                    b.Navigation(\"CronTriggers\");\n\n                    b.Navigation(\"SimplePropertyTriggers\");\n\n                    b.Navigation(\"SimpleTriggers\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503105406_InitQuartz.cs",
    "content": "﻿using Microsoft.EntityFrameworkCore.Migrations;\n\n#nullable disable\n\nnamespace Ray.BiliBiliTool.Web.Migrations\n{\n    /// <inheritdoc />\n    public partial class InitQuartz : Migration\n    {\n        /// <inheritdoc />\n        protected override void Up(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.CreateTable(\n                name: \"QRTZ_CALENDARS\",\n                columns: table => new\n                {\n                    SCHED_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    CALENDAR_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    CALENDAR = table.Column<byte[]>(type: \"bytea\", nullable: false),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\n                        \"PK_QRTZ_CALENDARS\",\n                        x => new { x.SCHED_NAME, x.CALENDAR_NAME }\n                    );\n                }\n            );\n\n            migrationBuilder.CreateTable(\n                name: \"QRTZ_FIRED_TRIGGERS\",\n                columns: table => new\n                {\n                    SCHED_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    ENTRY_ID = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_GROUP = table.Column<string>(type: \"text\", nullable: false),\n                    INSTANCE_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    FIRED_TIME = table.Column<long>(type: \"bigint\", nullable: false),\n                    SCHED_TIME = table.Column<long>(type: \"bigint\", nullable: false),\n                    PRIORITY = table.Column<int>(type: \"integer\", nullable: false),\n                    STATE = table.Column<string>(type: \"text\", nullable: false),\n                    JOB_NAME = table.Column<string>(type: \"text\", nullable: true),\n                    JOB_GROUP = table.Column<string>(type: \"text\", nullable: true),\n                    IS_NONCONCURRENT = table.Column<bool>(type: \"bool\", nullable: false),\n                    REQUESTS_RECOVERY = table.Column<bool>(type: \"bool\", nullable: true),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\n                        \"PK_QRTZ_FIRED_TRIGGERS\",\n                        x => new { x.SCHED_NAME, x.ENTRY_ID }\n                    );\n                }\n            );\n\n            migrationBuilder.CreateTable(\n                name: \"QRTZ_JOB_DETAILS\",\n                columns: table => new\n                {\n                    SCHED_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    JOB_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    JOB_GROUP = table.Column<string>(type: \"text\", nullable: false),\n                    DESCRIPTION = table.Column<string>(type: \"text\", nullable: true),\n                    JOB_CLASS_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    IS_DURABLE = table.Column<bool>(type: \"bool\", nullable: false),\n                    IS_NONCONCURRENT = table.Column<bool>(type: \"bool\", nullable: false),\n                    IS_UPDATE_DATA = table.Column<bool>(type: \"bool\", nullable: false),\n                    REQUESTS_RECOVERY = table.Column<bool>(type: \"bool\", nullable: false),\n                    JOB_DATA = table.Column<byte[]>(type: \"bytea\", nullable: true),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\n                        \"PK_QRTZ_JOB_DETAILS\",\n                        x => new\n                        {\n                            x.SCHED_NAME,\n                            x.JOB_NAME,\n                            x.JOB_GROUP,\n                        }\n                    );\n                }\n            );\n\n            migrationBuilder.CreateTable(\n                name: \"QRTZ_LOCKS\",\n                columns: table => new\n                {\n                    SCHED_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    LOCK_NAME = table.Column<string>(type: \"text\", nullable: false),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_QRTZ_LOCKS\", x => new { x.SCHED_NAME, x.LOCK_NAME });\n                }\n            );\n\n            migrationBuilder.CreateTable(\n                name: \"QRTZ_PAUSED_TRIGGER_GRPS\",\n                columns: table => new\n                {\n                    SCHED_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_GROUP = table.Column<string>(type: \"text\", nullable: false),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\n                        \"PK_QRTZ_PAUSED_TRIGGER_GRPS\",\n                        x => new { x.SCHED_NAME, x.TRIGGER_GROUP }\n                    );\n                }\n            );\n\n            migrationBuilder.CreateTable(\n                name: \"QRTZ_SCHEDULER_STATE\",\n                columns: table => new\n                {\n                    SCHED_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    INSTANCE_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    LAST_CHECKIN_TIME = table.Column<long>(type: \"bigint\", nullable: false),\n                    CHECKIN_INTERVAL = table.Column<long>(type: \"bigint\", nullable: false),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\n                        \"PK_QRTZ_SCHEDULER_STATE\",\n                        x => new { x.SCHED_NAME, x.INSTANCE_NAME }\n                    );\n                }\n            );\n\n            migrationBuilder.CreateTable(\n                name: \"QRTZ_TRIGGERS\",\n                columns: table => new\n                {\n                    SCHED_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_GROUP = table.Column<string>(type: \"text\", nullable: false),\n                    JOB_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    JOB_GROUP = table.Column<string>(type: \"text\", nullable: false),\n                    DESCRIPTION = table.Column<string>(type: \"text\", nullable: true),\n                    NEXT_FIRE_TIME = table.Column<long>(type: \"bigint\", nullable: true),\n                    PREV_FIRE_TIME = table.Column<long>(type: \"bigint\", nullable: true),\n                    PRIORITY = table.Column<int>(type: \"integer\", nullable: true),\n                    TRIGGER_STATE = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_TYPE = table.Column<string>(type: \"text\", nullable: false),\n                    START_TIME = table.Column<long>(type: \"bigint\", nullable: false),\n                    END_TIME = table.Column<long>(type: \"bigint\", nullable: true),\n                    CALENDAR_NAME = table.Column<string>(type: \"text\", nullable: true),\n                    MISFIRE_INSTR = table.Column<short>(type: \"smallint\", nullable: true),\n                    JOB_DATA = table.Column<byte[]>(type: \"bytea\", nullable: true),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\n                        \"PK_QRTZ_TRIGGERS\",\n                        x => new\n                        {\n                            x.SCHED_NAME,\n                            x.TRIGGER_NAME,\n                            x.TRIGGER_GROUP,\n                        }\n                    );\n                    table.ForeignKey(\n                        name: \"FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS_SCHED_NAME_JOB_NAME_JOB_GROUP\",\n                        columns: x => new\n                        {\n                            x.SCHED_NAME,\n                            x.JOB_NAME,\n                            x.JOB_GROUP,\n                        },\n                        principalTable: \"QRTZ_JOB_DETAILS\",\n                        principalColumns: new[] { \"SCHED_NAME\", \"JOB_NAME\", \"JOB_GROUP\" },\n                        onDelete: ReferentialAction.Cascade\n                    );\n                }\n            );\n\n            migrationBuilder.CreateTable(\n                name: \"QRTZ_BLOB_TRIGGERS\",\n                columns: table => new\n                {\n                    SCHED_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_GROUP = table.Column<string>(type: \"text\", nullable: false),\n                    BLOB_DATA = table.Column<byte[]>(type: \"bytea\", nullable: true),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\n                        \"PK_QRTZ_BLOB_TRIGGERS\",\n                        x => new\n                        {\n                            x.SCHED_NAME,\n                            x.TRIGGER_NAME,\n                            x.TRIGGER_GROUP,\n                        }\n                    );\n                    table.ForeignKey(\n                        name: \"FK_QRTZ_BLOB_TRIGGERS_QRTZ_TRIGGERS_SCHED_NAME_TRIGGER_NAME_TRIGGER_GROUP\",\n                        columns: x => new\n                        {\n                            x.SCHED_NAME,\n                            x.TRIGGER_NAME,\n                            x.TRIGGER_GROUP,\n                        },\n                        principalTable: \"QRTZ_TRIGGERS\",\n                        principalColumns: new[] { \"SCHED_NAME\", \"TRIGGER_NAME\", \"TRIGGER_GROUP\" },\n                        onDelete: ReferentialAction.Cascade\n                    );\n                }\n            );\n\n            migrationBuilder.CreateTable(\n                name: \"QRTZ_CRON_TRIGGERS\",\n                columns: table => new\n                {\n                    SCHED_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_GROUP = table.Column<string>(type: \"text\", nullable: false),\n                    CRON_EXPRESSION = table.Column<string>(type: \"text\", nullable: false),\n                    TIME_ZONE_ID = table.Column<string>(type: \"text\", nullable: true),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\n                        \"PK_QRTZ_CRON_TRIGGERS\",\n                        x => new\n                        {\n                            x.SCHED_NAME,\n                            x.TRIGGER_NAME,\n                            x.TRIGGER_GROUP,\n                        }\n                    );\n                    table.ForeignKey(\n                        name: \"FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS_SCHED_NAME_TRIGGER_NAME_TRIGGER_GROUP\",\n                        columns: x => new\n                        {\n                            x.SCHED_NAME,\n                            x.TRIGGER_NAME,\n                            x.TRIGGER_GROUP,\n                        },\n                        principalTable: \"QRTZ_TRIGGERS\",\n                        principalColumns: new[] { \"SCHED_NAME\", \"TRIGGER_NAME\", \"TRIGGER_GROUP\" },\n                        onDelete: ReferentialAction.Cascade\n                    );\n                }\n            );\n\n            migrationBuilder.CreateTable(\n                name: \"QRTZ_SIMPLE_TRIGGERS\",\n                columns: table => new\n                {\n                    SCHED_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_GROUP = table.Column<string>(type: \"text\", nullable: false),\n                    REPEAT_COUNT = table.Column<long>(type: \"bigint\", nullable: false),\n                    REPEAT_INTERVAL = table.Column<long>(type: \"bigint\", nullable: false),\n                    TIMES_TRIGGERED = table.Column<long>(type: \"bigint\", nullable: false),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\n                        \"PK_QRTZ_SIMPLE_TRIGGERS\",\n                        x => new\n                        {\n                            x.SCHED_NAME,\n                            x.TRIGGER_NAME,\n                            x.TRIGGER_GROUP,\n                        }\n                    );\n                    table.ForeignKey(\n                        name: \"FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS_SCHED_NAME_TRIGGER_NAME_TRIGGER_GROUP\",\n                        columns: x => new\n                        {\n                            x.SCHED_NAME,\n                            x.TRIGGER_NAME,\n                            x.TRIGGER_GROUP,\n                        },\n                        principalTable: \"QRTZ_TRIGGERS\",\n                        principalColumns: new[] { \"SCHED_NAME\", \"TRIGGER_NAME\", \"TRIGGER_GROUP\" },\n                        onDelete: ReferentialAction.Cascade\n                    );\n                }\n            );\n\n            migrationBuilder.CreateTable(\n                name: \"QRTZ_SIMPROP_TRIGGERS\",\n                columns: table => new\n                {\n                    SCHED_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_NAME = table.Column<string>(type: \"text\", nullable: false),\n                    TRIGGER_GROUP = table.Column<string>(type: \"text\", nullable: false),\n                    STR_PROP_1 = table.Column<string>(type: \"text\", nullable: true),\n                    STR_PROP_2 = table.Column<string>(type: \"text\", nullable: true),\n                    STR_PROP_3 = table.Column<string>(type: \"text\", nullable: true),\n                    INT_PROP_1 = table.Column<int>(type: \"integer\", nullable: true),\n                    INT_PROP_2 = table.Column<int>(type: \"integer\", nullable: true),\n                    LONG_PROP_1 = table.Column<long>(type: \"bigint\", nullable: true),\n                    LONG_PROP_2 = table.Column<long>(type: \"bigint\", nullable: true),\n                    DEC_PROP_1 = table.Column<decimal>(type: \"numeric\", nullable: true),\n                    DEC_PROP_2 = table.Column<decimal>(type: \"numeric\", nullable: true),\n                    BOOL_PROP_1 = table.Column<bool>(type: \"bool\", nullable: true),\n                    BOOL_PROP_2 = table.Column<bool>(type: \"bool\", nullable: true),\n                    TIME_ZONE_ID = table.Column<string>(type: \"text\", nullable: true),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\n                        \"PK_QRTZ_SIMPROP_TRIGGERS\",\n                        x => new\n                        {\n                            x.SCHED_NAME,\n                            x.TRIGGER_NAME,\n                            x.TRIGGER_GROUP,\n                        }\n                    );\n                    table.ForeignKey(\n                        name: \"FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS_SCHED_NAME_TRIGGER_NAME_TRIGGER_GROUP\",\n                        columns: x => new\n                        {\n                            x.SCHED_NAME,\n                            x.TRIGGER_NAME,\n                            x.TRIGGER_GROUP,\n                        },\n                        principalTable: \"QRTZ_TRIGGERS\",\n                        principalColumns: new[] { \"SCHED_NAME\", \"TRIGGER_NAME\", \"TRIGGER_GROUP\" },\n                        onDelete: ReferentialAction.Cascade\n                    );\n                }\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IDX_QRTZ_FT_JOB_GROUP\",\n                table: \"QRTZ_FIRED_TRIGGERS\",\n                column: \"JOB_GROUP\"\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IDX_QRTZ_FT_JOB_NAME\",\n                table: \"QRTZ_FIRED_TRIGGERS\",\n                column: \"JOB_NAME\"\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IDX_QRTZ_FT_JOB_REQ_RECOVERY\",\n                table: \"QRTZ_FIRED_TRIGGERS\",\n                column: \"REQUESTS_RECOVERY\"\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IDX_QRTZ_FT_TRIG_GROUP\",\n                table: \"QRTZ_FIRED_TRIGGERS\",\n                column: \"TRIGGER_GROUP\"\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IDX_QRTZ_FT_TRIG_INST_NAME\",\n                table: \"QRTZ_FIRED_TRIGGERS\",\n                column: \"INSTANCE_NAME\"\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IDX_QRTZ_FT_TRIG_NAME\",\n                table: \"QRTZ_FIRED_TRIGGERS\",\n                column: \"TRIGGER_NAME\"\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IDX_QRTZ_FT_TRIG_NM_GP\",\n                table: \"QRTZ_FIRED_TRIGGERS\",\n                columns: new[] { \"SCHED_NAME\", \"TRIGGER_NAME\", \"TRIGGER_GROUP\" }\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IDX_QRTZ_J_REQ_RECOVERY\",\n                table: \"QRTZ_JOB_DETAILS\",\n                column: \"REQUESTS_RECOVERY\"\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IDX_QRTZ_T_NEXT_FIRE_TIME\",\n                table: \"QRTZ_TRIGGERS\",\n                column: \"NEXT_FIRE_TIME\"\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IDX_QRTZ_T_NFT_ST\",\n                table: \"QRTZ_TRIGGERS\",\n                columns: new[] { \"NEXT_FIRE_TIME\", \"TRIGGER_STATE\" }\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IDX_QRTZ_T_STATE\",\n                table: \"QRTZ_TRIGGERS\",\n                column: \"TRIGGER_STATE\"\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_QRTZ_TRIGGERS_SCHED_NAME_JOB_NAME_JOB_GROUP\",\n                table: \"QRTZ_TRIGGERS\",\n                columns: new[] { \"SCHED_NAME\", \"JOB_NAME\", \"JOB_GROUP\" }\n            );\n        }\n\n        /// <inheritdoc />\n        protected override void Down(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.DropTable(name: \"QRTZ_BLOB_TRIGGERS\");\n\n            migrationBuilder.DropTable(name: \"QRTZ_CALENDARS\");\n\n            migrationBuilder.DropTable(name: \"QRTZ_CRON_TRIGGERS\");\n\n            migrationBuilder.DropTable(name: \"QRTZ_FIRED_TRIGGERS\");\n\n            migrationBuilder.DropTable(name: \"QRTZ_LOCKS\");\n\n            migrationBuilder.DropTable(name: \"QRTZ_PAUSED_TRIGGER_GRPS\");\n\n            migrationBuilder.DropTable(name: \"QRTZ_SCHEDULER_STATE\");\n\n            migrationBuilder.DropTable(name: \"QRTZ_SIMPLE_TRIGGERS\");\n\n            migrationBuilder.DropTable(name: \"QRTZ_SIMPROP_TRIGGERS\");\n\n            migrationBuilder.DropTable(name: \"QRTZ_TRIGGERS\");\n\n            migrationBuilder.DropTable(name: \"QRTZ_JOB_DETAILS\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503164108_Update.Designer.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing Ray.BiliBiliTool.Infrastructure.EF;\n\n#nullable disable\n\nnamespace Ray.BiliBiliTool.Web.Migrations\n{\n    [DbContext(typeof(BiliDbContext))]\n    [Migration(\"20250503164108_Update\")]\n    partial class Update\n    {\n        /// <inheritdoc />\n        protected override void BuildTargetModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder.HasAnnotation(\"ProductVersion\", \"8.0.3\");\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<byte[]>(\"BlobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"BLOB_DATA\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_BLOB_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"CalendarName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CALENDAR_NAME\");\n\n                    b.Property<byte[]>(\"Calendar\")\n                        .IsRequired()\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"CALENDAR\");\n\n                    b.HasKey(\"SchedulerName\", \"CalendarName\");\n\n                    b.ToTable(\"QRTZ_CALENDARS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"CronExpression\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CRON_EXPRESSION\");\n\n                    b.Property<string>(\"TimeZoneId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TIME_ZONE_ID\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_CRON_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"EntryId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"ENTRY_ID\");\n\n                    b.Property<long>(\"FiredTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"FIRED_TIME\");\n\n                    b.Property<string>(\"InstanceName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"INSTANCE_NAME\");\n\n                    b.Property<bool>(\"IsNonConcurrent\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_NONCONCURRENT\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<int>(\"Priority\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PRIORITY\");\n\n                    b.Property<bool?>(\"RequestsRecovery\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"REQUESTS_RECOVERY\");\n\n                    b.Property<long>(\"ScheduledTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"SCHED_TIME\");\n\n                    b.Property<string>(\"State\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STATE\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.HasKey(\"SchedulerName\", \"EntryId\");\n\n                    b.HasIndex(\"InstanceName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_INST_NAME\");\n\n                    b.HasIndex(\"JobGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_GROUP\");\n\n                    b.HasIndex(\"JobName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_NAME\");\n\n                    b.HasIndex(\"RequestsRecovery\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_REQ_RECOVERY\");\n\n                    b.HasIndex(\"TriggerGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_GROUP\");\n\n                    b.HasIndex(\"TriggerName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_NAME\");\n\n                    b.HasIndex(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_NM_GP\");\n\n                    b.ToTable(\"QRTZ_FIRED_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"DESCRIPTION\");\n\n                    b.Property<bool>(\"IsDurable\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_DURABLE\");\n\n                    b.Property<bool>(\"IsNonConcurrent\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_NONCONCURRENT\");\n\n                    b.Property<bool>(\"IsUpdateData\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_UPDATE_DATA\");\n\n                    b.Property<string>(\"JobClassName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_CLASS_NAME\");\n\n                    b.Property<byte[]>(\"JobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"JOB_DATA\");\n\n                    b.Property<bool>(\"RequestsRecovery\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"REQUESTS_RECOVERY\");\n\n                    b.HasKey(\"SchedulerName\", \"JobName\", \"JobGroup\");\n\n                    b.HasIndex(\"RequestsRecovery\")\n                        .HasDatabaseName(\"IDX_QRTZ_J_REQ_RECOVERY\");\n\n                    b.ToTable(\"QRTZ_JOB_DETAILS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"LockName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"LOCK_NAME\");\n\n                    b.HasKey(\"SchedulerName\", \"LockName\");\n\n                    b.ToTable(\"QRTZ_LOCKS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_PAUSED_TRIGGER_GRPS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"InstanceName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"INSTANCE_NAME\");\n\n                    b.Property<long>(\"CheckInInterval\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"CHECKIN_INTERVAL\");\n\n                    b.Property<long>(\"LastCheckInTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LAST_CHECKIN_TIME\");\n\n                    b.HasKey(\"SchedulerName\", \"InstanceName\");\n\n                    b.ToTable(\"QRTZ_SCHEDULER_STATE\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<bool?>(\"BooleanProperty1\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"BOOL_PROP_1\");\n\n                    b.Property<bool?>(\"BooleanProperty2\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"BOOL_PROP_2\");\n\n                    b.Property<decimal?>(\"DecimalProperty1\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"DEC_PROP_1\");\n\n                    b.Property<decimal?>(\"DecimalProperty2\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"DEC_PROP_2\");\n\n                    b.Property<int?>(\"IntegerProperty1\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"INT_PROP_1\");\n\n                    b.Property<int?>(\"IntegerProperty2\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"INT_PROP_2\");\n\n                    b.Property<long?>(\"LongProperty1\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LONG_PROP_1\");\n\n                    b.Property<long?>(\"LongProperty2\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LONG_PROP_2\");\n\n                    b.Property<string>(\"StringProperty1\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_1\");\n\n                    b.Property<string>(\"StringProperty2\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_2\");\n\n                    b.Property<string>(\"StringProperty3\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_3\");\n\n                    b.Property<string>(\"TimeZoneId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TIME_ZONE_ID\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_SIMPROP_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<long>(\"RepeatCount\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"REPEAT_COUNT\");\n\n                    b.Property<long>(\"RepeatInterval\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"REPEAT_INTERVAL\");\n\n                    b.Property<long>(\"TimesTriggered\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"TIMES_TRIGGERED\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_SIMPLE_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"CalendarName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CALENDAR_NAME\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"DESCRIPTION\");\n\n                    b.Property<long?>(\"EndTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"END_TIME\");\n\n                    b.Property<byte[]>(\"JobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"JOB_DATA\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"JobName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<short?>(\"MisfireInstruction\")\n                        .HasColumnType(\"smallint\")\n                        .HasColumnName(\"MISFIRE_INSTR\");\n\n                    b.Property<long?>(\"NextFireTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"NEXT_FIRE_TIME\");\n\n                    b.Property<long?>(\"PreviousFireTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"PREV_FIRE_TIME\");\n\n                    b.Property<int?>(\"Priority\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PRIORITY\");\n\n                    b.Property<long>(\"StartTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"START_TIME\");\n\n                    b.Property<string>(\"TriggerState\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_STATE\");\n\n                    b.Property<string>(\"TriggerType\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_TYPE\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.HasIndex(\"NextFireTime\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_NEXT_FIRE_TIME\");\n\n                    b.HasIndex(\"TriggerState\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_STATE\");\n\n                    b.HasIndex(\"NextFireTime\", \"TriggerState\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_NFT_ST\");\n\n                    b.HasIndex(\"SchedulerName\", \"JobName\", \"JobGroup\");\n\n                    b.ToTable(\"QRTZ_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Ray.BiliBiliTool.Domain.ExecutionLog\", b =>\n                {\n                    b.Property<long>(\"LogId\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<long>(\"DateAddedUtc\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"ErrorMessage\")\n                        .HasMaxLength(8000)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<long?>(\"FireTimeUtc\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<bool?>(\"IsException\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<bool?>(\"IsSuccess\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<bool?>(\"IsVetoed\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<TimeSpan?>(\"JobRunTime\")\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"LogType\")\n                        .IsRequired()\n                        .HasColumnType(\"varchar(20)\");\n\n                    b.Property<string>(\"Result\")\n                        .HasMaxLength(8000)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<int?>(\"RetryCount\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"ReturnCode\")\n                        .HasMaxLength(28)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"RunInstanceId\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<long?>(\"ScheduleFireTimeUtc\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.HasKey(\"LogId\");\n\n                    b.HasIndex(\"RunInstanceId\")\n                        .IsUnique();\n\n                    b.HasIndex(\"DateAddedUtc\", \"LogType\");\n\n                    b.HasIndex(\"TriggerName\", \"TriggerGroup\", \"JobName\", \"JobGroup\", \"DateAddedUtc\");\n\n                    b.ToTable(\"bili_execution_logs\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"BlobTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"CronTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"SimplePropertyTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"SimpleTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", \"JobDetail\")\n                        .WithMany(\"Triggers\")\n                        .HasForeignKey(\"SchedulerName\", \"JobName\", \"JobGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"JobDetail\");\n                });\n\n            modelBuilder.Entity(\"Ray.BiliBiliTool.Domain.ExecutionLog\", b =>\n                {\n                    b.OwnsOne(\"Ray.BiliBiliTool.Domain.ExecutionLogDetail\", \"ExecutionLogDetail\", b1 =>\n                        {\n                            b1.Property<long>(\"LogId\")\n                                .HasColumnType(\"INTEGER\");\n\n                            b1.Property<int?>(\"ErrorCode\")\n                                .HasColumnType(\"INTEGER\");\n\n                            b1.Property<string>(\"ErrorHelpLink\")\n                                .HasMaxLength(1000)\n                                .HasColumnType(\"TEXT\");\n\n                            b1.Property<string>(\"ErrorStackTrace\")\n                                .HasColumnType(\"TEXT\");\n\n                            b1.Property<string>(\"ExecutionDetails\")\n                                .HasColumnType(\"TEXT\");\n\n                            b1.HasKey(\"LogId\");\n\n                            b1.ToTable(\"bili_execution_log_details\", (string)null);\n\n                            b1.WithOwner()\n                                .HasForeignKey(\"LogId\");\n                        });\n\n                    b.Navigation(\"ExecutionLogDetail\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", b =>\n                {\n                    b.Navigation(\"Triggers\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.Navigation(\"BlobTriggers\");\n\n                    b.Navigation(\"CronTriggers\");\n\n                    b.Navigation(\"SimplePropertyTriggers\");\n\n                    b.Navigation(\"SimpleTriggers\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250503164108_Update.cs",
    "content": "﻿using System;\nusing Microsoft.EntityFrameworkCore.Migrations;\n\n#nullable disable\n\nnamespace Ray.BiliBiliTool.Web.Migrations\n{\n    /// <inheritdoc />\n    public partial class Update : Migration\n    {\n        /// <inheritdoc />\n        protected override void Up(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.CreateTable(\n                name: \"bili_execution_logs\",\n                columns: table => new\n                {\n                    LogId = table\n                        .Column<long>(type: \"INTEGER\", nullable: false)\n                        .Annotation(\"Sqlite:Autoincrement\", true),\n                    RunInstanceId = table.Column<string>(\n                        type: \"TEXT\",\n                        maxLength: 256,\n                        nullable: true\n                    ),\n                    LogType = table.Column<string>(type: \"varchar(20)\", nullable: false),\n                    JobName = table.Column<string>(type: \"TEXT\", maxLength: 256, nullable: true),\n                    JobGroup = table.Column<string>(type: \"TEXT\", maxLength: 256, nullable: true),\n                    TriggerName = table.Column<string>(\n                        type: \"TEXT\",\n                        maxLength: 256,\n                        nullable: true\n                    ),\n                    TriggerGroup = table.Column<string>(\n                        type: \"TEXT\",\n                        maxLength: 256,\n                        nullable: true\n                    ),\n                    ScheduleFireTimeUtc = table.Column<long>(type: \"INTEGER\", nullable: true),\n                    FireTimeUtc = table.Column<long>(type: \"INTEGER\", nullable: true),\n                    JobRunTime = table.Column<TimeSpan>(type: \"TEXT\", nullable: true),\n                    RetryCount = table.Column<int>(type: \"INTEGER\", nullable: true),\n                    Result = table.Column<string>(type: \"TEXT\", maxLength: 8000, nullable: true),\n                    ErrorMessage = table.Column<string>(\n                        type: \"TEXT\",\n                        maxLength: 8000,\n                        nullable: true\n                    ),\n                    IsVetoed = table.Column<bool>(type: \"INTEGER\", nullable: true),\n                    IsException = table.Column<bool>(type: \"INTEGER\", nullable: true),\n                    IsSuccess = table.Column<bool>(type: \"INTEGER\", nullable: true),\n                    ReturnCode = table.Column<string>(type: \"TEXT\", maxLength: 28, nullable: true),\n                    DateAddedUtc = table.Column<long>(type: \"INTEGER\", nullable: false),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_bili_execution_logs\", x => x.LogId);\n                }\n            );\n\n            migrationBuilder.CreateTable(\n                name: \"bili_execution_log_details\",\n                columns: table => new\n                {\n                    LogId = table.Column<long>(type: \"INTEGER\", nullable: false),\n                    ExecutionDetails = table.Column<string>(type: \"TEXT\", nullable: true),\n                    ErrorStackTrace = table.Column<string>(type: \"TEXT\", nullable: true),\n                    ErrorCode = table.Column<int>(type: \"INTEGER\", nullable: true),\n                    ErrorHelpLink = table.Column<string>(\n                        type: \"TEXT\",\n                        maxLength: 1000,\n                        nullable: true\n                    ),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_bili_execution_log_details\", x => x.LogId);\n                    table.ForeignKey(\n                        name: \"FK_bili_execution_log_details_bili_execution_logs_LogId\",\n                        column: x => x.LogId,\n                        principalTable: \"bili_execution_logs\",\n                        principalColumn: \"LogId\",\n                        onDelete: ReferentialAction.Cascade\n                    );\n                }\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_bili_execution_logs_DateAddedUtc_LogType\",\n                table: \"bili_execution_logs\",\n                columns: new[] { \"DateAddedUtc\", \"LogType\" }\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_bili_execution_logs_RunInstanceId\",\n                table: \"bili_execution_logs\",\n                column: \"RunInstanceId\",\n                unique: true\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_bili_execution_logs_TriggerName_TriggerGroup_JobName_JobGroup_DateAddedUtc\",\n                table: \"bili_execution_logs\",\n                columns: new[]\n                {\n                    \"TriggerName\",\n                    \"TriggerGroup\",\n                    \"JobName\",\n                    \"JobGroup\",\n                    \"DateAddedUtc\",\n                }\n            );\n        }\n\n        /// <inheritdoc />\n        protected override void Down(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.DropTable(name: \"bili_execution_log_details\");\n\n            migrationBuilder.DropTable(name: \"bili_execution_logs\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250510130427_AddBiliLogs.Designer.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing Ray.BiliBiliTool.Infrastructure.EF;\n\n#nullable disable\n\nnamespace Ray.BiliBiliTool.Web.Migrations\n{\n    [DbContext(typeof(BiliDbContext))]\n    [Migration(\"20250510130427_AddBiliLogs\")]\n    partial class AddBiliLogs\n    {\n        /// <inheritdoc />\n        protected override void BuildTargetModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder.HasAnnotation(\"ProductVersion\", \"8.0.3\");\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<byte[]>(\"BlobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"BLOB_DATA\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_BLOB_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"CalendarName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CALENDAR_NAME\");\n\n                    b.Property<byte[]>(\"Calendar\")\n                        .IsRequired()\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"CALENDAR\");\n\n                    b.HasKey(\"SchedulerName\", \"CalendarName\");\n\n                    b.ToTable(\"QRTZ_CALENDARS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"CronExpression\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CRON_EXPRESSION\");\n\n                    b.Property<string>(\"TimeZoneId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TIME_ZONE_ID\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_CRON_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"EntryId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"ENTRY_ID\");\n\n                    b.Property<long>(\"FiredTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"FIRED_TIME\");\n\n                    b.Property<string>(\"InstanceName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"INSTANCE_NAME\");\n\n                    b.Property<bool>(\"IsNonConcurrent\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_NONCONCURRENT\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<int>(\"Priority\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PRIORITY\");\n\n                    b.Property<bool?>(\"RequestsRecovery\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"REQUESTS_RECOVERY\");\n\n                    b.Property<long>(\"ScheduledTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"SCHED_TIME\");\n\n                    b.Property<string>(\"State\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STATE\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.HasKey(\"SchedulerName\", \"EntryId\");\n\n                    b.HasIndex(\"InstanceName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_INST_NAME\");\n\n                    b.HasIndex(\"JobGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_GROUP\");\n\n                    b.HasIndex(\"JobName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_NAME\");\n\n                    b.HasIndex(\"RequestsRecovery\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_REQ_RECOVERY\");\n\n                    b.HasIndex(\"TriggerGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_GROUP\");\n\n                    b.HasIndex(\"TriggerName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_NAME\");\n\n                    b.HasIndex(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_NM_GP\");\n\n                    b.ToTable(\"QRTZ_FIRED_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"DESCRIPTION\");\n\n                    b.Property<bool>(\"IsDurable\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_DURABLE\");\n\n                    b.Property<bool>(\"IsNonConcurrent\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_NONCONCURRENT\");\n\n                    b.Property<bool>(\"IsUpdateData\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_UPDATE_DATA\");\n\n                    b.Property<string>(\"JobClassName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_CLASS_NAME\");\n\n                    b.Property<byte[]>(\"JobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"JOB_DATA\");\n\n                    b.Property<bool>(\"RequestsRecovery\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"REQUESTS_RECOVERY\");\n\n                    b.HasKey(\"SchedulerName\", \"JobName\", \"JobGroup\");\n\n                    b.HasIndex(\"RequestsRecovery\")\n                        .HasDatabaseName(\"IDX_QRTZ_J_REQ_RECOVERY\");\n\n                    b.ToTable(\"QRTZ_JOB_DETAILS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"LockName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"LOCK_NAME\");\n\n                    b.HasKey(\"SchedulerName\", \"LockName\");\n\n                    b.ToTable(\"QRTZ_LOCKS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_PAUSED_TRIGGER_GRPS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"InstanceName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"INSTANCE_NAME\");\n\n                    b.Property<long>(\"CheckInInterval\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"CHECKIN_INTERVAL\");\n\n                    b.Property<long>(\"LastCheckInTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LAST_CHECKIN_TIME\");\n\n                    b.HasKey(\"SchedulerName\", \"InstanceName\");\n\n                    b.ToTable(\"QRTZ_SCHEDULER_STATE\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<bool?>(\"BooleanProperty1\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"BOOL_PROP_1\");\n\n                    b.Property<bool?>(\"BooleanProperty2\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"BOOL_PROP_2\");\n\n                    b.Property<decimal?>(\"DecimalProperty1\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"DEC_PROP_1\");\n\n                    b.Property<decimal?>(\"DecimalProperty2\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"DEC_PROP_2\");\n\n                    b.Property<int?>(\"IntegerProperty1\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"INT_PROP_1\");\n\n                    b.Property<int?>(\"IntegerProperty2\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"INT_PROP_2\");\n\n                    b.Property<long?>(\"LongProperty1\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LONG_PROP_1\");\n\n                    b.Property<long?>(\"LongProperty2\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LONG_PROP_2\");\n\n                    b.Property<string>(\"StringProperty1\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_1\");\n\n                    b.Property<string>(\"StringProperty2\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_2\");\n\n                    b.Property<string>(\"StringProperty3\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_3\");\n\n                    b.Property<string>(\"TimeZoneId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TIME_ZONE_ID\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_SIMPROP_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<long>(\"RepeatCount\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"REPEAT_COUNT\");\n\n                    b.Property<long>(\"RepeatInterval\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"REPEAT_INTERVAL\");\n\n                    b.Property<long>(\"TimesTriggered\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"TIMES_TRIGGERED\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_SIMPLE_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"CalendarName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CALENDAR_NAME\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"DESCRIPTION\");\n\n                    b.Property<long?>(\"EndTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"END_TIME\");\n\n                    b.Property<byte[]>(\"JobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"JOB_DATA\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"JobName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<short?>(\"MisfireInstruction\")\n                        .HasColumnType(\"smallint\")\n                        .HasColumnName(\"MISFIRE_INSTR\");\n\n                    b.Property<long?>(\"NextFireTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"NEXT_FIRE_TIME\");\n\n                    b.Property<long?>(\"PreviousFireTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"PREV_FIRE_TIME\");\n\n                    b.Property<int?>(\"Priority\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PRIORITY\");\n\n                    b.Property<long>(\"StartTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"START_TIME\");\n\n                    b.Property<string>(\"TriggerState\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_STATE\");\n\n                    b.Property<string>(\"TriggerType\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_TYPE\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.HasIndex(\"NextFireTime\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_NEXT_FIRE_TIME\");\n\n                    b.HasIndex(\"TriggerState\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_STATE\");\n\n                    b.HasIndex(\"NextFireTime\", \"TriggerState\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_NFT_ST\");\n\n                    b.HasIndex(\"SchedulerName\", \"JobName\", \"JobGroup\");\n\n                    b.ToTable(\"QRTZ_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Ray.BiliBiliTool.Domain.BiliLogs\", b =>\n                {\n                    b.Property<long>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"INTEGER\")\n                        .HasColumnName(\"id\");\n\n                    b.Property<string>(\"Exception\")\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"exception\");\n\n                    b.Property<string>(\"FireInstanceIdComputed\")\n                        .ValueGeneratedOnAddOrUpdate()\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"fireInstanceIdComputed\")\n                        .HasComputedColumnSql(\"json_extract(Properties, '$.FireInstanceId')\", false);\n\n                    b.Property<string>(\"Level\")\n                        .IsRequired()\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"level\");\n\n                    b.Property<string>(\"Properties\")\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"properties\");\n\n                    b.Property<string>(\"RenderedMessage\")\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"renderedMessage\");\n\n                    b.Property<DateTime>(\"Timestamp\")\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"timeStamp\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"FireInstanceIdComputed\")\n                        .HasDatabaseName(\"IX_Logs_FireInstanceIdComputed\");\n\n                    b.ToTable(\"bili_logs\");\n                });\n\n            modelBuilder.Entity(\"Ray.BiliBiliTool.Domain.ExecutionLog\", b =>\n                {\n                    b.Property<long>(\"LogId\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<long>(\"DateAddedUtc\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"ErrorMessage\")\n                        .HasMaxLength(8000)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<long?>(\"FireTimeUtc\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<bool?>(\"IsException\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<bool?>(\"IsSuccess\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<bool?>(\"IsVetoed\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<TimeSpan?>(\"JobRunTime\")\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"LogType\")\n                        .IsRequired()\n                        .HasColumnType(\"varchar(20)\");\n\n                    b.Property<string>(\"Result\")\n                        .HasMaxLength(8000)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<int?>(\"RetryCount\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"ReturnCode\")\n                        .HasMaxLength(28)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"RunInstanceId\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<long?>(\"ScheduleFireTimeUtc\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.HasKey(\"LogId\");\n\n                    b.HasIndex(\"RunInstanceId\")\n                        .IsUnique();\n\n                    b.HasIndex(\"DateAddedUtc\", \"LogType\");\n\n                    b.HasIndex(\"TriggerName\", \"TriggerGroup\", \"JobName\", \"JobGroup\", \"DateAddedUtc\");\n\n                    b.ToTable(\"bili_execution_logs\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"BlobTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"CronTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"SimplePropertyTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"SimpleTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", \"JobDetail\")\n                        .WithMany(\"Triggers\")\n                        .HasForeignKey(\"SchedulerName\", \"JobName\", \"JobGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"JobDetail\");\n                });\n\n            modelBuilder.Entity(\"Ray.BiliBiliTool.Domain.ExecutionLog\", b =>\n                {\n                    b.OwnsOne(\"Ray.BiliBiliTool.Domain.ExecutionLogDetail\", \"ExecutionLogDetail\", b1 =>\n                        {\n                            b1.Property<long>(\"LogId\")\n                                .HasColumnType(\"INTEGER\");\n\n                            b1.Property<int?>(\"ErrorCode\")\n                                .HasColumnType(\"INTEGER\");\n\n                            b1.Property<string>(\"ErrorHelpLink\")\n                                .HasMaxLength(1000)\n                                .HasColumnType(\"TEXT\");\n\n                            b1.Property<string>(\"ErrorStackTrace\")\n                                .HasColumnType(\"TEXT\");\n\n                            b1.Property<string>(\"ExecutionDetails\")\n                                .HasColumnType(\"TEXT\");\n\n                            b1.HasKey(\"LogId\");\n\n                            b1.ToTable(\"bili_execution_log_details\", (string)null);\n\n                            b1.WithOwner()\n                                .HasForeignKey(\"LogId\");\n                        });\n\n                    b.Navigation(\"ExecutionLogDetail\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", b =>\n                {\n                    b.Navigation(\"Triggers\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.Navigation(\"BlobTriggers\");\n\n                    b.Navigation(\"CronTriggers\");\n\n                    b.Navigation(\"SimplePropertyTriggers\");\n\n                    b.Navigation(\"SimpleTriggers\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250510130427_AddBiliLogs.cs",
    "content": "﻿using System;\nusing Microsoft.EntityFrameworkCore.Migrations;\n\n#nullable disable\n\nnamespace Ray.BiliBiliTool.Web.Migrations\n{\n    /// <inheritdoc />\n    public partial class AddBiliLogs : Migration\n    {\n        /// <inheritdoc />\n        protected override void Up(MigrationBuilder migrationBuilder)\n        {\n            try\n            {\n                migrationBuilder.DropTable(name: \"bili_logs\");\n            }\n            catch\n            {\n                // ignored\n            }\n            migrationBuilder.CreateTable(\n                name: \"bili_logs\",\n                columns: table => new\n                {\n                    id = table\n                        .Column<long>(type: \"INTEGER\", nullable: false)\n                        .Annotation(\"Sqlite:Autoincrement\", true),\n                    timeStamp = table.Column<DateTime>(type: \"TEXT\", nullable: false),\n                    level = table.Column<string>(type: \"TEXT\", nullable: false),\n                    exception = table.Column<string>(type: \"TEXT\", nullable: true),\n                    renderedMessage = table.Column<string>(type: \"TEXT\", nullable: true),\n                    properties = table.Column<string>(type: \"TEXT\", nullable: true),\n                    fireInstanceIdComputed = table.Column<string>(\n                        type: \"TEXT\",\n                        nullable: true,\n                        computedColumnSql: \"json_extract(Properties, '$.FireInstanceId')\",\n                        stored: false\n                    ),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_bili_logs\", x => x.id);\n                }\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_Logs_FireInstanceIdComputed\",\n                table: \"bili_logs\",\n                column: \"fireInstanceIdComputed\"\n            );\n        }\n\n        /// <inheritdoc />\n        protected override void Down(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.DropTable(name: \"bili_logs\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.Designer.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing Ray.BiliBiliTool.Infrastructure.EF;\n\n#nullable disable\n\nnamespace Ray.BiliBiliTool.Web.Migrations\n{\n    [DbContext(typeof(BiliDbContext))]\n    [Migration(\"20250615100041_AddUser\")]\n    partial class AddUser\n    {\n        /// <inheritdoc />\n        protected override void BuildTargetModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder.HasAnnotation(\"ProductVersion\", \"8.0.3\");\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<byte[]>(\"BlobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"BLOB_DATA\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_BLOB_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"CalendarName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CALENDAR_NAME\");\n\n                    b.Property<byte[]>(\"Calendar\")\n                        .IsRequired()\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"CALENDAR\");\n\n                    b.HasKey(\"SchedulerName\", \"CalendarName\");\n\n                    b.ToTable(\"QRTZ_CALENDARS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"CronExpression\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CRON_EXPRESSION\");\n\n                    b.Property<string>(\"TimeZoneId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TIME_ZONE_ID\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_CRON_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"EntryId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"ENTRY_ID\");\n\n                    b.Property<long>(\"FiredTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"FIRED_TIME\");\n\n                    b.Property<string>(\"InstanceName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"INSTANCE_NAME\");\n\n                    b.Property<bool>(\"IsNonConcurrent\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_NONCONCURRENT\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<int>(\"Priority\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PRIORITY\");\n\n                    b.Property<bool?>(\"RequestsRecovery\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"REQUESTS_RECOVERY\");\n\n                    b.Property<long>(\"ScheduledTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"SCHED_TIME\");\n\n                    b.Property<string>(\"State\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STATE\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.HasKey(\"SchedulerName\", \"EntryId\");\n\n                    b.HasIndex(\"InstanceName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_INST_NAME\");\n\n                    b.HasIndex(\"JobGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_GROUP\");\n\n                    b.HasIndex(\"JobName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_NAME\");\n\n                    b.HasIndex(\"RequestsRecovery\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_REQ_RECOVERY\");\n\n                    b.HasIndex(\"TriggerGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_GROUP\");\n\n                    b.HasIndex(\"TriggerName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_NAME\");\n\n                    b.HasIndex(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_NM_GP\");\n\n                    b.ToTable(\"QRTZ_FIRED_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"DESCRIPTION\");\n\n                    b.Property<bool>(\"IsDurable\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_DURABLE\");\n\n                    b.Property<bool>(\"IsNonConcurrent\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_NONCONCURRENT\");\n\n                    b.Property<bool>(\"IsUpdateData\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_UPDATE_DATA\");\n\n                    b.Property<string>(\"JobClassName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_CLASS_NAME\");\n\n                    b.Property<byte[]>(\"JobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"JOB_DATA\");\n\n                    b.Property<bool>(\"RequestsRecovery\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"REQUESTS_RECOVERY\");\n\n                    b.HasKey(\"SchedulerName\", \"JobName\", \"JobGroup\");\n\n                    b.HasIndex(\"RequestsRecovery\")\n                        .HasDatabaseName(\"IDX_QRTZ_J_REQ_RECOVERY\");\n\n                    b.ToTable(\"QRTZ_JOB_DETAILS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"LockName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"LOCK_NAME\");\n\n                    b.HasKey(\"SchedulerName\", \"LockName\");\n\n                    b.ToTable(\"QRTZ_LOCKS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_PAUSED_TRIGGER_GRPS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"InstanceName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"INSTANCE_NAME\");\n\n                    b.Property<long>(\"CheckInInterval\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"CHECKIN_INTERVAL\");\n\n                    b.Property<long>(\"LastCheckInTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LAST_CHECKIN_TIME\");\n\n                    b.HasKey(\"SchedulerName\", \"InstanceName\");\n\n                    b.ToTable(\"QRTZ_SCHEDULER_STATE\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<bool?>(\"BooleanProperty1\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"BOOL_PROP_1\");\n\n                    b.Property<bool?>(\"BooleanProperty2\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"BOOL_PROP_2\");\n\n                    b.Property<decimal?>(\"DecimalProperty1\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"DEC_PROP_1\");\n\n                    b.Property<decimal?>(\"DecimalProperty2\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"DEC_PROP_2\");\n\n                    b.Property<int?>(\"IntegerProperty1\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"INT_PROP_1\");\n\n                    b.Property<int?>(\"IntegerProperty2\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"INT_PROP_2\");\n\n                    b.Property<long?>(\"LongProperty1\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LONG_PROP_1\");\n\n                    b.Property<long?>(\"LongProperty2\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LONG_PROP_2\");\n\n                    b.Property<string>(\"StringProperty1\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_1\");\n\n                    b.Property<string>(\"StringProperty2\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_2\");\n\n                    b.Property<string>(\"StringProperty3\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_3\");\n\n                    b.Property<string>(\"TimeZoneId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TIME_ZONE_ID\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_SIMPROP_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<long>(\"RepeatCount\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"REPEAT_COUNT\");\n\n                    b.Property<long>(\"RepeatInterval\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"REPEAT_INTERVAL\");\n\n                    b.Property<long>(\"TimesTriggered\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"TIMES_TRIGGERED\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_SIMPLE_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"CalendarName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CALENDAR_NAME\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"DESCRIPTION\");\n\n                    b.Property<long?>(\"EndTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"END_TIME\");\n\n                    b.Property<byte[]>(\"JobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"JOB_DATA\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"JobName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<short?>(\"MisfireInstruction\")\n                        .HasColumnType(\"smallint\")\n                        .HasColumnName(\"MISFIRE_INSTR\");\n\n                    b.Property<long?>(\"NextFireTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"NEXT_FIRE_TIME\");\n\n                    b.Property<long?>(\"PreviousFireTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"PREV_FIRE_TIME\");\n\n                    b.Property<int?>(\"Priority\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PRIORITY\");\n\n                    b.Property<long>(\"StartTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"START_TIME\");\n\n                    b.Property<string>(\"TriggerState\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_STATE\");\n\n                    b.Property<string>(\"TriggerType\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_TYPE\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.HasIndex(\"NextFireTime\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_NEXT_FIRE_TIME\");\n\n                    b.HasIndex(\"TriggerState\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_STATE\");\n\n                    b.HasIndex(\"NextFireTime\", \"TriggerState\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_NFT_ST\");\n\n                    b.HasIndex(\"SchedulerName\", \"JobName\", \"JobGroup\");\n\n                    b.ToTable(\"QRTZ_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Ray.BiliBiliTool.Domain.BiliLogs\", b =>\n                {\n                    b.Property<long>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"INTEGER\")\n                        .HasColumnName(\"id\");\n\n                    b.Property<string>(\"Exception\")\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"exception\");\n\n                    b.Property<string>(\"FireInstanceIdComputed\")\n                        .ValueGeneratedOnAddOrUpdate()\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"fireInstanceIdComputed\")\n                        .HasComputedColumnSql(\"json_extract(Properties, '$.FireInstanceId')\", false);\n\n                    b.Property<string>(\"Level\")\n                        .IsRequired()\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"level\");\n\n                    b.Property<string>(\"Properties\")\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"properties\");\n\n                    b.Property<string>(\"RenderedMessage\")\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"renderedMessage\");\n\n                    b.Property<DateTime>(\"Timestamp\")\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"timeStamp\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"FireInstanceIdComputed\")\n                        .HasDatabaseName(\"IX_Logs_FireInstanceIdComputed\");\n\n                    b.ToTable(\"bili_logs\");\n                });\n\n            modelBuilder.Entity(\"Ray.BiliBiliTool.Domain.ExecutionLog\", b =>\n                {\n                    b.Property<long>(\"LogId\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<long>(\"DateAddedUtc\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"ErrorMessage\")\n                        .HasMaxLength(8000)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<long?>(\"FireTimeUtc\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<bool?>(\"IsException\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<bool?>(\"IsSuccess\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<bool?>(\"IsVetoed\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<TimeSpan?>(\"JobRunTime\")\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"LogType\")\n                        .IsRequired()\n                        .HasColumnType(\"varchar(20)\");\n\n                    b.Property<string>(\"Result\")\n                        .HasMaxLength(8000)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<int?>(\"RetryCount\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"ReturnCode\")\n                        .HasMaxLength(28)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"RunInstanceId\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<long?>(\"ScheduleFireTimeUtc\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.HasKey(\"LogId\");\n\n                    b.HasIndex(\"RunInstanceId\")\n                        .IsUnique();\n\n                    b.HasIndex(\"DateAddedUtc\", \"LogType\");\n\n                    b.HasIndex(\"TriggerName\", \"TriggerGroup\", \"JobName\", \"JobGroup\", \"DateAddedUtc\");\n\n                    b.ToTable(\"bili_execution_logs\");\n                });\n\n            modelBuilder.Entity(\"Ray.BiliBiliTool.Domain.User\", b =>\n                {\n                    b.Property<long>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"INTEGER\")\n                        .HasColumnName(\"id\");\n\n                    b.Property<string>(\"PasswordHash\")\n                        .IsRequired()\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"Roles\")\n                        .IsRequired()\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"Salt\")\n                        .IsRequired()\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"Username\")\n                        .IsRequired()\n                        .HasMaxLength(50)\n                        .HasColumnType(\"TEXT\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"Username\")\n                        .IsUnique();\n\n                    b.ToTable(\"bili_User\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"BlobTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"CronTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"SimplePropertyTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"SimpleTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", \"JobDetail\")\n                        .WithMany(\"Triggers\")\n                        .HasForeignKey(\"SchedulerName\", \"JobName\", \"JobGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"JobDetail\");\n                });\n\n            modelBuilder.Entity(\"Ray.BiliBiliTool.Domain.ExecutionLog\", b =>\n                {\n                    b.OwnsOne(\"Ray.BiliBiliTool.Domain.ExecutionLogDetail\", \"ExecutionLogDetail\", b1 =>\n                        {\n                            b1.Property<long>(\"LogId\")\n                                .HasColumnType(\"INTEGER\");\n\n                            b1.Property<int?>(\"ErrorCode\")\n                                .HasColumnType(\"INTEGER\");\n\n                            b1.Property<string>(\"ErrorHelpLink\")\n                                .HasMaxLength(1000)\n                                .HasColumnType(\"TEXT\");\n\n                            b1.Property<string>(\"ErrorStackTrace\")\n                                .HasColumnType(\"TEXT\");\n\n                            b1.Property<string>(\"ExecutionDetails\")\n                                .HasColumnType(\"TEXT\");\n\n                            b1.HasKey(\"LogId\");\n\n                            b1.ToTable(\"bili_execution_log_details\", (string)null);\n\n                            b1.WithOwner()\n                                .HasForeignKey(\"LogId\");\n                        });\n\n                    b.Navigation(\"ExecutionLogDetail\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", b =>\n                {\n                    b.Navigation(\"Triggers\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.Navigation(\"BlobTriggers\");\n\n                    b.Navigation(\"CronTriggers\");\n\n                    b.Navigation(\"SimplePropertyTriggers\");\n\n                    b.Navigation(\"SimpleTriggers\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/20250615100041_AddUser.cs",
    "content": "﻿using Microsoft.EntityFrameworkCore.Migrations;\n\n#nullable disable\n\nnamespace Ray.BiliBiliTool.Web.Migrations\n{\n    /// <inheritdoc />\n    public partial class AddUser : Migration\n    {\n        /// <inheritdoc />\n        protected override void Up(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.CreateTable(\n                name: \"bili_user\",\n                columns: table => new\n                {\n                    Id = table\n                        .Column<long>(type: \"INTEGER\", nullable: false)\n                        .Annotation(\"Sqlite:Autoincrement\", true),\n                    Username = table.Column<string>(type: \"TEXT\", maxLength: 50, nullable: false),\n                    PasswordHash = table.Column<string>(type: \"TEXT\", nullable: false),\n                    Salt = table.Column<string>(type: \"TEXT\", nullable: false),\n                    Roles = table.Column<string>(type: \"TEXT\", nullable: false),\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_bili_user\", x => x.Id);\n                }\n            );\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_bili_user_Username\",\n                table: \"bili_user\",\n                column: \"Username\",\n                unique: true\n            );\n        }\n\n        /// <inheritdoc />\n        protected override void Down(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.DropTable(name: \"bili_user\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/Migrations/BiliDbContextModelSnapshot.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing Ray.BiliBiliTool.Infrastructure.EF;\n\n#nullable disable\n\nnamespace Ray.BiliBiliTool.Web.Migrations\n{\n    [DbContext(typeof(BiliDbContext))]\n    partial class BiliDbContextModelSnapshot : ModelSnapshot\n    {\n        protected override void BuildModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder.HasAnnotation(\"ProductVersion\", \"8.0.3\");\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<byte[]>(\"BlobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"BLOB_DATA\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_BLOB_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"CalendarName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CALENDAR_NAME\");\n\n                    b.Property<byte[]>(\"Calendar\")\n                        .IsRequired()\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"CALENDAR\");\n\n                    b.HasKey(\"SchedulerName\", \"CalendarName\");\n\n                    b.ToTable(\"QRTZ_CALENDARS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"CronExpression\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CRON_EXPRESSION\");\n\n                    b.Property<string>(\"TimeZoneId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TIME_ZONE_ID\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_CRON_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"EntryId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"ENTRY_ID\");\n\n                    b.Property<long>(\"FiredTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"FIRED_TIME\");\n\n                    b.Property<string>(\"InstanceName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"INSTANCE_NAME\");\n\n                    b.Property<bool>(\"IsNonConcurrent\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_NONCONCURRENT\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<int>(\"Priority\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PRIORITY\");\n\n                    b.Property<bool?>(\"RequestsRecovery\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"REQUESTS_RECOVERY\");\n\n                    b.Property<long>(\"ScheduledTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"SCHED_TIME\");\n\n                    b.Property<string>(\"State\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STATE\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.HasKey(\"SchedulerName\", \"EntryId\");\n\n                    b.HasIndex(\"InstanceName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_INST_NAME\");\n\n                    b.HasIndex(\"JobGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_GROUP\");\n\n                    b.HasIndex(\"JobName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_NAME\");\n\n                    b.HasIndex(\"RequestsRecovery\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_JOB_REQ_RECOVERY\");\n\n                    b.HasIndex(\"TriggerGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_GROUP\");\n\n                    b.HasIndex(\"TriggerName\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_NAME\");\n\n                    b.HasIndex(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .HasDatabaseName(\"IDX_QRTZ_FT_TRIG_NM_GP\");\n\n                    b.ToTable(\"QRTZ_FIRED_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"DESCRIPTION\");\n\n                    b.Property<bool>(\"IsDurable\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_DURABLE\");\n\n                    b.Property<bool>(\"IsNonConcurrent\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_NONCONCURRENT\");\n\n                    b.Property<bool>(\"IsUpdateData\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"IS_UPDATE_DATA\");\n\n                    b.Property<string>(\"JobClassName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_CLASS_NAME\");\n\n                    b.Property<byte[]>(\"JobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"JOB_DATA\");\n\n                    b.Property<bool>(\"RequestsRecovery\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"REQUESTS_RECOVERY\");\n\n                    b.HasKey(\"SchedulerName\", \"JobName\", \"JobGroup\");\n\n                    b.HasIndex(\"RequestsRecovery\")\n                        .HasDatabaseName(\"IDX_QRTZ_J_REQ_RECOVERY\");\n\n                    b.ToTable(\"QRTZ_JOB_DETAILS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"LockName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"LOCK_NAME\");\n\n                    b.HasKey(\"SchedulerName\", \"LockName\");\n\n                    b.ToTable(\"QRTZ_LOCKS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_PAUSED_TRIGGER_GRPS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"InstanceName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"INSTANCE_NAME\");\n\n                    b.Property<long>(\"CheckInInterval\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"CHECKIN_INTERVAL\");\n\n                    b.Property<long>(\"LastCheckInTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LAST_CHECKIN_TIME\");\n\n                    b.HasKey(\"SchedulerName\", \"InstanceName\");\n\n                    b.ToTable(\"QRTZ_SCHEDULER_STATE\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<bool?>(\"BooleanProperty1\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"BOOL_PROP_1\");\n\n                    b.Property<bool?>(\"BooleanProperty2\")\n                        .HasColumnType(\"bool\")\n                        .HasColumnName(\"BOOL_PROP_2\");\n\n                    b.Property<decimal?>(\"DecimalProperty1\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"DEC_PROP_1\");\n\n                    b.Property<decimal?>(\"DecimalProperty2\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"DEC_PROP_2\");\n\n                    b.Property<int?>(\"IntegerProperty1\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"INT_PROP_1\");\n\n                    b.Property<int?>(\"IntegerProperty2\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"INT_PROP_2\");\n\n                    b.Property<long?>(\"LongProperty1\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LONG_PROP_1\");\n\n                    b.Property<long?>(\"LongProperty2\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"LONG_PROP_2\");\n\n                    b.Property<string>(\"StringProperty1\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_1\");\n\n                    b.Property<string>(\"StringProperty2\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_2\");\n\n                    b.Property<string>(\"StringProperty3\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"STR_PROP_3\");\n\n                    b.Property<string>(\"TimeZoneId\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TIME_ZONE_ID\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_SIMPROP_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<long>(\"RepeatCount\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"REPEAT_COUNT\");\n\n                    b.Property<long>(\"RepeatInterval\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"REPEAT_INTERVAL\");\n\n                    b.Property<long>(\"TimesTriggered\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"TIMES_TRIGGERED\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.ToTable(\"QRTZ_SIMPLE_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.Property<string>(\"SchedulerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"SCHED_NAME\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_NAME\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_GROUP\");\n\n                    b.Property<string>(\"CalendarName\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"CALENDAR_NAME\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"DESCRIPTION\");\n\n                    b.Property<long?>(\"EndTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"END_TIME\");\n\n                    b.Property<byte[]>(\"JobData\")\n                        .HasColumnType(\"bytea\")\n                        .HasColumnName(\"JOB_DATA\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_GROUP\");\n\n                    b.Property<string>(\"JobName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"JOB_NAME\");\n\n                    b.Property<short?>(\"MisfireInstruction\")\n                        .HasColumnType(\"smallint\")\n                        .HasColumnName(\"MISFIRE_INSTR\");\n\n                    b.Property<long?>(\"NextFireTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"NEXT_FIRE_TIME\");\n\n                    b.Property<long?>(\"PreviousFireTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"PREV_FIRE_TIME\");\n\n                    b.Property<int?>(\"Priority\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PRIORITY\");\n\n                    b.Property<long>(\"StartTime\")\n                        .HasColumnType(\"bigint\")\n                        .HasColumnName(\"START_TIME\");\n\n                    b.Property<string>(\"TriggerState\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_STATE\");\n\n                    b.Property<string>(\"TriggerType\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"TRIGGER_TYPE\");\n\n                    b.HasKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\");\n\n                    b.HasIndex(\"NextFireTime\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_NEXT_FIRE_TIME\");\n\n                    b.HasIndex(\"TriggerState\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_STATE\");\n\n                    b.HasIndex(\"NextFireTime\", \"TriggerState\")\n                        .HasDatabaseName(\"IDX_QRTZ_T_NFT_ST\");\n\n                    b.HasIndex(\"SchedulerName\", \"JobName\", \"JobGroup\");\n\n                    b.ToTable(\"QRTZ_TRIGGERS\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Ray.BiliBiliTool.Domain.BiliLogs\", b =>\n                {\n                    b.Property<long>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"INTEGER\")\n                        .HasColumnName(\"id\");\n\n                    b.Property<string>(\"Exception\")\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"exception\");\n\n                    b.Property<string>(\"FireInstanceIdComputed\")\n                        .ValueGeneratedOnAddOrUpdate()\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"fireInstanceIdComputed\")\n                        .HasComputedColumnSql(\"json_extract(Properties, '$.FireInstanceId')\", false);\n\n                    b.Property<string>(\"Level\")\n                        .IsRequired()\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"level\");\n\n                    b.Property<string>(\"Properties\")\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"properties\");\n\n                    b.Property<string>(\"RenderedMessage\")\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"renderedMessage\");\n\n                    b.Property<DateTime>(\"Timestamp\")\n                        .HasColumnType(\"TEXT\")\n                        .HasColumnName(\"timeStamp\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"FireInstanceIdComputed\")\n                        .HasDatabaseName(\"IX_Logs_FireInstanceIdComputed\");\n\n                    b.ToTable(\"bili_logs\");\n                });\n\n            modelBuilder.Entity(\"Ray.BiliBiliTool.Domain.ExecutionLog\", b =>\n                {\n                    b.Property<long>(\"LogId\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<long>(\"DateAddedUtc\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"ErrorMessage\")\n                        .HasMaxLength(8000)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<long?>(\"FireTimeUtc\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<bool?>(\"IsException\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<bool?>(\"IsSuccess\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<bool?>(\"IsVetoed\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"JobGroup\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"JobName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<TimeSpan?>(\"JobRunTime\")\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"LogType\")\n                        .IsRequired()\n                        .HasColumnType(\"varchar(20)\");\n\n                    b.Property<string>(\"Result\")\n                        .HasMaxLength(8000)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<int?>(\"RetryCount\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"ReturnCode\")\n                        .HasMaxLength(28)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"RunInstanceId\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<long?>(\"ScheduleFireTimeUtc\")\n                        .HasColumnType(\"INTEGER\");\n\n                    b.Property<string>(\"TriggerGroup\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"TriggerName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"TEXT\");\n\n                    b.HasKey(\"LogId\");\n\n                    b.HasIndex(\"RunInstanceId\")\n                        .IsUnique();\n\n                    b.HasIndex(\"DateAddedUtc\", \"LogType\");\n\n                    b.HasIndex(\"TriggerName\", \"TriggerGroup\", \"JobName\", \"JobGroup\", \"DateAddedUtc\");\n\n                    b.ToTable(\"bili_execution_logs\");\n                });\n\n            modelBuilder.Entity(\"Ray.BiliBiliTool.Domain.User\", b =>\n                {\n                    b.Property<long>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"INTEGER\")\n                        .HasColumnName(\"id\");\n\n                    b.Property<string>(\"PasswordHash\")\n                        .IsRequired()\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"Roles\")\n                        .IsRequired()\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"Salt\")\n                        .IsRequired()\n                        .HasColumnType(\"TEXT\");\n\n                    b.Property<string>(\"Username\")\n                        .IsRequired()\n                        .HasMaxLength(50)\n                        .HasColumnType(\"TEXT\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"Username\")\n                        .IsUnique();\n\n                    b.ToTable(\"bili_user\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"BlobTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"CronTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"SimplePropertyTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", \"Trigger\")\n                        .WithMany(\"SimpleTriggers\")\n                        .HasForeignKey(\"SchedulerName\", \"TriggerName\", \"TriggerGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"Trigger\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.HasOne(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", \"JobDetail\")\n                        .WithMany(\"Triggers\")\n                        .HasForeignKey(\"SchedulerName\", \"JobName\", \"JobGroup\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"JobDetail\");\n                });\n\n            modelBuilder.Entity(\"Ray.BiliBiliTool.Domain.ExecutionLog\", b =>\n                {\n                    b.OwnsOne(\"Ray.BiliBiliTool.Domain.ExecutionLogDetail\", \"ExecutionLogDetail\", b1 =>\n                        {\n                            b1.Property<long>(\"LogId\")\n                                .HasColumnType(\"INTEGER\");\n\n                            b1.Property<int?>(\"ErrorCode\")\n                                .HasColumnType(\"INTEGER\");\n\n                            b1.Property<string>(\"ErrorHelpLink\")\n                                .HasMaxLength(1000)\n                                .HasColumnType(\"TEXT\");\n\n                            b1.Property<string>(\"ErrorStackTrace\")\n                                .HasColumnType(\"TEXT\");\n\n                            b1.Property<string>(\"ExecutionDetails\")\n                                .HasColumnType(\"TEXT\");\n\n                            b1.HasKey(\"LogId\");\n\n                            b1.ToTable(\"bili_execution_log_details\", (string)null);\n\n                            b1.WithOwner()\n                                .HasForeignKey(\"LogId\");\n                        });\n\n                    b.Navigation(\"ExecutionLogDetail\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail\", b =>\n                {\n                    b.Navigation(\"Triggers\");\n                });\n\n            modelBuilder.Entity(\"AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger\", b =>\n                {\n                    b.Navigation(\"BlobTriggers\");\n\n                    b.Navigation(\"CronTriggers\");\n\n                    b.Navigation(\"SimplePropertyTriggers\");\n\n                    b.Navigation(\"SimpleTriggers\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/README.md",
    "content": "## Add new migration\n\n```bash\ncd ./src/Ray.BiliBiliTool.Web\ndotnet ef migrations add AddUser --project ../Ray.BiliBiliTool.Infrastructure.EF\n```\n\n## Remove migration\n\n```bash\ndotnet ef migrations remove --project ../Ray.BiliBiliTool.Infrastructure.EF\n```\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Infrastructure.EF/Ray.BiliBiliTool.Infrastructure.EF.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore.Design\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore.Sqlite\" />\n    <PackageReference Include=\"AppAny.Quartz.EntityFrameworkCore.Migrations.SQLite\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Domain\\Ray.BiliBiliTool.Domain.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Infrastructure\\Ray.BiliBiliTool.Infrastructure.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Auth/CustomAuthStateProvider.cs",
    "content": "using System.Security.Claims;\nusing Microsoft.AspNetCore.Components.Authorization;\n\nnamespace Ray.BiliBiliTool.Web.Auth;\n\npublic class CustomAuthStateProvider(IHttpContextAccessor httpContextAccessor)\n    : AuthenticationStateProvider\n{\n    public override Task<AuthenticationState> GetAuthenticationStateAsync()\n    {\n        var identity = new ClaimsIdentity();\n        var user = httpContextAccessor.HttpContext?.User;\n\n        if (user?.Identity?.IsAuthenticated == true)\n        {\n            identity = new ClaimsIdentity(user.Claims, \"Cookies\");\n        }\n\n        return Task.FromResult(new AuthenticationState(new ClaimsPrincipal(identity)));\n    }\n\n    public void NotifyAuthenticationStateChanged()\n    {\n        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/App.razor",
    "content": "﻿<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <base href=\"/\"/>\n    <link rel=\"stylesheet\" href=\"bootstrap/bootstrap.min.css\"/>\n    <link rel=\"stylesheet\" href=\"app.css\"/>\n    <link rel=\"stylesheet\" href=\"Ray.BiliBiliTool.Web.styles.css\"/>\n    <link rel=\"icon\" type=\"image/png\" href=\"favicon.png\"/>\n    <link href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap\" rel=\"stylesheet\" />\n    <link href=\"_content/MudBlazor/MudBlazor.min.css\" rel=\"stylesheet\" />\n    <HeadOutlet/>\n</head>\n\n<body>\n<Routes/>\n<script src=\"_framework/blazor.web.js\"></script>\n<script src=\"_content/MudBlazor/MudBlazor.min.js\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor",
    "content": "<h3>BlazingJob</h3>\n\n@code {\n    \n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor.cs",
    "content": "using BlazingQuartz.Core.Models;\nusing BlazingQuartz.Core.Services;\nusing BlazingQuartz.Jobs.Abstractions;\nusing Microsoft.AspNetCore.Components;\nusing MudBlazor;\nusing Ray.BiliBiliTool.Web.Services;\n\nnamespace Ray.BiliBiliTool.Web.Components.Comps;\n\npublic partial class BlazingJob : ComponentBase\n{\n    [Inject]\n    private ISchedulerDefinitionService SchedulerDefSvc { get; set; } = null!;\n\n    [Inject]\n    private ISchedulerService SchedulerSvc { get; set; } = null!;\n\n    [Inject]\n    private IDialogService DialogSvc { get; set; } = null!;\n\n    [Inject]\n    private ILogger<BlazingJob> Logger { get; set; } = null!;\n\n    [Inject]\n    private IJobUIProvider JobUIProvider { get; set; } = null!;\n\n    [Parameter]\n    [EditorRequired]\n    public JobDetailModel JobDetail { get; set; } = new();\n\n    [Parameter]\n    public bool IsReadOnly { get; set; } = false;\n\n    [Parameter]\n    public bool IsValid { get; set; }\n\n    [Parameter]\n    public EventCallback<bool> IsValidChanged { get; set; }\n\n    private Key OriginalJobKey = new(string.Empty, \"No Group\");\n\n    private IEnumerable<Type> AvailableJobTypes = Enumerable.Empty<Type>();\n    private IEnumerable<string>? ExistingJobGroups;\n    private MudForm _form = null!;\n    private Type? JobUIType = null;\n    private Dictionary<string, object> JobUITypeParameters = new();\n    private DynamicComponent? _jobUIComponent;\n\n    protected override async Task OnInitializedAsync()\n    {\n        var types = SchedulerDefSvc.GetJobTypes();\n        var typeList = new HashSet<Type>(types);\n        if (JobDetail.JobClass != null)\n        {\n            typeList.Add(JobDetail.JobClass);\n            await OnJobClassValueChanged(JobDetail.JobClass);\n        }\n        AvailableJobTypes = typeList;\n\n        OriginalJobKey = new(JobDetail.Name, JobDetail.Group);\n    }\n\n    async Task<IEnumerable<string>> SearchJobGroup(string value)\n    {\n        if (ExistingJobGroups == null)\n        {\n            ExistingJobGroups = await SchedulerSvc.GetJobGroups();\n        }\n\n        if (string.IsNullOrEmpty(value))\n            return ExistingJobGroups;\n\n        var matches = ExistingJobGroups\n            .Where(x => x.Contains(value, StringComparison.InvariantCultureIgnoreCase))\n            .ToList();\n\n        if (!matches.Any(x => x == value))\n            matches.Add(value);\n\n        return matches;\n    }\n\n    private void OnSetIsValid(bool value)\n    {\n        if (IsValid == value)\n            return;\n        IsValid = value;\n        IsValidChanged.InvokeAsync(value);\n    }\n\n    public async Task Validate()\n    {\n        var jobUI = _jobUIComponent?.Instance as IJobUI;\n\n        if (jobUI != null)\n        {\n            if (!await jobUI.ApplyChanges())\n            {\n                OnSetIsValid(false);\n                return;\n            }\n        }\n\n        await _form.Validate();\n    }\n\n    private async Task<string?> ValidateJobName(string name)\n    {\n        if (string.IsNullOrEmpty(name))\n            return \"Job name is required\";\n\n        // accept if same as original\n        if (OriginalJobKey.Equals(name, JobDetail.Group))\n            return null;\n\n        if (IsReadOnly)\n        {\n            Logger.LogDebug(\"Skip checking of job name uniqueness if in readonly mode\");\n            return null;\n        }\n\n        var detail = await SchedulerSvc.GetJobDetail(name, JobDetail.Group);\n\n        if (detail != null)\n            return \"Job name already in used. Please choose another name or group.\";\n\n        return null;\n    }\n\n    private async Task OnJobClassValueChanged(Type jobType)\n    {\n        JobDetail.JobClass = jobType;\n\n        // clear previous changes\n        var jobUI = _jobUIComponent?.Instance as IJobUI;\n        if (jobUI != null)\n            await jobUI.ClearChanges();\n\n        var jobUIType = JobUIProvider.GetJobUIType(jobType.FullName);\n        JobUITypeParameters.Clear();\n        JobUITypeParameters[nameof(IsReadOnly)] = IsReadOnly;\n        if (jobUIType == typeof(DefaultJobUI))\n            JobUITypeParameters[nameof(JobDetail)] = JobDetail;\n        else\n            JobUITypeParameters[nameof(JobDetail.JobDataMap)] = JobDetail.JobDataMap;\n        JobUIType = jobUIType;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor.css",
    "content": ""
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Comps/BlazingJob.razor.js",
    "content": "export class BlazingJob {\n  \n}\n\nwindow.BlazingJob = BlazingJob;"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor",
    "content": "<h3>BlazingTrigger</h3>\n\n@code {\n    \n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\n\nnamespace Ray.BiliBiliTool.Web.Components.Comps;\n\npublic partial class BlazingTrigger : ComponentBase\n{\n    public Task Validate()\n    {\n        throw new NotImplementedException();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor.css",
    "content": ""
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Comps/BlazingTrigger.razor.js",
    "content": "export class BlazingTrigger {\n  \n}\n\nwindow.BlazingTrigger = BlazingTrigger;"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Comps/DefaultJobUI.razor",
    "content": "<h3>DefaultJobUI</h3>\n\n@code {\n    \n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Comps/DefaultJobUI.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\n\nnamespace Ray.BiliBiliTool.Web.Components.Comps;\n\npublic partial class DefaultJobUI : ComponentBase { }\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Comps/ScheduleDialog.razor",
    "content": "@using BlazingQuartz.Core.Models\n@using BlazingQuartz.Core.Services\n@inject ISchedulerService SchSvc\n\n<MudDialog>\n    <DialogContent>\n        <MudButtonGroup OverrideStyles=\"false\" Class=\"pb-4\">\n            <MudButton Color=\"Color.Default\" Size=\"Size.Large\" DisableElevation=\"true\"\n                    Variant=\"@(SelectedTab == ScheduleDialogTab.Job ? Variant.Filled : Variant.Text)\"\n                    OnClick=\"() => OnSelectedTabChanged(ScheduleDialogTab.Job)\">Job Details</MudButton>\n            <MudButton Color=\"Color.Default\" Size=\"Size.Large\" DisableElevation=\"true\"\n                    Variant=\"@(SelectedTab == ScheduleDialogTab.Trigger ? Variant.Filled : Variant.Text)\"\n                    OnClick=\"() => OnSelectedTabChanged(ScheduleDialogTab.Trigger)\">Trigger Details</MudButton>\n        </MudButtonGroup>\n\n        @if (SelectedTab == ScheduleDialogTab.Job)\n        {\n            <BlazingJob @ref=\"_jobPanel\" JobDetail=\"JobDetail\" @bind-IsValid=\"_jobDetailIsValid\"\n                        IsReadOnly=\"IsReadOnlyJobDetail\"/>\n        }\n        else\n        {\n            <BlazingTrigger @ref=\"_triggerPanel\" TriggerDetail=\"TriggerDetail\" @bind-IsValid=\"_triggerDetailIsValid\" />\n        }\n    </DialogContent>\n    <DialogActions>\n        <div class=\"d-flex justify-space-between flex-grow-1 pa-3\">\n            <MudButton OnClick=\"OnCancel\" Variant=\"Variant.Filled\"\n                Style=\"width: 100px;\">Cancel</MudButton>\n            <div class=\"d-flex gap-4\">\n                <MudButton @ref=\"_backBtn\"\n                    Style=\"width: 100px;\"\n                    Disabled=\"@(SelectedTab == ScheduleDialogTab.Job)\"\n                    Color=\"Color.Secondary\"\n                    StartIcon=\"@Icons.Material.Filled.NavigateBefore\"\n                    Variant=\"Variant.Filled\"\n                    OnClick=\"OnBack\">Back</MudButton>\n                <MudButton Color=\"Color.Primary\"\n                    Style=\"width: 100px;\"\n                    OnClick=\"OnSubmit\"\n                    EndIcon=\"@_nextIcon\"\n                    Variant=\"Variant.Filled\">@_nextText</MudButton>\n            </div>\n        </div>\n\n\n    </DialogActions>\n</MudDialog>\n@code {\n    [CascadingParameter] MudDialog MudDialog { get; set; } = null!;\n    [Parameter] public JobDetailModel JobDetail { get; set; } = new();\n    [Parameter] public TriggerDetailModel TriggerDetail { get; set; } = new();\n    [Parameter] public bool IsReadOnlyJobDetail { get; set; } = false;\n    [Parameter] public ScheduleDialogTab SelectedTab { get; set; } = ScheduleDialogTab.Job;\n\n    private bool _jobDetailIsValid;\n    private bool _triggerDetailIsValid;\n\n    private MudButton _backBtn = null!;\n    private string _nextText = \"Next\";\n    private string? _nextIcon = Icons.Material.Filled.NavigateNext;\n\n    private BlazingJob _jobPanel = null!;\n    private BlazingTrigger _triggerPanel = null!;\n\n    protected override void OnInitialized()\n    {\n        if (SelectedTab == ScheduleDialogTab.Trigger)\n        {\n            _jobDetailIsValid = true;\n            _nextText = \"Save\";\n            _nextIcon = null;\n        }\n    }\n\n    private async Task OnSelectedTabChanged(ScheduleDialogTab tab)\n    {\n        if (SelectedTab == tab)\n            return;\n\n        // validate before change tab\n        if (SelectedTab == ScheduleDialogTab.Job)\n        {\n            await _jobPanel.Validate();\n            if (!_jobDetailIsValid)\n                return;\n        }\n\n        SelectedTab = tab;\n\n        // update text\n        if (SelectedTab == ScheduleDialogTab.Job)\n        {\n            _nextText = \"Next\";\n            _nextIcon = Icons.Material.Filled.NavigateNext;\n        }\n        else if (SelectedTab == ScheduleDialogTab.Trigger)\n        {\n            if (string.IsNullOrEmpty(TriggerDetail.Name) &&\n                !string.IsNullOrEmpty(JobDetail.Name))\n            {\n                // use job name as trigger name when trigger name not yet specified\n                // determine if trigger name can be used\n                var exists = await SchSvc.ContainsTriggerKey(JobDetail.Name, TriggerDetail.Group);\n                if (!exists)\n                    TriggerDetail.Name = JobDetail.Name;\n            }\n            _nextText = \"Save\";\n            _nextIcon = null;\n        }\n    }\n\n    async Task OnBack()\n    {\n        await OnSelectedTabChanged(ScheduleDialogTab.Job);\n    }\n\n    async Task OnSubmit()\n    {\n        if (SelectedTab == ScheduleDialogTab.Job)\n        {\n            await OnSelectedTabChanged(ScheduleDialogTab.Trigger);\n            return;\n        }\n\n        await _triggerPanel.Validate();\n\n        if (!_jobDetailIsValid || !_triggerDetailIsValid)\n        {\n            return;\n        }\n\n        await MudDialog.CloseAsync(DialogResult.Ok((JobDetail, TriggerDetail)));\n    }\n\n    void OnCancel() => MudDialog.CloseAsync();\n}\n\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Layout/MainLayout.razor",
    "content": "﻿@using Microsoft.AspNetCore.Components.Authorization\n@inherits LayoutComponentBase\n\n<div class=\"page\">\n    <div class=\"sidebar\">\n        <NavMenu/>\n    </div>\n\n    <main>\n        <div class=\"top-row px-4\">\n            <a href=\"https://github.com/RayWangQvQ/BiliBiliToolPro\" target=\"_blank\">About</a>\n            <AuthorizeView>\n                <Authorized>\n                    <a href=\"/auth/logout\" class=\"nav-link\">Log out</a>\n                </Authorized>\n            </AuthorizeView>\n        </div>\n\n        <article class=\"content px-4\">\n            @Body\n        </article>\n    </main>\n</div>\n\n<div id=\"blazor-error-ui\">\n    An unhandled error has occurred.\n    <a href=\"\" class=\"reload\">Reload</a>\n    <a class=\"dismiss\">🗙</a>\n</div>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Layout/MainLayout.razor.css",
    "content": ".page {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n}\n\nmain {\n    flex: 1;\n}\n\n.sidebar {\n    background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);\n}\n\n.top-row {\n    background-color: #f7f7f7;\n    border-bottom: 1px solid #d6d5d5;\n    justify-content: flex-end;\n    height: 3.5rem;\n    display: flex;\n    align-items: center;\n}\n\n    .top-row ::deep a, .top-row ::deep .btn-link {\n        white-space: nowrap;\n        margin-left: 1.5rem;\n        text-decoration: none;\n    }\n\n    .top-row ::deep a:hover, .top-row ::deep .btn-link:hover {\n        text-decoration: underline;\n    }\n\n    .top-row ::deep a:first-child {\n        overflow: hidden;\n        text-overflow: ellipsis;\n    }\n\n@media (max-width: 640.98px) {\n    .top-row {\n        justify-content: space-between;\n    }\n\n    .top-row ::deep a, .top-row ::deep .btn-link {\n        margin-left: 0;\n    }\n}\n\n@media (min-width: 641px) {\n    .page {\n        flex-direction: row;\n    }\n\n    .sidebar {\n        width: 250px;\n        height: 100vh;\n        position: sticky;\n        top: 0;\n    }\n\n    .top-row {\n        position: sticky;\n        top: 0;\n        z-index: 1;\n    }\n\n    .top-row.auth ::deep a:first-child {\n        flex: 1;\n        text-align: right;\n        width: 0;\n    }\n\n    .top-row, article {\n        padding-left: 2rem !important;\n        padding-right: 1.5rem !important;\n    }\n}\n\n#blazor-error-ui {\n    background: lightyellow;\n    bottom: 0;\n    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);\n    display: none;\n    left: 0;\n    padding: 0.6rem 1.25rem 0.7rem 1.25rem;\n    position: fixed;\n    width: 100%;\n    z-index: 1000;\n}\n\n    #blazor-error-ui .dismiss {\n        cursor: pointer;\n        position: absolute;\n        right: 0.75rem;\n        top: 0.5rem;\n    }\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Layout/NavMenu.razor",
    "content": "﻿@rendermode InteractiveServer\n\n<div class=\"top-row ps-3 navbar navbar-dark\">\n    <div class=\"container-fluid\">\n        <a class=\"navbar-brand\" href=\"\">\n            <img src=\"/favicon.png\" alt=\"Logo\" class=\"navbar-logo\" />\n            BiliTool\n        </a>\n    </div>\n</div>\n\n<input type=\"checkbox\" title=\"Navigation menu\" class=\"navbar-toggler\" id=\"navToggler\"/>\n\n<div class=\"nav-scrollable\">\n    <nav class=\"flex-column\">\n        <div class=\"nav-item px-3\">\n            <NavLink class=\"nav-link\" href=\"\" Match=\"NavLinkMatch.All\">\n                <span class=\"bi bi-house-door-fill-nav-menu\" aria-hidden=\"true\"></span> Home\n            </NavLink>\n        </div>\n\n        <div class=\"nav-item px-3\">\n            <NavLink class=\"nav-link\" href=\"Schedules\">\n                <span class=\"bi bi-calendar-nav-menu\" aria-hidden=\"true\"></span> Schedules\n            </NavLink>\n        </div>\n\n        <div class=\"nav-item px-3\">\n            <div @onclick=\"@(() => { _showConfigSubMenu = !_showConfigSubMenu; })\"\n                 class=\"nav-link d-flex justify-content-between align-items-center\">\n                <div>\n                    <span class=\"bi bi-sliders me-1\" aria-hidden=\"true\"></span> Configurations\n                </div>\n                <span class=\"bi @(_showConfigSubMenu ? \"bi-chevron-down\" : \"bi-chevron-right\")\" aria-hidden=\"true\"></span>\n            </div>\n        </div>\n\n        @if (_showConfigSubMenu)\n        {\n            <div class=\"submenu-container\">\n                <div class=\"nav-item px-3 ms-4 submenu-item\">\n                    <NavLink class=\"nav-link\" href=\"Configurations/DailyJobConfig\">\n                        <span class=\"bi bi-coin\" aria-hidden=\"true\"></span> Daily Job\n                    </NavLink>\n                </div>\n\n                <div class=\"nav-item px-3 ms-4 submenu-item\">\n                    <NavLink class=\"nav-link\" href=\"Configurations/MangaTaskConfig\">\n                        <span class=\"bi bi-book\" aria-hidden=\"true\"></span> Manga Task\n                    </NavLink>\n                </div>\n\n                <div class=\"nav-item px-3 ms-4 submenu-item\">\n                    <NavLink class=\"nav-link\" href=\"Configurations/MangaPrivilegeTaskConfig\">\n                        <span class=\"bi bi-book-half\" aria-hidden=\"true\"></span> Manga Privilege\n                    </NavLink>\n                </div>\n\n                <div class=\"nav-item px-3 ms-4 submenu-item\">\n                    <NavLink class=\"nav-link\" href=\"Configurations/Silver2CoinTaskConfig\">\n                        <span class=\"bi bi-currency-exchange\" aria-hidden=\"true\"></span> Silver2Coin\n                    </NavLink>\n                </div>\n\n                <div class=\"nav-item px-3 ms-4 submenu-item\">\n                    <NavLink class=\"nav-link\" href=\"Configurations/ChargeTaskConfig\">\n                        <span class=\"bi bi-lightning-charge\" aria-hidden=\"true\"></span> Charge Task\n                    </NavLink>\n                </div>\n\n                <div class=\"nav-item px-3 ms-4 submenu-item\">\n                    <NavLink class=\"nav-link\" href=\"Configurations/VipPrivilegeConfig\">\n                        <span class=\"bi bi-star\" aria-hidden=\"true\"></span> VIP Privilege\n                    </NavLink>\n                </div>\n\n                <div class=\"nav-item px-3 ms-4 submenu-item\">\n                    <NavLink class=\"nav-link\" href=\"Configurations/VipBigPointConfig\">\n                        <span class=\"bi bi-award\" aria-hidden=\"true\"></span> VIP Big Point\n                    </NavLink>\n                </div>\n\n                <div class=\"nav-item px-3 ms-4 submenu-item\">\n                    <NavLink class=\"nav-link\" href=\"Configurations/LiveLotteryTaskConfig\">\n                        <span class=\"bi bi-gift\" aria-hidden=\"true\"></span> Live Lottery\n                    </NavLink>\n                </div>\n\n                <div class=\"nav-item px-3 ms-4 submenu-item\">\n                    <NavLink class=\"nav-link\" href=\"Configurations/LiveFansMedalTaskConfig\">\n                        <span class=\"bi bi-badge-cc\" aria-hidden=\"true\"></span> Fans Medal\n                    </NavLink>\n                </div>\n\n                <div class=\"nav-item px-3 ms-4 submenu-item\">\n                    <NavLink class=\"nav-link\" href=\"Configurations/UnfollowBatchedTaskConfig\">\n                        <span class=\"bi bi-person-dash\" aria-hidden=\"true\"></span> Unfollow Batch\n                    </NavLink>\n                </div>\n            </div>\n        }\n\n        <div class=\"nav-item px-3\">\n            <NavLink class=\"nav-link\" href=\"Admin\">\n                <span class=\"bi bi-person-circle\" aria-hidden=\"true\"></span> Admin\n            </NavLink>\n        </div>\n    </nav>\n</div>\n\n@code {\n    private bool _showConfigSubMenu;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Layout/NavMenu.razor.css",
    "content": ".navbar-toggler {\n    appearance: none;\n    cursor: pointer;\n    width: 3.5rem;\n    height: 2.5rem;\n    color: white;\n    position: absolute;\n    top: 0.5rem;\n    right: 1rem;\n    border: 1px solid rgba(255, 255, 255, 0.1);\n    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);\n}\n\n.navbar-toggler:checked {\n    background-color: rgba(255, 255, 255, 0.5);\n}\n\n.top-row {\n    height: 3.5rem;\n    background-color: rgba(0,0,0,0.4);\n}\n\n.navbar-brand {\n    font-size: 1.1rem;\n}\n\n.navbar-logo {\n    height: 40px;\n    margin-right: 10px;\n}\n\n.bi {\n    display: inline-block;\n    position: relative;\n    width: 1.25rem;\n    height: 1.25rem;\n    margin-right: 0.75rem;\n    top: -1px;\n    background-size: cover;\n}\n\n.bi-house-door-fill-nav-menu {\n    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\");\n}\n\n.bi-plus-square-fill-nav-menu {\n    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\");\n}\n\n.bi-list-nested-nav-menu {\n    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\");\n}\n\n.bi-clock-nav-menu {\n    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\");\n}\n\n.bi-calendar-nav-menu {\n    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\");\n}\n\n.bi-person-circle {\n    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\");\n}\n\n.bi-sliders {\n    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\");\n}\n\n.bi-coin {\n    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\");\n}\n\n.bi-book {\n    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\");\n}\n\n.bi-book-half {\n    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\");\n}\n\n.bi-currency-exchange {\n    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\");\n}\n\n.bi-lightning-charge {\n    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\");\n}\n\n.bi-star {\n    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\");\n}\n\n.bi-award {\n    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\");\n}\n\n.bi-gift {\n    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\");\n}\n\n.bi-badge-cc {\n    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\");\n}\n\n.bi-person-dash {\n    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\");\n}\n\n.bi-chevron-down {\n    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\");\n}\n\n.bi-chevron-right {\n    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\");\n}\n\n.nav-item {\n    font-size: 0.9rem;\n    padding-bottom: 0.5rem;\n}\n\n    .nav-item:first-of-type {\n        padding-top: 1rem;\n    }\n\n    .nav-item:last-of-type {\n        padding-bottom: 1rem;\n    }\n\n    .nav-item ::deep .nav-link {\n        color: #d7d7d7;\n        background: none;\n        border: none;\n        border-radius: 4px;\n        height: 3rem;\n        display: flex;\n        align-items: center;\n        line-height: 3rem;\n        width: 100%;\n    }\n\n.nav-item ::deep a.active {\n    background-color: rgba(255,255,255,0.37);\n    color: white;\n}\n\n.nav-item ::deep .nav-link:hover {\n    background-color: rgba(255,255,255,0.1);\n    color: white;\n}\n\n.nav-scrollable {\n    display: none;\n}\n\n.navbar-toggler:checked ~ .nav-scrollable {\n    display: block;\n}\n\n.submenu-container {\n    background-color: rgba(0, 0, 0, 0.1);\n    margin-bottom: 0.5rem;\n    border-radius: 0 0 4px 4px;\n    padding: 0.5rem 0;\n}\n\n.submenu-item {\n    position: relative;\n    padding-left: 1rem !important;\n}\n\n@media (min-width: 641px) {\n    .navbar-toggler {\n        display: none;\n    }\n\n    .nav-scrollable {\n        /* Never collapse the sidebar for wide screens */\n        display: block;\n\n        /* Allow sidebar to scroll for tall menus */\n        height: calc(100vh - 3.5rem);\n        overflow-y: auto;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor",
    "content": "@page \"/Admin\"\n@using Microsoft.AspNetCore.Authorization\n@rendermode InteractiveServer\n@attribute [Authorize]\n\n<PageTitle>Change Password</PageTitle>\n\n<MudText Typo=\"Typo.h5\">Change Password</MudText>\n\n<MudCard Class=\"mx-auto my-4\" Style=\"max-width: 500px;\">\n    <MudCardContent>\n        @if (!string.IsNullOrEmpty(_errorMessage))\n        {\n            <MudAlert Severity=\"Severity.Error\" Class=\"mb-3\">@_errorMessage</MudAlert>\n        }\n        @if (!string.IsNullOrEmpty(_successMessage))\n        {\n            <MudAlert Severity=\"Severity.Success\" Class=\"mb-3\">@_successMessage</MudAlert>\n        }\n        <MudTextField @bind-Value=\"_username\" Label=\"New User Name\" Required=\"true\" RequiredError=\"Username cannot be empty\" />\n        <MudTextField @bind-Value=\"_currentPassword\" Label=\"Current Password\" Required=\"true\" RequiredError=\"Password cannot be empty\"\n                      InputType=\"@_currentPasswordInput\" Adornment=\"Adornment.End\"\n                      AdornmentIcon=\"@_currentPasswordInputIcon\" OnAdornmentClick=\"ToggleCurrentPasswordVisibility\" />\n        <MudTextField @bind-Value=\"_newPassword\" Label=\"New Password\" Required=\"true\" RequiredError=\"Password cannot be empty\"\n                      InputType=\"@_passwordInput\" Adornment=\"Adornment.End\"\n                      AdornmentIcon=\"@_passwordInputIcon\" OnAdornmentClick=\"TogglePasswordVisibility\" />\n        <MudTextField @bind-Value=\"_confirmPassword\" Label=\"Confirm Password\" Required=\"true\" RequiredError=\"Password cannot be empty\"\n                      InputType=\"@_passwordInput\" Adornment=\"Adornment.End\"\n                      AdornmentIcon=\"@_passwordInputIcon\" OnAdornmentClick=\"TogglePasswordVisibility\" />\n    </MudCardContent>\n    <MudCardActions>\n        <MudButton Variant=\"Variant.Filled\" Color=\"Color.Primary\" OnClick=\"ChangePasswordAsync\"\n                   Class=\"ml-auto\">Submit</MudButton>\n    </MudCardActions>\n</MudCard>\n\n<MudThemeProvider/>\n<MudDialogProvider/>\n<MudSnackbarProvider/>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\nusing MudBlazor;\nusing Ray.BiliBiliTool.Web.Services;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages;\n\npublic partial class Admin : ComponentBase\n{\n    [Inject]\n    private NavigationManager NavigationManager { get; set; } = null!;\n\n    [Inject]\n    private IAuthService AuthService { get; set; } = null!;\n\n    private string _username = \"\";\n    private string _currentPassword = \"\";\n    private string _newPassword = \"\";\n    private string _confirmPassword = \"\";\n    private string _errorMessage = \"\";\n    private string _successMessage = \"\";\n\n    private bool _passwordVisibility;\n    private InputType _passwordInput = InputType.Password;\n    private string _passwordInputIcon = Icons.Material.Filled.VisibilityOff;\n\n    private bool _currentPasswordVisibility;\n    private InputType _currentPasswordInput = InputType.Password;\n    private string _currentPasswordInputIcon = Icons.Material.Filled.VisibilityOff;\n\n    private void TogglePasswordVisibility()\n    {\n        if (_passwordVisibility)\n        {\n            _passwordVisibility = false;\n            _passwordInputIcon = Icons.Material.Filled.VisibilityOff;\n            _passwordInput = InputType.Password;\n        }\n        else\n        {\n            _passwordVisibility = true;\n            _passwordInputIcon = Icons.Material.Filled.Visibility;\n            _passwordInput = InputType.Text;\n        }\n    }\n\n    private void ToggleCurrentPasswordVisibility()\n    {\n        if (_currentPasswordVisibility)\n        {\n            _currentPasswordVisibility = false;\n            _currentPasswordInputIcon = Icons.Material.Filled.VisibilityOff;\n            _currentPasswordInput = InputType.Password;\n        }\n        else\n        {\n            _currentPasswordVisibility = true;\n            _currentPasswordInputIcon = Icons.Material.Filled.Visibility;\n            _currentPasswordInput = InputType.Text;\n        }\n    }\n\n    protected override async Task OnInitializedAsync()\n    {\n        _username = await AuthService.GetAdminUserNameAsync();\n    }\n\n    private async Task ChangePasswordAsync()\n    {\n        _errorMessage = \"\";\n        _successMessage = \"\";\n\n        if (_newPassword != _confirmPassword)\n        {\n            _errorMessage = \"The new password and the confirm password do not match\";\n            return;\n        }\n\n        if (string.IsNullOrWhiteSpace(_newPassword))\n        {\n            _errorMessage = \"Password cannot be empty\";\n            return;\n        }\n\n        try\n        {\n            await AuthService.ChangePasswordAsync(_username, _currentPassword, _newPassword);\n            _successMessage = \"Update Successful, you will be logged out in 2 seconds\";\n            await Task.Delay(2000);\n            _currentPassword = \"\";\n            _newPassword = \"\";\n            _confirmPassword = \"\";\n            NavigationManager.NavigateTo(\"/auth/logout\", true);\n        }\n        catch (Exception e)\n        {\n            _errorMessage = e.Message;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor.css",
    "content": ""
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Admin.razor.js",
    "content": "export class Admin {\n  \n}\n\nwindow.Admin = Admin;"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/BaseConfigComponent.cs",
    "content": "using BlazingQuartz.Core.Services;\nusing Microsoft.AspNetCore.Components;\nusing Microsoft.Extensions.Options;\nusing Quartz;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Config.SQLite;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Configs;\n\npublic abstract class BaseConfigComponent<T> : ComponentBase\n    where T : BaseConfigOptions, new()\n{\n    [Inject]\n    protected IConfiguration Configuration { get; set; } = null!;\n\n    [Inject]\n    protected ISchedulerService? SchedulerService { get; set; }\n\n    [Inject]\n    protected ISchedulerFactory? SchedulerFactory { get; set; }\n\n    [Inject]\n    protected ILogger<BaseConfigComponent<T>> Logger { get; set; } = null!;\n\n    protected T _config = new();\n    protected bool _isLoading = true;\n    protected MarkupString? _saveMessage;\n    protected bool _saveSuccess;\n\n    protected abstract IOptionsMonitor<T> OptionsMonitor { get; }\n\n    /// <summary>\n    /// 获取对应的任务JobKey，如果返回null则不控制定时任务\n    /// </summary>\n    protected virtual JobKey? GetJobKey() => null;\n\n    /// <summary>\n    /// 获取触发器名称\n    /// </summary>\n    protected virtual string GetTriggerName(JobKey jobKey) => $\"{jobKey}.Cron.Trigger\";\n\n    protected override async Task OnInitializedAsync()\n    {\n        await LoadConfigAsync();\n    }\n\n    protected Task LoadConfigAsync()\n    {\n        _isLoading = true;\n        _saveMessage = null;\n\n        try\n        {\n            _config = OptionsMonitor.CurrentValue;\n        }\n        catch (Exception ex)\n        {\n            _saveMessage = new MarkupString($\"Failed to load configuration: {ex.Message}\");\n            _saveSuccess = false;\n        }\n        finally\n        {\n            _isLoading = false;\n            StateHasChanged();\n        }\n\n        return Task.CompletedTask;\n    }\n\n    protected virtual async Task HandleValidSubmitAsync()\n    {\n        _isLoading = true;\n        _saveMessage = null;\n\n        try\n        {\n            // 保存配置\n            var sqliteProvider = GetSqliteConfigurationProvider();\n            if (sqliteProvider == null)\n            {\n                throw new InvalidOperationException(\"Unable to get SqliteConfigurationProvider\");\n            }\n\n            var configValues = _config.ToConfigDictionary();\n            sqliteProvider.BatchSet(configValues);\n\n            // 如果有对应的定时任务，同步更新 Quartz 任务状态和 Cron 表达式\n            var jobKey = GetJobKey();\n            if (jobKey != null && SchedulerService != null)\n            {\n                // 更新 Cron 表达式\n                await UpdateJobCronAsync(jobKey, _config.Cron);\n\n                // 控制任务启停\n                await ControlScheduledJobAsyc(jobKey, _config.IsEnable);\n            }\n\n            _saveMessage = GetSaveSuccessMessage();\n            _saveSuccess = true;\n        }\n        catch (Exception ex)\n        {\n            _saveMessage = new MarkupString($\"Failed to save configuration: {ex.Message}\");\n            _saveSuccess = false;\n        }\n        finally\n        {\n            _isLoading = false;\n            StateHasChanged();\n        }\n    }\n\n    private async Task ControlScheduledJobAsyc(JobKey jobKey, bool isEnable)\n    {\n        var triggerName = GetTriggerName(jobKey);\n        var triggerGroup = Constants.BiliJobGroup;\n\n        if (isEnable)\n        {\n            // 启用任务：恢复触发器\n            await SchedulerService!.ResumeTrigger(triggerName, triggerGroup);\n        }\n        else\n        {\n            // 禁用任务：暂停触发器\n            await SchedulerService!.PauseTrigger(triggerName, triggerGroup);\n        }\n    }\n\n    private async Task UpdateJobCronAsync(JobKey jobKey, string? cronExpression)\n    {\n        if (string.IsNullOrWhiteSpace(cronExpression) || SchedulerFactory == null)\n            return;\n\n        var triggerName = GetTriggerName(jobKey);\n        var triggerKey = new TriggerKey(triggerName, Constants.BiliJobGroup);\n\n        try\n        {\n            var scheduler = await SchedulerFactory.GetScheduler();\n\n            // 创建新的 Cron 触发器\n            var newTrigger = TriggerBuilder\n                .Create()\n                .WithIdentity(triggerKey)\n                .ForJob(jobKey)\n                .WithCronSchedule(cronExpression)\n                .Build();\n\n            // 重新调度触发器（替换现有的触发器）\n            await scheduler.RescheduleJob(triggerKey, newTrigger);\n        }\n        catch (Exception ex)\n        {\n            Logger.LogError(ex, \"Failed to update cron expression for job {JobKey}\", jobKey);\n        }\n    }\n\n    private MarkupString GetSaveSuccessMessage()\n    {\n        var jobKey = GetJobKey();\n        if (jobKey == null)\n        {\n            return new MarkupString(\"Configuration saved successfully!\");\n        }\n\n        var status = _config.IsEnable ? \"enabled\" : \"disabled\";\n        return new MarkupString(\n            $\"Configuration saved successfully!<br/>{jobKey} has been {status}.\"\n        );\n    }\n\n    private SqliteConfigurationProvider? GetSqliteConfigurationProvider()\n    {\n        if (Configuration is IConfigurationRoot configRoot)\n        {\n            foreach (var provider in configRoot.Providers)\n            {\n                if (provider is SqliteConfigurationProvider sqliteProvider)\n                {\n                    return sqliteProvider;\n                }\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/ChargeTaskConfig.razor",
    "content": "@page \"/Configurations/ChargeTaskConfig\"\n@using Microsoft.AspNetCore.Authorization\n@using Ray.BiliBiliTool.Config.Options;\n@attribute [Authorize]\n@rendermode InteractiveServer\n@inherits BaseConfigComponent<ChargeTaskOptions>\n\n<PageTitle>免费B币券充电任务配置</PageTitle>\n\n<MudContainer>\n    <MudText Typo=\"Typo.h4\" Class=\"mb-4\">免费B币券充电任务配置</MudText>\n\n    @if (_isLoading)\n    {\n        <MudProgressCircular Color=\"Color.Primary\" Indeterminate=\"true\" />\n    }\n    else\n    {\n        <EditForm Model=\"@_config\" OnValidSubmit=\"HandleValidSubmitAsync\">\n            <DataAnnotationsValidator />\n\n            <!-- 基础配置卡片 -->\n            <MudCard Class=\"mb-4\">\n                <MudCardHeader>\n                    <CardHeaderContent>\n                        <MudText Typo=\"Typo.h6\">基础配置</MudText>\n                    </CardHeaderContent>\n                </MudCardHeader>\n                <MudCardContent>\n                    <MudGrid>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudSwitch T=\"bool\" @bind-Value=\"_config.IsEnable\"\n                                Label=\"启用任务\" Color=\"Color.Primary\" />\n                        </MudItem>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudTextField T=\"string\" @bind-Value=\"_config.Cron\" Label=\"定时任务Cron表达式\"\n                                HelperText=\"例如：0 0 12 * * ? (每天中午12点执行)\"\n                                Disabled=\"!_config.IsEnable\" />\n                        </MudItem>\n                    </MudGrid>\n                </MudCardContent>\n            </MudCard>\n\n            <!-- 详细配置卡片 -->\n            @if (_config.IsEnable)\n            {\n                <MudCard>\n                    <MudCardHeader>\n                        <CardHeaderContent>\n                            <MudText Typo=\"Typo.h6\">详细配置</MudText>\n                        </CardHeaderContent>\n                    </MudCardHeader>\n                    <MudCardContent>\n                        <MudGrid>\n                            <MudItem xs=\"12\" md=\"6\">\n                                <MudTextField T=\"string\" @bind-Value=\"_config.ChargeComment\" Label=\"充电后留言\"\n                                              HelperText=\"为空时将随机选择一条默认留言\" />\n                            </MudItem>\n\n                            <MudItem xs=\"12\" md=\"6\">\n                                <MudSwitch T=\"bool\" Value=\"_isSpecifyUpToggled\" ValueChanged=\"OnSpecifyUpToggled\"\n                                           Label=\"指定其他UP\" Color=\"Color.Primary\"\n                                           HelperText=\"开启后可以指定充电给其他UP主\" />\n                                @if (_isSpecifyUpToggled)\n                                {\n                                    <div>\n                                        <div style=\"display: flex; align-items: end; gap: 8px;\">\n                                            <MudTextField T=\"string\" @bind-Value=\"_config.AutoChargeUpId\" Label=\"充电Up主Id\"\n                                                          HelperText=\"阿B最新策略已不允许为自己充电了，可以填自己小号id，或者支持作者的。开源不易，感谢支持~\" Class=\"mt-3\" style=\"flex: 1;\"/>\n                                            <MudButton Variant=\"Variant.Outlined\" Color=\"Color.Success\" Size=\"Size.Small\"\n                                                       OnClick=\"() => SetSupportAuthor()\" Class=\"mt-3\" style=\"white-space: nowrap;\">\n                                                支持作者\n                                            </MudButton>\n                                        </div>\n                                    </div>\n                                }\n                            </MudItem>\n                        </MudGrid>\n                    </MudCardContent>\n                </MudCard>\n            }\n\n            <!-- 操作按钮 -->\n            <MudCard Class=\"mt-4\">\n                <MudCardActions>\n                    <MudButton ButtonType=\"ButtonType.Submit\" Variant=\"Variant.Filled\" Color=\"Color.Primary\">保存配置</MudButton>\n                    <MudButton Variant=\"Variant.Outlined\" Color=\"Color.Secondary\" OnClick=\"LoadConfigAsync\" Class=\"ml-2\">重新加载</MudButton>\n                </MudCardActions>\n            </MudCard>\n        </EditForm>\n\n        @if (_saveMessage.HasValue)\n        {\n            <MudAlert Severity=\"@(_saveSuccess ? Severity.Success : Severity.Error)\" Class=\"mt-3\">\n                @_saveMessage\n            </MudAlert>\n        }\n    }\n</MudContainer>\n\n<MudThemeProvider/>\n<MudPopoverProvider/>\n<MudDialogProvider/>\n<MudSnackbarProvider/>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/ChargeTaskConfig.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\nusing Microsoft.Extensions.Options;\nusing Quartz;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Web.Jobs;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Configs;\n\npublic partial class ChargeTaskConfig : BaseConfigComponent<ChargeTaskOptions>\n{\n    [Inject]\n    private IOptionsMonitor<ChargeTaskOptions> ChargeTaskOptionsMonitor { get; set; } = null!;\n\n    protected override IOptionsMonitor<ChargeTaskOptions> OptionsMonitor =>\n        ChargeTaskOptionsMonitor;\n\n    private bool _isSpecifyUpToggled;\n\n    protected override Task OnInitializedAsync()\n    {\n        if (!string.IsNullOrWhiteSpace(OptionsMonitor.CurrentValue.AutoChargeUpId))\n        {\n            _isSpecifyUpToggled = true;\n        }\n        return base.OnInitializedAsync();\n    }\n\n    private void OnSpecifyUpToggled(bool isSpecified)\n    {\n        _isSpecifyUpToggled = !_isSpecifyUpToggled;\n        StateHasChanged();\n    }\n\n    private Task SetSupportAuthor()\n    {\n        _config.AutoChargeUpId = \"-1\";\n        return Task.CompletedTask;\n    }\n\n    protected override JobKey GetJobKey() => ChargeJob.Key;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor",
    "content": "@page \"/Configurations/DailyJobConfig\"\n@using Microsoft.AspNetCore.Authorization\n@using Ray.BiliBiliTool.Config.Options;\n@attribute [Authorize]\n@rendermode InteractiveServer\n@inherits BaseConfigComponent<DailyTaskOptions>\n\n<PageTitle>每日任务配置</PageTitle>\n\n<MudContainer>\n    <MudText Typo=\"Typo.h4\" Class=\"mb-4\">每日任务配置</MudText>\n\n    @if (_isLoading)\n    {\n        <MudProgressCircular Color=\"Color.Primary\" Indeterminate=\"true\" />\n    }\n    else\n    {\n        <EditForm Model=\"@_config\" OnValidSubmit=\"HandleValidSubmitAsync\">\n            <DataAnnotationsValidator />\n\n            <!-- 基础配置卡片 -->\n            <MudCard Class=\"mb-4\">\n                <MudCardHeader>\n                    <CardHeaderContent>\n                        <MudText Typo=\"Typo.h6\">基础配置</MudText>\n                    </CardHeaderContent>\n                </MudCardHeader>\n                <MudCardContent>\n                    <MudGrid>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudSwitch T=\"bool\" @bind-Value=\"_config.IsEnable\"\n                                Label=\"启用任务\" Color=\"Color.Primary\" />\n                        </MudItem>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudTextField T=\"string\" @bind-Value=\"_config.Cron\" Label=\"定时任务Cron表达式\"\n                                HelperText=\"例如：0 0 6 * * ? (每天早上6点执行)\"\n                                Disabled=\"!_config.IsEnable\" />\n                        </MudItem>\n                    </MudGrid>\n                </MudCardContent>\n            </MudCard>\n\n            <!-- 详细配置卡片 -->\n            @if (_config.IsEnable)\n            {\n                <MudCard>\n                    <MudCardHeader>\n                        <CardHeaderContent>\n                            <MudText Typo=\"Typo.h6\">详细配置</MudText>\n                        </CardHeaderContent>\n                    </MudCardHeader>\n                    <MudCardContent>\n                        <MudGrid>\n                            <MudItem xs=\"12\" md=\"6\">\n                                <MudSwitch T=\"bool\" @bind-Value=\"_config.IsWatchVideo\"\n                                    Label=\"是否观看视频\" Color=\"Color.Primary\" />\n\n                                <MudSwitch T=\"bool\" @bind-Value=\"_config.IsShareVideo\"\n                                    Label=\"是否分享视频\" Color=\"Color.Primary\" Class=\"mt-3\" />\n\n                                <MudSwitch T=\"bool\" @bind-Value=\"_config.IsDonateCoinForArticle\"\n                                    Label=\"是否开启专栏投币\" Color=\"Color.Primary\" Class=\"mt-3\" />\n\n                                <MudNumericField T=\"int\" @bind-Value=\"_config.NumberOfCoins\" Label=\"每日投币数 [0-5]\"\n                                    Min=\"0\" Max=\"5\" Class=\"mt-3\" />\n\n                                <MudNumericField T=\"int\" @bind-Value=\"_config.NumberOfProtectedCoins\" Label=\"要保留的硬币数量\"\n                                    Min=\"0\" Class=\"mt-3\" />\n                            </MudItem>\n\n                            <MudItem xs=\"12\" md=\"6\">\n                                <MudSwitch T=\"bool\" @bind-Value=\"_config.SaveCoinsWhenLv6\"\n                                    Label=\"达到六级后开始白嫖\" Color=\"Color.Primary\" />\n\n                                <MudSwitch T=\"bool\" @bind-Value=\"_config.SelectLike\"\n                                    Label=\"投币时点赞\" Color=\"Color.Primary\" Class=\"mt-3\" />\n\n                                <MudTextField T=\"string\" @bind-Value=\"_config.SupportUpIds\" Label=\"支持的up主Id集合（用逗号分隔）\"\n                                    HelperText=\"配置后会优先从指定的up主下挑选视频进行观看、分享和投币\" Class=\"mt-3\" />\n\n                                <MudSelect T=\"string\" @bind-Value=\"_config.DevicePlatform\" Label=\"执行客户端操作时的平台\" Class=\"mt-3\">\n                                    <MudSelectItem Value=\"@(\"android\")\">Android</MudSelectItem>\n                                    <MudSelectItem Value=\"@(\"ios\")\">iOS</MudSelectItem>\n                                </MudSelect>\n                            </MudItem>\n                        </MudGrid>\n                    </MudCardContent>\n                </MudCard>\n            }\n\n            <!-- 操作按钮 -->\n            <MudCard Class=\"mt-4\">\n                <MudCardActions>\n                    <MudButton ButtonType=\"ButtonType.Submit\" Variant=\"Variant.Filled\" Color=\"Color.Primary\">保存配置</MudButton>\n                    <MudButton Variant=\"Variant.Outlined\" Color=\"Color.Secondary\" OnClick=\"LoadConfigAsync\" Class=\"ml-2\">重新加载</MudButton>\n                </MudCardActions>\n            </MudCard>\n        </EditForm>\n\n        @if (_saveMessage.HasValue)\n        {\n            <MudAlert Severity=\"@(_saveSuccess ? Severity.Success : Severity.Error)\" Class=\"mt-3\">\n                @_saveMessage\n            </MudAlert>\n        }\n    }\n</MudContainer>\n\n<MudThemeProvider/>\n<MudPopoverProvider/>\n<MudDialogProvider/>\n<MudSnackbarProvider/>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\nusing Microsoft.Extensions.Options;\nusing Quartz;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Web.Jobs;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Configs;\n\npublic partial class DailyJobConfig : BaseConfigComponent<DailyTaskOptions>\n{\n    [Inject]\n    private IOptionsMonitor<DailyTaskOptions> DailyTaskOptionsMonitor { get; set; } = null!;\n\n    protected override IOptionsMonitor<DailyTaskOptions> OptionsMonitor => DailyTaskOptionsMonitor;\n\n    protected override JobKey GetJobKey() => DailyJob.Key;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor.css",
    "content": ""
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/DailyJobConfig.razor.js",
    "content": "export class DailyJobConfig {\n  \n}\n\nwindow.DailyJobConfig = DailyJobConfig;"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveFansMedalTaskConfig.razor",
    "content": "@page \"/Configurations/LiveFansMedalTaskConfig\"\n@using Microsoft.AspNetCore.Authorization\n@using Ray.BiliBiliTool.Config.Options;\n@attribute [Authorize]\n@rendermode InteractiveServer\n@inherits BaseConfigComponent<LiveFansMedalTaskOptions>\n\n<PageTitle>直播粉丝牌任务配置</PageTitle>\n\n<MudContainer>\n    <MudText Typo=\"Typo.h4\" Class=\"mb-4\">直播粉丝牌任务配置</MudText>\n\n    @if (_isLoading)\n    {\n        <MudProgressCircular Color=\"Color.Primary\" Indeterminate=\"true\" />\n    }\n    else\n    {\n        <EditForm Model=\"@_config\" OnValidSubmit=\"HandleValidSubmitAsync\">\n            <DataAnnotationsValidator />\n\n            <!-- 基础配置卡片 -->\n            <MudCard Class=\"mb-4\">\n                <MudCardHeader>\n                    <CardHeaderContent>\n                        <MudText Typo=\"Typo.h6\">基础配置</MudText>\n                    </CardHeaderContent>\n                </MudCardHeader>\n                <MudCardContent>\n                    <MudGrid>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudSwitch T=\"bool\" @bind-Value=\"_config.IsEnable\"\n                                Label=\"启用任务\" Color=\"Color.Primary\" />\n                        </MudItem>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudTextField T=\"string\" @bind-Value=\"_config.Cron\" Label=\"定时任务Cron表达式\"\n                                HelperText=\"例如：0 5 0 * * ? (每天凌晨0点5分执行)\"\n                                Disabled=\"!_config.IsEnable\" />\n                        </MudItem>\n                    </MudGrid>\n                </MudCardContent>\n            </MudCard>\n\n            <!-- 详细配置卡片 -->\n            @if (_config.IsEnable)\n            {\n                <MudCard>\n                    <MudCardHeader>\n                        <CardHeaderContent>\n                            <MudText Typo=\"Typo.h6\">详细配置</MudText>\n                        </CardHeaderContent>\n                    </MudCardHeader>\n                    <MudCardContent>\n                        <MudGrid>\n                            <MudItem xs=\"12\" md=\"6\">\n                                <MudTextField T=\"string\" @bind-Value=\"_config.DanmakuContent\" Label=\"自定义发送弹幕内容\"\n                                    HelperText=\"如 '打卡' 等来触发直播间内机器人关键词\" />\n\n                                <MudNumericField T=\"int\" @bind-Value=\"_config.HeartBeatNumber\" Label=\"心跳包发送的个数/挂机的时间(分钟)\"\n                                    Min=\"1\" Class=\"mt-3\" />\n\n                                <MudNumericField T=\"int\" @bind-Value=\"_config.HeartBeatSendGiveUpThreshold\" Label=\"心跳包发送连续失败多少次时放弃\"\n                                    Min=\"1\" Class=\"mt-3\" />\n\n                                <MudSwitch T=\"bool\" @bind-Value=\"_config.IsSkipLevel20Medal\"\n                                    Label=\"是否跳过粉丝牌等级 >= 20 的\" Color=\"Color.Primary\" Class=\"mt-3\" />\n                            </MudItem>\n\n                            <MudItem xs=\"12\" md=\"6\">\n                                <MudNumericField T=\"int\" @bind-Value=\"_config.LikeNumber\" Label=\"点赞次数\"\n                                    Min=\"0\" HelperText=\"默认值为30（用于点亮粉丝勋章）\" />\n\n                                <MudNumericField T=\"int\" @bind-Value=\"_config.SendDanmakuNumber\" Label=\"发送弹幕次数\"\n                                    Min=\"0\" Class=\"mt-3\" />\n\n                                <MudNumericField T=\"int\" @bind-Value=\"_config.SendDanmakugiveUpThreshold\" Label=\"弹幕发送失败多少次时放弃\"\n                                    Min=\"1\" Class=\"mt-3\" />\n                            </MudItem>\n                        </MudGrid>\n                    </MudCardContent>\n                </MudCard>\n            }\n\n            <!-- 操作按钮 -->\n            <MudCard Class=\"mt-4\">\n                <MudCardActions>\n                    <MudButton ButtonType=\"ButtonType.Submit\" Variant=\"Variant.Filled\" Color=\"Color.Primary\">保存配置</MudButton>\n                    <MudButton Variant=\"Variant.Outlined\" Color=\"Color.Secondary\" OnClick=\"LoadConfigAsync\" Class=\"ml-2\">重新加载</MudButton>\n                </MudCardActions>\n            </MudCard>\n        </EditForm>\n\n        @if (_saveMessage.HasValue)\n        {\n            <MudAlert Severity=\"@(_saveSuccess ? Severity.Success : Severity.Error)\" Class=\"mt-3\">\n                @_saveMessage\n            </MudAlert>\n        }\n    }\n</MudContainer>\n\n<MudThemeProvider/>\n<MudPopoverProvider/>\n<MudDialogProvider/>\n<MudSnackbarProvider/>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveFansMedalTaskConfig.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\nusing Microsoft.Extensions.Options;\nusing Quartz;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Web.Jobs;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Configs;\n\npublic partial class LiveFansMedalTaskConfig : BaseConfigComponent<LiveFansMedalTaskOptions>\n{\n    [Inject]\n    private IOptionsMonitor<LiveFansMedalTaskOptions> LiveFansMedalTaskOptionsMonitor { get; set; } =\n        null!;\n\n    protected override IOptionsMonitor<LiveFansMedalTaskOptions> OptionsMonitor =>\n        LiveFansMedalTaskOptionsMonitor;\n\n    protected override JobKey GetJobKey() => LiveFansMedalJob.Key;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveLotteryTaskConfig.razor",
    "content": "@page \"/Configurations/LiveLotteryTaskConfig\"\n@using Microsoft.AspNetCore.Authorization\n@using Ray.BiliBiliTool.Config.Options;\n@attribute [Authorize]\n@rendermode InteractiveServer\n@inherits BaseConfigComponent<LiveLotteryTaskOptions>\n\n<PageTitle>直播抽奖任务配置</PageTitle>\n\n<MudContainer>\n    <MudText Typo=\"Typo.h4\" Class=\"mb-4\">直播抽奖任务配置</MudText>\n\n    @if (_isLoading)\n    {\n        <MudProgressCircular Color=\"Color.Primary\" Indeterminate=\"true\" />\n    }\n    else\n    {\n        <EditForm Model=\"@_config\" OnValidSubmit=\"HandleValidSubmitAsync\">\n            <DataAnnotationsValidator />\n\n            <!-- 基础配置卡片 -->\n            <MudCard Class=\"mb-4\">\n                <MudCardHeader>\n                    <CardHeaderContent>\n                        <MudText Typo=\"Typo.h6\">基础配置</MudText>\n                    </CardHeaderContent>\n                </MudCardHeader>\n                <MudCardContent>\n                    <MudGrid>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudSwitch T=\"bool\" @bind-Value=\"_config.IsEnable\"\n                                Label=\"启用任务\" Color=\"Color.Primary\" />\n                        </MudItem>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudTextField T=\"string\" @bind-Value=\"_config.Cron\" Label=\"定时任务Cron表达式\"\n                                HelperText=\"例如：0 0 22 * * ? (每天晚上10点执行)\"\n                                Disabled=\"!_config.IsEnable\" />\n                        </MudItem>\n                    </MudGrid>\n                </MudCardContent>\n            </MudCard>\n\n            <!-- 详细配置卡片 -->\n            @if (_config.IsEnable)\n            {\n                <MudCard>\n                    <MudCardHeader>\n                        <CardHeaderContent>\n                            <MudText Typo=\"Typo.h6\">详细配置</MudText>\n                        </CardHeaderContent>\n                    </MudCardHeader>\n                    <MudCardContent>\n                        <MudGrid>\n                            <MudItem xs=\"12\" md=\"6\">\n                                <MudTextField T=\"string\" @bind-Value=\"_config.ExcludeAwardNames\" Label=\"排除的奖品名称关键字\"\n                                    HelperText=\"根据关键字排除包含这些文字的奖品名称，多个用|分隔\" />\n\n                                <MudTextField T=\"string\" @bind-Value=\"_config.IncludeAwardNames\" Label=\"包含的奖品名称关键字\" Class=\"mt-3\"\n                                    HelperText=\"根据关键字指定奖品名称必须包含的文字，多个用|分隔\" />\n\n                                <MudSwitch T=\"bool\" @bind-Value=\"_config.AutoGroupFollowings\"\n                                    Label=\"抽奖结束后是否自动将关注的主播分组到天选时刻分组\" Color=\"Color.Primary\" Class=\"mt-3\" />\n                            </MudItem>\n\n                            <MudItem xs=\"12\" md=\"6\">\n                                <MudTextField T=\"string\" @bind-Value=\"_config.DenyUids\" Label=\"主播Uid黑名单\"\n                                    HelperText=\"一般是中奖后的老赖，多个用英文逗号分隔，配置后不会参加黑名单中的主播的抽奖活动\" />\n                            </MudItem>\n                        </MudGrid>\n                    </MudCardContent>\n                </MudCard>\n            }\n\n            <!-- 操作按钮 -->\n            <MudCard Class=\"mt-4\">\n                <MudCardActions>\n                    <MudButton ButtonType=\"ButtonType.Submit\" Variant=\"Variant.Filled\" Color=\"Color.Primary\">保存配置</MudButton>\n                    <MudButton Variant=\"Variant.Outlined\" Color=\"Color.Secondary\" OnClick=\"LoadConfigAsync\" Class=\"ml-2\">重新加载</MudButton>\n                </MudCardActions>\n            </MudCard>\n        </EditForm>\n\n        @if (_saveMessage.HasValue)\n        {\n            <MudAlert Severity=\"@(_saveSuccess ? Severity.Success : Severity.Error)\" Class=\"mt-3\">\n                @_saveMessage\n            </MudAlert>\n        }\n    }\n</MudContainer>\n\n<MudThemeProvider/>\n<MudPopoverProvider/>\n<MudDialogProvider/>\n<MudSnackbarProvider/>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/LiveLotteryTaskConfig.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\nusing Microsoft.Extensions.Options;\nusing Quartz;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Web.Jobs;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Configs;\n\npublic partial class LiveLotteryTaskConfig : BaseConfigComponent<LiveLotteryTaskOptions>\n{\n    [Inject]\n    private IOptionsMonitor<LiveLotteryTaskOptions> LiveLotteryTaskOptionsMonitor { get; set; } =\n        null!;\n\n    protected override IOptionsMonitor<LiveLotteryTaskOptions> OptionsMonitor =>\n        LiveLotteryTaskOptionsMonitor;\n\n    protected override JobKey GetJobKey() => LiveLotteryJob.Key;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaPrivilegeTaskConfig.razor",
    "content": "@page \"/Configurations/MangaPrivilegeTaskConfig\"\n@using Microsoft.AspNetCore.Authorization\n@using Ray.BiliBiliTool.Config.Options;\n@attribute [Authorize]\n@rendermode InteractiveServer\n@inherits BaseConfigComponent<MangaPrivilegeTaskOptions>\n\n<PageTitle>漫画特权任务配置</PageTitle>\n\n<MudContainer>\n    <MudText Typo=\"Typo.h4\" Class=\"mb-4\">漫画特权任务配置</MudText>\n\n    @if (_isLoading)\n    {\n        <MudProgressCircular Color=\"Color.Primary\" Indeterminate=\"true\" />\n    }\n    else\n    {\n        <EditForm Model=\"@_config\" OnValidSubmit=\"HandleValidSubmitAsync\">\n            <DataAnnotationsValidator />\n\n            <!-- 基础配置卡片 -->\n            <MudCard Class=\"mb-4\">\n                <MudCardHeader>\n                    <CardHeaderContent>\n                        <MudText Typo=\"Typo.h6\">基础配置</MudText>\n                    </CardHeaderContent>\n                </MudCardHeader>\n                <MudCardContent>\n                    <MudGrid>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudSwitch T=\"bool\" @bind-Value=\"_config.IsEnable\"\n                                Label=\"启用任务\" Color=\"Color.Primary\" />\n                        </MudItem>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudTextField T=\"string\" @bind-Value=\"_config.Cron\" Label=\"定时任务Cron表达式\"\n                                HelperText=\"例如：0 0 15 * * ? (每天下午3点执行)\"\n                                Disabled=\"!_config.IsEnable\" />\n                        </MudItem>\n                    </MudGrid>\n                </MudCardContent>\n            </MudCard>\n\n            <!-- 操作按钮 -->\n            <MudCard Class=\"mt-4\">\n                <MudCardActions>\n                    <MudButton ButtonType=\"ButtonType.Submit\" Variant=\"Variant.Filled\" Color=\"Color.Primary\">保存配置</MudButton>\n                    <MudButton Variant=\"Variant.Outlined\" Color=\"Color.Secondary\" OnClick=\"LoadConfigAsync\" Class=\"ml-2\">重新加载</MudButton>\n                </MudCardActions>\n            </MudCard>\n        </EditForm>\n\n        @if (_saveMessage.HasValue)\n        {\n            <MudAlert Severity=\"@(_saveSuccess ? Severity.Success : Severity.Error)\" Class=\"mt-3\">\n                @_saveMessage\n            </MudAlert>\n        }\n    }\n</MudContainer>\n\n<MudThemeProvider/>\n<MudPopoverProvider/>\n<MudDialogProvider/>\n<MudSnackbarProvider/>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaPrivilegeTaskConfig.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\nusing Microsoft.Extensions.Options;\nusing Quartz;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Web.Jobs;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Configs;\n\npublic partial class MangaPrivilegeTaskConfig : BaseConfigComponent<MangaPrivilegeTaskOptions>\n{\n    [Inject]\n    private IOptionsMonitor<MangaPrivilegeTaskOptions> MangaPrivilegeTaskOptionsMonitor { get; set; } =\n        null!;\n\n    protected override IOptionsMonitor<MangaPrivilegeTaskOptions> OptionsMonitor =>\n        MangaPrivilegeTaskOptionsMonitor;\n\n    protected override JobKey GetJobKey() => MangaPrivilegeJob.Key;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaTaskConfig.razor",
    "content": "@page \"/Configurations/MangaTaskConfig\"\n@using Microsoft.AspNetCore.Authorization\n@using Ray.BiliBiliTool.Config.Options;\n@attribute [Authorize]\n@rendermode InteractiveServer\n@inherits BaseConfigComponent<MangaTaskOptions>\n\n<PageTitle>漫画任务配置</PageTitle>\n\n<MudContainer>\n    <MudText Typo=\"Typo.h4\" Class=\"mb-4\">漫画任务配置</MudText>\n\n    @if (_isLoading)\n    {\n        <MudProgressCircular Color=\"Color.Primary\" Indeterminate=\"true\" />\n    }\n    else\n    {\n        <EditForm Model=\"@_config\" OnValidSubmit=\"HandleValidSubmitAsync\">\n            <DataAnnotationsValidator />\n\n            <!-- 基础配置卡片 -->\n            <MudCard Class=\"mb-4\">\n                <MudCardHeader>\n                    <CardHeaderContent>\n                        <MudText Typo=\"Typo.h6\">基础配置</MudText>\n                    </CardHeaderContent>\n                </MudCardHeader>\n                <MudCardContent>\n                    <MudGrid>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudSwitch T=\"bool\" @bind-Value=\"_config.IsEnable\"\n                                Label=\"启用任务\" Color=\"Color.Primary\" />\n                        </MudItem>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudTextField T=\"string\" @bind-Value=\"_config.Cron\" Label=\"定时任务Cron表达式\"\n                                HelperText=\"例如：0 0 14 * * ? (每天下午2点执行)\"\n                                Disabled=\"!_config.IsEnable\" />\n                        </MudItem>\n                    </MudGrid>\n                </MudCardContent>\n            </MudCard>\n\n            <!-- 详细配置卡片 -->\n            @if (_config.IsEnable)\n            {\n                <MudCard>\n                    <MudCardHeader>\n                        <CardHeaderContent>\n                            <MudText Typo=\"Typo.h6\">详细配置</MudText>\n                        </CardHeaderContent>\n                    </MudCardHeader>\n                    <MudCardContent>\n                        <MudGrid>\n                            <MudItem xs=\"12\" md=\"6\">\n                                <MudNumericField T=\"long\" @bind-Value=\"_config.CustomComicId\" Label=\"自定义漫画阅读 comic_id\"\n                                    HelperText=\"若不清楚含义请勿修改\" />\n                            </MudItem>\n                            <MudItem xs=\"12\" md=\"6\">\n                                <MudNumericField T=\"long\" @bind-Value=\"_config.CustomEpId\" Label=\"自定义漫画阅读 ep_id\"\n                                    HelperText=\"若不清楚含义请勿修改\" />\n                            </MudItem>\n                        </MudGrid>\n                    </MudCardContent>\n                </MudCard>\n            }\n\n            <!-- 操作按钮 -->\n            <MudCard Class=\"mt-4\">\n                <MudCardActions>\n                    <MudButton ButtonType=\"ButtonType.Submit\" Variant=\"Variant.Filled\" Color=\"Color.Primary\">保存配置</MudButton>\n                    <MudButton Variant=\"Variant.Outlined\" Color=\"Color.Secondary\" OnClick=\"LoadConfigAsync\" Class=\"ml-2\">重新加载</MudButton>\n                </MudCardActions>\n            </MudCard>\n        </EditForm>\n\n        @if (_saveMessage.HasValue)\n        {\n            <MudAlert Severity=\"@(_saveSuccess ? Severity.Success : Severity.Error)\" Class=\"mt-3\">\n                @_saveMessage\n            </MudAlert>\n        }\n    }\n</MudContainer>\n\n<MudThemeProvider/>\n<MudPopoverProvider/>\n<MudDialogProvider/>\n<MudSnackbarProvider/>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/MangaTaskConfig.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\nusing Microsoft.Extensions.Options;\nusing Quartz;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Web.Jobs;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Configs;\n\npublic partial class MangaTaskConfig : BaseConfigComponent<MangaTaskOptions>\n{\n    [Inject]\n    private IOptionsMonitor<MangaTaskOptions> MangaTaskOptionsMonitor { get; set; } = null!;\n\n    protected override IOptionsMonitor<MangaTaskOptions> OptionsMonitor => MangaTaskOptionsMonitor;\n\n    protected override JobKey GetJobKey() => MangaJob.Key;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/Silver2CoinTaskConfig.razor",
    "content": "@page \"/Configurations/Silver2CoinTaskConfig\"\n@using Microsoft.AspNetCore.Authorization\n@using Ray.BiliBiliTool.Config.Options;\n@attribute [Authorize]\n@rendermode InteractiveServer\n@inherits BaseConfigComponent<Silver2CoinTaskOptions>\n\n<PageTitle>银瓜子兑换硬币任务配置</PageTitle>\n\n<MudContainer>\n    <MudText Typo=\"Typo.h4\" Class=\"mb-4\">银瓜子兑换硬币任务配置</MudText>\n\n    @if (_isLoading)\n    {\n        <MudProgressCircular Color=\"Color.Primary\" Indeterminate=\"true\" />\n    }\n    else\n    {\n        <EditForm Model=\"@_config\" OnValidSubmit=\"HandleValidSubmitAsync\">\n            <DataAnnotationsValidator />\n\n            <!-- 基础配置卡片 -->\n            <MudCard Class=\"mb-4\">\n                <MudCardHeader>\n                    <CardHeaderContent>\n                        <MudText Typo=\"Typo.h6\">基础配置</MudText>\n                    </CardHeaderContent>\n                </MudCardHeader>\n                <MudCardContent>\n                    <MudGrid>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudSwitch T=\"bool\" @bind-Value=\"_config.IsEnable\"\n                                Label=\"启用任务\" Color=\"Color.Primary\" />\n                        </MudItem>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudTextField T=\"string\" @bind-Value=\"_config.Cron\" Label=\"定时任务Cron表达式\"\n                                HelperText=\"例如：0 0 8 * * ? (每天早上8点执行)\"\n                                Disabled=\"!_config.IsEnable\" />\n                        </MudItem>\n                    </MudGrid>\n                </MudCardContent>\n            </MudCard>\n\n            <!-- 操作按钮 -->\n            <MudCard Class=\"mt-4\">\n                <MudCardActions>\n                    <MudButton ButtonType=\"ButtonType.Submit\" Variant=\"Variant.Filled\" Color=\"Color.Primary\">保存配置</MudButton>\n                    <MudButton Variant=\"Variant.Outlined\" Color=\"Color.Secondary\" OnClick=\"LoadConfigAsync\" Class=\"ml-2\">重新加载</MudButton>\n                </MudCardActions>\n            </MudCard>\n        </EditForm>\n\n        @if (_saveMessage.HasValue)\n        {\n            <MudAlert Severity=\"@(_saveSuccess ? Severity.Success : Severity.Error)\" Class=\"mt-3\">\n                @_saveMessage\n            </MudAlert>\n        }\n    }\n</MudContainer>\n\n<MudThemeProvider/>\n<MudPopoverProvider/>\n<MudDialogProvider/>\n<MudSnackbarProvider/>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/Silver2CoinTaskConfig.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\nusing Microsoft.Extensions.Options;\nusing Quartz;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Web.Jobs;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Configs;\n\npublic partial class Silver2CoinTaskConfig : BaseConfigComponent<Silver2CoinTaskOptions>\n{\n    [Inject]\n    private IOptionsMonitor<Silver2CoinTaskOptions> Silver2CoinTaskOptionsMonitor { get; set; } =\n        null!;\n\n    protected override IOptionsMonitor<Silver2CoinTaskOptions> OptionsMonitor =>\n        Silver2CoinTaskOptionsMonitor;\n\n    protected override JobKey GetJobKey() => Silver2CoinJob.Key;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/UnfollowBatchedTaskConfig.razor",
    "content": "@page \"/Configurations/UnfollowBatchedTaskConfig\"\n@using Microsoft.AspNetCore.Authorization\n@using Ray.BiliBiliTool.Config.Options;\n@attribute [Authorize]\n@rendermode InteractiveServer\n@inherits BaseConfigComponent<UnfollowBatchedTaskOptions>\n\n<PageTitle>批量取关任务配置</PageTitle>\n\n<MudContainer>\n    <MudText Typo=\"Typo.h4\" Class=\"mb-4\">批量取关任务配置</MudText>\n\n    @if (_isLoading)\n    {\n        <MudProgressCircular Color=\"Color.Primary\" Indeterminate=\"true\" />\n    }\n    else\n    {\n        <EditForm Model=\"@_config\" OnValidSubmit=\"HandleValidSubmitAsync\">\n            <DataAnnotationsValidator />\n\n            <!-- 基础配置卡片 -->\n            <MudCard Class=\"mb-4\">\n                <MudCardHeader>\n                    <CardHeaderContent>\n                        <MudText Typo=\"Typo.h6\">基础配置</MudText>\n                    </CardHeaderContent>\n                </MudCardHeader>\n                <MudCardContent>\n                    <MudGrid>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudSwitch T=\"bool\" @bind-Value=\"_config.IsEnable\"\n                                Label=\"启用任务\" Color=\"Color.Primary\" />\n                        </MudItem>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudTextField T=\"string\" @bind-Value=\"_config.Cron\" Label=\"定时任务Cron表达式\"\n                                HelperText=\"例如：0 0 6 1 * ? (每月1号早上6点执行)\"\n                                Disabled=\"!_config.IsEnable\" />\n                        </MudItem>\n                    </MudGrid>\n                </MudCardContent>\n            </MudCard>\n\n            <!-- 详细配置卡片 -->\n            @if (_config.IsEnable)\n            {\n                <MudCard>\n                    <MudCardHeader>\n                        <CardHeaderContent>\n                            <MudText Typo=\"Typo.h6\">详细配置</MudText>\n                        </CardHeaderContent>\n                    </MudCardHeader>\n                    <MudCardContent>\n                        <MudGrid>\n                            <MudItem xs=\"12\" md=\"6\">\n                                <MudTextField T=\"string\" @bind-Value=\"_config.GroupName\" Label=\"取关的分组名称\"\n                                    HelperText=\"默认为'天选时刻'\" />\n\n                                <MudNumericField T=\"int\" @bind-Value=\"_config.Count\" Label=\"本次取关个数\"\n                                    Min=\"0\" HelperText=\"倒序，从后往前取关\" Class=\"mt-3\" />\n                            </MudItem>\n\n                            <MudItem xs=\"12\" md=\"6\">\n                                <MudTextField T=\"string\" @bind-Value=\"_config.RetainUids\" Label=\"白名单（保留的UpId）\"\n                                    HelperText=\"多个用英文逗号分隔，配置后，批量取关时不会取关配置的Up\" />\n                            </MudItem>\n                        </MudGrid>\n                    </MudCardContent>\n                </MudCard>\n            }\n\n            <!-- 操作按钮 -->\n            <MudCard Class=\"mt-4\">\n                <MudCardActions>\n                    <MudButton ButtonType=\"ButtonType.Submit\" Variant=\"Variant.Filled\" Color=\"Color.Primary\">保存配置</MudButton>\n                    <MudButton Variant=\"Variant.Outlined\" Color=\"Color.Secondary\" OnClick=\"LoadConfigAsync\" Class=\"ml-2\">重新加载</MudButton>\n                </MudCardActions>\n            </MudCard>\n        </EditForm>\n\n        @if (_saveMessage.HasValue)\n        {\n            <MudAlert Severity=\"@(_saveSuccess ? Severity.Success : Severity.Error)\" Class=\"mt-3\">\n                @_saveMessage\n            </MudAlert>\n        }\n    }\n</MudContainer>\n\n<MudThemeProvider/>\n<MudPopoverProvider/>\n<MudDialogProvider/>\n<MudSnackbarProvider/>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/UnfollowBatchedTaskConfig.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\nusing Microsoft.Extensions.Options;\nusing Quartz;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Web.Jobs;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Configs;\n\npublic partial class UnfollowBatchedTaskConfig : BaseConfigComponent<UnfollowBatchedTaskOptions>\n{\n    [Inject]\n    private IOptionsMonitor<UnfollowBatchedTaskOptions> UnfollowBatchedTaskOptionsMonitor { get; set; } =\n        null!;\n\n    protected override IOptionsMonitor<UnfollowBatchedTaskOptions> OptionsMonitor =>\n        UnfollowBatchedTaskOptionsMonitor;\n\n    protected override JobKey GetJobKey() => UnfollowBatchedJob.Key;\n\n    protected override async Task OnInitializedAsync()\n    {\n        // 确保配置对象有默认的GroupName值\n        if (string.IsNullOrEmpty(_config.GroupName))\n        {\n            _config.GroupName = \"天选时刻\";\n        }\n        await base.OnInitializedAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipBigPointConfig.razor",
    "content": "@page \"/Configurations/VipBigPointConfig\"\n@using Microsoft.AspNetCore.Authorization\n@using Ray.BiliBiliTool.Config.Options;\n@attribute [Authorize]\n@rendermode InteractiveServer\n@inherits BaseConfigComponent<VipBigPointOptions>\n\n<PageTitle>会员大积分配置</PageTitle>\n\n<MudContainer>\n    <MudText Typo=\"Typo.h4\" Class=\"mb-4\">会员大积分配置</MudText>\n\n    @if (_isLoading)\n    {\n        <MudProgressCircular Color=\"Color.Primary\" Indeterminate=\"true\" />\n    }\n    else\n    {\n        <EditForm Model=\"@_config\" OnValidSubmit=\"HandleValidSubmitAsync\">\n            <DataAnnotationsValidator />\n\n            <!-- 基础配置卡片 -->\n            <MudCard Class=\"mb-4\">\n                <MudCardHeader>\n                    <CardHeaderContent>\n                        <MudText Typo=\"Typo.h6\">基础配置</MudText>\n                    </CardHeaderContent>\n                </MudCardHeader>\n                <MudCardContent>\n                    <MudGrid>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudSwitch T=\"bool\" @bind-Value=\"_config.IsEnable\"\n                                Label=\"启用任务\" Color=\"Color.Primary\" />\n                        </MudItem>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudTextField T=\"string\" @bind-Value=\"_config.Cron\" Label=\"定时任务Cron表达式\"\n                                HelperText=\"例如：0 7 1 * * ? (每天凌晨1点7分执行)\"\n                                Disabled=\"!_config.IsEnable\" />\n                        </MudItem>\n                    </MudGrid>\n                </MudCardContent>\n            </MudCard>\n\n            <!-- 详细配置卡片 -->\n            @if (_config.IsEnable)\n            {\n                <MudCard>\n                    <MudCardHeader>\n                        <CardHeaderContent>\n                            <MudText Typo=\"Typo.h6\">详细配置</MudText>\n                        </CardHeaderContent>\n                    </MudCardHeader>\n                    <MudCardContent>\n                        <MudGrid>\n                            <MudItem xs=\"12\">\n                                <MudTextField T=\"string\" @bind-Value=\"_config.ViewBangumis\" Label=\"自定义番剧的ssid\"\n                                    HelperText=\"若不清楚含义请勿修改（默认为名侦探柯南），多个用逗号分隔\" />\n                            </MudItem>\n                        </MudGrid>\n                    </MudCardContent>\n                </MudCard>\n            }\n\n            <!-- 操作按钮 -->\n            <MudCard Class=\"mt-4\">\n                <MudCardActions>\n                    <MudButton ButtonType=\"ButtonType.Submit\" Variant=\"Variant.Filled\" Color=\"Color.Primary\">保存配置</MudButton>\n                    <MudButton Variant=\"Variant.Outlined\" Color=\"Color.Secondary\" OnClick=\"LoadConfigAsync\" Class=\"ml-2\">重新加载</MudButton>\n                </MudCardActions>\n            </MudCard>\n        </EditForm>\n\n        @if (_saveMessage.HasValue)\n        {\n            <MudAlert Severity=\"@(_saveSuccess ? Severity.Success : Severity.Error)\" Class=\"mt-3\">\n                @_saveMessage\n            </MudAlert>\n        }\n    }\n</MudContainer>\n\n<MudThemeProvider/>\n<MudPopoverProvider/>\n<MudDialogProvider/>\n<MudSnackbarProvider/>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipBigPointConfig.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\nusing Microsoft.Extensions.Options;\nusing Quartz;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Web.Jobs;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Configs;\n\npublic partial class VipBigPointConfig : BaseConfigComponent<VipBigPointOptions>\n{\n    [Inject]\n    private IOptionsMonitor<VipBigPointOptions> VipBigPointOptionsMonitor { get; set; } = null!;\n\n    protected override IOptionsMonitor<VipBigPointOptions> OptionsMonitor =>\n        VipBigPointOptionsMonitor;\n\n    protected override JobKey GetJobKey() => VipBigPointJob.Key;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipPrivilegeConfig.razor",
    "content": "@page \"/Configurations/VipPrivilegeConfig\"\n@using Microsoft.AspNetCore.Authorization\n@using Ray.BiliBiliTool.Config.Options;\n@attribute [Authorize]\n@rendermode InteractiveServer\n@inherits BaseConfigComponent<VipPrivilegeOptions>\n\n<PageTitle>会员特权配置</PageTitle>\n\n<MudContainer>\n    <MudText Typo=\"Typo.h4\" Class=\"mb-4\">会员特权配置</MudText>\n\n    @if (_isLoading)\n    {\n        <MudProgressCircular Color=\"Color.Primary\" Indeterminate=\"true\" />\n    }\n    else\n    {\n        <EditForm Model=\"@_config\" OnValidSubmit=\"HandleValidSubmitAsync\">\n            <DataAnnotationsValidator />\n\n            <!-- 基础配置卡片 -->\n            <MudCard Class=\"mb-4\">\n                <MudCardHeader>\n                    <CardHeaderContent>\n                        <MudText Typo=\"Typo.h6\">基础配置</MudText>\n                    </CardHeaderContent>\n                </MudCardHeader>\n                <MudCardContent>\n                    <MudGrid>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudSwitch T=\"bool\" @bind-Value=\"_config.IsEnable\"\n                                Label=\"启用任务\" Color=\"Color.Primary\" />\n                        </MudItem>\n                        <MudItem xs=\"12\" md=\"6\">\n                            <MudTextField T=\"string\" @bind-Value=\"_config.Cron\" Label=\"定时任务Cron表达式\"\n                                HelperText=\"例如：0 0 1 * * ? (每天凌晨1点执行)\"\n                                Disabled=\"!_config.IsEnable\" />\n                        </MudItem>\n                    </MudGrid>\n                </MudCardContent>\n            </MudCard>\n\n            <!-- 操作按钮 -->\n            <MudCard Class=\"mt-4\">\n                <MudCardActions>\n                    <MudButton ButtonType=\"ButtonType.Submit\" Variant=\"Variant.Filled\" Color=\"Color.Primary\">保存配置</MudButton>\n                    <MudButton Variant=\"Variant.Outlined\" Color=\"Color.Secondary\" OnClick=\"LoadConfigAsync\" Class=\"ml-2\">重新加载</MudButton>\n                </MudCardActions>\n            </MudCard>\n        </EditForm>\n\n        @if (_saveMessage.HasValue)\n        {\n            <MudAlert Severity=\"@(_saveSuccess ? Severity.Success : Severity.Error)\" Class=\"mt-3\">\n                @_saveMessage\n            </MudAlert>\n        }\n    }\n</MudContainer>\n\n<MudThemeProvider/>\n<MudPopoverProvider/>\n<MudDialogProvider/>\n<MudSnackbarProvider/>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Configs/VipPrivilegeConfig.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\nusing Microsoft.Extensions.Options;\nusing Quartz;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Web.Jobs;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Configs;\n\npublic partial class VipPrivilegeConfig : BaseConfigComponent<VipPrivilegeOptions>\n{\n    [Inject]\n    private IOptionsMonitor<VipPrivilegeOptions> VipPrivilegeOptionsMonitor { get; set; } = null!;\n\n    protected override IOptionsMonitor<VipPrivilegeOptions> OptionsMonitor =>\n        VipPrivilegeOptionsMonitor;\n\n    protected override JobKey GetJobKey() => VipPrivilegeJob.Key;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Error.razor",
    "content": "﻿@page \"/Error\"\n@using System.Diagnostics\n\n<PageTitle>Error</PageTitle>\n\n<h1 class=\"text-danger\">Error.</h1>\n<h2 class=\"text-danger\">An error occurred while processing your request.</h2>\n\n@if (ShowRequestId)\n{\n    <p>\n        <strong>Request ID:</strong> <code>@RequestId</code>\n    </p>\n}\n\n<h3>Development Mode</h3>\n<p>\n    Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.\n</p>\n<p>\n    <strong>The Development environment shouldn't be enabled for deployed applications.</strong>\n    It can result in displaying sensitive information from exceptions to end users.\n    For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>\n    and restarting the app.\n</p>\n\n@code{\n    [CascadingParameter] private HttpContext? HttpContext { get; set; }\n\n    private string? RequestId { get; set; }\n    private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);\n\n    protected override void OnInitialized() =>\n        RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;\n\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Home.razor",
    "content": "﻿@page \"/\"\n\n<PageTitle>Home</PageTitle>\n\n<h1>Hello, world!</h1>\n\nWelcome to your new app.\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor",
    "content": "@page \"/login\"\n@rendermode InteractiveServer\n\n@if (_loginError)\n{\n    <MudAlert Severity=\"Severity.Error\" Elevation=\"0\" Class=\"mx-auto my-4\" Style=\"max-width: 500px;\">\n        Incorrect username or password. Please try again.\n    </MudAlert>\n}\n\n<form action=\"/auth/login\" method=\"post\">\n    <input type=\"hidden\" name=\"returnUrl\" value=\"@returnUrl\" />\n\n    <MudCard Class=\"mx-auto my-4\" Style=\"max-width: 500px;\">\n        <MudCardHeader>\n            <MudText Typo=\"Typo.h5\">Log in</MudText>\n        </MudCardHeader>\n        <MudCardContent>\n            <MudTextField @bind-Value=\"_username\" Name=\"username\" Label=\"User Name\" Required=\"true\" RequiredError=\"Username cannot be empty\" />\n            <MudTextField @bind-Value=\"_password\" Name=\"password\" Label=\"Password\" Required=\"true\" RequiredError=\"Password cannot be empty\"\n                          InputType=\"@_passwordInput\" Adornment=\"Adornment.End\"\n                          AdornmentIcon=\"@_passwordInputIcon\" OnAdornmentClick=\"TogglePasswordVisibility\" />\n        </MudCardContent>\n        <MudCardActions>\n            <MudButton ButtonType=\"ButtonType.Submit\" Variant=\"Variant.Filled\" Color=\"Color.Primary\"\n                       Class=\"ml-auto\">Log in</MudButton>\n        </MudCardActions>\n    </MudCard>\n</form>\n\n<MudThemeProvider/>\n<MudPopoverProvider/>\n<MudDialogProvider/>\n<MudSnackbarProvider/>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor.cs",
    "content": "using Microsoft.AspNetCore.Components;\nusing Microsoft.AspNetCore.WebUtilities;\nusing MudBlazor;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages;\n\npublic partial class Login : ComponentBase\n{\n    [Inject]\n    private NavigationManager NavigationManager { get; set; } = null!;\n\n    private string _username = \"\";\n    private string _password = \"\";\n\n    private bool _passwordVisibility;\n    private InputType _passwordInput = InputType.Password;\n    private string _passwordInputIcon = Icons.Material.Filled.VisibilityOff;\n\n    private void TogglePasswordVisibility()\n    {\n        if (_passwordVisibility)\n        {\n            _passwordVisibility = false;\n            _passwordInputIcon = Icons.Material.Filled.VisibilityOff;\n            _passwordInput = InputType.Password;\n        }\n        else\n        {\n            _passwordVisibility = true;\n            _passwordInputIcon = Icons.Material.Filled.Visibility;\n            _passwordInput = InputType.Text;\n        }\n    }\n\n    private string? returnUrl;\n    private bool _loginError = false;\n\n    protected override void OnInitialized()\n    {\n        var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);\n        var query = QueryHelpers.ParseQuery(uri.Query);\n        if (query.TryGetValue(\"returnUrl\", out var param))\n        {\n            returnUrl = param.First();\n        }\n        if (query.TryGetValue(\"error\", out var errorParam) && bool.TryParse(errorParam.FirstOrDefault(), out var parsed) && parsed)\n        {\n            _loginError = true;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor.css",
    "content": ""
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Login.razor.js",
    "content": "export class Login {\n  \n}\n\nwindow.Login = Login;"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/HistoryDialog.razor",
    "content": "@rendermode InteractiveServer\n@attribute [StreamRendering]\n@using Ray.BiliBiliTool.Domain\n@using Ray.BiliBiliTool.Web.Extensions\n\n<MudDialog>\n    <TitleContent>\n        <div class=\"d-flex\">\n            <MudText Typo=\"Typo.h5\">Execution History</MudText>\n            <MudIconButton Icon=\"@Icons.Material.Filled.Refresh\"\n                           OnClick=\"OnRefreshHistory\" />\n        </div>\n    </TitleContent>\n    <DialogContent>\n        <MudContainer Style=\"max-height: 300px; overflow-y: scroll\">\n            <MudTimeline TimelinePosition=\"TimelinePosition.Start\" TimelineAlign=\"TimelineAlign.Start\">\n                @{\n                    DateTimeOffset? lastDate = null;\n\n                    foreach (var log in ExecutionLogs)\n                    {\n                        if (lastDate == null ||\n                            lastDate.Value.Date != (log.FireTimeUtc?.Date ?? log.DateAddedUtc.Date))\n                        {\n                            lastDate = log.FireTimeUtc.HasValue ? log.FireTimeUtc.Value.Date : log.DateAddedUtc.Date;\n                            <MudTimelineItem Color=\"Color.Primary\" Size=\"Size.Medium\">\n                                <MudText Typo=\"Typo.button\">\n                                    @lastDate.Value.Date.ToLongDateString()\n                                </MudText>\n                            </MudTimelineItem>\n                        }\n                        var finishTimeUtc = log.GetFinishTimeUtc();\n                        @if (log.IsException ?? false)\n                        {\n                            <MudTimelineItem Color=\"Color.Transparent\"\n                                TimelineAlign=\"TimelineAlign.End\">\n                                <ItemDot>\n                                    <MudIcon Size=\"Size.Small\" Color=\"Color.Error\" Icon=\"@Icons.Material.Filled.Error\"/>\n                                </ItemDot>\n                                <ItemContent>\n                                    <MudText Typo=\"Typo.button\">\n                                        @GetExecutionTime(log)\n                                        @if (log.JobRunTime.HasValue)\n                                        {\n                                            <span class=\"pl-1\" style=\"text-transform: none;\">\n                                                (@log.JobRunTime.Value.ToHumanTimeString(4))\n                                            </span>\n                                        }\n                                    </MudText>\n                                    <MudText Typo=\"Typo.body2\" Class=\"mud-text-secondary\">@log.GetShortExceptionMessage()</MudText>\n                                    <MudButton Variant=\"Variant.Text\"\n                                            Class=\"pa-0 ma-0\"\n                                            Style=\"text-transform:none;\"\n                                            OnClick=\"@(() => OnMoreDetails(log, \"Error Details\"))\"\n                                            Color=\"Color.Primary\">Error Details</MudButton>\n                                </ItemContent>\n                            </MudTimelineItem>\n                        }\n                        else\n                        {\n                            <MudTimelineItem Color=\"@GetTimelineDotColor(log)\"\n                                            TimelineAlign=\"TimelineAlign.End\">\n                                <MudText Typo=\"Typo.button\">\n                                    @GetExecutionTime(log)\n                                    @if(log.JobRunTime.HasValue)\n                                    {\n                                        <span class=\"pl-1\" style=\"text-transform: none;\">\n                                            (@log.JobRunTime.Value.ToHumanTimeString(4))\n                                        </span>\n                                    }\n                                </MudText>\n                                <MudText Typo=\"Typo.body2\" Class=\"mud-text-secondary\">\n                                    @log.GetShortResultMessage()\n                                </MudText>\n                                @if (log.ShowExecutionDetailButton())\n                                {\n                                    <MudButton Variant=\"Variant.Text\"\n                                            Class=\"pa-0 ma-0\"\n                                            Style=\"text-transform:none;\"\n                                            OnClick=\"@(() => OnMoreDetails(log, \"Execution Details\"))\"\n                                            Color=\"Color.Primary\">Execution Details</MudButton>\n                                }\n                            </MudTimelineItem>\n                        }\n                    }\n                }\n\n                @if (HasMore)\n                {\n                    <MudTimelineItem TimelineAlign=\"TimelineAlign.End\">\n                        <MudButton Variant=\"Variant.Text\"\n                                   Style=\"text-transform:none;\"\n                                   OnClick=\"GetMoreLogs\"\n                                   Color=\"Color.Primary\">Load More</MudButton>\n                    </MudTimelineItem>\n                }\n            </MudTimeline>\n        </MudContainer>\n    </DialogContent>\n    <DialogActions>\n        <MudButton OnClick=\"Close\">Close</MudButton>\n    </DialogActions>\n</MudDialog>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/HistoryDialog.razor.cs",
    "content": "using System.Collections.ObjectModel;\nusing System.Text;\nusing BlazingQuartz.Core.Models;\nusing BlazingQuartz.Core.Services;\nusing Microsoft.AspNetCore.Components;\nusing MudBlazor;\nusing Ray.BiliBiliTool.Domain;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Schedules;\n\npublic partial class HistoryDialog : ComponentBase\n{\n    [CascadingParameter]\n    IMudDialogInstance MudDialog { get; set; } = null!;\n\n    [Inject]\n    private IDialogService DialogSvc { get; set; } = null!;\n\n    [Inject]\n    IExecutionLogService LogSvc { get; set; } = null!;\n\n    [EditorRequired]\n    [Parameter]\n    public Key JobKey { get; set; } = null!;\n\n    [EditorRequired]\n    [Parameter]\n    public Key? TriggerKey { get; set; }\n\n    ObservableCollection<ExecutionLog> ExecutionLogs { get; set; } = new();\n    bool HasMore { get; set; } = false;\n\n    private PageMetadata? _lastPageMeta;\n    private long _firstLogId;\n\n    void Close() => MudDialog.Cancel();\n\n    protected override async Task OnInitializedAsync()\n    {\n        await OnRefreshHistory();\n    }\n\n    private async Task GetMoreLogs()\n    {\n        PageMetadata pageMeta;\n        if (_lastPageMeta == null)\n        {\n            pageMeta = new(0, 5);\n        }\n        else\n        {\n            pageMeta = _lastPageMeta with { Page = _lastPageMeta.Page + 1 };\n        }\n\n        var result = await LogSvc.GetLatestExecutionLog(\n            JobKey.Name,\n            JobKey.Group ?? BlazingQuartz.Constants.DEFAULT_GROUP,\n            TriggerKey?.Name,\n            TriggerKey?.Group,\n            pageMeta,\n            _firstLogId\n        );\n\n        _lastPageMeta = result.PageMetadata;\n        if (pageMeta.Page == 0)\n        {\n            _firstLogId = result.FirstOrDefault()?.LogId ?? 0;\n        }\n\n        result.ForEach(l => ExecutionLogs.Add(l));\n\n        HasMore = result.Count == pageMeta.PageSize;\n    }\n\n    private async Task OnRefreshHistory()\n    {\n        ExecutionLogs.Clear();\n        _lastPageMeta = null;\n        _firstLogId = 0;\n        HasMore = false;\n\n        await GetMoreLogs();\n    }\n\n    private void OnMoreDetails(ExecutionLog log, string title)\n    {\n        var options = new DialogOptions\n        {\n            CloseOnEscapeKey = true,\n            FullWidth = true,\n            MaxWidth = MaxWidth.Medium,\n        };\n\n        var parameters = new DialogParameters { [\"ExecutionLog\"] = log };\n        // var dlg = DialogSvc.Show<ExecutionDetailsDialog>(title, parameters, options);\n    }\n\n    private string GetExecutionTime(ExecutionLog log)\n    {\n        // when fire time is available, display time range\n        // otherwise just display date added\n        if (log.FireTimeUtc.HasValue)\n        {\n            StringBuilder strBldr = new(\n                log.FireTimeUtc.Value.LocalDateTime.ToShortDateString()\n                    + \" \"\n                    + log.FireTimeUtc.Value.LocalDateTime.ToLongTimeString()\n            );\n\n            var finishTime = log.GetFinishTimeUtc();\n            if (finishTime.HasValue)\n            {\n                strBldr.Append(\" - \");\n                if (finishTime.Value.LocalDateTime.Date != log.FireTimeUtc.Value.LocalDateTime.Date)\n                {\n                    // display ending date\n                    strBldr.Append(finishTime.Value.LocalDateTime.ToShortDateString() + \" \");\n                }\n\n                strBldr.Append(finishTime.Value.LocalDateTime.ToLongTimeString());\n            }\n            return strBldr.ToString();\n        }\n        else\n        {\n            return log.DateAddedUtc.LocalDateTime.ToShortDateString()\n                + \" \"\n                + log.DateAddedUtc.LocalDateTime.ToLongTimeString();\n        }\n    }\n\n    private Color GetTimelineDotColor(ExecutionLog log)\n    {\n        return log.LogType switch\n        {\n            LogType.ScheduleJob => ((log.IsException ?? false) ? Color.Error : Color.Success),\n            _ => Color.Tertiary,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/LogsDialog.razor",
    "content": "@rendermode InteractiveServer\n@attribute [StreamRendering]\n@using Ray.BiliBiliTool.Domain\n@using Ray.BiliBiliTool.Web.Extensions\n\n<MudDialog>\n    <TitleContent>\n        <div class=\"d-flex\">\n            <MudText Typo=\"Typo.h5\">Logs</MudText>\n            <MudIconButton Icon=\"@Icons.Material.Filled.Refresh\"\n                           OnClick=\"OnRefreshLogs\" />\n        </div>\n    </TitleContent>\n    <DialogContent>\n        <div class=\"terminal-container\">\n            <div class=\"terminal-header\">\n                <div class=\"terminal-title\">Logs Terminal</div>\n                <div class=\"terminal-controls\">\n                    <div class=\"control-dot dot-red\"></div>\n                    <div class=\"control-dot dot-yellow\"></div>\n                    <div class=\"control-dot dot-green\"></div>\n                </div>\n            </div>\n\n            <div class=\"terminal-window\" id=\"logContainer\" @ref=\"_logContainerReference\">\n                @if (_loading && _logs.Count == 0)\n                {\n                    <div class=\"log-entry\">\n                        <span class=\"log-timestamp\">@DateTime.Now.ToString(\"yyyy-MM-dd HH:mm:ss\")</span>\n                        <span class=\"log-level log-level-info\">SYSTEM</span>\n                        <span class=\"log-message\">Loading logs from database...</span>\n                    </div>\n                }\n                else\n                {\n                    @foreach (var log in _logs)\n                    {\n                        <div class=\"log-entry\">\n                            <span class=\"log-timestamp\">@log.Timestamp.ToLocalTime().ToString(\"yyyy-MM-dd HH:mm:ss\")</span>\n                            <span class=\"log-level @GetLogLevelClass(log.Level)\">@log.FormattedLogLevel</span>\n                            <span class=\"log-message\">@((MarkupString)(log.RenderedMessage?.Replace(Environment.NewLine, \"<br />\")??\"\"))</span>\n                            @if (!string.IsNullOrEmpty(log.Exception))\n                            {\n                                <span class=\"log-message\">@log.Exception</span>\n                            }\n                        </div>\n                    }\n                }\n                <div class=\"log-entry cursor\">\n                    <span class=\"log-timestamp\">@DateTime.Now.ToLocalTime().ToString(\"yyyy-MM-dd HH:mm:ss\")</span>\n                </div>\n            </div>\n\n            <div class=\"terminal-footer\">\n                <div class=\"connection-status\">\n                    <span class=\"status-indicator status-connected\"></span>\n                    <span>LIVE</span>\n                </div>\n                <div>\n                    <MudIconButton Icon=\"@Icons.Material.Filled.Refresh\" Color=\"Color.Inherit\" Size=\"Size.Small\"\n                                   Class=\"clear-button\" OnClick=\"OnRefreshLogs\" />\n                    <MudIconButton Icon=\"@Icons.Material.Filled.Clear\" Color=\"Color.Inherit\" Size=\"Size.Small\"\n                                   Class=\"clear-button\" OnClick=\"ClearDisplay\" />\n                </div>\n                <div>\n                    <span>Auto-refresh: 3s</span>\n                </div>\n            </div>\n        </div>\n    </DialogContent>\n    <DialogActions>\n        <MudButton OnClick=\"Close\">Close</MudButton>\n    </DialogActions>\n</MudDialog>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/LogsDialog.razor.cs",
    "content": "using BlazingQuartz.Core.Models;\nusing BlazingQuartz.Core.Services;\nusing Microsoft.AspNetCore.Components;\nusing Microsoft.EntityFrameworkCore;\nusing MudBlazor;\nusing Ray.BiliBiliTool.Domain;\nusing Ray.BiliBiliTool.Infrastructure.EF;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Schedules;\n\npublic partial class LogsDialog : ComponentBase\n{\n    [CascadingParameter]\n    private IMudDialogInstance MudDialog { get; set; } = null!;\n\n    [Inject]\n    private IDialogService DialogSvc { get; set; } = null!;\n\n    [Inject]\n    IExecutionLogService LogSvc { get; set; } = null!;\n\n    [Inject]\n    private IDbContextFactory<BiliDbContext> DbFactory { get; set; } = null!;\n\n    [EditorRequired]\n    [Parameter]\n    public Key JobKey { get; set; } = null!;\n\n    [EditorRequired]\n    [Parameter]\n    public Key? TriggerKey { get; set; }\n\n    void Close() => MudDialog.Cancel();\n\n    private List<BiliLogs> _logs = new();\n    private bool _loading = true;\n    private Timer? _timer;\n    private CancellationTokenSource _cancellationTokenSource = new();\n    private ElementReference _logContainerReference;\n    private string? _fireInstanceId;\n\n    protected override async Task OnInitializedAsync()\n    {\n        await using var context = await DbFactory.CreateDbContextAsync();\n        var execution = await context\n            .ExecutionLogs.Where(x => x.JobName == JobKey.Name && x.TriggerName == TriggerKey!.Name)\n            .OrderByDescending(x => x.FireTimeUtc)\n            .FirstOrDefaultAsync();\n        _fireInstanceId = execution?.RunInstanceId;\n\n        if (_fireInstanceId == null)\n        {\n            return;\n        }\n\n        await OnRefreshLogs();\n        _timer = new Timer(\n            async _ =>\n            {\n                await InvokeAsync(async () =>\n                {\n                    await OnRefreshLogs();\n                    StateHasChanged();\n                });\n            },\n            null,\n            TimeSpan.Zero,\n            TimeSpan.FromSeconds(3)\n        );\n\n        await base.OnInitializedAsync();\n    }\n\n    private async Task OnRefreshLogs()\n    {\n        _loading = true;\n\n        try\n        {\n            await using var context = await DbFactory.CreateDbContextAsync();\n            _logs = await context\n                .BiliLogs.Where(x => x.FireInstanceIdComputed == _fireInstanceId)\n                .OrderBy(l => l.Timestamp)\n                .Take(300) // 限制记录数量，避免加载过多数据\n                .ToListAsync(_cancellationTokenSource.Token);\n        }\n        catch (Exception ex)\n        {\n            // 在生产环境中应该使用日志系统记录异常\n            Console.WriteLine($\"加载日志失败: {ex.Message}\");\n        }\n        finally\n        {\n            _loading = false;\n            StateHasChanged();\n        }\n    }\n\n    private string GetLogLevelClass(string logLevel)\n    {\n        return logLevel.ToLower() switch\n        {\n            \"error\" => \"log-level-error\",\n            \"warning\" => \"log-level-warning\",\n            \"debug\" => \"log-level-debug\",\n            _ => \"log-level-info\",\n        };\n    }\n\n    private void ClearDisplay()\n    {\n        _logs.Clear();\n        StateHasChanged();\n    }\n\n    public void Dispose()\n    {\n        _timer?.Dispose();\n        _cancellationTokenSource.Cancel();\n        _cancellationTokenSource.Dispose();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/LogsDialog.razor.css",
    "content": "/*\n * 终端风格日志显示的全局样式\n * 放置在wwwroot/css/terminal.css\n */\n\n.terminal-container {\n    display: flex;\n    flex-direction: column;\n    height: 75vh;\n    border-radius: 6px;\n    overflow: hidden;\n}\n\n.terminal-window {\n    background-color: #0c0c0c;\n    color: #cccccc;\n    font-family: 'Consolas', 'Courier New', monospace;\n    padding: 16px;\n    overflow-y: auto;\n    flex: 1;\n    min-height: 0;\n}\n\n/* 日志条目 */\n.log-entry {\n    padding: 2px 0;\n    border-bottom: 1px solid #333333;\n    animation: fadeIn 0.3s ease-in-out;\n    display: flex;\n}\n\n/* 时间戳 */\n.log-timestamp {\n    color: #888888;\n    margin-right: 8px;\n    white-space: nowrap;\n}\n\n/* 日志级别通用样式 */\n.log-level {\n    margin-right: 8px;\n    padding: 0 6px;\n    text-align: center;\n    border-radius: 3px;\n    white-space: nowrap;\n    min-width: 50px;\n}\n\n/* 不同日志级别的颜色 */\n.log-level-info {\n    background-color: #2d5a8a;\n    color: #a8d7ff;\n}\n\n.log-level-error {\n    background-color: #8a2d2d;\n    color: #ffbaba;\n}\n\n.log-level-warning {\n    background-color: #8a7d2d;\n    color: #ffe7a8;\n}\n\n.log-level-debug {\n    background-color: #444444;\n    color: #cccccc;\n}\n\n/* 日志消息 */\n.log-message {\n    flex-grow: 1;\n    word-break: break-word;\n}\n\n/* 日志来源 */\n.log-source {\n    color: #888888;\n    margin-left: 8px;\n    white-space: nowrap;\n}\n\n/* 终端标题栏 */\n.terminal-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 8px 16px;\n    background-color: #333;\n    border-radius: 6px 6px 0 0;\n}\n\n.terminal-title {\n    color: white;\n    font-weight: bold;\n}\n\n/* 终端控制按钮 */\n.terminal-controls {\n    display: flex;\n    gap: 8px;\n}\n\n.control-dot {\n    width: 12px;\n    height: 12px;\n    border-radius: 50%;\n}\n\n.dot-red {\n    background-color: #ff5f56;\n}\n\n.dot-yellow {\n    background-color: #ffbd2e;\n}\n\n.dot-green {\n    background-color: #27c93f;\n}\n\n/* 闪烁效果 */\n.blink {\n    animation: blink-animation 1s steps(5, start) infinite;\n}\n\n/* 终端底部状态栏 */\n.terminal-footer {\n    background-color: #333;\n    color: #ccc;\n    padding: 8px 16px;\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    border-radius: 0 0 6px 6px;\n}\n\n/* 光标效果 */\n.cursor::after {\n    content: \"$_\";\n    animation: blink 1s step-end infinite;\n}\n\n/* 动画定义 */\n@keyframes blink {\n    from, to { opacity: 1; }\n    50% { opacity: 0; }\n}\n\n@keyframes fadeIn {\n    from { opacity: 0; transform: translateY(-5px); }\n    to { opacity: 1; transform: translateY(0); }\n}\n\n/* 连接状态样式 */\n.connection-status {\n    display: flex;\n    align-items: center;\n    font-size: 0.8rem;\n}\n\n.status-indicator {\n    width: 8px;\n    height: 8px;\n    border-radius: 50%;\n    margin-right: 6px;\n}\n\n.status-connected {\n    background-color: #27c93f;\n}\n\n.status-icon {\n    font-size: 16px;\n    margin-right: 4px;\n}\n\n/* 按钮悬停效果 */\n.clear-button:hover {\n    background-color: rgba(255, 255, 255, 0.1);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor",
    "content": "@page \"/Schedules\"\n@attribute [Authorize]\n@rendermode InteractiveServer\n@attribute [StreamRendering]\n@using BlazingQuartz\n@using BlazingQuartz.Core.Models\n@using Microsoft.AspNetCore.Authorization\n@using Ray.BiliBiliTool.Web.Extensions\n\n<PageTitle>Schedules</PageTitle>\n\n<div>\n    <MudText Typo=\"Typo.h5\">Schedules</MudText>\n\n    <MudDataGrid @ref=\"_scheduleDataGrid\" Items=\"@ScheduledJobs\"\n                 MultiSelection=\"true\" Filterable=\"false\"\n                 ShowColumnOptions=\"false\" SortMode=\"SortMode.None\"\n                 RowStyleFunc=\"@ScheduleRowStyleFunc\"\n                 Breakpoint=\"Breakpoint.None\"\n                 Hideable=\"true\" Groupable=\"true\" GroupExpanded=\"true\"\n                 RowsPerPage=\"50\">\n        <ColGroup>\n            <col/> @* checkbox *@\n            <col style=\"width: 40px\"/> @* status *@\n            <col style=\"width: 20%\"/> @* job name *@\n            <col style=\"width: 20%\"/> @* trigger *@\n            <col style=\"width: 120px\"/> @* dchedule type *@\n            <col style=\"width: 15%\"/> @* job full name *@\n            <col style=\"width: 120px\"/> @* next run *@\n            <col style=\"width: 120px\"/> @* last type *@\n            <col style=\"width: 110px\"/> @* action *@\n        </ColGroup>\n        <Columns>\n            <SelectColumn T=\"ScheduleModel\" ShowInFooter=\"false\"/>\n\n            <PropertyColumn Title=\"Job Group\" Property=\"x => x.JobGroup\"\n                            Grouping=\"true\" Hidden=\"true\" ShowColumnOptions=\"false\" GroupBy=\"@_groupDefinition\">\n                <GroupTemplate>\n                    <span style=\"font-weight:bold\">\n                        Job Group: @context.Grouping.Key\n                        <MudChip Size=\"Size.Small\" Variant=\"Variant.Outlined\">Total: @context.Grouping.Count()</MudChip>\n                    </span>\n                </GroupTemplate>\n            </PropertyColumn>\n\n            <TemplateColumn Title=\"Status\" Filterable=\"false\" Groupable=\"false\"\n                            HeaderStyle=\"text-align: center;padding: 0px\"\n                            CellStyle=\"text-align: center;padding: 0px\"\n                            Sortable=\"false\"\n                            ShowColumnOptions=\"false\">\n                <CellTemplate>\n                    @switch (context.Item.JobStatus)\n                    {\n                        case JobStatus.Running:\n                            <MudProgressCircular Color=\"Color.Primary\" Size=\"Size.Small\"\n                                                 Style=\"vertical-align: middle;\"\n                                                 title=\"Running\"\n                                                 Indeterminate=\"true\"/>\n                            break;\n                        case JobStatus.Idle:\n                            <MudIcon Icon=\"@Icons.Material.Filled.Circle\" Class=\"green-text\"\n                                     Style=\"vertical-align: middle;\"\n                                     Title=\"Idle\"\n                                     Size=\"Size.Medium\"/>\n                            break;\n                        case JobStatus.Paused:\n                            <MudIcon Icon=\"@Icons.Material.Filled.Circle\" Class=\"grey-text\"\n                                     Style=\"vertical-align: middle;\"\n                                     Title=\"Paused\"\n                                     Size=\"Size.Medium\"/>\n                            break;\n                        case JobStatus.NoSchedule:\n                            <MudIcon Icon=\"@Icons.Material.Filled.AutoDelete\" Class=\"grey-text\"\n                                     Style=\"vertical-align: middle;\"\n                                     Title=\"Schedule completed and job got deleted\"\n                                     Size=\"Size.Small\"/>\n                            break;\n                        case JobStatus.Error:\n                            <MudTooltip>\n                                <ChildContent>\n                                    <MudIcon Icon=\"@Icons.Material.Outlined.ErrorOutline\" Size=\"Size.Small\"\n                                             Style=\"vertical-align: middle;\"\n                                             Color=\"@Color.Error\"/>\n                                </ChildContent>\n                                <TooltipContent>\n                                    <div style=\"max-width: 220px; overflow-wrap: break-word;\">\n                                        @if (!string.IsNullOrEmpty(context.Item.ExceptionMessage))\n                                        {\n                                            @(\"Job has error. \" + context.Item.ExceptionMessage)\n                                        }\n                                        else\n                                        {\n                                            @(\"Job has error.\")\n                                        }\n                                    </div>\n                                </TooltipContent>\n\n                            </MudTooltip>\n                            break;\n                        case JobStatus.NoTrigger:\n                            <MudIcon Icon=\"@Icons.Material.Filled.AlarmOff\" Class=\"grey-text\"\n                                     Style=\"vertical-align: middle;\"\n                                     Title=\"No trigger\"\n                                     Size=\"Size.Small\"/>\n                            break;\n                    }\n                </CellTemplate>\n            </TemplateColumn>\n            <PropertyColumn Title=\"Job Name\" Property=\"x => x.JobName\" ShowColumnOptions=\"false\">\n                <CellTemplate>\n                    <MudTooltip Duration=\"1000\" Text=\"@context.Item.JobDescription\">\n                        @context.Item.JobName\n                    </MudTooltip>\n                </CellTemplate>\n            </PropertyColumn>\n            <PropertyColumn Title=\"Trigger\" Property=\"x => x.TriggerName\"\n                            ShowColumnOptions=\"false\">\n                <CellTemplate>\n                    @if (context.Item.JobStatus == JobStatus.NoTrigger)\n                    {\n                        <MudText Typo=\"Typo.body1\">--</MudText>\n                    }\n                    else\n                    {\n                        <MudTooltip Duration=\"1000\" Text=\"@context.Item.TriggerDescription\">\n                            @context.Item.TriggerName\n                        </MudTooltip>\n                    }\n                </CellTemplate>\n            </PropertyColumn>\n            <PropertyColumn Title=\"Schedule\" Property=\"x => x.TriggerType\">\n                <CellTemplate>\n                    @if (context.Item.JobStatus == JobStatus.NoTrigger)\n                    {\n                        <MudText Typo=\"Typo.body1\">--</MudText>\n                    }\n                    else\n                    {\n                        @if (context.Item.TriggerDetail == null)\n                        {\n                            <div class=\"d-flex align-center\">\n                                <MudIcon Icon=\"@context.Item.TriggerType.GetTriggerTypeIcon()\"\n                                         Title=\"@(context.Item.TriggerType.ToString())\" Size=\"Size.Small\"\n                                         Class=\"mr-1\"/>\n                                @(context.Item.TriggerType == TriggerType.Unknown ? context.Item.TriggerTypeClassName == null ? TriggerType.Unknown.ToString() : context.Item.TriggerTypeClassName : context.Item.TriggerType)\n                            </div>\n                        }\n                        else\n                        {\n                            <MudTooltip>\n                                <ChildContent>\n                                    <div class=\"d-flex align-center\">\n                                        <MudIcon Icon=\"@context.Item.TriggerType.GetTriggerTypeIcon()\"\n                                                 Title=\"@(context.Item.TriggerType.ToString())\" Size=\"Size.Small\"\n                                                 Class=\"mr-1\"/>\n                                        @(context.Item.TriggerType == TriggerType.Unknown ? context.Item.TriggerTypeClassName == null ? TriggerType.Unknown.ToString() : context.Item.TriggerTypeClassName : context.Item.TriggerType)\n                                    </div>\n                                </ChildContent>\n                                <TooltipContent>\n                                    <div\n                                        style=\"max-width: 220px; overflow-wrap: break-word;\">@(context.Item.TriggerDetail?.ToSummaryString())</div>\n                                </TooltipContent>\n                            </MudTooltip>\n                        }\n                    }\n                </CellTemplate>\n            </PropertyColumn>\n            <PropertyColumn Title=\"Job Type\" Property=\"x => x.JobType\">\n                <CellTemplate>\n                    <MudTooltip Duration=\"1000\" Text=\"@context.Item.JobType\">\n                        @context.Item.GetJobTypeShortName()\n                    </MudTooltip>\n                </CellTemplate>\n            </PropertyColumn>\n            <TemplateColumn Title=\"Next Run\" Groupable=\"false\" Filterable=\"false\"\n                            ShowColumnOptions=\"false\">\n                <CellTemplate>\n                    @if (context.Item.JobStatus == JobStatus.NoTrigger)\n                    {\n                        <MudText Typo=\"Typo.body1\">--</MudText>\n                    }\n                    else\n                    {\n                        @context.Item.NextTriggerTime?.LocalDateTime.ToString(\"yyyy-MM-dd HH:mm:ss\")\n                    }\n                </CellTemplate>\n            </TemplateColumn>\n            <TemplateColumn Title=\"Last Run\" Groupable=\"false\" Filterable=\"false\"\n                            ShowColumnOptions=\"false\">\n                <CellTemplate>\n                    <div class=\"d-flex gap-1\">\n                        @if (!string.IsNullOrEmpty(context.Item.ExceptionMessage)\n                             && context.Item.JobStatus != JobStatus.Error)\n                        {\n                            <MudTooltip>\n                                <ChildContent>\n                                    <MudIcon Icon=\"@Icons.Material.Filled.Error\" Size=\"Size.Small\"\n                                             Color=\"@Color.Error\"/>\n                                </ChildContent>\n                                <TooltipContent>\n                                    <div\n                                        style=\"max-width: 220px; overflow-wrap: break-word;\">@context.Item.ExceptionMessage</div>\n                                </TooltipContent>\n\n                            </MudTooltip>\n                        }\n\n                        @if (context.Item.JobStatus == JobStatus.NoTrigger &&\n                             !context.Item.PreviousTriggerTime.HasValue)\n                        {\n                            <MudText Typo=\"Typo.body1\">--</MudText>\n                        }\n                        else\n                        {\n                            @context.Item.PreviousTriggerTime?.LocalDateTime.ToString(\"yyyy-MM-dd HH:mm:ss\")\n                        }\n                    </div>\n                </CellTemplate>\n            </TemplateColumn>\n            <TemplateColumn Title=\"Actions\" Groupable=\"false\" Filterable=\"false\" ShowColumnOptions=\"false\"\n                            HeaderStyle=\"padding-left: 0px;\"\n                            CellClass=\"tab__actions\"\n                            Sortable=\"false\">\n                <CellTemplate>\n                    <MudTooltip Duration=\"1000\" Text=\"Trigger Now\">\n                        <MudIconButton Icon=\"@Icons.Material.Filled.PlayCircleOutline\"\n                                       Size=\"Size.Small\"\n                                       Disabled=\"@IsTriggerNowActionDisabled(context.Item)\"\n                                       OnClick=\"@(async () => await OnTriggerNow(context.Item))\"></MudIconButton>\n                    </MudTooltip>\n                    <MudTooltip Duration=\"1000\" Text=\"Logs\">\n                        <MudIconButton Icon=\"@Icons.Material.Filled.History\"\n                                       Size=\"Size.Small\"\n                                       OnClick=\"@(() => OnLogs(context.Item))\"></MudIconButton>\n                    </MudTooltip>\n                    <MudMenu Icon=\"@Icons.Material.Filled.MoreHoriz\" Size=\"Size.Small\" Style=\"display: inline-block;\">\n                        @if (context.Item.JobStatus is JobStatus.Paused or JobStatus.NoTrigger)\n                        {\n                            <MudMenuItem Disabled=\"@IsRunActionDisabled(context.Item)\" OnClick=\"@(async () => await OnResumeScheduleJob(context.Item))\">\n                                <div class=\"d-flex gap-1\">\n                                    <MudIcon Icon=\"@Icons.Material.Filled.PlayArrow\" Title=\"Enable\" Size=\"Size.Small\"/>\n                                    <MudText Typo=\"Typo.body1\">Enable</MudText>\n                                </div>\n                            </MudMenuItem>\n                        }\n                        else\n                        {\n                            <MudMenuItem Disabled=\"@IsPauseActionDisabled(context.Item)\" OnClick=\"@(async () => await OnPauseScheduleJob(context.Item))\">\n                                <div class=\"d-flex gap-1\">\n                                    <MudIcon Icon=\"@Icons.Material.Filled.Pause\" Title=\"Disable\" Size=\"Size.Small\"/>\n                                    <MudText Typo=\"Typo.body1\">Disable</MudText>\n                                </div>\n                            </MudMenuItem>\n                        }\n                        <MudMenuItem Disabled=\"@IsHistoryActionDisabled(context.Item)\" OnClick=\"@(() => OnJobHistory(context.Item))\">\n                            <div class=\"d-flex gap-1\">\n                                <MudIcon Icon=\"@Icons.Material.Filled.ListAlt\" Title=\"History\" Size=\"Size.Small\"/>\n                                <MudText Typo=\"Typo.body1\">History</MudText>\n                            </div>\n                        </MudMenuItem>\n                    </MudMenu>\n                </CellTemplate>\n            </TemplateColumn>\n        </Columns>\n        <PagerContent>\n            <MudDataGridPager T=\"ScheduleModel\"/>\n        </PagerContent>\n    </MudDataGrid>\n</div>\n\n<MudThemeProvider/>\n<MudPopoverProvider/>\n<MudDialogProvider/>\n<MudSnackbarProvider/>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor.cs",
    "content": "using System.Collections.ObjectModel;\nusing BlazingQuartz;\nusing BlazingQuartz.Core.Events;\nusing BlazingQuartz.Core.Models;\nusing BlazingQuartz.Core.Services;\nusing BlazingQuartz.Jobs.Abstractions;\nusing Microsoft.AspNetCore.Components;\nusing MudBlazor;\nusing Quartz;\nusing Ray.BiliBiliTool.Domain;\nusing Ray.BiliBiliTool.Web.Extensions;\n\nnamespace Ray.BiliBiliTool.Web.Components.Pages.Schedules;\n\npublic partial class Schedules : ComponentBase, IDisposable\n{\n    private ScheduleJobFilter _filter = new();\n    private readonly Func<ScheduleModel, object> _groupDefinition = x => x.JobGroup;\n    private MudDataGrid<ScheduleModel> _scheduleDataGrid = new();\n\n    [Inject]\n    private ILogger<Schedules> Logger { get; set; } = null!;\n\n    [Inject]\n    private ISchedulerService SchedulerSvc { get; set; } = null!;\n\n    [Inject]\n    private ISchedulerListenerService SchedulerListenerSvc { get; set; } = null!;\n\n    [Inject]\n    private IExecutionLogService ExecutionLogSvc { get; set; } = null!;\n\n    [Inject]\n    private IDialogService DialogSvc { get; set; } = null!;\n\n    [Inject]\n    private ISnackbar Snackbar { get; set; } = null!;\n\n    private ObservableCollection<ScheduleModel> ScheduledJobs { get; } = [];\n\n    private static Func<ScheduleModel, int, string> ScheduleRowStyleFunc =>\n        (model, i) =>\n        {\n            if (model.JobStatus == JobStatus.NoSchedule || model.JobStatus == JobStatus.Error)\n                return \"background-color:var(--mud-palette-background-grey)\";\n\n            return \"\";\n        };\n\n    protected override async Task OnInitializedAsync()\n    {\n        RegisterEventListeners();\n        await RefreshJobs();\n    }\n\n    public void Dispose() => UnRegisterEventListeners();\n\n    internal bool IsRunActionDisabled(ScheduleModel model) =>\n        model.JobStatus == JobStatus.NoSchedule || model.JobStatus == JobStatus.NoTrigger;\n\n    internal bool IsPauseActionDisabled(ScheduleModel model) =>\n        model.JobStatus == JobStatus.NoSchedule\n        || model.JobStatus == JobStatus.Error\n        || model.JobStatus == JobStatus.NoTrigger;\n\n    internal bool IsTriggerNowActionDisabled(ScheduleModel model) =>\n        model.JobStatus == JobStatus.NoSchedule\n        || model.JobStatus == JobStatus.Error\n        || model.JobStatus == JobStatus.Running;\n\n    internal bool IsHistoryActionDisabled(ScheduleModel model) =>\n        model.JobStatus == JobStatus.NoSchedule;\n\n    private void RegisterEventListeners()\n    {\n        SchedulerListenerSvc.OnJobToBeExecuted += SchedulerListenerSvc_OnJobToBeExecuted;\n        SchedulerListenerSvc.OnJobScheduled += SchedulerListenerSvc_OnJobScheduled;\n        SchedulerListenerSvc.OnJobWasExecuted += SchedulerListenerSvc_OnJobWasExecuted;\n        SchedulerListenerSvc.OnTriggerFinalized += SchedulerListenerSvc_OnTriggerFinalized;\n        SchedulerListenerSvc.OnJobDeleted += SchedulerListenerSvc_OnJobDeleted;\n        SchedulerListenerSvc.OnJobUnscheduled += SchedulerListenerSvc_OnJobUnscheduled;\n        SchedulerListenerSvc.OnTriggerResumed += SchedulerListenerSvc_OnTriggerResumed;\n        SchedulerListenerSvc.OnTriggerPaused += SchedulerListenerSvc_OnTriggerPaused;\n    }\n\n    private async Task RefreshJobs()\n    {\n        ScheduledJobs.Clear();\n\n        IAsyncEnumerable<ScheduleModel> jobs = SchedulerSvc.GetAllJobsAsync(_filter);\n        await foreach (ScheduleModel job in jobs)\n        {\n            ScheduledJobs.Add(job);\n        }\n\n        if (ScheduledJobs.Any())\n            await _scheduleDataGrid.ExpandAllGroupsAsync();\n\n        await UpdateScheduleModelsLastExecution();\n    }\n\n    private void UnRegisterEventListeners()\n    {\n        SchedulerListenerSvc.OnJobToBeExecuted -= SchedulerListenerSvc_OnJobToBeExecuted;\n        SchedulerListenerSvc.OnJobScheduled -= SchedulerListenerSvc_OnJobScheduled;\n        SchedulerListenerSvc.OnJobWasExecuted -= SchedulerListenerSvc_OnJobWasExecuted;\n        SchedulerListenerSvc.OnTriggerFinalized -= SchedulerListenerSvc_OnTriggerFinalized;\n        SchedulerListenerSvc.OnJobDeleted -= SchedulerListenerSvc_OnJobDeleted;\n        SchedulerListenerSvc.OnJobUnscheduled -= SchedulerListenerSvc_OnJobUnscheduled;\n        SchedulerListenerSvc.OnTriggerResumed -= SchedulerListenerSvc_OnTriggerResumed;\n        SchedulerListenerSvc.OnTriggerPaused -= SchedulerListenerSvc_OnTriggerPaused;\n    }\n\n    private async void SchedulerListenerSvc_OnTriggerPaused(object? sender, EventArgs<TriggerKey> e)\n    {\n        TriggerKey triggerKey = e.Args;\n\n        await InvokeAsync(() =>\n        {\n            ScheduleModel? model = FindScheduleModelByTrigger(triggerKey).SingleOrDefault();\n            if (model != null)\n            {\n                model.JobStatus = JobStatus.Paused;\n                StateHasChanged();\n            }\n        });\n    }\n\n    private async void SchedulerListenerSvc_OnTriggerResumed(\n        object? sender,\n        EventArgs<TriggerKey> e\n    )\n    {\n        TriggerKey triggerKey = e.Args;\n\n        await InvokeAsync(() =>\n        {\n            ScheduleModel? model = FindScheduleModelByTrigger(triggerKey).SingleOrDefault();\n            if (model != null)\n            {\n                model.JobStatus = JobStatus.Idle;\n                StateHasChanged();\n            }\n        });\n    }\n\n    private async void SchedulerListenerSvc_OnJobUnscheduled(\n        object? sender,\n        EventArgs<TriggerKey> e\n    )\n    {\n        Logger.LogInformation(\"Job trigger {triggerKey} got unscheduled\", e.Args);\n        await OnTriggerRemoved(e.Args);\n    }\n\n    private async void SchedulerListenerSvc_OnJobDeleted(object? sender, EventArgs<JobKey> e)\n    {\n        JobKey jobKey = e.Args;\n        Logger.LogInformation(\"Delete all schedule job {jobKey}\", jobKey);\n\n        await InvokeAsync(() =>\n        {\n            List<ScheduleModel> modelList = ScheduledJobs\n                .Where(s => s.JobName == jobKey.Name && s.JobGroup == jobKey.Group)\n                .ToList();\n            modelList.ForEach(s => ScheduledJobs.Remove(s));\n        });\n    }\n\n    private async void SchedulerListenerSvc_OnTriggerFinalized(\n        object? sender,\n        EventArgs<ITrigger> e\n    )\n    {\n        TriggerKey triggerKey = e.Args.Key;\n        Logger.LogInformation(\"Trigger {triggerKey} finalized\", triggerKey);\n\n        await OnTriggerRemoved(triggerKey);\n    }\n\n    private async Task OnTriggerRemoved(TriggerKey triggerKey) =>\n        await InvokeAsync(async () =>\n        {\n            ScheduleModel? model;\n            try\n            {\n                model = FindScheduleModelByTrigger(triggerKey).SingleOrDefault();\n            }\n            catch (Exception ex)\n            {\n                Snackbar.Add(\n                    $\"Cannot update trigger status. Found more than one schedule with trigger {triggerKey}\",\n                    Severity.Warning\n                );\n                Logger.LogWarning(\n                    ex,\n                    \"Cannot update trigger status. Found more than one schedule with trigger {triggerKey}\",\n                    triggerKey\n                );\n                return;\n            }\n\n            if (model is not null)\n            {\n                if (model.JobName == null || model.JobStatus == JobStatus.Error)\n                {\n                    // Just remove if no way to get job details\n                    // if status is error, means get job details will throw exception\n                    ScheduledJobs.Remove(model);\n                }\n                else\n                {\n                    JobDetailModel? jobDetail = await SchedulerSvc.GetJobDetail(\n                        model.JobName,\n                        model.JobGroup\n                    );\n\n                    if (jobDetail != null && jobDetail.IsDurable)\n                    {\n                        // see if similar job name already exists\n                        bool similarJobNameExists = ScheduledJobs.Any(s =>\n                            s != model && s.JobName == model.JobName && s.JobGroup == model.JobGroup\n                        );\n                        if (similarJobNameExists)\n                        {\n                            // delete this duplicate no trigger job\n                            ScheduledJobs.Remove(model);\n                        }\n                        else\n                        {\n                            model.JobStatus = JobStatus.NoTrigger;\n                            model.ClearTrigger();\n                        }\n                    }\n                    else\n                    {\n                        model.JobStatus = JobStatus.NoSchedule;\n                    }\n                }\n\n                StateHasChanged();\n            }\n        });\n\n    private async void SchedulerListenerSvc_OnJobWasExecuted(\n        object? sender,\n        JobWasExecutedEventArgs e\n    )\n    {\n        JobKey jobKey = e.JobExecutionContext.JobDetail.Key;\n        TriggerKey triggerKey = e.JobExecutionContext.Trigger.Key;\n\n        await InvokeAsync(() =>\n        {\n            ScheduleModel? model = FindScheduleModel(jobKey, triggerKey).SingleOrDefault();\n            if (model is not null)\n            {\n                model.PreviousTriggerTime = e.JobExecutionContext.FireTimeUtc;\n                model.NextTriggerTime = e.JobExecutionContext.NextFireTimeUtc;\n                model.JobStatus = JobStatus.Idle;\n                bool? isSuccess = e.JobExecutionContext.GetIsSuccess();\n                if (e.JobException != null)\n                    model.ExceptionMessage = e.JobException.Message;\n                else if (isSuccess.HasValue && !isSuccess.Value)\n                    model.ExceptionMessage = e.JobExecutionContext.GetReturnCodeAndResult();\n\n                StateHasChanged();\n            }\n        });\n    }\n\n    private async void SchedulerListenerSvc_OnJobScheduled(object? sender, EventArgs<ITrigger> e)\n    {\n        if (\n            !_filter.IncludeSystemJobs\n            && (\n                e.Args.JobKey.Group == BlazingQuartz.Constants.SYSTEM_GROUP\n                || e.Args.Key.Group == BlazingQuartz.Constants.SYSTEM_GROUP\n            )\n        )\n        {\n            // system job is not visible, skip this event\n            return;\n        }\n\n        await InvokeAsync(async () =>\n        {\n            ScheduleModel model = await SchedulerSvc.GetScheduleModelAsync(e.Args);\n            ScheduledJobs.Add(model);\n        });\n    }\n\n    private async void SchedulerListenerSvc_OnJobToBeExecuted(\n        object? sender,\n        EventArgs<IJobExecutionContext> e\n    )\n    {\n        JobKey jobKey = e.Args.JobDetail.Key;\n        TriggerKey triggerKey = e.Args.Trigger.Key;\n\n        await InvokeAsync(() =>\n        {\n            ScheduleModel? model = FindScheduleModel(jobKey, triggerKey).SingleOrDefault();\n            if (model is not null)\n            {\n                model.JobStatus = JobStatus.Running;\n\n                StateHasChanged();\n            }\n        });\n    }\n\n    private IEnumerable<ScheduleModel> FindScheduleModelByTrigger(TriggerKey triggerKey) =>\n        ScheduledJobs.Where(j =>\n            j.EqualsTriggerKey(triggerKey)\n            && j.JobStatus != JobStatus.NoSchedule\n            && j.JobStatus != JobStatus.NoTrigger\n        );\n\n    private IEnumerable<ScheduleModel> FindScheduleModel(JobKey jobKey, TriggerKey? triggerKey) =>\n        ScheduledJobs.Where(j =>\n            j.Equals(jobKey, triggerKey)\n            && (\n                (j.JobStatus != JobStatus.NoSchedule && j.JobStatus != JobStatus.NoTrigger)\n                || (j.JobStatus == JobStatus.Error && j.TriggerName != null)\n            )\n        );\n\n    private async Task UpdateScheduleModelsLastExecution()\n    {\n        var latestResult = new PageMetadata(0, 1);\n        var scheduleJobType = new HashSet<LogType> { LogType.ScheduleJob };\n\n        foreach (ScheduleModel schModel in ScheduledJobs)\n        {\n            if (string.IsNullOrEmpty(schModel.JobName))\n                continue;\n\n            PagedList<ExecutionLog> latestLogList = await ExecutionLogSvc.GetLatestExecutionLog(\n                schModel.JobName,\n                schModel.JobGroup,\n                schModel.TriggerName,\n                schModel.TriggerGroup,\n                latestResult,\n                logTypes: scheduleJobType\n            );\n\n            if (latestLogList != null && latestLogList.Any())\n            {\n                ExecutionLog latestLog = latestLogList.First();\n                if (!schModel.PreviousTriggerTime.HasValue)\n                {\n                    schModel.PreviousTriggerTime = latestLog.FireTimeUtc;\n                }\n\n                if (latestLog.IsSuccess.HasValue && !latestLog.IsSuccess.Value)\n                {\n                    schModel.ExceptionMessage = latestLog.GetShortResultMessage();\n                }\n                else if (latestLog.IsException ?? false)\n                {\n                    schModel.ExceptionMessage = latestLog.GetShortExceptionMessage();\n                }\n            }\n        }\n    }\n\n    private async Task OnResumeScheduleJob(ScheduleModel model)\n    {\n        if (model.TriggerName == null)\n        {\n            Snackbar.Add(\"Cannot resume schedule. Trigger name is null.\", Severity.Error);\n            return;\n        }\n\n        await SchedulerSvc.ResumeTrigger(model.TriggerName, model.TriggerGroup);\n    }\n\n    private async Task OnPauseScheduleJob(ScheduleModel model)\n    {\n        if (model.TriggerName == null)\n        {\n            Snackbar.Add(\"Cannot pause schedule. Trigger name is null.\", Severity.Error);\n            return;\n        }\n\n        await SchedulerSvc.PauseTrigger(model.TriggerName, model.TriggerGroup);\n    }\n\n    private void OnJobHistory(ScheduleModel model)\n    {\n        if (model.JobName == null)\n        {\n            // not possible?\n            return;\n        }\n\n        var options = new DialogOptions\n        {\n            CloseOnEscapeKey = true,\n            FullWidth = true,\n            MaxWidth = MaxWidth.Medium,\n        };\n\n        var parameters = new DialogParameters\n        {\n            [\"JobKey\"] = new Key(model.JobName, model.JobGroup),\n            [\"TriggerKey\"] =\n                model.TriggerName != null\n                    ? new Key(\n                        model.TriggerName,\n                        model.TriggerGroup ?? BlazingQuartz.Constants.DEFAULT_GROUP\n                    )\n                    : null,\n        };\n        DialogSvc.ShowAsync<HistoryDialog>(\"Execution History\", parameters, options);\n    }\n\n    private void OnLogs(ScheduleModel model)\n    {\n        if (model.JobName == null)\n        {\n            return;\n        }\n\n        var options = new DialogOptions\n        {\n            CloseOnEscapeKey = true,\n            FullWidth = true,\n            MaxWidth = MaxWidth.Large,\n        };\n\n        var parameters = new DialogParameters\n        {\n            [\"JobKey\"] = new Key(model.JobName, model.JobGroup),\n            [\"TriggerKey\"] =\n                model.TriggerName != null\n                    ? new Key(\n                        model.TriggerName,\n                        model.TriggerGroup ?? BlazingQuartz.Constants.DEFAULT_GROUP\n                    )\n                    : null,\n        };\n        DialogSvc.ShowAsync<LogsDialog>(\"Logs\", parameters, options);\n    }\n\n    private async Task OnTriggerNow(ScheduleModel model)\n    {\n        if (model.JobName == null)\n        {\n            Snackbar.Add(\"Cannot add trigger. Check if job still exists.\", Severity.Error);\n            return;\n        }\n\n        bool? result = await DialogSvc.ShowMessageBox(\n            title: \"Confirm\",\n            markupMessage: (MarkupString)\"Do you want to trigger this job now?\",\n            yesText: \"Trigger\",\n            cancelText: \"Cancel\"\n        );\n\n        if (result != true)\n        {\n            return;\n        }\n\n        await SchedulerSvc.TriggerJob(model.JobName, model.JobGroup);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor.css",
    "content": ""
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Pages/Schedules/Schedules.razor.js",
    "content": "export class Schedules {\n  \n}\n\nwindow.Schedules = Schedules;"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/Routes.razor",
    "content": "﻿@using Microsoft.AspNetCore.Components.Authorization\n\n<CascadingAuthenticationState>\n    <Router AppAssembly=\"typeof(Program).Assembly\" AdditionalAssemblies=\"new[] { typeof(Client._Imports).Assembly }\">\n        <Found Context=\"routeData\">\n            <AuthorizeRouteView RouteData=\"@routeData\" DefaultLayout=\"typeof(Layout.MainLayout)\">\n                <NotAuthorized>\n                    <MudContainer MaxWidth=\"MaxWidth.Small\" Class=\"my-4\">\n                        <MudAlert Severity=\"Severity.Error\">您无权访问此页面，请先登录。</MudAlert>\n                        <MudButton Variant=\"Variant.Filled\" Color=\"Color.Primary\" Link=\"/login\" Class=\"mt-2\">\n                            前往登录\n                        </MudButton>\n                    </MudContainer>\n                </NotAuthorized>\n            </AuthorizeRouteView>\n            <FocusOnNavigate RouteData=\"routeData\" Selector=\"h1\"/>\n        </Found>\n        <NotFound>\n            <PageTitle>Not found</PageTitle>\n            <LayoutView Layout=\"typeof(Layout.MainLayout)\">\n                <p role=\"alert\">Sorry, there's nothing at this address.</p>\n            </LayoutView>\n        </NotFound>\n    </Router>\n</CascadingAuthenticationState>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Components/_Imports.razor",
    "content": "﻿@using System.Net.Http\n@using System.Net.Http.Json\n@using Microsoft.AspNetCore.Components.Forms\n@using Microsoft.AspNetCore.Components.Routing\n@using Microsoft.AspNetCore.Components.Web\n@using static Microsoft.AspNetCore.Components.Web.RenderMode\n@using Microsoft.AspNetCore.Components.Web.Virtualization\n@using Microsoft.JSInterop\n@using Ray.BiliBiliTool.Web\n@using Ray.BiliBiliTool.Web.Client\n@using Ray.BiliBiliTool.Web.Components\n@using MudBlazor\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Constants.cs",
    "content": "namespace Ray.BiliBiliTool.Web;\n\npublic static class Constants\n{\n    public const string BiliJobGroup = \"BiliJob\";\n}\n\npublic enum ScheduleDialogTab\n{\n    Job,\n    Trigger,\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Controllers/AuthController.cs",
    "content": "using System.Security.Claims;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.AspNetCore.Mvc;\nusing Ray.BiliBiliTool.Web.Services;\n\nnamespace Ray.BiliBiliTool.Web.Controllers;\n\n[ApiController]\n[Route(\"auth\")]\npublic class AuthController(IAuthService authService) : ControllerBase\n{\n    [HttpPost(\"login\")]\n    public async Task<IActionResult> Login(\n        [FromForm] string username,\n        [FromForm] string password,\n        [FromForm] string? returnUrl\n    )\n    {\n        var claimsIdentity = await authService.LoginAsync(username, password);\n\n        if (claimsIdentity.IsAuthenticated)\n        {\n            var authProperties = new AuthenticationProperties\n            {\n                IsPersistent = true,\n                ExpiresUtc = DateTimeOffset.UtcNow.AddDays(30),\n                RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : \"/\",\n            };\n\n            await HttpContext.SignInAsync(\n                CookieAuthenticationDefaults.AuthenticationScheme,\n                new ClaimsPrincipal(claimsIdentity),\n                authProperties\n            );\n\n            returnUrl = Url.IsLocalUrl(returnUrl) ? returnUrl : \"/\";\n\n            return Redirect(returnUrl);\n        }\n\n        return Redirect($\"/login?error=true&returnUrl={Uri.EscapeDataString(returnUrl ?? \"/\")}\");\n    }\n\n    [HttpGet(\"logout\")]\n    public async Task<IActionResult> Logout()\n    {\n        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);\n        return Redirect(\"/login\");\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Controllers/TestController.cs",
    "content": "using Microsoft.AspNetCore.Mvc;\n\nnamespace Ray.BiliBiliTool.Web.Controllers;\n\n[ApiController]\n[Route(\"test\")]\npublic class TestController(IConfiguration config) : ControllerBase\n{\n    [HttpGet(\"config\")]\n    public async Task<IActionResult> Config()\n    {\n        await Task.Delay(1);\n        var testConfig = config[\"DailyTaskConfig:NumberOfCoins\"];\n        return Ok(testConfig);\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Extensions/ExecutionLogExtensions.cs",
    "content": "using System.Text;\nusing Ray.BiliBiliTool.Domain;\n\nnamespace Ray.BiliBiliTool.Web.Extensions;\n\npublic static class ExecutionLogExtensions\n{\n    private const int RESULT_DISPLAY_LENGTH = 80;\n\n    public static string GetShortResultMessage(this ExecutionLog log)\n    {\n        StringBuilder strBldr = new StringBuilder();\n\n        if (log.ReturnCode != null)\n        {\n            strBldr.Append(\"Return \" + log.ReturnCode + \". \");\n        }\n\n        if (log.Result != null)\n        {\n            var shortResult = log.Result.Substring(\n                0,\n                Math.Min(log.Result.Length, RESULT_DISPLAY_LENGTH)\n            );\n            strBldr.Append(shortResult);\n        }\n        else if (log.LogType == LogType.ScheduleJob)\n        {\n            if (log.IsSuccess is null)\n            {\n                strBldr.Append(\"Executing...\");\n            }\n            else if (log.IsSuccess.Value)\n            {\n                strBldr.Append(\"Job executed successfully.\");\n            }\n            else\n            {\n                strBldr.Append(\"Failed to execute job.\");\n            }\n        }\n\n        return strBldr.ToString();\n    }\n\n    public static string GetShortExceptionMessage(this ExecutionLog log)\n    {\n        StringBuilder strBldr = new StringBuilder();\n\n        if (log.ReturnCode != null)\n        {\n            strBldr.Append(\"Return \" + log.ReturnCode + \". \");\n        }\n\n        if (log.ErrorMessage != null)\n        {\n            strBldr.Append(\n                log.ErrorMessage.Substring(\n                    0,\n                    Math.Min(log.ErrorMessage.Length, RESULT_DISPLAY_LENGTH)\n                )\n            );\n            return strBldr.ToString();\n        }\n\n        return string.Empty;\n    }\n\n    public static bool ShowExecutionDetailButton(this ExecutionLog log) =>\n        log.ExecutionLogDetail?.ExecutionDetails != null\n        || (log.Result?.Length ?? 0) > RESULT_DISPLAY_LENGTH;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Extensions/ModelExtensions.cs",
    "content": "using BlazingQuartz;\nusing BlazingQuartz.Core.Models;\nusing MudBlazor;\nusing Quartz;\nusing IntervalUnit = BlazingQuartz.IntervalUnit;\n\nnamespace Ray.BiliBiliTool.Web.Extensions;\n\npublic static class ModelExtensions\n{\n    public static string GetTriggerTypeIcon(this TriggerType triggerType)\n    {\n        switch (triggerType)\n        {\n            case TriggerType.Cron:\n                return Icons.Material.Filled.Schedule;\n            case TriggerType.Daily:\n                return Icons.Material.Filled.Alarm;\n            case TriggerType.Simple:\n                return Icons.Material.Filled.Repeat;\n            case TriggerType.Calendar:\n                return Icons.Material.Filled.CalendarMonth;\n            default:\n                return Icons.Material.Filled.Settings;\n        }\n    }\n\n    public static DataMapType GetDataMapType(this KeyValuePair<string, object> kv)\n    {\n        /*\n        bool\tSystem.Boolean\n        byte\tSystem.Byte\n        sbyte\tSystem.SByte\n        char\tSystem.Char\n        decimal\tSystem.Decimal\n        double\tSystem.Double\n        float\tSystem.Single\n        int\tSystem.Int32\n        uint\tSystem.UInt32\n        nint\tSystem.IntPtr\n        nuint\tSystem.UIntPtr\n        long\tSystem.Int64\n        ulong\tSystem.UInt64\n        short\tSystem.Int16\n        ushort\tSystem.UInt16\n        */\n        switch (kv.Value.GetType().FullName)\n        {\n            case \"System.String\":\n                return DataMapType.String;\n            case \"System.Int32\":\n                return DataMapType.Integer;\n            case \"System.Int64\":\n                return DataMapType.Long;\n            case \"System.Boolean\":\n                return DataMapType.Bool;\n            case \"System.Single\":\n                return DataMapType.Float;\n            case \"System.Decimal\":\n                return DataMapType.Decimal;\n            case \"System.Double\":\n                return DataMapType.Double;\n            case \"System.Int16\":\n                return DataMapType.Short;\n            case \"System.Char\":\n                return DataMapType.Char;\n        }\n\n        return DataMapType.Object;\n    }\n\n    public static string GetDataMapTypeDescription(this KeyValuePair<string, object> kv)\n    {\n        var mapType = kv.GetDataMapType();\n        if (mapType == DataMapType.Object)\n        {\n            return $\"Object ({kv.Value.GetType().FullName})\";\n        }\n\n        return mapType.ToString();\n    }\n\n    /// <summary>\n    /// Converts <see cref=\"TimeSpan\"/> objects to a simple human-readable string.  Examples: 3.1 seconds, 2 minutes, 4.23 hours, etc.\n    /// </summary>\n    /// <param name=\"span\">The timespan.</param>\n    /// <param name=\"significantDigits\">Significant digits to use for output.</param>\n    /// <returns></returns>\n    public static string ToHumanTimeString(this TimeSpan span, int significantDigits = 3)\n    {\n        var format = \"G\" + significantDigits;\n        return span.TotalMilliseconds < 1000\n            ? span.TotalMilliseconds.ToString(format) + \" ms\"\n            : (\n                span.TotalSeconds < 60\n                    ? span.TotalSeconds.ToString(format)\n                        + (span.TotalSeconds == 1 ? \" sec\" : \" secs\")\n                    : (\n                        span.TotalMinutes < 60\n                            ? span.TotalMinutes.ToString(format)\n                                + (span.TotalMinutes == 1 ? \" min\" : \" mins\")\n                            : (\n                                span.TotalHours < 24\n                                    ? span.TotalHours.ToString(format)\n                                        + (span.TotalHours == 1 ? \" hr\" : \" hrs\")\n                                    : span.TotalDays.ToString(format)\n                                        + (span.TotalDays == 1 ? \" day\" : \" days\")\n                            )\n                    )\n            );\n    }\n\n    public static bool EqualsTriggerKey(this ScheduleModel model, TriggerKey triggerKey)\n    {\n        return model.TriggerName == triggerKey.Name && model.TriggerGroup == triggerKey.Group;\n    }\n\n    public static bool Equals(this ScheduleModel model, JobKey? jobKey, TriggerKey? triggerKey)\n    {\n        if (jobKey != null && triggerKey != null)\n            return model.JobName == jobKey.Name\n                && model.JobGroup == jobKey.Group\n                && model.TriggerName == triggerKey.Name\n                && model.TriggerGroup == triggerKey.Group;\n\n        if (jobKey != null && triggerKey == null)\n            return model.JobName == jobKey.Name\n                && model.JobGroup == jobKey.Group\n                && model.TriggerName == null\n                && model.TriggerGroup == null;\n\n        // less possible\n        if (jobKey == null && triggerKey != null)\n            return model.TriggerName == triggerKey.Name\n                && model.TriggerGroup == triggerKey.Group\n                && model.JobName == null\n                && model.JobGroup == BlazingQuartz.Constants.DEFAULT_GROUP;\n\n        return model.JobName == null && model.TriggerName == null && model.TriggerGroup == null;\n    }\n\n    public static TriggerType GetTriggerType(this ITrigger trigger)\n    {\n        if (trigger is ICronTrigger)\n            return TriggerType.Cron;\n        if (trigger is ISimpleTrigger)\n            return TriggerType.Simple;\n        if (trigger is ICalendarIntervalTrigger)\n            return TriggerType.Calendar;\n        if (trigger is IDailyTimeIntervalTrigger)\n            return TriggerType.Daily;\n\n        return TriggerType.Unknown;\n    }\n\n    public static TimeOfDay ToTimeOfDay(this TimeSpan timeSpan)\n    {\n        return new TimeOfDay(timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds);\n    }\n\n    public static Quartz.IntervalUnit ToQuartzIntervalUnit(this IntervalUnit value)\n    {\n        return Enum.Parse<Quartz.IntervalUnit>(value.ToString());\n    }\n\n    public static IntervalUnit ToBlazingQuartzIntervalUnit(this Quartz.IntervalUnit value)\n    {\n        return Enum.Parse<IntervalUnit>(value.ToString());\n    }\n\n    public static JobKey ToJobKey(this Key key)\n    {\n        return key.Group == null ? new JobKey(key.Name) : new JobKey(key.Name, key.Group);\n    }\n\n    public static TriggerKey ToTriggerKey(this Key key)\n    {\n        return key.Group == null ? new TriggerKey(key.Name) : new TriggerKey(key.Name, key.Group);\n    }\n\n    /// <summary>\n    /// Return closest non null stack trace of exception.\n    /// Loop until null InnerException to get stack trace.\n    /// </summary>\n    /// <param name=\"exception\"></param>\n    /// <returns>null if inner exceptions does not have stack trace</returns>\n    public static string? NonNullStackTrace(this Exception exception)\n    {\n        Exception? currentException = exception;\n        while (currentException.StackTrace == null)\n        {\n            currentException = currentException.InnerException;\n            if (currentException == null)\n                break;\n        }\n        return currentException?.StackTrace;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Extensions/ServiceCollectionExtension.cs",
    "content": "﻿using Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.AspNetCore.Components.Authorization;\nusing Ray.BiliBiliTool.Web.Auth;\nusing Ray.BiliBiliTool.Web.Services;\n\nnamespace Ray.BiliBiliTool.Web.Extensions;\n\npublic static class ServiceCollectionExtension\n{\n    public static IServiceCollection AddWebServices(this IServiceCollection services)\n    {\n        services.AddScoped<IAuthService, AuthService>();\n\n        return services;\n    }\n\n    public static IServiceCollection AddAuthServices(this IServiceCollection services)\n    {\n        services.AddAuthenticationCore();\n        services.AddAuthorizationCore();\n        services\n            .AddAuthentication(options =>\n            {\n                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;\n            })\n            .AddCookie(options =>\n            {\n                options.Cookie.Name = \"BiliToolWebAuth\";\n                options.LoginPath = \"/login\";\n                options.ExpireTimeSpan = TimeSpan.FromDays(30);\n            });\n        services.AddHttpContextAccessor();\n        services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Extensions/ServiceCollectionQuartzConfiguratorExtensions.cs",
    "content": "using Quartz;\nusing Ray.BiliBiliTool.Web.Jobs;\n\nnamespace Ray.BiliBiliTool.Web.Extensions;\n\npublic static class ServiceCollectionQuartzConfiguratorExtensions\n{\n    private const string DefaultCron = \"0 0 0 1 1 ?\";\n\n    public static IServiceCollectionQuartzConfigurator AddBiliJobs(\n        this IServiceCollectionQuartzConfigurator quartz,\n        IConfiguration configuration\n    )\n    {\n        // Login job\n        quartz.AddJob<LoginJob>(opts => opts.WithIdentity(LoginJob.Key));\n        quartz.AddTrigger(opts =>\n            opts.ForJob(LoginJob.Key)\n                .WithIdentity($\"{LoginJob.Key}.Cron.Trigger\", Constants.BiliJobGroup)\n                .WithCronSchedule(DefaultCron)\n        );\n\n        // Daily job\n        quartz.AddJob<DailyJob>(opts => opts.WithIdentity(DailyJob.Key));\n        quartz.AddTrigger(opts =>\n            opts.ForJob(DailyJob.Key)\n                .WithIdentity($\"{DailyJob.Key}.Cron.Trigger\", Constants.BiliJobGroup)\n                .WithCronSchedule(configuration[\"DailyTaskConfig:Cron\"] ?? DefaultCron)\n        );\n\n        // Manga job\n        quartz.AddJob<MangaJob>(opts => opts.WithIdentity(MangaJob.Key));\n        quartz.AddTrigger(opts =>\n            opts.ForJob(MangaJob.Key)\n                .WithIdentity($\"{MangaJob.Key}.Cron.Trigger\", Constants.BiliJobGroup)\n                .WithCronSchedule(configuration[\"MangaTaskConfig:Cron\"] ?? DefaultCron)\n        );\n\n        // MangaPrivilege job\n        quartz.AddJob<MangaPrivilegeJob>(opts => opts.WithIdentity(MangaPrivilegeJob.Key));\n        quartz.AddTrigger(opts =>\n            opts.ForJob(MangaPrivilegeJob.Key)\n                .WithIdentity($\"{MangaPrivilegeJob.Key}.Cron.Trigger\", Constants.BiliJobGroup)\n                .WithCronSchedule(configuration[\"MangaPrivilegeTaskConfig:Cron\"] ?? DefaultCron)\n        );\n\n        // ReceiveVipPrivilege job\n        quartz.AddJob<VipPrivilegeJob>(opts => opts.WithIdentity(VipPrivilegeJob.Key));\n        quartz.AddTrigger(opts =>\n            opts.ForJob(VipPrivilegeJob.Key)\n                .WithIdentity($\"{VipPrivilegeJob.Key}.Cron.Trigger\", Constants.BiliJobGroup)\n                .WithCronSchedule(\n                    configuration[\"VipPrivilegeConfig:Cron\"] ?? DefaultCron\n                )\n        );\n\n        // Silver2Coin job\n        quartz.AddJob<Silver2CoinJob>(opts => opts.WithIdentity(Silver2CoinJob.Key));\n        quartz.AddTrigger(opts =>\n            opts.ForJob(Silver2CoinJob.Key)\n                .WithIdentity($\"{Silver2CoinJob.Key}.Cron.Trigger\", Constants.BiliJobGroup)\n                .WithCronSchedule(configuration[\"Silver2CoinTaskConfig:Cron\"] ?? DefaultCron)\n        );\n\n        // Charge job\n        quartz.AddJob<ChargeJob>(opts => opts.WithIdentity(ChargeJob.Key));\n        quartz.AddTrigger(opts =>\n            opts.ForJob(ChargeJob.Key)\n                .WithIdentity($\"{ChargeJob.Key}.Cron.Trigger\", Constants.BiliJobGroup)\n                .WithCronSchedule(configuration[\"ChargeTaskConfig:Cron\"] ?? DefaultCron)\n        );\n\n        // Vip big point job\n        quartz.AddJob<VipBigPointJob>(opts => opts.WithIdentity(VipBigPointJob.Key));\n        quartz.AddTrigger(opts =>\n            opts.ForJob(VipBigPointJob.Key)\n                .WithIdentity($\"{VipBigPointJob.Key}.Cron.Trigger\", Constants.BiliJobGroup)\n                .WithCronSchedule(configuration[\"VipBigPointConfig:Cron\"] ?? DefaultCron)\n        );\n\n        // Live lottery job\n        quartz.AddJob<LiveLotteryJob>(opts => opts.WithIdentity(LiveLotteryJob.Key));\n        quartz.AddTrigger(opts =>\n            opts.ForJob(LiveLotteryJob.Key)\n                .WithIdentity($\"{LiveLotteryJob.Key}.Cron.Trigger\", Constants.BiliJobGroup)\n                .WithCronSchedule(configuration[\"LiveLotteryTaskConfig:Cron\"] ?? DefaultCron)\n        );\n\n        // Live fans medal job\n        quartz.AddJob<LiveFansMedalJob>(opts => opts.WithIdentity(LiveFansMedalJob.Key));\n        quartz.AddTrigger(opts =>\n            opts.ForJob(LiveFansMedalJob.Key)\n                .WithIdentity($\"{LiveFansMedalJob.Key}.Cron.Trigger\", Constants.BiliJobGroup)\n                .WithCronSchedule(configuration[\"LiveFansMedalTaskConfig:Cron\"] ?? DefaultCron)\n        );\n\n        // Unfollow batched job\n        quartz.AddJob<UnfollowBatchedJob>(opts => opts.WithIdentity(UnfollowBatchedJob.Key));\n        quartz.AddTrigger(opts =>\n            opts.ForJob(UnfollowBatchedJob.Key)\n                .WithIdentity($\"{UnfollowBatchedJob.Key}.Cron.Trigger\", Constants.BiliJobGroup)\n                .WithCronSchedule(configuration[\"UnfollowBatchedTaskConfig:Cron\"] ?? DefaultCron)\n        );\n\n        // Test bili job\n        quartz.AddJob<TestBiliJob>(opts => opts.WithIdentity(TestBiliJob.Key));\n        quartz.AddTrigger(opts =>\n            opts.ForJob(TestBiliJob.Key)\n                .WithIdentity($\"{TestBiliJob.Key}.Cron.Trigger\", Constants.BiliJobGroup)\n                .WithCronSchedule(DefaultCron)\n        );\n\n        return quartz;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Jobs/BaseJob.cs",
    "content": "using System.Reflection;\nusing Quartz;\nusing Ray.Serilog.Sinks.Batched;\nusing Serilog.Context;\n\nnamespace Ray.BiliBiliTool.Web.Jobs;\n\npublic abstract class BaseJob<TJob>(ILogger<TJob> logger) : IJob\n    where TJob : BaseJob<TJob>\n{\n    public async Task Execute(IJobExecutionContext context)\n    {\n        var fireInstanceId = context.FireInstanceId;\n\n        using (LogContext.PushProperty(\"FireInstanceId\", fireInstanceId))\n        using (\n            LogContext.PushProperty(\n                Ray.Serilog.Sinks.Batched.Constants.GroupPropertyKey,\n                fireInstanceId\n            )\n        )\n        {\n            try\n            {\n                await DoExecuteAsync(context);\n            }\n            catch (Exception e)\n            {\n                logger.LogError(e, e.Message);\n            }\n            finally\n            {\n                logger.LogInformation(\"---\");\n                logger.LogInformation(\n                    \"v{version} 开源 by {url}\",\n                    typeof(Program).Assembly.GetName().Version?.ToString(),\n                    Config.Constants.SourceCodeUrl + Environment.NewLine\n                );\n            }\n        }\n\n        try\n        {\n            await BatchSinkManager.FlushAsync(fireInstanceId);\n        }\n        catch (Exception ex)\n        {\n            logger.LogWarning(ex, \"Fail to push logs\");\n        }\n    }\n\n    protected abstract Task DoExecuteAsync(IJobExecutionContext context);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Jobs/ChargeJob.cs",
    "content": "using Quartz;\nusing Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Web.Jobs;\n\npublic class ChargeJob(ILogger<ChargeJob> logger, IChargeTaskAppService appService)\n    : BaseJob<ChargeJob>(logger)\n{\n    private readonly ILogger<ChargeJob> _logger = logger;\n    public static readonly JobKey Key = new(nameof(ChargeJob), Constants.BiliJobGroup);\n\n    protected override async Task DoExecuteAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation($\"{nameof(ChargeJob)} started.\");\n        await appService.DoTaskAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Jobs/DailyJob.cs",
    "content": "using Quartz;\nusing Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Web.Jobs;\n\npublic class DailyJob(ILogger<DailyJob> logger, IDailyTaskAppService appService)\n    : BaseJob<DailyJob>(logger)\n{\n    private readonly ILogger<DailyJob> _logger = logger;\n    public static readonly JobKey Key = new(nameof(DailyJob), Constants.BiliJobGroup);\n\n    protected override async Task DoExecuteAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation($\"{nameof(DailyJob)} started.\");\n        await appService.DoTaskAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Jobs/LiveFansMedalJob.cs",
    "content": "using Quartz;\nusing Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Web.Jobs;\n\npublic class LiveFansMedalJob(ILogger<LiveFansMedalJob> logger, ILiveFansMedalAppService appService)\n    : BaseJob<LiveFansMedalJob>(logger)\n{\n    private readonly ILogger<LiveFansMedalJob> _logger = logger;\n    public static readonly JobKey Key = new(nameof(LiveFansMedalJob), Constants.BiliJobGroup);\n\n    protected override async Task DoExecuteAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation($\"{nameof(LiveFansMedalJob)} started.\");\n        await appService.DoTaskAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Jobs/LiveLotteryJob.cs",
    "content": "using Quartz;\nusing Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Web.Jobs;\n\npublic class LiveLotteryJob(ILogger<LiveLotteryJob> logger, ILiveLotteryTaskAppService appService)\n    : BaseJob<LiveLotteryJob>(logger)\n{\n    private readonly ILogger<LiveLotteryJob> _logger = logger;\n    public static readonly JobKey Key = new(nameof(LiveLotteryJob), Constants.BiliJobGroup);\n\n    protected override async Task DoExecuteAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation($\"{nameof(LiveLotteryJob)} started.\");\n        await appService.DoTaskAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Jobs/LoginJob.cs",
    "content": "using Quartz;\nusing Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Web.Jobs;\n\npublic class LoginJob(ILogger<LoginJob> logger, ILoginTaskAppService appService)\n    : BaseJob<LoginJob>(logger)\n{\n    private readonly ILogger<LoginJob> _logger = logger;\n    public static readonly JobKey Key = new(nameof(LoginJob), Constants.BiliJobGroup);\n\n    protected override async Task DoExecuteAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation($\"{nameof(LoginJob)} started.\");\n        await appService.DoTaskAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Jobs/MangaJob.cs",
    "content": "using Quartz;\nusing Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Web.Jobs;\n\npublic class MangaJob(ILogger<MangaJob> logger, IMangaTaskAppService appService)\n    : BaseJob<MangaJob>(logger)\n{\n    private readonly ILogger<MangaJob> _logger = logger;\n    public static readonly JobKey Key = new(nameof(MangaJob), Constants.BiliJobGroup);\n\n    protected override async Task DoExecuteAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation($\"{nameof(MangaJob)} started.\");\n        await appService.DoTaskAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Jobs/MangaPrivilegeJob.cs",
    "content": "using Quartz;\nusing Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Web.Jobs;\n\npublic class MangaPrivilegeJob(\n    ILogger<MangaPrivilegeJob> logger,\n    IMangaPrivilegeTaskAppService appService\n) : BaseJob<MangaPrivilegeJob>(logger)\n{\n    private readonly ILogger<MangaPrivilegeJob> _logger = logger;\n    public static readonly JobKey Key = new(nameof(MangaPrivilegeJob), Constants.BiliJobGroup);\n\n    protected override async Task DoExecuteAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation($\"{nameof(MangaPrivilegeJob)} started.\");\n        await appService.DoTaskAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Jobs/Silver2CoinJob.cs",
    "content": "using Quartz;\nusing Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Web.Jobs;\n\npublic class Silver2CoinJob(ILogger<Silver2CoinJob> logger, ISilver2CoinTaskAppService appService)\n    : BaseJob<Silver2CoinJob>(logger)\n{\n    private readonly ILogger<Silver2CoinJob> _logger = logger;\n    public static readonly JobKey Key = new(nameof(Silver2CoinJob), Constants.BiliJobGroup);\n\n    protected override async Task DoExecuteAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation($\"{nameof(Silver2CoinJob)} started.\");\n        await appService.DoTaskAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Jobs/TestBiliJob.cs",
    "content": "using Quartz;\nusing Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Web.Jobs;\n\npublic class TestBiliJob(ILogger<TestBiliJob> logger, ITestAppService appService)\n    : BaseJob<TestBiliJob>(logger)\n{\n    private readonly ILogger<TestBiliJob> _logger = logger;\n    public static readonly JobKey Key = new(nameof(TestBiliJob), Constants.BiliJobGroup);\n\n    protected override async Task DoExecuteAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation($\"{nameof(TestBiliJob)} started.\");\n        await appService.DoTaskAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Jobs/UnfollowBatchedJob.cs",
    "content": "using Quartz;\nusing Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Web.Jobs;\n\npublic class UnfollowBatchedJob(\n    ILogger<UnfollowBatchedJob> logger,\n    IUnfollowBatchedTaskAppService appService\n) : BaseJob<UnfollowBatchedJob>(logger)\n{\n    private readonly ILogger<UnfollowBatchedJob> _logger = logger;\n    public static readonly JobKey Key = new(nameof(UnfollowBatchedJob), Constants.BiliJobGroup);\n\n    protected override async Task DoExecuteAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation($\"{nameof(UnfollowBatchedJob)} started.\");\n        await appService.DoTaskAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Jobs/VipBigPointJob.cs",
    "content": "using Quartz;\nusing Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Web.Jobs;\n\npublic class VipBigPointJob(ILogger<VipBigPointJob> logger, IVipBigPointAppService appService)\n    : BaseJob<VipBigPointJob>(logger)\n{\n    private readonly ILogger<VipBigPointJob> _logger = logger;\n    public static readonly JobKey Key = new(nameof(VipBigPointJob), Constants.BiliJobGroup);\n\n    protected override async Task DoExecuteAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation($\"{nameof(VipBigPointJob)} started.\");\n        await appService.DoTaskAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Jobs/VipPrivilegeJob.cs",
    "content": "using Quartz;\nusing Ray.BiliBiliTool.Application.Contracts;\n\nnamespace Ray.BiliBiliTool.Web.Jobs;\n\npublic class VipPrivilegeJob(\n    ILogger<VipPrivilegeJob> logger,\n    IVipPrivilegeTaskAppService appService\n) : BaseJob<VipPrivilegeJob>(logger)\n{\n    private readonly ILogger<VipPrivilegeJob> _logger = logger;\n    public static readonly JobKey Key = new(nameof(VipPrivilegeJob), Constants.BiliJobGroup);\n\n    protected override async Task DoExecuteAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation($\"{nameof(VipPrivilegeJob)} started.\");\n        await appService.DoTaskAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Program.cs",
    "content": "using BlazingQuartz;\nusing BlazingQuartz.Core;\nusing Microsoft.OpenApi.Models;\nusing MudBlazor.Services;\nusing Quartz;\nusing Quartz.Impl.AdoJobStore;\nusing Ray.BiliBiliTool.Agent.Extensions;\nusing Ray.BiliBiliTool.Application.Extensions;\nusing Ray.BiliBiliTool.Config.Extensions;\nusing Ray.BiliBiliTool.Config.SQLite;\nusing Ray.BiliBiliTool.DomainService.Extensions;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Ray.BiliBiliTool.Infrastructure.EF;\nusing Ray.BiliBiliTool.Infrastructure.EF.Extensions;\nusing Ray.BiliBiliTool.Web.Components;\nusing Ray.BiliBiliTool.Web.Extensions;\nusing Serilog;\nusing Serilog.Debugging;\n\nSelfLog.Enable(Console.Error);\nLog.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger();\n\ntry\n{\n    var builder = WebApplication.CreateBuilder(args);\n\n    builder.Configuration.AddJsonFile(\"config/cookies.json\", optional: true, reloadOnChange: true);\n    var sqliteConnStr = builder.Configuration.GetConnectionString(\"Sqlite\");\n    if (!string.IsNullOrEmpty(sqliteConnStr))\n    {\n        builder.Configuration.AddSqlite(\n            connectionString: sqliteConnStr,\n            tableName: Ray.BiliBiliTool.Config.Constants.SqliteTableName,\n            keyColumnName: \"Key\",\n            valueColumnName: \"Value\"\n        );\n    }\n\n    builder\n        .Services.AddRazorComponents()\n        .AddInteractiveServerComponents()\n        .AddInteractiveWebAssemblyComponents();\n    builder.Services.AddControllers();\n\n    builder.Services.AddEndpointsApiExplorer();\n    builder.Services.AddSwaggerGen(c =>\n    {\n        c.SwaggerDoc(\n            \"v1\",\n            new OpenApiInfo\n            {\n                Title = \"BiliBiliToolPro API\",\n                Version = \"v1\",\n                Description = \"BiliBiliToolPro的API接口文档\",\n                Contact = new OpenApiContact\n                {\n                    Name = \"BiliBiliToolPro\",\n                    Url = new Uri(\"https://github.com/RayWangQvQ/BiliBiliToolPro\"),\n                },\n            }\n        );\n    });\n\n    builder.Services.AddMudServices();\n\n    builder.Services.AddEF();\n\n    builder.Services.AddSerilog(\n        (services, lc) =>\n            lc\n                .ReadFrom.Configuration(builder.Configuration)\n                .ReadFrom.Services(services)\n                .Enrich.FromLogContext()\n                .WriteTo.SQLite(\n                    sqliteDbPath: sqliteConnStr?.Split(';')[0].Split('=')[1],\n                    tableName: \"bili_logs\",\n                    storeTimestampInUtc: true,\n                    batchSize: 7\n                )\n    );\n\n    // Add BlazingQuartz\n    builder.Services.Configure<BlazingQuartzUIOptions>(\n        builder.Configuration.GetSection(\"BlazingQuartz\")\n    );\n    builder.Services.AddBlazingQuartz();\n    builder.Services.AddMudServices();\n\n    builder.Services.AddQuartz(q =>\n    {\n        q.UsePersistentStore(storeOptions =>\n        {\n            storeOptions.UseMicrosoftSQLite(sqlLiteOptions =>\n            {\n                sqlLiteOptions.UseDriverDelegate<SQLiteDelegate>();\n                sqlLiteOptions.ConnectionString =\n                    sqliteConnStr ?? throw new InvalidOperationException();\n                sqlLiteOptions.TablePrefix = \"QRTZ_\";\n            });\n            storeOptions.UseSystemTextJsonSerializer();\n        });\n\n        q.AddBiliJobs(builder.Configuration);\n    });\n    builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);\n\n    builder\n        .Services.AddWebServices()\n        .AddAuthServices()\n        .AddAppServices()\n        .AddDomainServices()\n        .AddBiliBiliConfigs(builder.Configuration)\n        .AddBiliBiliClientApi(builder.Configuration);\n\n    var app = builder.Build();\n\n    Global.ServiceProviderRoot = app.Services;\n\n    using var scope = app.Services.CreateScope();\n    var dbInitializer = scope.ServiceProvider.GetRequiredService<DbInitializer>();\n    dbInitializer.InitializeAsync().Wait();\n\n    if (app.Environment.IsDevelopment())\n    {\n        app.UseWebAssemblyDebugging();\n    }\n    else\n    {\n        app.UseExceptionHandler(\"/Error\", createScopeForErrors: true);\n        app.UseHsts();\n    }\n\n    app.UseHttpsRedirection();\n\n    app.UseStaticFiles();\n    app.UseAntiforgery();\n\n    app.UseSerilogRequestLogging();\n\n    app.MapControllers();\n    app.MapRazorComponents<App>()\n        .AddInteractiveServerRenderMode()\n        .AddInteractiveWebAssemblyRenderMode()\n        .AddAdditionalAssemblies(typeof(Ray.BiliBiliTool.Web.Client._Imports).Assembly);\n\n    app.UseSwagger();\n    app.UseSwaggerUI(c =>\n    {\n        c.SwaggerEndpoint(\"/swagger/v1/swagger.json\", \"BiliBiliToolPro API V1\");\n        c.RoutePrefix = \"swagger\";\n    });\n\n    app.Run();\n}\ncatch (Exception ex)\n{\n    Log.Fatal(ex, \"Application terminated unexpectedly\");\n}\nfinally\n{\n    Log.CloseAndFlush();\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Properties/launchSettings.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/launchsettings.json\",\n    \"profiles\": {\n      \"http\": {\n        \"commandName\": \"Project\",\n        \"dotnetRunMessages\": true,\n        \"launchBrowser\": true,\n        \"inspectUri\": \"{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}\",\n        \"applicationUrl\": \"http://localhost:5000\",\n        \"environmentVariables\": {\n          \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n        }\n      },\n      \"https\": {\n        \"commandName\": \"Project\",\n        \"dotnetRunMessages\": true,\n        \"launchBrowser\": true,\n        \"inspectUri\": \"{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}\",\n        \"applicationUrl\": \"https://localhost:5001;http://localhost:5000\",\n        \"environmentVariables\": {\n          \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n        }\n      },\n      \"http_charles\": {\n        \"commandName\": \"Project\",\n        \"dotnetRunMessages\": true,\n        \"launchBrowser\": true,\n        \"inspectUri\": \"{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}\",\n        \"applicationUrl\": \"http://localhost:5000\",\n        \"environmentVariables\": {\n          \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n          \"HTTP_PROXY\": \"http://192.168.1.10:8888\",\n          \"HTTPS_PROXY\": \"http://192.168.1.10:8888\"\n        }\n      }\n    }\n  }\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Ray.BiliBiliTool.Web.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <Import Project=\"..\\..\\common.props\" />\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <UserSecretsId>1970628b-9d9e-45d1-b337-32d4d1e64e95</UserSecretsId>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore.Design\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"MudBlazor\" />\n    <PackageReference Include=\"Swashbuckle.AspNetCore\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Components.WebAssembly.Server\" />\n    <PackageReference Include=\"Quartz\" />\n    <PackageReference Include=\"Quartz.AspNetCore\" />\n    <PackageReference Include=\"Quartz.Serialization.SystemTextJson\" />\n    <PackageReference Include=\"Serilog.AspNetCore\" />\n    <PackageReference Include=\"Serilog.Sinks.SQLite.Microsoft\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.CoolPushBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.DingTalkBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.GotifyBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.MicrosoftTeamsBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.OtherApiBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.PushPlusBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.ServerChanBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.TelegramBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.WorkWeiXinAppBatched\" />\n    <PackageReference Include=\"Ray.Serilog.Sinks.WorkWeiXinBatched\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\BlazingQuartz.Core\\BlazingQuartz.Core.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Application\\Ray.BiliBiliTool.Application.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Infrastructure.EF\\Ray.BiliBiliTool.Infrastructure.EF.csproj\" />\n    <ProjectReference Include=\"..\\Ray.BiliBiliTool.Web.Client\\Ray.BiliBiliTool.Web.Client.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <Compile Remove=\"Auth\\JwtAuthStateProvider.cs\" />\n  </ItemGroup>\n  <ItemGroup>\n    <Folder Include=\"Logs\\\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Services/AuthService.cs",
    "content": "using System.Security.Claims;\nusing Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.EntityFrameworkCore;\nusing Ray.BiliBiliTool.Infrastructure.EF;\nusing Ray.BiliBiliTool.Infrastructure.Helpers;\n\nnamespace Ray.BiliBiliTool.Web.Services;\n\npublic interface IAuthService\n{\n    Task<ClaimsIdentity> LoginAsync(string username, string password);\n    Task ChangePasswordAsync(string username, string currentPassword, string newPassword);\n    Task<string> GetAdminUserNameAsync();\n}\n\npublic class AuthService(IDbContextFactory<BiliDbContext> dbFactory) : IAuthService\n{\n    public async Task<ClaimsIdentity> LoginAsync(string username, string password)\n    {\n        await using var context = await dbFactory.CreateDbContextAsync();\n        var user = await context.Users.FirstOrDefaultAsync(u => u.Username == username);\n\n        if (user != null && PasswordHelper.VerifyPassword(password, user.Salt, user.PasswordHash))\n        {\n            var claims = new List<Claim> { new(ClaimTypes.Name, username) };\n            claims.AddRange(user.Roles.Select(role => new Claim(ClaimTypes.Role, role)));\n\n            var claimsIdentity = new ClaimsIdentity(\n                claims,\n                CookieAuthenticationDefaults.AuthenticationScheme\n            );\n\n            return claimsIdentity;\n        }\n\n        return new ClaimsIdentity();\n    }\n\n    public async Task ChangePasswordAsync(\n        string username,\n        string currentPassword,\n        string newPassword\n    )\n    {\n        await using var context = await dbFactory.CreateDbContextAsync();\n        var user = await context.Users.FirstAsync(u => u.Id == 1);\n\n        if (!PasswordHelper.VerifyPassword(currentPassword, user.Salt, user.PasswordHash))\n        {\n            throw new Exception(\"Current password is incorrect.\");\n        }\n\n        var (hash, salt) = PasswordHelper.HashPassword(newPassword);\n\n        user.Salt = salt;\n        user.PasswordHash = hash;\n        user.Username = username;\n\n        await context.SaveChangesAsync();\n    }\n\n    public async Task<string> GetAdminUserNameAsync()\n    {\n        await using var context = await dbFactory.CreateDbContextAsync();\n        var user = await context.Users.FirstAsync(u => u.Id == 1);\n        return user.Username;\n    }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/Services/IJobUIProvider.cs",
    "content": "namespace Ray.BiliBiliTool.Web.Services;\n\npublic interface IJobUIProvider\n{\n    Type GetJobUIType(string? jobTypeFullName);\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/appsettings.Development.json",
    "content": "{\n  \"DetailedErrors\": true\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/appsettings.json",
    "content": "{\n  \"PlatformType\": \"Web\",\n  \"AllowedHosts\": \"*\",\n\n  \"ConnectionStrings\": {\n    \"Sqlite\": \"Data Source=./config/BiliBiliTool.db;Cache=Shared\"\n  },\n\n  \"Security\": {\n    \"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\n    \"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\n    \"WebProxy\": \"\" //代理，格式为http://host:port，如果有鉴权则为user:password@http://host:port\n  },\n\n  \"DailyTaskConfig\": {\n    \"Cron\": \"0 0 15 * * ?\",\n    \"IsEnable\": true,\n    \"IsWatchVideo\": true, //是否观看视频\n    \"IsShareVideo\": true, //是否分享视频\n    \"IsDonateCoinForArticle\": false,\n    \"NumberOfCoins\": 5, //每日设定的投币数 [0,5]\n    \"NumberOfProtectedCoins\": 0, // 要保留的硬币数量 [0,int_max]，0 为不保留，int_max 通常取 (2^31)-1\n    \"SaveCoinsWhenLv6\": false, //达到六级后是否开始白嫖[false,true]\n    \"SelectLike\": true, //投币时是否同时点赞[false,true]\n    \"SupportUpIds\": \"\", //优先选择支持的up主Id集合，多个以英文逗号分隔，如：\"123,456\"。配置后会优先从指定的up主下挑选视频进行观看、分享和投币，不配置或配置为-1则表示没有特别支持的up，会从关注和排行耪中随机获取支持视频\n    \"DevicePlatform\": \"android\", //执行客户端操作时的平台 [ios,android]\n  },\n\n  \"MangaTaskConfig\": {\n    \"Cron\": \"0 0 14 * * ?\",\n    \"IsEnable\": true,\n    \"CustomComicId\": 27355, //自定义漫画阅读 comic_id，若不清楚含义请勿修改\n    \"CustomEpId\": 381662 //自定义漫画阅读 ep_id，若不清楚含义请勿修改\n  },\n\n  \"MangaPrivilegeTaskConfig\": {\n    \"Cron\": \"0 0 15 * * ?\",\n    \"IsEnable\": true\n  },\n\n  \"Silver2CoinTaskConfig\": {\n    \"Cron\": \"0 0 8 * * ?\",\n    \"IsEnable\": true\n  },\n\n  \"ChargeTaskConfig\": {\n    \"Cron\": \"0 0 12 28 * ?\",\n    \"IsEnable\": true,\n    \"AutoChargeUpId\": \"\", //指定支持的UP主Id\n    \"ChargeComment\": \"\" //充电后留言\n  },\n\n  \"VipPrivilegeConfig\": {\n    \"Cron\": \"0 0 1 * * ?\",\n    \"IsEnable\": true\n  },\n\n  \"VipBigPointConfig\": {\n    \"Cron\": \"0 7 1 * * ?\",\n    \"IsEnable\": true,\n    \"ViewBangumis\": \"33378\" // 自定义番剧的ssid，若不清楚含义请勿修改（默认为名侦探柯南）\n  },\n\n  \"LiveLotteryTaskConfig\": {\n    \"Cron\": \"0 0 22 * * ?\",\n    \"IsEnable\": true,\n    \"ExcludeAwardNames\": \"舰|船|航海|代金券|自拍|照|写真|图|提督\", //根据关键字排除包含这些文字的奖品名称，多个用“|”分隔，如“照|舰|船|航海|代金券|自拍”\n    \"IncludeAwardNames\": \"\", //根据关键字指定奖品名称必须包含的文字，多个用“|”分隔，如“红包|现金|块|元”\n    \"AutoGroupFollowings\": true, //抽奖结束后是否自动将关注的主播分组到“天选时刻”分组，值域[true,false]\n    \"DenyUids\": \"65566781,1277481241,1643654862,603676925\" //主播Uid黑名单（一般是中奖后的老赖），多个用英文逗号分隔，配置后不会参加黑名单中的主播的抽奖活动\n  },\n\n  \"LiveFansMedalTaskConfig\": {\n    \"Cron\": \"0 5 0 * * ?\",\n    \"IsEnable\": true,\n    \"DanmakuContent\": \"OvO\",\n    \"HeartBeatNumber\": 70, //直播间观看的时长，单位为分钟\",\n    \"HeartBeatSendGiveUpThreshold\": 5, //当心跳包发送连续失败多少次时放弃\n    \"IsSkipLevel20Medal\": true // 是否跳过粉丝牌等级 >=0 的\n  },\n\n  \"UnfollowBatchedTaskConfig\": {\n    \"Cron\": \"0 0 6 1 * ?\",\n    \"IsEnable\": true,\n    \"GroupName\": \"天选时刻\", //取关的分组名称\n    \"Count\": 20, //本次取关个数（倒序，从后往前取关）\n    \"RetainUids\": \"\" //白名单（保留的UpId），多个用英文都好分隔，配置后，批量取关时不会取关配置的Up\n  },\n\n  \"Serilog\": {\n    \"Using\": [\n      \"Serilog.Sinks.Console\",\n      \"Serilog.Sinks.Debug\",\n      \"Serilog.Sinks.File\",\n      \"Ray.Serilog.Sinks.TelegramBatched\",\n      \"Ray.Serilog.Sinks.WorkWeiXinBatched\",\n      \"Ray.Serilog.Sinks.DingTalkBatched\",\n      \"Ray.Serilog.Sinks.ServerChanBatched\",\n      \"Ray.Serilog.Sinks.CoolPushBatched\",\n      \"Ray.Serilog.Sinks.OtherApiBatched\",\n      \"Ray.Serilog.Sinks.PushPlusBatched\",\n      \"Ray.Serilog.Sinks.MicrosoftTeamsBatched\",\n      \"Ray.Serilog.Sinks.WorkWeiXinAppBatched\",\n      \"Ray.Serilog.Sinks.GotifyBatched\"\n    ],\n    \"MinimumLevel\": {\n      \"Default\": \"Information\",\n      \"Override\": {\n        \"Microsoft.AspNetCore\": \"Warning\",\n        \"Microsoft.AspNetCore.HttpLogging\": \"Warning\",\n        \"System.Net.Http.HttpClient\": \"Warning\",\n        \"Quartz.SQL\": \"Warning\"\n      }\n    },\n    \"WriteTo\": [\n      //0.Console\n      {\n        \"Name\": \"Console\",\n        \"Args\": {\n          \"restrictedToMinimumLevel\": \"Information\",\n          \"outputTemplate\": \"[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}\"\n        }\n      },\n      //1.Debug\n      { \"Name\": \"Debug\" },\n      //2.File\n      {\n        \"Name\": \"File\",\n        \"Args\": {\n          \"path\": \"Logs/log.txt\",\n          \"restrictedToMinimumLevel\": \"Verbose\",\n          \"rollingInterval\": 3\n        }\n      },\n\n      //3.Telegram机器人（https://core.telegram.org/bots/api#available-methods）\n      {\n        \"Name\": \"TelegramBatched\",\n        \"Args\": {\n          \"botToken\": \"\",\n          \"chatId\": \"\",\n          \"restrictedToMinimumLevel\": \"Information\",\n          \"proxy\": \"\", //代理，user:password@host:port\n          \"apiHost\": \"https://api.telegram.org\" //可以替换成自己搭建的反代host（https://hostloc.com/thread-805441-1-1.html）\n        }\n      },\n      //4.企业微信机器人（https://work.weixin.qq.com/api/doc/90000/90136/91770）\n      {\n        \"Name\": \"WorkWeiXinBatched\",\n        \"Args\": {\n          \"webHookUrl\": \"\", //群机器人生成\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //5.钉钉机器人（https://developers.dingtalk.com/document/app/overview-of-group-robots）\n      {\n        \"Name\": \"DingTalkBatched\",\n        \"Args\": {\n          \"webHookUrl\": \"\", //群机器人生成\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //6.Server酱（http://sc.ftqq.com/9.version）\n      {\n        \"Name\": \"ServerChanBatched\",\n        \"Args\": {\n          \"scKey\": \"\", //已过时，待删除\n          \"turboScKey\": \"\", //平台生成的ScKey\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //7.酷推\n      {\n        \"Name\": \"CoolPushBatched\",\n        \"Args\": {\n          \"sKey\": \"\",\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //8.自定义Api\n      {\n        \"Name\": \"OtherApiBatched\",\n        \"Args\": {\n          \"api\": \"\",\n          \"placeholder\": \"#msg#\", //占位符\n          \"bodyJsonTemplate\": \"{\\\"msgtype\\\":\\\"markdown\\\",\\\"markdown\\\":{\\\"content\\\":#msg#}}\", //json模板，会当作post的body，占位符会被日志内容替换（日志文本为json字符串，已经带有引号，所有模板中占位符不用使用引号包裹，如例子所示为企业微信的标准推送格式）\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //9.PushPlus（http://www.pushplus.plus/doc/）\n      {\n        \"Name\": \"PushPlusBatched\",\n        \"Args\": {\n          \"token\": \"\",\n          \"channel\": \"\", //渠道,值域[wechat,webhook,cp,sms,mail]，分别对应[微信公众号，指定第三方webhook，企业微信应用，短信，邮件]\n          \"topic\": \"\", //群组编码,用于群发，没有就不填(不填仅发送给自己)；channel为webhook时无效\n          \"webhook\": \"\", //webhook编码(不是地址)，仅在channel使用webhook渠道和CP渠道时需要填写\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //10.MicrosoftTeams\n      {\n        \"Name\": \"MicrosoftTeamsBatched\",\n        \"Args\": {\n          \"webhook\": \"\", //webhook完整地址\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //11.企业微信应用推送\n      {\n        \"Name\": \"WorkWeiXinAppBatched\",\n        \"Args\": {\n          \"corpId\": \"\", //必填\n          \"agentId\": \"\", //必填\n          \"secret\": \"\", //必填\n          \"toUser\": \"@all\",\n          \"toParty\": \"\",\n          \"toTag\": \"\",\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      },\n      //12.gotify推送\n      {\n        \"Name\": \"GotifyBatched\",\n        \"Args\": {\n          \"host\": \"\", //必填，如https://www.mygotify.com\n          \"token\": \"\", //必填，应用（app）的token\n          \"restrictedToMinimumLevel\": \"Information\"\n        }\n      }\n    ],\n    \"Enrich\": [ \"FromLogContext\" ]\n  }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web/wwwroot/app.css",
    "content": "html, body {\n    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;\n}\n\na, .btn-link {\n    color: #006bb7;\n}\n\n.btn-primary {\n    color: #fff;\n    background-color: #1b6ec2;\n    border-color: #1861ac;\n}\n\n.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {\n  box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;\n}\n\n.content {\n    padding-top: 1.1rem;\n}\n\nh1:focus {\n    outline: none;\n}\n\n.valid.modified:not([type=checkbox]) {\n    outline: 1px solid #26b050;\n}\n\n.invalid {\n    outline: 1px solid #e50000;\n}\n\n.validation-message {\n    color: #e50000;\n}\n\n.blazor-error-boundary {\n    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;\n    padding: 1rem 1rem 1rem 3.7rem;\n    color: white;\n}\n\n    .blazor-error-boundary::after {\n        content: \"An error has occurred.\"\n    }\n\n.darker-border-checkbox.form-check-input {\n    border-color: #929292;\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web.Client/Pages/Counter.razor",
    "content": "﻿@page \"/counter\"\n@rendermode InteractiveAuto\n\n<PageTitle>Counter</PageTitle>\n\n<h1>Counter</h1>\n\n<p role=\"status\">Current count: @currentCount</p>\n\n<button class=\"btn btn-primary\" @onclick=\"IncrementCount\">Click me</button>\n\n@code {\n    private int currentCount = 0;\n\n    private void IncrementCount()\n    {\n        currentCount++;\n    }\n\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web.Client/Program.cs",
    "content": "using Microsoft.AspNetCore.Components.WebAssembly.Hosting;\nusing MudBlazor.Services;\n\nvar builder = WebAssemblyHostBuilder.CreateDefault(args);\n\nbuilder.Services.AddMudServices();\n\nawait builder.Build().RunAsync();\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web.Client/Ray.BiliBiliTool.Web.Client.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.BlazorWebAssembly\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>\n    <StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.AspNetCore.Components.WebAssembly\" />\n    <PackageReference Include=\"MudBlazor\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web.Client/_Imports.razor",
    "content": "﻿@using System.Net.Http\n@using System.Net.Http.Json\n@using Microsoft.AspNetCore.Components.Forms\n@using Microsoft.AspNetCore.Components.Routing\n@using Microsoft.AspNetCore.Components.Web\n@using static Microsoft.AspNetCore.Components.Web.RenderMode\n@using Microsoft.AspNetCore.Components.Web.Virtualization\n@using Microsoft.JSInterop\n@using Ray.BiliBiliTool.Web.Client\n@using MudBlazor\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web.Client/wwwroot/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Ray.BiliBiliTool.Web.Client/wwwroot/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  }\n}\n"
  },
  {
    "path": "tencentScf/README.md",
    "content": "# 腾讯云云函数（SCF）部署说明\r\n<!-- TOC depthFrom:2 -->\r\n\r\n- [腾讯云云函数（SCF）部署说明](#腾讯云云函数scf部署说明)\r\n  - [1. 介绍](#1-介绍)\r\n  - [2. 注册账号](#2-注册账号)\r\n  - [3. 部署](#3-部署)\r\n    - [3.1. 方式一：Actions自动部署（推荐）](#31-方式一actions自动部署推荐)\r\n      - [3.1.1. 复刻本仓库到自己的GitHub仓库](#311-复刻本仓库到自己的github仓库)\r\n      - [3.1.2. 到腾讯云获取密钥](#312-到腾讯云获取密钥)\r\n      - [3.1.3. 通过 Secrets 添加配置](#313-通过-secrets-添加配置)\r\n        - [3.1.3.1. 密钥相关配置](#3131-密钥相关配置)\r\n        - [3.1.3.2. 云函数配置](#3132-云函数配置)\r\n        - [3.1.3.3. 自动定时更新部署配置](#3133-自动定时更新部署配置)\r\n      - [3.1.4. 手动执行Actions](#314-手动执行actions)\r\n      - [3.1.5. 测试云函数](#315-测试云函数)\r\n    - [3.2. 方式二：上传zip包部署](#32-方式二上传zip包部署)\r\n      - [3.2.1. 下载压缩包到本地](#321-下载压缩包到本地)\r\n      - [3.2.2. 云函数控制台新增函数服务](#322-云函数控制台新增函数服务)\r\n      - [3.2.3. 手动运行测试](#323-手动运行测试)\r\n      - [3.2.4. 配置触发器，设定运行时间和频率](#324-配置触发器设定运行时间和频率)\r\n  - [4. 测试](#4-测试)\r\n  - [5. 关于腾讯云日志](#5-关于腾讯云日志)\r\n\r\n<!-- /TOC -->\r\n\r\n## 1. 介绍\r\n见[腾讯云官网](https://cloud.tencent.com/document/product/583)\r\n\r\n## 2. 注册账号\r\n注册成功后，需要激活云函数 SCF 功能。因为会赠送免费额度，所以正常使用是免费的。\r\n\r\n## 3. 部署\r\n以下两种方式任选一个适合自己需求的即可。\r\n### 3.1. 方式一：Actions自动部署（推荐）\r\n该方式可以实现自动定期部署，保证代码始终的最新版本的。\r\n#### 3.1.1. 复刻本仓库到自己的GitHub仓库\r\n点击本 GitHub 仓库右上角的 `Fork` 按钮，复刻本项目到自己的仓库。\r\n\r\n#### 3.1.2. 到腾讯云获取密钥\r\n[官方对授权的说明](https://cloud.tencent.com/document/product/1154/43006)\r\n\r\n手动部署时，可以通过扫描二维码来进行授权。但为了避免人工的重复授权，我们将采用密钥授权的方式，步骤如下：\r\n\r\n**Ⅰ.登录腾讯云后，点击进入[腾讯云密钥管理](https://console.cloud.tencent.com/cam/capi)页面**\r\n\r\n如果提示创建子账号，可以先拒绝。简单起见，直接使用主账号创建即可。\r\n\r\n**Ⅱ.点击新建密钥按钮，创建一个密钥。复制并保存 `SecretId` 和 `SecretKey` 两项内容，待会儿会用到它们**\r\n\r\n如下图所示：\r\n![新建密钥](../docs/imgs/tencent-scf-secret.png)\r\n\r\n#### 3.1.3. 通过 Secrets 添加配置\r\n进入自己 fork 的仓库，点击 Settings-> Secrets-> New repository secret，添加如下 Secrets 作为配置：\r\n\r\n##### 3.1.3.1. 密钥相关配置\r\n这两个配置决定了会部署到哪个腾讯云账户的 SCF 下面。\r\n\r\n| 配置名称 | Name | Value |\r\n| :----: | :----: | :----: |\r\n| Id | `TENCENT_SECRET_ID` | 刚才在官网获取到的 SecretId |\r\n| Key | `TENCENT_SECRET_KEY` | 刚才在官网获取到的 SecretKey |\r\n\r\n##### 3.1.3.2. 云函数配置\r\nActions 使用 `Serverless Framework` 来部署，通过 `serverless.yml` 来配置函数信息（如函数应用名称、区域、环境变量和触发器等）。\r\n\r\n配置步骤如下：\r\n\r\n**Ⅰ.拷贝或下载 [serverless.yml](./serverless.yml) 文件内容到本地文件中，开始编辑内容**\r\n\r\n其中主要需要改的是最下方的环境变量，这些环境变量在部署后会添加到云函数中（即云函数控制台看到的环境变量集合），它们将作为应用配置传入 bilibili_tool\r\n\r\n例如，你需要增加一个**环境变量**，在最下方添加内容应该是（注意缩进问题）：\r\n\r\n```yaml\r\n  environment:\r\n    variables: # 根据自己的需要修改\r\n      Ray_BiliBiliCookies__1: 123 # cookie，必填\r\n      Ray_Security__RandomSleepMaxMin: 20\r\n      Ray_Security__IntervalSecondsBetweenRequestApi: 20\r\n```\r\n\r\n注意：其中 cookie 是必填项，其他应用配置的也可以通过添加相应的环境变量实现，建议自己添加UA、推送等配置。（更多配置项请参考[配置说明文档](../docs/configuration.md)，仅用于参考环境变量，请只关注表格中的`环境变量`一项，并参照上面的方式添加）\r\n\r\n> 如果你不熟悉 yml 或者部署时遇到格式问题，建议花几分钟阅读 [YAML 入门教程](https://www.runoob.com/w3cnote/yaml-intro.html)\r\n> 常犯错误：\r\n> Q：我在 Github Secrets 中添加了 `NUMBEROFCOINS`，值为 2，为啥投币数量还是 5。\r\n> A：所有自定义配置项（环境变量） _不能通过 Github Secrets 添加_，只能写在 `serverless.yml` 中。你应该参考上面的例子添加环境变量 `Ray_DailyTaskConfig__NumberOfCoins`。\r\n\r\n其他 `serverless.yml` 可选配置内容请参考[官方说明](https://github.com/serverless-components/tencent-scf/blob/master/docs/configure.md)\r\n\r\n**Ⅱ.拷贝修改后的内容，将其整个添加到 secrets 中**\r\n\r\n| 配置名称 | Name | Value |\r\n| :----: | :----: | :----: |\r\n| serverless.yml内容 | `TENCENT_SERVERLESS_YML` | 刚才拷贝的整个文件内容 |\r\n\r\n如下图所示：\r\n![新增serverless.yml配置到secret](../docs/imgs/tencent-scf-secret_yml.png)\r\n\r\n##### 3.1.3.3. 自动定时更新部署配置\r\n\r\n添加如下配置开启自动定时部署：\r\n\r\n| 配置名称 | Name | Value |\r\n| :----: | :----: | :----: |\r\n| 定时自动部署 | `IS_AUTO_DEPLOY_TENCENT_SCF` | true |\r\n\r\n自动部署只是定时会将自己仓库的代码部署到云函数，想要自动更新，还需要开启仓库的自动同步，详见常见问题文档中的 《我 Fork 之后如何同步原作者的更新内容？》章节\r\n\r\n#### 3.1.4. 手动执行Actions\r\n在自己仓库页面，依次点击 Actions ——> auto-deploy-tencent-scf ——> Run workfolw ,手动触发部署工作流。\r\n\r\n如下图所示：\r\n![运行actions](../docs/imgs/tencent-scf-actions.png)\r\n\r\n#### 3.1.5. 测试云函数\r\n如果部署成功，那么登录自己的腾讯云函数控制台，就可以看到对应的函数应用了。\r\n\r\n请参考下节《测试》进行手动测试函数运行。\r\n\r\n### 3.2. 方式二：上传zip包部署\r\n该方式比较简单直观，但是代码是上传是固定版本，想更新的话需要再次手动上传。\r\n#### 3.2.1. 下载压缩包到本地\r\n点击[BiliBiliToolPro/release](https://github.com/RayWangQvQ/BiliBiliToolPro/releases)，选择最新版本的 `tencent-scf.zip` ，下载到本地\r\n#### 3.2.2. 云函数控制台新增函数服务\r\n**Ⅰ.进入[云函数控制台](https://console.cloud.tencent.com/scf/)，单击左侧导航栏【函数服务】，进入“函数服务”页面。顶部地域选择一个靠近自己地址的，点击新建按钮。**\r\n\r\n如下图：\r\n\r\n![tencent-scf-create.png](../docs/imgs/tencent-scf-create.png)\r\n\r\n**Ⅱ.填写基本信息**\r\n* 创建方式：选择自定义创建\r\n* 函数名称：bilibili_tool\r\n* 地域：刚才已经选过了\r\n* 运行环境：CustomRuntime\r\n* 函数代码提交方式：本地上传zip包\r\n* 执行方法：index.main_handler\r\n* 函数代码：点击后选择之前本地下载好的zip包\r\n\r\n如下图：\r\n\r\n![tencent-scf-create-basic.png](../docs/imgs/tencent-scf-create-basic.png)\r\n\r\n**Ⅲ.点击展开高级配置，添加配置**\r\n* 初始化超时时间：30\r\n* 执行超时时间：86400（会警告超范围，先不用管，下面开启异步之后就好了）\r\n* 环境变量（这里先加 2 个配置就行了，后续可以再添加其他的）：\r\n    * cookie 配置：key 为 `Ray_BiliBiliCookies__1` ， value 为之前浏览器抓取到的cookie字符串\r\n    * 随机睡眠配置：key 为 `Ray_Security__RandomSleepMaxMin` ，value 为 `0` （为了方便测试，所以先关掉，后面测好之后再删掉该配置，或者自己改一个value值）\r\n    * 指定任务：key 为 `Ray_RunTasks` ，value 为 `Test` （供首次部署时测试使用，后续将通过触发器传递功能任务）\r\n\r\n如下图：\r\n\r\n![tencent-scf-create-env.png](../docs/imgs/tencent-scf-create-env.png)\r\n\r\n**Ⅳ.继续下滚，找到执行配置模块：**\r\n* 异步执行：勾选启用\r\n* 状态追踪：勾选启用\r\n\r\n如下图：\r\n\r\n![tencent-scf-create-async.png](../docs/imgs/tencent-scf-create-async.png)\r\n\r\n**Ⅴ.点击完成按钮，创建函数**\r\n\r\n触发器配置先不用管，可以等测试完成后再添加\r\n\r\n#### 3.2.3. 手动运行测试\r\n参考下节《测试》进行手动测试。\r\n\r\n#### 3.2.4. 配置触发器，设定运行时间和频率\r\n**Ⅰ.点击左侧【触发管理】导航，点击“创建触发器”按钮**\r\n\r\n如下图：\r\n\r\n![tencent-scf-trigger-create.png](../docs/imgs/tencent-scf-trigger-create.png)\r\n\r\n**Ⅱ.填写触发器信息**\r\n* 触发方式：定时触发\r\n* 定时任务名称：DailyTask\r\n* 触发周期：自定义触发周期\r\n* Cron表达式：自己根据需求指定，10 15 * * * 表示每天15点10分运行，不会的可以做下搜索工作，规则很简单\r\n* 附加信息：是\r\n* 信息内容：`Daily`\r\n* 立即启用：勾选启用\r\n填完后点击提交按钮提交，即可完成。如下图：\r\n\r\n![tencent-scf-trigger-add.png](../docs/imgs/tencent-scf-trigger-add.png)\r\n\r\n这里的附加信息将作为runTasks（欲运行的任务编码）配置，通过命令行传入程序。想多个任务共用一个触发器的话，可以使用&号拼接任务编码，填入附加信息，如 `Daily&LiveLottery`\r\n\r\n等到触发器设定的时间，对应的触发器就会去运行应用，自动完成任务。\r\n\r\n## 4. 测试\r\n\r\n**Ⅰ.成功部署好函数后，会看到如下的函数管理页面，点击顶部函数代码 Tab 页，准备测试。**\r\n\r\n如下图：\r\n\r\n![tencent-scf-test-1.png](../docs/imgs/tencent-scf-test-1.png)\r\n\r\n**Ⅱ.下拉，找到测试按钮，点击运行测试，页面下方会同步显示日志。如果运行正常，则表示部署已成功。**\r\n\r\n如下图:\r\n\r\n![tencent-scf-test-2](../docs/imgs/tencent-scf-test-2.png)\r\n\r\n**Ⅲ.返回函数配置页面，将之前配置的环境变量`Ray_RunTasks`删除，后续函数将自动执行触发器中配置的功能任务**\r\n\r\n## 5. 关于腾讯云日志\r\n\r\n**Ⅰ.腾讯云关于CLS日志的免费额度说明如下：**\r\n\r\n![Tencent-log-docs-1.png](../docs/imgs/Tencent-log-docs-1.png)\r\n\r\n**Ⅱ.实测每日运行函数日志花费如图：**\r\n\r\n![Tencent-log-bill-1.png](../docs/imgs/Tencent-log-bill-1.png)\r\n\r\n**Ⅲ.如果需要完全白嫖(即不需要任何费用)的话，可以切至[腾讯云日志服务页](https://console.cloud.tencent.com/cls/overview)**\r\n\r\n**Ⅳ.点击侧边栏的日志主题，并找到云函数所在地域，如图所示：**\r\n\r\n![Tencent-logpage-1.png](../docs/imgs/Tencent-logpage-1.png)\r\n\r\n**Ⅴ.点击删除，则将删除此日志主题，云函数因为无法定位到日志集，就不会产生额外费用。**\r\n"
  },
  {
    "path": "tencentScf/bootstrap",
    "content": "#! /bin/bash\nset -euo pipefail\n\nFuncFile=\"$(echo $_HANDLER | cut -d. -f1).sh\"\necho \"[step 1]初始化，开始加载函数文件 $FuncFile\"\nsource ./$FuncFile\necho \"=>完成\"\n\necho \"[step 2]初始化完成，开始发送ready信号\"\ncurl -d \" \" -X POST -s \"http://$SCF_RUNTIME_API:$SCF_RUNTIME_API_PORT/runtime/init/ready\"\necho \"=>完成\"\n\necho \"[step 3]开始循环监听处理事件调用\"\nwhile true\ndo\n HEADERS=\"$(mktemp)\"\n echo \"=>长轮询获取事件\"\n EVENT_DATA=$(curl -sS -LD \"$HEADERS\" -X GET -s \"http://$SCF_RUNTIME_API:$SCF_RUNTIME_API_PORT/runtime/invocation/next\")\n echo \"=>监听到事件：$EVENT_DATA\"\n echo \"=>调用函数,开始处理事件\"\n $(echo \"$_HANDLER\" | cut -d. -f2) \"$EVENT_DATA\"\n RESPONSE=$EVENT_DATA\n echo \"=>返回 $RESPONSE\"\n echo \"=>推送函数处理结果\"\n curl -X POST -s \"http://$SCF_RUNTIME_API:$SCF_RUNTIME_API_PORT/runtime/invocation/response\"  -d \"$RESPONSE\"\ndone"
  },
  {
    "path": "tencentScf/index.sh",
    "content": "echo \"成功加载index.sh函数文件\"\n\nfunction main_handler () {\n    echo \"进入main_handler\"\n    EVENT_DATA=$1\n    echo \"$EVENT_DATA\" 1>&2;\n    runTasks=\"\"\n    if [[ $EVENT_DATA == *Message* ]]\n    then\n        eventMsg=$(echo $EVENT_DATA | grep -Po 'Message[\" :]+\\K[^\"]+')\n        echo \"触发事件中的附加消息（任务编码）为：$eventMsg\"\n        runTasks=\"--runTasks=$eventMsg\"\n    else\n\t    echo \"触发事件中未包含附加消息（任务编码）\"\n    fi\n    echo \"开始运行BiliBiliTool......\"\n    ./Ray.BiliBiliTool.Console $runTasks\n    echo \"函数结束\"\n}\n"
  },
  {
    "path": "tencentScf/publish.bat",
    "content": "::https://docs.microsoft.com/zh-cn/dotnet/core/tools/dotnet-publish\n\ncd ../src/Ray.BiliBiliTool.Console\n\ndotnet publish --configuration Release --runtime linux-x64 --self-contained true -p:PublishTrimmed=true -o ./bin/Publish/tencent-scf\ncopy /y ..\\..\\tencentScf\\* .\\bin\\Publish\\tencent-scf\\\n\npause\n"
  },
  {
    "path": "tencentScf/publish.sh",
    "content": "# https://docs.microsoft.com/zh-cn/dotnet/core/tools/dotnet-publish\n\ncd ../src/Ray.BiliBiliTool.Console\n\ndotnet publish --configuration Release --runtime linux-x64 --self-contained true -p:PublishTrimmed=true -o ../../tencentScf/bin/publish\n\ncd ../../tencentScf\ncp -r ./bootstrap ./index.sh ./bin/publish/\n\ncd ./bin/publish\nchmod 755 index.sh bootstrap\nzip -r -q ../tencent-scf.zip ./*"
  },
  {
    "path": "tencentScf/serverless.yml",
    "content": "# https://github.com/serverless-components/tencent-scf/blob/master/docs/configure.md\n\n#组件信息\ncomponent: scf\nname: bilibili_tool\n\n#组件参数配置\ninputs:\n  name: ${name}_scf\n  namespace: default\n  enableRoleAuth: false\n  src: ./bin/publish/\n  handler: index.main_handler\n  runtime: CustomRuntime\n  region: ap-guangzhou # 函数所在区域，默认广州\n  description: This is a function in ${app} application.\n  memorySize: 128\n  initTimeout: 30\n  timeout: 86400\n  asyncRunEnable: true\n  traceEnable: true\n  events: # 根据自己的需要修改\n    - timer: # 每日任务触发器\n        parameters:\n          name: DailyTask\n          cronExpression: \"0 30 12 * * * *\"\n          enable: true\n          argument: Daily\n    - timer: # 天选抽奖触发器\n        parameters:\n          name: LiveLotteryTask\n          cronExpression: \"0 0 */4 * * * *\"\n          enable: true\n          argument: LiveLottery\n    - timer: # 自动取关\n        parameters:\n          name: UnfollowBatchedTask\n          cronExpression: \"0 0 0 * * MON *\"\n          enable: true\n          argument: UnfollowBatched\n    - timer: # 大会员积分\n        parameters:\n          name: VipBigPointTask\n          cronExpression: \"0 07 01 * * * *\"\n          enable: true\n          argument: VipBigPoint\n  environment:\n    variables: # 根据自己的需要修改\n      Ray_BiliBiliCookies__1: 123 # cookie，必填\n"
  },
  {
    "path": "test/AppServiceTest/AppServiceTest.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n    <PackageReference Include=\"xunit\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ray.BiliBiliTool.Console\\Ray.BiliBiliTool.Console.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/AppServiceTest/DailyTask/DonateCoinsTest.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.Infrastructure;\n\nnamespace AppServiceTest.DailyTask\n{\n    public class DonateCoinsTest\n    {\n        public DonateCoinsTest()\n        {\n            Program.CreateHost(new[] { \"--ENVIRONMENT=Development\" });\n        }\n\n        [Fact]\n        public void Test1()\n        {\n            using var scope = Global.ServiceProviderRoot.CreateScope();\n            var appService = scope.ServiceProvider.GetRequiredService<IDailyTaskAppService>();\n        }\n    }\n}\n"
  },
  {
    "path": "test/AppServiceTest/Usings.cs",
    "content": "global using Ray.BiliBiliTool.Console;\nglobal using Xunit;\n"
  },
  {
    "path": "test/AppServiceTest/VipServiceTest.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Application.Contracts;\nusing Ray.BiliBiliTool.DomainService.Dtos;\nusing Ray.BiliBiliTool.Infrastructure;\n\nnamespace AppServiceTest;\n\npublic class VipServiceTest\n{\n    public VipServiceTest()\n    {\n        Program.CreateHost(new[] { \"--ENVIRONMENT=Development\" });\n    }\n\n    [Fact]\n    public async Task CompleteV2Test()\n    {\n        using var scope = Global.ServiceProviderRoot.CreateScope();\n        var api = scope.ServiceProvider.GetRequiredService<IVipBigPointApi>();\n        var res = await api.CompleteV2(new ReceiveOrCompleteTaskRequest(\"dress-view\"), null);\n        Assert.True(res.Code == 0);\n    }\n\n    [Fact]\n    public async Task ReceiveV2Test()\n    {\n        using var scope = Global.ServiceProviderRoot.CreateScope();\n        var api = scope.ServiceProvider.GetRequiredService<IVipBigPointApi>();\n        var res = await api.ReceiveV2(new ReceiveOrCompleteTaskRequest(\"ogvwatchnew\"), null);\n        Assert.True(res.Code == 0);\n    }\n}\n"
  },
  {
    "path": "test/BiliAgentTest/BiliAgentTest.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <UserSecretsId>a6e5b261-0fe9-49e1-82e1-02349db119b4</UserSecretsId>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n    <PackageReference Include=\"xunit\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ray.BiliBiliTool.Console\\Ray.BiliBiliTool.Console.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/BiliAgentTest/LiveTraceApiTest.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Console;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\nusing Xunit;\n\nnamespace BiliAgentTest\n{\n    public class LiveTraceApiTest\n    {\n        public LiveTraceApiTest()\n        {\n            Program.CreateHost(new[] { \"--ENVIRONMENT=Development\" });\n        }\n\n        [Fact]\n        public void WebHeartBeat_Normal_Success()\n        {\n            using var scope = Global.ServiceProviderRoot.CreateScope();\n\n            var ck = scope.ServiceProvider.GetRequiredService<CookieStrFactory<BiliCookie>>();\n            var api = scope.ServiceProvider.GetRequiredService<ILiveTraceApi>();\n\n            var request = new WebHeartBeatRequest(63666, 60);\n\n            var re = api.WebHeartBeat(request, null).Result;\n\n            Assert.Equal(0, re.Code);\n            Assert.Equal(\"0\", re.Message);\n            Assert.Equal(60, re.Data.Next_interval);\n        }\n    }\n}\n"
  },
  {
    "path": "test/BiliAgentTest/VideoApiTest.cs",
    "content": "using System.Threading.Tasks;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Console;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\nusing Xunit;\n\nnamespace BiliAgentTest;\n\npublic class VideoApiTest\n{\n    public VideoApiTest()\n    {\n        Program.CreateHost(new[] { \"--ENVIRONMENT=Development\" });\n    }\n\n    [Fact]\n    public void GetLiveWalletStatus_Normal_Success()\n    {\n        using var scope = Global.ServiceProviderRoot.CreateScope();\n\n        var ck = scope.ServiceProvider.GetRequiredService<CookieStrFactory<BiliCookie>>();\n        var api = scope.ServiceProvider.GetRequiredService<IVideoApi>();\n\n        var req = new GetAlreadyDonatedCoinsRequest(248097491);\n        BiliApiResponse<DonatedCoinsForVideo>? re = api.GetDonatedCoinsForVideo(req, null).Result;\n\n        if (ck.Count > 0)\n        {\n            Assert.True(re.Code == 0 && re.Data.Multiply >= 0);\n        }\n        else\n        {\n            Assert.False(re.Code != 0);\n        }\n    }\n\n    [Fact]\n    public async Task GetBangumiTest()\n    {\n        using var scope = Global.ServiceProviderRoot.CreateScope();\n\n        var ck = scope.ServiceProvider.GetRequiredService<CookieStrFactory<BiliCookie>>();\n        var api = scope.ServiceProvider.GetRequiredService<IVideoApi>();\n        var req = await api.GetBangumiBySsid(46508, null);\n\n        Assert.Equal(0, req.Code);\n    }\n\n    [Fact]\n    public async Task GetRandomVideoOfRanking()\n    {\n        using var scope = Global.ServiceProviderRoot.CreateScope();\n\n        var ck = scope.ServiceProvider.GetRequiredService<CookieStrFactory<BiliCookie>>();\n        var api = scope.ServiceProvider.GetRequiredService<IVideoWithoutCookieApi>();\n        var req = await api.GetRegionRankingVideosV2();\n\n        Assert.Equal(0, req.Code);\n    }\n}\n"
  },
  {
    "path": "test/ConfigTest/ConfigTest.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <IsPackable>false</IsPackable>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n    <PackageReference Include=\"xunit\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ray.BiliBiliTool.Console\\Ray.BiliBiliTool.Console.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/ConfigTest/TestDefaultValue.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Text.Json;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Config;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Console;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Xunit;\n\nnamespace ConfigTest\n{\n    public class TestDefaultValue\n    {\n        public TestDefaultValue()\n        {\n            Program.CreateHost(null);\n        }\n\n        [Fact]\n        public void Test1()\n        {\n            using var scope = Global.ServiceProviderRoot.CreateScope();\n\n            var options = scope.ServiceProvider.GetRequiredService<\n                IOptionsMonitor<DailyTaskOptions>\n            >();\n            var re = options.CurrentValue.NumberOfCoins;\n            Debug.WriteLine(re);\n        }\n    }\n}\n"
  },
  {
    "path": "test/ConfigTest/UnitTest1.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing System.Net;\nusing System.Net.Http;\nusing System.Text.Json;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Options;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Config;\nusing Ray.BiliBiliTool.Config.Options;\nusing Ray.BiliBiliTool.Console;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Xunit;\n\nnamespace ConfigTest\n{\n    public class UnitTest1\n    {\n        [Fact]\n        public void WebProxyTest()\n        {\n            Program.CreateHost(new string[] { });\n            string proxyAddress = Global.ConfigurationRoot[\"Security:WebProxy\"];\n\n            if (!proxyAddress.IsNullOrEmpty())\n            {\n                WebProxy webProxy = new WebProxy();\n\n                //user:password@host:port http proxy only .Tested with tinyproxy-1.11.0-rc1\n                if (proxyAddress.Contains(\"@\"))\n                {\n                    string userPass = proxyAddress.Split(\"@\")[0];\n                    string address = proxyAddress.Split(\"@\")[1];\n\n                    string proxyUser = userPass.Split(\":\")[0];\n                    string proxyPass = userPass.Split(\":\")[1];\n\n                    webProxy.Address = new Uri(\"http://\" + address);\n                    webProxy.Credentials = new NetworkCredential(proxyUser, proxyPass);\n                }\n                else\n                {\n                    webProxy.Address = new Uri(proxyAddress);\n                }\n\n                HttpClient.DefaultProxy = webProxy;\n\n                HttpClient httpClient = new HttpClient();\n                var response = httpClient.GetAsync(\"http://api.ipify.org/\");\n                var resultIp = response.Result.Content.ReadAsStringAsync().Result;\n                Debug.WriteLine(String.Format(\"��ǰIP�� {0}\", resultIp));\n            }\n        }\n\n        [Fact]\n        public void Test1()\n        {\n            Program.CreateHost(new string[] { });\n\n            string s = Global.ConfigurationRoot[\"BiliBiliCookie:UserId\"];\n            Debug.WriteLine(s);\n\n            string logLevel = Global.ConfigurationRoot[\n                \"Serilog:WriteTo:0:Args:restrictedToMinimumLevel\"\n            ];\n            Debug.WriteLine(logLevel);\n\n            var cookie = Global.ServiceProviderRoot.GetRequiredService<BiliCookie>();\n\n            Debug.WriteLine(\n                JsonSerializer.Serialize(cookie, new JsonSerializerOptions { WriteIndented = true })\n            );\n            Assert.True(!string.IsNullOrWhiteSpace(cookie.UserId));\n        }\n\n        /// <summary>\n        /// ���Ի�������Key�ķָ���\n        /// </summary>\n        [Fact]\n        public void TestEnvKeyDelimiter()\n        {\n            Environment.SetEnvironmentVariable(\"Ray_BiliBiliCookie__UserId\", \"123\");\n            Program.CreateHost(null);\n\n            string result = Global.ConfigurationRoot[\"BiliBiliCookie:UserId\"];\n\n            Assert.Equal(\"123\", result);\n        }\n\n        [Fact]\n        public void LoadPrefixConfigByEnvWithNoError()\n        {\n            Environment.SetEnvironmentVariable(\"Ray_BiliBiliCookie\", \"UserId: 123\");\n            Program.CreateHost(new string[] { });\n\n            string result = Global.ConfigurationRoot[\"BiliBiliCookie\"];\n\n            Assert.Equal(\"UserId: 123\", result);\n            Environment.SetEnvironmentVariable(\"Ray_BiliBiliCookie\", null);\n        }\n\n        [Fact]\n        public void LoadPrefixConfigByEnvWhenValueIsNullWithNoError2()\n        {\n            Environment.SetEnvironmentVariable(\"Ray_BiliBiliCookie\", null);\n            Program.CreateHost(new string[] { });\n\n            string result = Global.ConfigurationRoot[\"BiliBiliCookie\"];\n\n            Assert.Null(result);\n        }\n\n        [Fact]\n        public void CoverConfigByEnvWithNoError()\n        {\n            Environment.SetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\", \"Production\");\n            Program.CreateHost(new string[] { });\n\n            string result = Global.ConfigurationRoot[\"IsPrd\"];\n\n            Assert.Equal(\"True\", result);\n            Environment.SetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\", null);\n        }\n\n        /// <summary>\n        /// Ϊ�����ֶ���ֵ\n        /// </summary>\n        [Fact]\n        public void TestSetConfiguration()\n        {\n            Environment.SetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\", \"Development\");\n            Program.CreateHost(new string[] { });\n\n            var options = Global.ServiceProviderRoot.GetRequiredService<\n                IOptionsMonitor<BiliBiliCookieOptions>\n            >();\n            Debug.WriteLine(options.CurrentValue.ToJsonStr());\n\n            //�ֶ���ֵ\n            //RayConfiguration.Root[\"BiliBiliCookie:UserId\"] = \"123456\";\n            //options.CurrentValue.UserId = \"123456\";\n\n            Debug.WriteLine(\n                $\"��Configuration��ȡ��{Global.ConfigurationRoot[\"BiliBiliCookie:UserId\"]}\"\n            );\n\n            Debug.WriteLine($\"����options��ȡ��{options.CurrentValue.ToJsonStr()}\");\n\n            var optionsNew = Global.ServiceProviderRoot.GetRequiredService<\n                IOptionsMonitor<BiliBiliCookieOptions>\n            >();\n            Debug.WriteLine($\"����options��ȡ��{optionsNew.CurrentValue.ToJsonStr()}\");\n        }\n\n        /// <summary>\n        /// Ϊ�����ֶ���ֵ\n        /// </summary>\n        [Fact]\n        public void TestHostDefaults()\n        {\n            Debug.WriteLine(Environment.GetEnvironmentVariable(HostDefaults.EnvironmentKey));\n        }\n\n        [Fact]\n        public void Test()\n        {\n            var s = \"0123456\";\n\n            var s1 = s[..2];\n\n            var s2 = s[4..];\n        }\n    }\n}\n"
  },
  {
    "path": "test/DomainServiceTest/ArticleDomainServiceTest.cs",
    "content": "﻿using Xunit.Abstractions;\n\nnamespace DomainServiceTest;\n\npublic class ArticleDomainServiceTest\n{\n    private readonly ITestOutputHelper _output;\n\n    public ArticleDomainServiceTest(ITestOutputHelper output)\n    {\n        _output = output;\n        Program.CreateHost(new[] { \"--ENVIRONMENT=Development\" });\n    }\n}\n"
  },
  {
    "path": "test/DomainServiceTest/CalculateUpgradeTimeTest.cs",
    "content": "﻿using Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\n\nnamespace DomainServiceTest;\n\npublic class CalculateUpgradeTimeTest\n{\n    public CalculateUpgradeTimeTest()\n    {\n        Program.CreateHost(new[] { \"--ENVIRONMENT=Development\" });\n    }\n\n    [Fact]\n    public void TestCalculateUpgradeTime()\n    {\n        using var scope = Global.ServiceProviderRoot.CreateScope();\n        var accountDomainService =\n            scope.ServiceProvider.GetRequiredService<IAccountDomainService>();\n        int needDay = accountDomainService.CalculateUpgradeTime(\n            new UserInfo\n            {\n                Money = 7,\n                Level_info = new LevelInfo()\n                {\n                    Current_level = 5,\n                    Current_exp = 100,\n                    Next_exp = 200,\n                },\n                Uname = \"uname\",\n                Wallet = new(),\n                Wbi_img = new() { img_url = \"\", sub_url = \"\" },\n            }\n        );\n        int needDay2 = accountDomainService.CalculateUpgradeTime(\n            new UserInfo()\n            {\n                Money = 7,\n                Level_info = new LevelInfo()\n                {\n                    Current_level = 5,\n                    Current_exp = 1000,\n                    Next_exp = 2000,\n                },\n                Uname = \"uname\",\n                Wallet = new(),\n                Wbi_img = new() { img_url = \"\", sub_url = \"\" },\n            }\n        );\n\n        Assert.Equal(1, needDay);\n        Assert.Equal(37, needDay2);\n    }\n}\n"
  },
  {
    "path": "test/DomainServiceTest/DomainServiceTest.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n    <PackageReference Include=\"xunit\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ray.BiliBiliTool.Console\\Ray.BiliBiliTool.Console.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/DomainServiceTest/DonateCoinDomainServiceTest.cs",
    "content": "namespace DomainServiceTest;\n\npublic class DonateCoinDomainServiceTest\n{\n    public DonateCoinDomainServiceTest()\n    {\n        Program.CreateHost(new[] { \"--ENVIRONMENT=Development\" });\n    }\n}\n"
  },
  {
    "path": "test/DomainServiceTest/Usings.cs",
    "content": "global using Microsoft.Extensions.DependencyInjection;\nglobal using Ray.BiliBiliTool.Console;\nglobal using Ray.BiliBiliTool.DomainService.Interfaces;\nglobal using Ray.BiliBiliTool.Infrastructure;\nglobal using Xunit;\n"
  },
  {
    "path": "test/DomainServiceTest/VideoDomainServiceTest.cs",
    "content": "using Ray.BiliBiliTool.Infrastructure.Helpers;\nusing Ray.Infrastructure.Helpers;\n\nnamespace DomainServiceTest\n{\n    public class VideoDomainServiceTest\n    {\n        public VideoDomainServiceTest()\n        {\n            Program.CreateHost(new[] { \"--ENVIRONMENT=Development\" });\n        }\n\n        [Fact]\n        public async Task GetVideoCountOfUp_Test()\n        {\n            using var scope = Global.ServiceProviderRoot.CreateScope();\n            var config = Global.ConfigurationRoot;\n            var domainService = scope.ServiceProvider.GetRequiredService<IVideoDomainService>();\n\n            await domainService.GetVideoCountOfUp(1585227649, null);\n        }\n\n        [Fact]\n        public async Task GetRandomVideoOfUp_Test()\n        {\n            using var scope = Global.ServiceProviderRoot.CreateScope();\n            var config = Global.ConfigurationRoot;\n            var domainService = scope.ServiceProvider.GetRequiredService<IVideoDomainService>();\n\n            await domainService.GetRandomVideoOfUp(1585227649, 1, null);\n        }\n    }\n}\n"
  },
  {
    "path": "test/InfrastructureTest/InfrastructureTest.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n    <PackageReference Include=\"xunit\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" />\n    <PackageReference Include=\"coverlet.collector\">\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/InfrastructureTest/Usings.cs",
    "content": "global using Xunit;\n"
  },
  {
    "path": "test/InfrastructureTest/WbiHelperTest.cs",
    "content": "using System.Diagnostics;\nusing System.Text.RegularExpressions;\n\nnamespace InfrastructureTest\n{\n    public class WbiHelperTest\n    {\n        [Fact]\n        public void Replace_Test()\n        {\n            string input = \"����һ�ΰ��������ַ�!@#$%^&*(')���ַ���\";\n            string pattern = \"[!'()*]\";\n            string replacement = \"\";\n\n            string output = Regex.Replace(input, pattern, replacement);\n            Debug.WriteLine(output);\n\n            Assert.Equal(\"����һ�ΰ��������ַ�@#$%^&���ַ���\", output);\n        }\n    }\n}\n"
  },
  {
    "path": "test/LogTest/LogConstants.cs",
    "content": "﻿using System;\n\nnamespace LogTest\n{\n    public class LogConstants\n    {\n        public static string Msg =\n            \"ℹ 版本号：\\\"1.1.0\\\"\\r\\nℹ 开源地址：\\\"https://github.com/RayWangQvQ/BiliBiliTool\\\"\\r\\nℹ 当前环境：\\\"Development\\\" \\r\\n\\r\\nℹ -----开始每日任务-----\\r\\n\\r\\nℹ ---开始【\\\"登录\\\"】---\\r\\nℹ 登录成功，用户名: \\\"在*楼\\\"\\r\\nℹ 硬币余额: 666.5\\r\\nℹ 距离升级到Lv6还有: 258天\\r\\nℹ ---结束---\\r\\n\\r\\nℹ ---开始【\\\"观看、分享视频\\\"】---\\r\\nℹ 获取随机视频：\\\"【周深&李克勤】“勤深深”组合《突如其来的爱情》日语+粤语无缝切换天作之合！\\\"\\r\\nℹ 今天已经观看过了，不需要再看啦\\r\\nℹ 视频分享成功\\r\\nℹ ---结束---\\r\\n\\r\\nℹ ---开始【\\\"投币\\\"】---\\r\\nℹ 今日已投0枚硬币，目标是投5枚，还需再投5枚\\r\\nℹ 投币前余额为 : 666.5\\r\\nℹ 为“\\\"【本宫吐槽】变态赛高：传说中的神番《永生之酒》，究竟神在哪里？\\\"”投币成功\\r\\nℹ 为“\\\"鲲鹏计算引领多样性计算新时代【思维+】\\\"”投币成功\\r\\nℹ 为“\\\"【瞎看什么】激突！王者告诉你爸爸的爸爸叫什么！\\\"”投币成功\\r\\nℹ 为“\\\"带着18个妹子去广东吃吃吃，没想到却把她们苦哭了！\\\"”投币成功\\r\\nℹ 为“\\\"沙盘推演：红25军长征记（第一阶段）激战独树镇  徐海东血战庾家河  年龄最小的长征队伍\\\"”投币成功\\r\\nℹ 投币任务完成，余额为: 661.5\\r\\nℹ ---结束---\\r\\n\\r\\nℹ ---开始【\\\"漫画签到\\\"】---\\r\\nℹ 完成漫画签到\\r\\nℹ ---结束---\\r\\n\\r\\nℹ ---开始【\\\"直播中心签到\\\"】---\\r\\nℹ 直播签到成功，本次签到获得\\\"3000点用户经验,2根辣条\\\",\\\"\\\"\\r\\nℹ ---结束---\\r\\n\\r\\nℹ ---开始【\\\"直播中心银瓜子兑换硬币\\\"】---\\r\\nℹ 银瓜子兑换硬币失败，原因：\\\"银瓜子余额不足\\\"\\r\\nℹ 当前银瓜子余额: 564\\r\\nℹ ---结束---\\r\\n\\r\\nℹ ---开始【\\\"每月领取大会员福利\\\"】---\\r\\nℹ 目标领取日期为1号，今天是29号，跳过领取任务\\r\\nℹ ---结束---\\r\\n\\r\\nℹ ---开始【\\\"每月领取大会员漫画权益\\\"】---\\r\\nℹ 目标领取日期为1号，今天是29号，跳过领取任务\\r\\nℹ ---结束---\\r\\n\\r\\nℹ ---开始【\\\"每月为自己充电\\\"】---\\r\\nℹ 目标充电日期为31号，今天是29号，跳过充电任务\\r\\nℹ ---结束---\\r\\n\\r\\nℹ -----全部任务已执行结束-----\\r\\n\\r\\nℹ 开始推送\\r\\n\";\n\n        public static string Msg2 =\n            $\"1{Environment.NewLine}2{Environment.NewLine}{Environment.NewLine}3\";\n    }\n}\n"
  },
  {
    "path": "test/LogTest/LogTest.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <IsPackable>false</IsPackable>\n    <UserSecretsId>5bc79f80-380e-4bcf-9c0b-30e98db3b935</UserSecretsId>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n    <PackageReference Include=\"xunit\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ray.BiliBiliTool.Console\\Ray.BiliBiliTool.Console.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/LogTest/TestCoolPush.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Ray.BiliBiliTool.Console;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Ray.Serilog.Sinks.CoolPushBatched;\nusing Xunit;\n\nnamespace LogTest\n{\n    public class TestCoolPush\n    {\n        private string _key;\n\n        public TestCoolPush()\n        {\n            Environment.SetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\", \"Development\");\n            Program.CreateHost(new string[] { \"ENVIRONMENT=Development\" });\n\n            _key = Global.ConfigurationRoot[\"Serilog:WriteTo:7:Args:sKey\"];\n        }\n\n        [Fact]\n        public async Task Test2()\n        {\n            if (string.IsNullOrEmpty(_key))\n            {\n                Debug.WriteLine(\"CoolPush key not configured, skipping test\");\n                return;\n            }\n\n            var client = new CoolPushApiClient(_key);\n            string msg = LogConstants.Msg2;\n\n            var result = await client.PushMessageAsync(msg);\n            Debug.WriteLine(result.Content.ReadAsStringAsync().Result);\n        }\n    }\n}\n"
  },
  {
    "path": "test/LogTest/TestDingTalk.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Ray.BiliBiliTool.Console;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Ray.Serilog.Sinks.DingTalkBatched;\nusing Xunit;\n\nnamespace LogTest\n{\n    public class TestDingTalk\n    {\n        private string _key;\n\n        public TestDingTalk()\n        {\n            Environment.SetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\", \"Development\");\n            Program.CreateHost([\"ENVIRONMENT=Development\"]);\n\n            // 添加空值检查\n            if (Global.ConfigurationRoot != null)\n            {\n                _key = Global.ConfigurationRoot[\"Serilog:WriteTo:5:Args:webHookUrl\"];\n            }\n            else\n            {\n                _key = \"test_key\"; // 默认测试值\n            }\n        }\n\n        [Fact]\n        public async Task Test2()\n        {\n            var client = new DingTalkApiClient(_key);\n\n            var title = \"这是标题\";\n            var msg = LogConstants.Msg2 + \"开始推送\";\n\n            var result = await client.PushMessageAsync(msg, title);\n            Debug.WriteLine(result.Content.ReadAsStringAsync().Result);\n        }\n    }\n}\n"
  },
  {
    "path": "test/LogTest/TestMicrosoftTeams.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Ray.BiliBiliTool.Console;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Ray.Serilog.Sinks.MicrosoftTeamsBatched;\nusing Xunit;\n\nnamespace LogTest\n{\n    public class TestMicrosoftTeams\n    {\n        private string _webhook;\n\n        public TestMicrosoftTeams()\n        {\n            Environment.SetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\", \"Development\");\n            Program.CreateHost(new string[] { \"ENVIRONMENT=Development\" });\n\n            _webhook = Global.ConfigurationRoot[\"Serilog:WriteTo:10:Args:webhook\"];\n        }\n\n        [Fact]\n        public async Task Test()\n        {\n            var client = new MicrosoftTeamsApiClient(webhook: _webhook);\n\n            var msg = LogConstants.Msg2;\n\n            var result = await client.PushMessageAsync(msg);\n            Debug.WriteLine(result.Content.ReadAsStringAsync().Result);\n\n            Assert.True(result.StatusCode == System.Net.HttpStatusCode.OK);\n        }\n    }\n}\n"
  },
  {
    "path": "test/LogTest/TestPushPlus.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Ray.BiliBiliTool.Console;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Ray.Serilog.Sinks.PushPlusBatched;\nusing Xunit;\n\nnamespace LogTest\n{\n    public class TestPushPlus\n    {\n        private string _token;\n        private string _channel;\n        private string _webhook;\n\n        public TestPushPlus()\n        {\n            Environment.SetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\", \"Development\");\n            Program.CreateHost(new string[] { \"ENVIRONMENT=Development\" });\n\n            _token = Global.ConfigurationRoot[\"Serilog:WriteTo:9:Args:token\"];\n            _channel = Global.ConfigurationRoot[\"Serilog:WriteTo:9:Args:channel\"];\n            _webhook = Global.ConfigurationRoot[\"Serilog:WriteTo:9:Args:webhook\"];\n        }\n\n        [Fact]\n        public async Task Test2()\n        {\n            var client = new PushPlusApiClient(_token, channel: _channel, webhook: _webhook);\n\n            var msg = LogConstants.Msg2;\n\n            var result = await client.PushMessageAsync(msg);\n            Debug.WriteLine(result.Content.ReadAsStringAsync().Result);\n        }\n    }\n}\n"
  },
  {
    "path": "test/LogTest/TestServerChan.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Ray.BiliBiliTool.Console;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Ray.Serilog.Sinks.ServerChanBatched;\nusing Xunit;\n\nnamespace LogTest\n{\n    public class TestServerChan\n    {\n        private string _scKey;\n        private string _turboScKey;\n\n        public TestServerChan()\n        {\n            Environment.SetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\", \"Development\");\n            Program.CreateHost(new string[] { \"ENVIRONMENT=Development\" });\n\n            _scKey = Global.ConfigurationRoot[\"Serilog:WriteTo:6:Args:scKey\"];\n            _turboScKey = Global.ConfigurationRoot[\"Serilog:WriteTo:6:Args:turboScKey\"];\n        }\n\n        [Fact]\n        public async Task Test()\n        {\n            var client = new ServerChanApiClient(_scKey);\n\n            string msg = LogConstants.Msg2;\n\n            var result = await client.PushMessageAsync(msg);\n            Debug.WriteLine(result.Content.ReadAsStringAsync().Result);\n\n            /*\n             * server酱的换行有问题，一个newline换不了，要两个\n             */\n        }\n\n        [Fact]\n        public async Task TestTurbo()\n        {\n            var client = new ServerChanTurboApiClient(_turboScKey);\n\n            string msg = LogConstants.Msg2;\n\n            var result = await client.PushMessageAsync(msg, \"测试\");\n            Debug.WriteLine(result.Content.ReadAsStringAsync().Result);\n\n            /*\n             * server酱的换行有问题，一个newline换不了，要两个\n             */\n        }\n    }\n}\n"
  },
  {
    "path": "test/LogTest/TestTelegram.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Ray.BiliBiliTool.Console;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Ray.Serilog.Sinks.TelegramBatched;\nusing Xunit;\n\nnamespace LogTest\n{\n    public class TestTelegram\n    {\n        private string _botToken;\n        private string _chatId;\n\n        public TestTelegram()\n        {\n            Environment.SetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\", \"Development\");\n            Program.CreateHost([\"ENVIRONMENT=Development\"]);\n\n            _botToken = Global.ConfigurationRoot?[\"Serilog:WriteTo:3:Args:botToken\"];\n            _chatId = Global.ConfigurationRoot?[\"Serilog:WriteTo:3:Args:chatId\"];\n        }\n\n        [Fact]\n        public async Task Test2()\n        {\n            var client = new TelegramApiClient(_botToken, _chatId);\n\n            string msg = LogConstants.Msg2;\n\n            var result = await client.PushMessageAsync(msg, \"标题\");\n            Debug.WriteLine(result.Content.ReadAsStringAsync().Result);\n\n            /*\n             * 如果指定markdown，星号会导致推送失败\n             */\n        }\n    }\n}\n"
  },
  {
    "path": "test/LogTest/TestWorkWeiXin.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Ray.BiliBiliTool.Console;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Ray.Serilog.Sinks.WorkWeiXinBatched;\nusing Xunit;\n\nnamespace LogTest\n{\n    public class TestWorkWeiXin\n    {\n        private string _key;\n\n        public TestWorkWeiXin()\n        {\n            Environment.SetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\", \"Development\");\n            Program.CreateHost(new string[] { \"ENVIRONMENT=Development\" });\n\n            _key = Global.ConfigurationRoot[\"Serilog:WriteTo:4:Args:webHookUrl\"];\n        }\n\n        [Fact]\n        public async Task Test2()\n        {\n            var client = new WorkWeiXinApiClient(_key, WorkWeiXinMsgType.text);\n\n            //string msg = LogConstants.Msg;\n            string msg = LogConstants.Msg2;\n\n            var result = await client.PushMessageAsync(msg);\n            Debug.WriteLine(result.Content.ReadAsStringAsync().Result);\n        }\n    }\n}\n"
  },
  {
    "path": "test/LogTest/TestWorkWeiXinApp.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Ray.BiliBiliTool.Console;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Ray.Serilog.Sinks.WorkWeiXinAppBatched;\nusing Xunit;\n\nnamespace LogTest\n{\n    public class TestWorkWeiXinApp\n    {\n        private string _agentId;\n        private string _secret;\n        private string _corpId;\n        private string _toUser;\n\n        public TestWorkWeiXinApp()\n        {\n            Environment.SetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\", \"Development\");\n            Program.CreateHost(new string[] { \"ENVIRONMENT=Development\" });\n\n            _agentId = Global.ConfigurationRoot[\"Serilog:WriteTo:11:Args:agentId\"];\n            _secret = Global.ConfigurationRoot[\"Serilog:WriteTo:11:Args:secret\"];\n            _corpId = Global.ConfigurationRoot[\"Serilog:WriteTo:11:Args:corpId\"];\n\n            _toUser = Global.ConfigurationRoot[\"Serilog:WriteTo:11:Args:toUser\"];\n        }\n\n        [Fact]\n        public async Task Test()\n        {\n            var client = new WorkWeiXinAppApiClient(_corpId, _agentId, _secret, _toUser);\n\n            var msg = LogConstants.Msg2;\n\n            var result = await client.PushMessageAsync(msg);\n            Debug.WriteLine(result.Content.ReadAsStringAsync().Result);\n\n            Assert.True(result.StatusCode == System.Net.HttpStatusCode.OK);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ray.BiliBiliTool.Agent.FunctionalTests/AccountApiTests.cs",
    "content": "﻿using FluentAssertions;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Console;\n\nnamespace Ray.BiliBiliTool.Agent.FunctionalTests;\n\npublic class AccountApiTests\n{\n    private readonly IAccountApi _api;\n\n    public AccountApiTests()\n    {\n        var envs = new List<string>\n        {\n            \"--ENVIRONMENT=Development\",\n            //\"HTTP_PROXY=localhost:8888\",\n            //\"HTTPS_PROXY=localhost:8888\"\n        };\n        IHost host = Program.CreateHost(envs.ToArray());\n        _api = host.Services.GetRequiredService<IAccountApi>();\n    }\n\n    [Fact]\n    public async Task GetCoinBalance_Normal_GetCoinBalance()\n    {\n        // Act\n        BiliApiResponse<CoinBalance> re = await _api.GetCoinBalanceAsync(null);\n\n        // Arrange\n\n        // Assert\n        re.Code.Should().Be(0);\n        re.Data.Money.Should().NotBeNull();\n    }\n}\n"
  },
  {
    "path": "test/Ray.BiliBiliTool.Agent.FunctionalTests/ArticleApiTests.cs",
    "content": "﻿using FluentAssertions;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Article;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Services;\nusing Ray.BiliBiliTool.Console;\n\nnamespace Ray.BiliBiliTool.Agent.FunctionalTests;\n\npublic class ArticleApiTests\n{\n    private readonly IArticleApi _api;\n\n    private readonly BiliCookie _ck;\n    private readonly IWbiService _wbiService;\n\n    public ArticleApiTests()\n    {\n        var envs = new List<string>\n        {\n            \"--ENVIRONMENT=Development\",\n            //\"HTTP_PROXY=localhost:8888\",\n            //\"HTTPS_PROXY=localhost:8888\"\n        };\n        IHost host = Program.CreateHost(envs.ToArray());\n        _ck = host.Services.GetRequiredService<BiliCookie>();\n        _wbiService = host.Services.GetRequiredService<IWbiService>();\n        _api = host.Services.GetRequiredService<IArticleApi>();\n    }\n\n    #region SearchUpArticlesByUpIdAsync\n\n    [Fact]\n    public async Task SearchUpArticlesByUpIdAsync_InputId_GetResultSuccess()\n    {\n        // Arrange\n        var mid = 1585227649;\n        var req = new SearchArticlesByUpIdDto() { mid = mid };\n        await _wbiService.SetWridAsync(req, null);\n\n        // Act\n        BiliApiResponse<SearchUpArticlesResponse> re = await _api.SearchUpArticlesByUpIdAsync(req);\n\n        // Assert\n        re.Code.Should().Be(0);\n        re.Data.Count.Should().BeGreaterThan(0);\n    }\n\n    #endregion\n\n    #region SearchArticleInfoAsync\n\n    [Fact]\n    public async Task SearchArticleInfoAsync_ValidId_GetResultSuccess()\n    {\n        // Arrange\n        var cvid = 34150576;\n\n        // Act\n        var re = await _api.SearchArticleInfoAsync(cvid);\n\n        // Assert\n        re.Code.Should().Be(0);\n        re.Data.Mid.Should().BeGreaterThan(0);\n        re.Data.Like.Should().BeGreaterThanOrEqualTo(1);\n    }\n\n    [Fact]\n    public async Task SearchArticleInfoAsync_InvalidId_NoResult()\n    {\n        // Arrange\n        var cvid = 123;\n\n        // Act\n        var re = await _api.SearchArticleInfoAsync(cvid);\n\n        // Assert\n        re.Code.Should().Be(-404);\n        re.Data.Should().BeNull();\n        re.Message.Should().BeEquivalentTo(\"啥都木有\");\n    }\n\n    #endregion\n\n\n    #region AddCoinForArticleAsync\n\n    [Fact]\n    public async Task AddCoinForArticleAsync_CoinSelf_Fail()\n    {\n        // Arrange\n        var selfCvId = 34150576; //todo\n        var req = new AddCoinForArticleRequest(selfCvId, long.Parse(_ck.UserId), _ck.BiliJct);\n\n        // Act\n        BiliBiliAgent.Dtos.BiliApiResponse re = await _api.AddCoinForArticleAsync(req, null);\n\n        // Assert\n        re.Code.Should().Be(34002);\n        re.Message.Should().BeEquivalentTo(\"up主不能自己投币\");\n    }\n\n    [Fact]\n    public async Task AddCoinForArticleAsync_Normal_Success()\n    {\n        // Arrange\n        var cvId = 34049005; //todo\n        var upId = 25150765; //todo\n        var req = new AddCoinForArticleRequest(cvId, upId, _ck.BiliJct);\n\n        // Act\n        BiliBiliAgent.Dtos.BiliApiResponse re = await _api.AddCoinForArticleAsync(req, null);\n\n        // Assert\n        re.Code.Should()\n            .BeOneOf(\n                0, // 成功\n                34005 // 超过投币上限啦~\n            );\n    }\n\n    #endregion\n\n    #region LikeAsync\n\n    [Fact]\n    public async Task LikeAsync_AlreadyLike_GetResultSuccess()\n    {\n        // Arrange\n        var cvid = 34150576;\n\n        // Act\n        var re = await _api.LikeAsync(cvid, _ck.BiliJct, null);\n\n        // Assert\n        re.Code.Should()\n            .BeOneOf(\n                new List<int>\n                {\n                    0,\n                    65006, //已赞过\n                }\n            );\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Ray.BiliBiliTool.Agent.FunctionalTests/ChargeApiTest.cs",
    "content": "using FluentAssertions;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Console;\n\nnamespace Ray.BiliBiliTool.Agent.FunctionalTests;\n\npublic class ChargeApiTest\n{\n    private readonly IChargeApi _target;\n\n    private readonly BiliCookie _ck;\n\n    public ChargeApiTest()\n    {\n        var envs = new List<string>\n        {\n            \"--ENVIRONMENT=Development\",\n            //\"HTTP_PROXY=localhost:8888\",\n            //\"HTTPS_PROXY=localhost:8888\"\n        };\n        IHost host = Program.CreateHost(envs.ToArray());\n        _ck = host.Services.GetRequiredService<BiliCookie>();\n        _target = host.Services.GetRequiredService<IChargeApi>();\n    }\n\n    #region ChargeV2Async\n\n    [Fact]\n    public async void ChargeV2Async_SendRequest_NotEnough()\n    {\n        // Arrange\n        var upId = 220893216;\n        var req = new ChargeRequest(2, upId, _ck.BiliJct);\n\n        // Act\n        BiliApiResponse<ChargeV2Response> re = await _target.ChargeV2Async(req, null);\n\n        // Assert\n        re.Code.Should().Be(0);\n        re.Data.Status.Should()\n            .BeOneOf(\n                -4, //bp.to.battery http failed, invalid args, errNo=800409904: B ������\n                4\n            );\n    }\n\n    #endregion\n\n    #region ChargeCommentAsync\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Ray.BiliBiliTool.Agent.FunctionalTests/DailyTaskApiTests.cs",
    "content": "﻿using FluentAssertions;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Console;\n\nnamespace Ray.BiliBiliTool.Agent.FunctionalTests;\n\npublic class DailyTaskApiTests\n{\n    private readonly IDailyTaskApi _api;\n\n    private readonly BiliCookie _ck;\n\n    public DailyTaskApiTests()\n    {\n        var envs = new List<string>\n        {\n            \"--ENVIRONMENT=Development\",\n            //\"HTTP_PROXY=localhost:8888\",\n            //\"HTTPS_PROXY=localhost:8888\"\n        };\n        IHost host = Program.CreateHost(envs.ToArray());\n        _ck = host.Services.GetRequiredService<BiliCookie>();\n        _api = host.Services.GetRequiredService<IDailyTaskApi>();\n    }\n\n    [Fact]\n    public async Task GetDailyTaskRewardInfo_Normal_Success()\n    {\n        // Act\n        BiliApiResponse<DailyTaskInfo> re = await _api.GetDailyTaskRewardInfoAsync(null);\n\n        // Arrange\n\n        // Assert\n        re.Code.Should().Be(0);\n        re.Data.Should().NotBeNull();\n    }\n\n    [Fact]\n    public async Task GetDonateCoinExp_Normal_Success()\n    {\n        // Act\n        BiliApiResponse<int> re = await _api.GetDonateCoinExpAsync(null);\n\n        // Arrange\n\n        // Assert\n        re.Code.Should().Be(0);\n        re.Data.Should().BeGreaterThanOrEqualTo(0);\n    }\n\n    [Fact]\n    public async Task ReceiveVipPrivilege_Normal_Success()\n    {\n        // Act\n        BiliApiResponse re = await _api.ReceiveVipPrivilegeAsync(\n            (int)VipPrivilegeType.BCoinCoupon,\n            _ck.BiliJct,\n            null\n        );\n\n        // Arrange\n\n        // Assert\n        re.Code.Should()\n            .BeOneOf(\n                0,\n                73319, //todo: sort out meannings\n                69801 //你已领取过该权益\n            );\n    }\n}\n"
  },
  {
    "path": "test/Ray.BiliBiliTool.Agent.FunctionalTests/HomeApiTests.cs",
    "content": "﻿using FluentAssertions;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Console;\n\nnamespace Ray.BiliBiliTool.Agent.FunctionalTests;\n\npublic class HomeApiTests\n{\n    private readonly IHomeApi _api;\n\n    private readonly BiliCookie _ck;\n\n    public HomeApiTests()\n    {\n        var envs = new List<string>\n        {\n            \"--ENVIRONMENT=Development\",\n            //\"HTTP_PROXY=localhost:8888\",\n            //\"HTTPS_PROXY=localhost:8888\"\n        };\n        IHost host = Program.CreateHost(envs.ToArray());\n        _ck = host.Services.GetRequiredService<BiliCookie>();\n        _api = host.Services.GetRequiredService<IHomeApi>();\n    }\n\n    [Fact]\n    public async Task GetHomePageAsync_Normal_Success()\n    {\n        // Act\n        HttpResponseMessage re = await _api.GetHomePageAsync(_ck.ToString());\n\n        // Arrange\n        var page = await re.Content.ReadAsStringAsync();\n\n        // Assert\n        re.IsSuccessStatusCode.Should().BeTrue();\n        page.Should().Contain(\"<title>哔哩哔哩 (゜-゜)つロ 干杯~-bilibili</title>\");\n    }\n}\n"
  },
  {
    "path": "test/Ray.BiliBiliTool.Agent.FunctionalTests/LiveApiTest.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing FluentAssertions;\nusing Microsoft.Extensions.DependencyInjection;\nusing Ray.BiliBiliTool.Agent;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Live;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Services;\nusing Ray.BiliBiliTool.Console;\nusing Ray.BiliBiliTool.Infrastructure;\nusing Ray.BiliBiliTool.Infrastructure.Cookie;\nusing Xunit;\n\nnamespace BiliAgentTest\n{\n    public class LiveApiTest\n    {\n        public LiveApiTest()\n        {\n            Program.CreateHost(new[] { \"--ENVIRONMENT=Development\" }); //Ä¬ÈÏPrd»·¾³£¬ÕâÀïÖ¸¶¨ÎªDevºó£¬¿ÉÒÔ¶ÁÈ¡µ½ÓÃ»§»úÃÜÅäÖÃ\n        }\n\n        [Fact]\n        [Obsolete]\n        public void GetExchangeSilverStatus_Normal_Success()\n        {\n            using var scope = Global.ServiceProviderRoot.CreateScope();\n            var api = scope.ServiceProvider.GetRequiredService<ILiveApi>();\n            var ck = scope.ServiceProvider.GetRequiredService<CookieStrFactory<BiliCookie>>();\n\n            BiliApiResponse<ExchangeSilverStatusResponse> re = api.GetExchangeSilverStatus(\n                null\n            ).Result;\n\n            if (ck.Count > 0)\n            {\n                Assert.True(re.Code == 0 && re.Message == \"0\");\n                Assert.True(re.Data.Silver >= 0);\n            }\n            else\n            {\n                Assert.False(re.Code != 0);\n            }\n        }\n\n        [Fact]\n        public void Silver2Coin_Normal_Success()\n        {\n            using var scope = Global.ServiceProviderRoot.CreateScope();\n\n            var ck = scope.ServiceProvider.GetRequiredService<CookieStrFactory<BiliCookie>>();\n            var api = scope.ServiceProvider.GetRequiredService<ILiveApi>();\n            var biliCookie = scope.ServiceProvider.GetRequiredService<BiliCookie>();\n\n            Silver2CoinRequest request = new(biliCookie.BiliJct);\n\n            BiliApiResponse<Silver2CoinResponse> re = api.Silver2Coin(request, null).Result;\n\n            if (re.Code == 0)\n            {\n                Assert.True(re.Data.Coin == 1);\n            }\n            else\n            {\n                Assert.False(string.IsNullOrWhiteSpace(re.Message));\n            }\n        }\n\n        [Fact]\n        public void GetLiveWalletStatus_Normal_Success()\n        {\n            using var scope = Global.ServiceProviderRoot.CreateScope();\n\n            var ck = scope.ServiceProvider.GetRequiredService<CookieStrFactory<BiliCookie>>();\n            var api = scope.ServiceProvider.GetRequiredService<ILiveApi>();\n\n            BiliApiResponse<LiveWalletStatusResponse> re = api.GetLiveWalletStatus(null).Result;\n\n            if (ck.Count > 0)\n            {\n                Assert.True(re.Code == 0 && re.Data.Silver_2_coin_left >= 0);\n            }\n            else\n            {\n                Assert.False(re.Code != 0);\n            }\n        }\n\n        [Fact]\n        public void GetMedalWall_Normal_Success()\n        {\n            using var scope = Global.ServiceProviderRoot.CreateScope();\n\n            var ck = scope.ServiceProvider.GetRequiredService<CookieStrFactory<BiliCookie>>();\n            var api = scope.ServiceProvider.GetRequiredService<ILiveApi>();\n\n            BiliApiResponse<MedalWallResponse> re = api.GetMedalWall(\"919174\", null).Result;\n\n            Assert.NotEmpty(re.Data.List);\n\n            var md = re.Data.List[0];\n            Assert.NotNull(md);\n            Assert.False(String.IsNullOrEmpty(md.Link));\n            Assert.False(String.IsNullOrEmpty(md.Target_name));\n            Assert.NotNull(md.Medal_info);\n            Assert.False(String.IsNullOrEmpty(md.Medal_info.Medal_name));\n            Assert.True(md.Medal_info.Medal_id > 0);\n        }\n\n        [Fact]\n        public void WearMedalWall_Normal_Success()\n        {\n            using var scope = Global.ServiceProviderRoot.CreateScope();\n\n            var ck = scope.ServiceProvider.GetRequiredService<CookieStrFactory<BiliCookie>>();\n            var api = scope.ServiceProvider.GetRequiredService<ILiveApi>();\n            var biliCookie = scope.ServiceProvider.GetRequiredService<BiliCookie>();\n\n            // 猫雷粉丝牌\n            var request = new WearMedalWallRequest(biliCookie.BiliJct, 365421); //todo\n\n            BiliApiResponse re = api.WearMedalWall(request, null).Result;\n\n            Assert.True(re.Code == 0);\n            re.Code.Should().BeOneOf(0, 1500005);\n        }\n\n        [Fact]\n        public async Task GetSpaceInfo_Normal_Success()\n        {\n            using var scope = Global.ServiceProviderRoot.CreateScope();\n\n            var ck = scope.ServiceProvider.GetRequiredService<CookieStrFactory<BiliCookie>>();\n            var api = scope.ServiceProvider.GetRequiredService<IUpInfoApi>();\n\n            var wbiService = scope.ServiceProvider.GetRequiredService<IWbiService>();\n\n            var req = new GetSpaceInfoDto() { mid = 919174L };\n\n            BiliApiResponse<GetSpaceInfoResponse> re = api.GetSpaceInfo(\n                req,\n                ck.GetCookie(0).ToString()\n            ).Result;\n\n            Assert.True(re.Code == 0);\n            Assert.NotNull(re.Data);\n            Assert.Equal(919174, re.Data.Mid);\n            Assert.NotNull(re.Data.Live_room);\n            Assert.Equal(3115258, re.Data.Live_room.Roomid);\n            Assert.False(String.IsNullOrEmpty(re.Data.Name));\n            Assert.False(String.IsNullOrEmpty(re.Data.Live_room.Title));\n        }\n\n        [Fact]\n        public void SendLiveDanmuku_Normal_Success()\n        {\n            using var scope = Global.ServiceProviderRoot.CreateScope();\n\n            var ck = scope.ServiceProvider.GetRequiredService<CookieStrFactory<BiliCookie>>();\n            var api = scope.ServiceProvider.GetRequiredService<ILiveApi>();\n            var biliCookie = scope.ServiceProvider.GetRequiredService<BiliCookie>();\n\n            var request = new SendLiveDanmukuRequest(biliCookie.BiliJct, 63666, \"63666\");\n\n            BiliApiResponse re = api.SendLiveDanmuku(request, null).Result;\n\n            Assert.True(re.Code == 0);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Ray.BiliBiliTool.Agent.FunctionalTests/Ray.BiliBiliTool.Agent.FunctionalTests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n    <PackageReference Include=\"xunit\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"FluentAssertions\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ray.BiliBiliTool.Console\\Ray.BiliBiliTool.Console.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <Using Include=\"Xunit\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/Ray.BiliBiliTool.Agent.FunctionalTests/VipBigPointApiTest.cs",
    "content": "﻿using FluentAssertions;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Mall;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.VipTask;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Console;\nusing Xunit.Abstractions;\n\nnamespace Ray.BiliBiliTool.Agent.FunctionalTests;\n\npublic class VipBigPointApiTest\n{\n    private readonly IVipBigPointApi _api;\n\n    private readonly ITestOutputHelper _output;\n    private readonly BiliCookie _ck;\n\n    public VipBigPointApiTest(ITestOutputHelper output)\n    {\n        _output = output;\n\n        var envs = new List<string>\n        {\n            \"--ENVIRONMENT=Development\",\n            //\"HTTP_PROXY=localhost:8888\",\n            //\"HTTPS_PROXY=localhost:8888\"\n        };\n        IHost host = Program.CreateHost(envs.ToArray());\n        _ck = host.Services.GetRequiredService<BiliCookie>();\n        _api = host.Services.GetRequiredService<IVipBigPointApi>();\n    }\n\n    [Fact]\n    public async Task GetTaskListAsync_Normal_Success()\n    {\n        // Arrange\n        // Act\n        BiliApiResponse<VipBigPointCombine> re = await _api.GetCombineAsync(null);\n\n        // Assert\n        re.Code.Should().Be(0);\n        re.Data.Should().NotBeNull();\n        re.Data.Task_info.Modules.Should().HaveCountGreaterThan(0);\n    }\n\n    [Fact]\n    public async Task SignAsync_Normal_Success()\n    {\n        // Arrange\n        var req = new SignRequest() { csrf = _ck.BiliJct };\n\n        // Act\n        BiliApiResponse re = await _api.SignAsync(req, null);\n        _output.WriteLine(re.ToJsonStr());\n\n        // Assert\n        re.Code.Should().Be(0);\n        re.Message.Should().BeEquivalentTo(\"success\");\n    }\n\n    [Fact]\n    public async Task GetVouchersInfoAsync_Normal_Success()\n    {\n        // Arrange\n        // Act\n        var re = await _api.GetVouchersInfoAsync(null);\n\n        // Assert\n        re.Code.Should().Be(0);\n        re.Data.List.Should().Contain(x => x.Type == 9);\n    }\n\n    [Fact]\n    public async Task GetVipExperienceAsync_Normal_Success()\n    {\n        // Arrange\n        var req = new VipExperienceRequest() { csrf = _ck.BiliJct };\n\n        // Act\n        BiliApiResponse re = await _api.ObtainVipExperienceAsync(req, null);\n\n        // Assert\n        re.Code.Should()\n            .BeOneOf(\n                new List<int>\n                {\n                    0,\n                    6034005, //任务未完成\n                    69198, //用户经验已经领取\n                }\n            );\n    }\n\n    [Fact]\n    public async Task CompleteAsync_Normal_Success()\n    {\n        // Arrange\n        var req = new ReceiveOrCompleteTaskRequest(\"dress-view\");\n\n        // Act\n        var re = await _api.CompleteAsync(req, null);\n\n        // Assert\n        re.Code.Should().Be(0);\n    }\n}\n"
  },
  {
    "path": "test/Ray.BiliBiliTool.Agent.FunctionalTests/VipMallApiTests.cs",
    "content": "﻿using FluentAssertions;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.ViewMall;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Interfaces;\nusing Ray.BiliBiliTool.Console;\n\nnamespace Ray.BiliBiliTool.Agent.FunctionalTests;\n\npublic class VipMallApiTests\n{\n    private readonly IVipMallApi _api;\n\n    private readonly BiliCookie _ck;\n\n    public VipMallApiTests()\n    {\n        var envs = new List<string>\n        {\n            \"--ENVIRONMENT=Development\",\n            //\"HTTP_PROXY=localhost:8888\",\n            //\"HTTPS_PROXY=localhost:8888\"\n        };\n        IHost host = Program.CreateHost(envs.ToArray());\n        _ck = host.Services.GetRequiredService<BiliCookie>();\n        _api = host.Services.GetRequiredService<IVipMallApi>();\n    }\n\n    [Fact]\n    public async Task ViewVipMallAsync_Normal_Success()\n    {\n        // Arrange\n        var req = new ViewVipMallRequest() { Csrf = _ck.BiliJct };\n\n        // Act\n        BiliApiResponse re = await _api.ViewVipMallAsync(req, null);\n\n        // Assert\n        re.Code.Should().Be(0);\n        re.Message.Should().BeEquivalentTo(\"SUCCESS\");\n    }\n}\n"
  },
  {
    "path": "test/Ray.BiliBiliTool.Agent.FunctionalTests/WbiServiceTest.cs",
    "content": "using FluentAssertions;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Dtos.Video;\nusing Ray.BiliBiliTool.Agent.BiliBiliAgent.Services;\nusing Ray.BiliBiliTool.Console;\n\nnamespace Ray.BiliBiliTool.Agent.FunctionalTests;\n\npublic class WbiServiceTest\n{\n    private readonly IWbiService _target;\n\n    public WbiServiceTest()\n    {\n        var envs = new List<string>\n        {\n            \"--ENVIRONMENT=Development\",\n            //\"HTTP_PROXY=localhost:8888\",\n            //\"HTTPS_PROXY=localhost:8888\"\n        };\n        IHost host = Program.CreateHost(envs.ToArray());\n        _target = host.Services.GetRequiredService<IWbiService>();\n    }\n\n    [Fact]\n    public async void SetWridAsync_SendRequest_SetWridSuccess()\n    {\n        // Arrange\n        var upId = 1585227649;\n        var req = new SearchVideosByUpIdDto()\n        {\n            mid = upId,\n            ps = 30,\n            tid = 0,\n            pn = 1,\n            keyword = \"\",\n            order = \"pubdate\",\n            platform = \"web\",\n            web_location = 1550101,\n            order_avoided = \"true\",\n        };\n\n        // Act\n        await _target.SetWridAsync(req, null);\n\n        // Assert\n        req.w_rid.Should().NotBeNullOrWhiteSpace();\n        req.wts.Should().NotBe(0);\n    }\n\n    [Fact]\n    public void EncWbi_InputParams_GetCorrectWbiResult()\n    {\n        // Arrange\n        var wbiDto = new WbiImg()\n        {\n            img_url = \"https://i0.hdslb.com/bfs/wbi/653657f524a547ac981ded72ea172057.png\",\n            sub_url = \"https://i0.hdslb.com/bfs/wbi/6e4909c702f846728e64f6007736a338.png\",\n        };\n        var dic = new Dictionary<string, string>()\n        {\n            { \"foo\", \"114\" },\n            { \"bar\", \"514\" },\n            { \"baz\", \"1919810\" },\n        };\n        var timeSpan = 1684746387;\n        var expectResult = \"d3cbd2a2316089117134038bf4caf442\";\n\n        // Act\n        var re = _target.EncWbi(dic, wbiDto.ImgKey, wbiDto.SubKey, timeSpan);\n\n        // Assert\n        re.w_rid.Should().BeEquivalentTo(expectResult);\n        re.wts.Should().Be(timeSpan);\n    }\n}\n"
  }
]