diff --git a/.editorconfig b/.editorconfig index 4cd39670..8427f385 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,31 +1,65 @@ +# EditorConfig for Elib2Ebook +# https://editorconfig.org + root = true [*] -charset = utf-8 +indent_style = space +indent_size = 4 end_of_line = lf -insert_final_newline = true +charset = utf-8 trim_trailing_whitespace = true -indent_style = space +insert_final_newline = true + +[*.{cs,razor,cshtml}] indent_size = 4 -tab_width = 4 +indent_style = space -[*.{json,map}] +[*.xml] indent_size = 2 -tab_width = 2 [*.{yml,yaml}] indent_size = 2 -tab_width = 2 + +[*.json] +indent_size = 2 [*.md] trim_trailing_whitespace = false +[*.sh] +end_of_line = lf + +[*.{sln,csproj,props}] +indent_size = 4 + +# .NET conventions [*.cs] -# C# Coding Conventions -csharp_style_pattern_matching_over_as_with_null_check = true:error -csharp_style_prefer_pattern_matching = true:suggestion -csharp_style_prefer_not_pattern = true:suggestion -dotnet_style_prefer_isnull_check_over_reference_equality_method = true:suggestion - -# Simplify 'default' expression warnings (IDE0034) -dotnet_diagnostic.IDE0034.severity = warning +# Sort usings +dotnet_sort_system_directives_first = true +# File-scoped namespaces +dotnet_style_namespace_match_folder = true +# var preferences +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_style_var_elsewhere = true:silent +# Expression-bodied members +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_properties = true:warning +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +# Null checking +csharp_style_throw_expression = true:warning +csharp_style_conditional_delegate_call = true:warning +# Modifiers +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning +# Braces +csharp_prefer_braces = true:silent +# Newline +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..a23999d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: 🐛 Bug Report +about: Сообщить об ошибке +title: '' +labels: bug +assignees: '' +--- + +## Описание ошибки +Чёткое и краткое описание бага. + +## Шаги для воспроизведения +1. Запустить: `Elib2EbookCli -u '...' -f '...'` +2. ... +3. Вижу ошибку + +## Ожидаемое поведение +Что должно было произойти? + +## Окружение +- **OS:** [например macOS 15, Windows 11, Ubuntu 24.04] +- **Версия .NET:** [если применимо] +- **Версия утилиты:** [--version или тег релиза] +- **Способ запуска:** [Portable / Docker / dotnet run] + +## Логи +``` +Вставьте логи сюда (запустите с --save-temp если нужно) +``` + +## Дополнительная информация +Любая другая информация, которая может помочь. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..7691c821 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Вопросы и обсуждения + url: https://github.com/OnlyFart/Elib2Ebook/discussions + about: Задайте вопрос или предложите обсуждение в Discussions \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..ecd5bc17 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: 🚀 Feature Request +about: Предложить новую функцию +title: '' +labels: enhancement +assignees: '' +--- + +## Проблема +Опишите проблему, которую вы хотите решить. Например: *"Мне не хватает поддержки сайта example.com"* + +## Предлагаемое решение +Чёткое описание того, что вы хотите видеть. + +## Альтернативы +Какие альтернативы вы рассматривали? + +## Дополнительная информация +Ссылки, примеры, скриншоты. \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..90f4d6ab --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,48 @@ +name: "CodeQL Analysis" + +on: + push: + branches: [master] + pull_request: + branches: [master] + schedule: + - cron: '0 6 * * 1' # каждый понедельник в 6 утра UTC + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 360 + permissions: + security-events: write + packages: read + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + language: ['csharp'] + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + queries: security-and-quality + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 9.0.x + + - name: Build + run: dotnet build -c release Elib2Ebook.sln + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dotnet.yml.disabled b/.github/workflows/dotnet.yml.disabled index f572167c..6a7cb87e 100644 --- a/.github/workflows/dotnet.yml.disabled +++ b/.github/workflows/dotnet.yml.disabled @@ -7,157 +7,201 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - + runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: - dotnet-version: 9.0.x - + dotnet-version: 10.0.x + - name: Restore dependencies - run: dotnet restore - + run: dotnet restore Elib2Ebook.sln + - name: Build - run: dotnet build --no-restore -c release - + run: dotnet build --no-restore -c release Elib2Ebook.sln + + test: + needs: build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Restore dependencies + run: dotnet restore Elib2Ebook.sln + + - name: Build + run: dotnet build --no-restore -c release Elib2Ebook.sln + - name: Test - run: dotnet test --no-build --verbosity normal - + run: dotnet test --no-build -c release --verbosity normal Elib2Ebook.sln + + publish: + needs: test + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v5 + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Restore dependencies + run: dotnet restore Elib2Ebook.sln + + - name: Build + run: dotnet build --no-restore -c release Elib2Ebook.sln + - name: Min run: > - dotnet publish -c release -p:PublishDir=../dist/min --no-self-contained - -p:PublishSingleFile=true -p:DebugType=embedded -p:PublishTrimmed=false - -p:EnableCompressionInSingleFile=false --use-current-runtime Elib2EbookCli - + dotnet publish -c release -p:PublishDir=../../dist/min --no-self-contained + -p:PublishSingleFile=true -p:IncludeAllContentForSelfExtract=true -p:DebugType=embedded -p:PublishTrimmed=false + -p:EnableCompressionInSingleFile=false --use-current-runtime src/Elib2EbookCli/Elib2EbookCli.csproj + - name: Portable run: > - dotnet publish -c release -p:PublishDir=../dist/portable --self-contained - -p:PublishSingleFile=true -p:DebugType=embedded - -p:EnableCompressionInSingleFile=true --use-current-runtime Elib2EbookCli + dotnet publish -c release -p:PublishDir=../../dist/portable --self-contained + -p:PublishSingleFile=true -p:IncludeAllContentForSelfExtract=true -p:DebugType=embedded + -p:EnableCompressionInSingleFile=true --use-current-runtime src/Elib2EbookCli/Elib2EbookCli.csproj - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: Elib2Ebook-${{ matrix.os }}-min path: ${{ github.workspace }}/dist/min - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: Elib2Ebook-${{ matrix.os }}-portable path: ${{ github.workspace }}/dist/portable - + check_version: name: Check version - needs: build + needs: test runs-on: ubuntu-latest if: github.ref == 'refs/heads/master' outputs: VERSION: ${{ steps.get-version.outputs.version }} - LAST_TAG: ${{ steps.last_release.outputs.tag_name }} + LAST_TAG: ${{ steps.last_release_tag.outputs.tag_name }} steps: - name: check repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - - uses: kzrnm/get-net-sdk-project-versions-action@v2 + - uses: kzrnm/get-net-sdk-project-versions-action@v3.1.0 id: get-version with: - proj-path: Core/Core.csproj + proj-path: Directory.Build.props - name: "get-last-release" id: last_release - uses: InsonusK/get-latest-release@v1.1.0 + uses: octokit/request-action@v3.0.0 with: - myToken: ${{ github.token }} - + route: GET /repos/{owner}/{repo}/releases/latest + owner: OnlyFart + repo: Elib2Ebook + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: extract-last-release-tag + id: last_release_tag + run: echo "tag_name=$(echo '${{ steps.last_release.outputs.data }}' | jq -r '.tag_name')" >> $GITHUB_OUTPUT + push_to_registry: name: push docker image to hub runs-on: ubuntu-latest needs: check_version if: ${{ needs.check_version.outputs.LAST_TAG != needs.check_version.outputs.VERSION }} + strategy: matrix: folder: [ Elib2EbookCli, Elib2EbookWeb ] steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4.2.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4.2.0 - name: check repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: login to docker registry - uses: docker/login-action@v3 + uses: docker/login-action@v4.4.0 with: username: ${{secrets.DOCKERHUB_USERNAME}} password: ${{secrets.DOCKERHUB_TOKEN}} - id: string - uses: ASzc/change-string-case-action@v6 - with: - string: ${{ matrix.folder }} + run: echo "lowercase=$(echo '${{ matrix.folder }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT - name: build and push docker image to registry - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7.3.0 with: platforms: linux/amd64, linux/arm64 - file: "${{ matrix.folder }}/Dockerfile" + file: "src/${{ matrix.folder }}/Dockerfile" push: true tags: ${{secrets.DOCKERHUB_NAMESPACE}}/${{ steps.string.outputs.lowercase }}:${{ needs.check_version.outputs.VERSION }},${{secrets.DOCKERHUB_NAMESPACE}}/${{ steps.string.outputs.lowercase }}:latest - + create_release: needs: check_version runs-on: ubuntu-latest if: ${{ needs.check_version.outputs.LAST_TAG != needs.check_version.outputs.VERSION }} outputs: - UPLOAD_URL: ${{ steps.create_release.outputs.upload_url }} - + TAG_NAME: ${{ steps.tag_name.outputs.tag }} + steps: - - name: release - uses: actions/create-release@v1 + - name: check repository + uses: actions/checkout@v5 + + - name: create release + uses: softprops/action-gh-release@v3.0.1 id: create_release with: draft: false prerelease: false - release_name: ${{needs.check_version.outputs.VERSION}} + name: ${{needs.check_version.outputs.VERSION}} tag_name: ${{needs.check_version.outputs.VERSION}} - env: - GITHUB_TOKEN: ${{ github.token }} - + token: ${{ github.token }} + + - name: save tag name + id: tag_name + run: echo "tag=${{needs.check_version.outputs.VERSION}}" >> $GITHUB_OUTPUT + upload_release: - needs: create_release + needs: [create_release, publish] runs-on: ubuntu-latest strategy: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] version: [ min, portable ] - + steps: - name: download-artifact-${{ matrix.os }}-${{ matrix.version }} - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: Elib2Ebook-${{ matrix.os }}-${{ matrix.version }} - path: ${{ github.workspace }}/${{ matrix.os }}-${{ matrix.version }} - - - name: Install zip - uses: montudor/action-zip@v0.1.0 - + path: ${{ github.workspace }}/${{ matrix.os }}-${{ matrix.version }} + - name: Zip output run: zip -qq -x "*.pdb" -r ../Elib2Ebook-${{ matrix.os }}-${{ matrix.version }}.zip . working-directory: ${{ github.workspace }}/${{ matrix.os }}-${{ matrix.version }} - + - name: upload ${{ matrix.os }}-${{ matrix.version }} asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ github.token }} + uses: softprops/action-gh-release@v3.0.1 with: - upload_url: ${{ needs.create_release.outputs.UPLOAD_URL }} - asset_path: Elib2Ebook-${{ matrix.os }}-${{ matrix.version }}.zip - asset_name: Elib2Ebook-${{ matrix.os }}-${{ matrix.version }}.zip - asset_content_type: application/zip - + tag_name: ${{ needs.create_release.outputs.TAG_NAME }} + files: Elib2Ebook-${{ matrix.os }}-${{ matrix.version }}.zip + token: ${{ github.token }} diff --git a/.gitignore b/.gitignore index 53bd48ec..63b7b32d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,37 @@ -**/bin/* -**/obj/* -**_ReSharper* -**/.sonar/* -**.suo -**.user -**.dbmdl -**.schemaview -DB/**/sql -**.orig -**.rdl.data -**/StyleCopViolations.xml -_references.js -*_*_*.csproj.swp -*_*_*.csproj -*.nupkg -**/node_modules/** -**/react-widgets/dist/** -**/react-widgets/*.log -**/.idea/ -**/logs/** -dist -.dist -**/.vs/ -.vscode/launch.json -.vscode/tasks.json - -.dotnet/ +# .NET +**/bin/ +**/obj/ +*.user +*.suo +*.vs/ +*.DotSettings.user +*.csproj.user + +# IDE +.idea/ +*/.idea/ +.vs/ +.vscode/ +_ReSharper*/ + +# Build +dist/ +.dist/ +*.nupkg +*.log +logs/ + +# OS +.DS_Store +Thumbs.db + +# Node (for any future frontend dependencies) +node_modules/ + +# NuGet +packages/ +*.nuget.props +*.nuget.targets + +# dotnet tools +.dotnet/ \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..4bdd0481 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,235 @@ +# Архитектура проекта + +## Общая структура + +Проект разделён на три основных слоя: + +``` +Elib2Ebook.sln +├── src/ +│ ├── Elib2Ebook.Domain/ # Модели данных +│ ├── Elib2Ebook.DomainServices/ # Бизнес-логика +│ ├── Elib2EbookCli/ # Консольное приложение +│ ├── Elib2EbookWeb/ # Веб-приложение (Blazor) +│ └── ExternalServices/ # Интеграция с внешними сайтами +└── tests/ + └── Elib2Ebook.DomainServices.Tests/ +``` + +--- + +## 1. Elib2Ebook.Domain — Модели данных + +Ядро предметной области. Не имеет зависимостей от внешних сервисов. + +``` +Elib2Ebook.Domain/ +└── Book/ + ├── Book.cs # Книга (название, автор, обложка, главы, серия, язык) + ├── Author.cs # Автор (имя, ссылка) + ├── Chapter.cs # Глава (название, контент, изображения) + ├── Seria.cs # Серия книг (название, номер) + ├── AdditionalFileCollection.cs # Коллекция доп. файлов (аудио, книги, изображения) + └── AdditionalTypeEnum.cs # Типы доп. файлов: books, audio, images +└── Common/ + ├── TempFile.cs # Временный файл (скачанное изображение и т.п.) + ├── IdChapter.cs # Глава, идентифицируемая по числовому ID + └── UrlChapter.cs # Глава, идентифицируемая по URL +``` + +**Ключевые особенности:** +- `Book` реализует `IDisposable` — освобождает обложку и доп. файлы +- `TempFile` — абстракция временного файла на диске с автоматическим удалением +- `AdditionalFileCollection` — коллекция доп. файлов с группировкой по типу + +--- + +## 2. Elib2Ebook.DomainServices — Бизнес-логика + +Содержит всю логику обработки книг: загрузку, сборку, работу с файлами. + +``` +Elib2Ebook.DomainServices/ +├── Configs/ +│ ├── Options.cs # Параметры командной строки (через CommandLine) +│ └── BookGetterConfig.cs # Конфигурация для HTTP-клиента, прокси, Flaresolverr +├── Builders/ +│ ├── BuilderBase.cs # Абстрактный базовый класс для сборщиков +│ ├── EpubBuilder.cs # Сборка EPUB +│ ├── Fb2Builder.cs # Сборка FB2 +│ ├── CbzBuilder.cs # Сборка CBZ (Comic Book Archive) +│ ├── JsonBuilder.cs # Сборка JSON (полные данные) +│ ├── JsonLiteBuilder.cs # Сборка JSON (только текст) +│ ├── TxtBuilder.cs # Сборка TXT +│ └── AdditionaFileBuilder.cs # Сохранение дополнительных файлов +├── Getters/ +│ └── GetterBase.cs # Абстрактный базовый класс для загрузчиков +├── Misc/ +│ ├── GetterFactory.cs # Фабрика загрузчиков (reflection-based) +│ ├── BuilderProvider.cs # Фабрика сборщиков (switch по формату) +│ └── TempFolder/ # Управление временными файлами +├── Extensions/ # Методы расширения для различных типов +│ ├── HtmlDocumentExtensions.cs +│ ├── HttpClientExtensions.cs +│ ├── StreamExtension.cs +│ ├── StringBuilderExtensions.cs +│ ├── StringExtensions.cs +│ ├── UriExtension.cs +│ ├── FileProviderExtensions.cs +│ └── ImageFormatExtensions.cs +├── External/ # Встроенная копия EpubSharp.dll +├── Patterns/ # Шаблоны для EPUB (CSS, шрифты, xhtml) +├── BookNameBuilder.cs # Построитель имени файла по шаблону +└── FileProvider.cs # Провайдер файловой системы +``` + +### Архитектура сборщиков (Builders) + +``` +BuilderBase (abstract) +├── GetFileName(Book) → "Автор - Название.epub" +├── Build(Book) +│ ├── BuildInternal(Book, fileName) — специфичная для формата сборка +│ └── SaveCover() — если опция --cover +│ +├── EpubBuilder — создаёт EPUB через EpubSharp + CSS/шрифты +├── Fb2Builder — создаёт FB2 через XDocument +├── CbzBuilder — создаёт CBZ (zip-архив с изображениями) +├── JsonBuilder — сериализует Book в JSON с полными данными +├── JsonLiteBuilder — сериализует только текст + заголовки глав +└── TxtBuilder — создаёт простой текстовый файл +``` + +### Архитектура загрузчиков (Getters) + +``` +GetterBase (abstract) +├── SystemUrl — базовый URL сайта +├── IsSameUrl(url) — проверка поддержки URL +├── Init() — инициализация (куки, заголовки) +├── Authorize() — авторизация на сайте +├── Get(url) → Book — загрузка книги +│ +└── Внешние реализации (через GetterFactory): + Elib2Ebook.ExternalServices.* +``` + +**Как загрузчик определяет свою привязку к сайту:** + +Каждый загрузчик переопределяет `SystemUrl`. Например: +```csharp +protected override Uri SystemUrl => new("https://author.today/"); +``` + +`GetterFactory.Get()` через рефлексию находит все публичные неабстрактные классы, наследующие `GetterBase`, и вызывает `IsSameUrl()` для каждого. Первый подошедший возвращается. + +--- + +## 3. ExternalServices — Внешние сервисы + +Каждый внешний сайт реализован как отдельная сборка (проект) в папке `ExternalServices`. Все сборки загружаются динамически через рефлексию в `GetterFactory`. + +``` +ExternalServices/ +├── Elib2Ebook.ExternalServices.AuthorToday/ — author.today +├── Elib2Ebook.ExternalServices.Litres/ — litres.ru +├── Elib2Ebook.ExternalServices.Litnet/ — litnet.com +├── Elib2Ebook.ExternalServices.LibSocial/ — mangalib.me, ranobelib.me, hentailib.me, v2.slashlib.me и др. +├── Elib2Ebook.ExternalServices.BooksYandex/ — books.yandex.ru (комиксы и книги) +├── Elib2Ebook.ExternalServices.Boosty/ — boosty.to +└── ... (всего 60+ проектов) +``` + +**Особенности:** +- Каждый проект содержит `*Getter.cs` — класс, наследующий `GetterBase` +- Проекты могут содержать подпапки `Types/` с DTO для API +- Некоторые проекты содержат несколько геттеров (например, `LibSocial` — общая кодовая база для семейства сайтов) + +--- + +## 4. Elib2EbookCli — Консольное приложение + +Точка входа `Program.cs`: + +``` +Parse args (CommandLine) + └→ Создать BookGetterConfig (HTTP-клиент, куки, прокси, Flaresolverr) + └→ GetterFactory.Get() — получить загрузчик по URL + ├── Init() + ├── Authorize() + └── Get(url) → Book + └→ Для каждого формата: + BuilderProvider.Get(format) → BuilderBase + └── Build(Book) +``` + +**Поток выполнения:** +1. Парсинг аргументов командной строки +2. Создание `BookGetterConfig` — конфигурации с HTTP-клиентом +3. Получение загрузчика через `GetterFactory.Get()` (reflection-based) +4. Инициализация загрузчика (куки, заголовки) +5. Авторизация (если указаны логин/пароль) +6. Для каждого URL: + 1. Загрузка книги через `getter.Get(url)` + 2. Для каждого формата: сборка файла через `BuilderProvider.Get(format).Build(book)` +7. Сохранение дополнительных файлов (если `--additional`) + +--- + +## 5. Elib2EbookWeb — Веб-приложение (Blazor) + +Blazor Static SSR приложение. + +``` +Elib2EbookWeb/ +├── Components/ +│ ├── App.razor # Корневой компонент +│ ├── Routes.razor # Маршрутизация +│ ├── BookForm.razor # Форма ввода URL и параметров +│ ├── ShortBook.razor # Компонент для отображения краткой информации +│ └── PWA.razor # PWA-компонент +├── Misc/ +│ └── SbWriter.cs # StringBuilder writer для веб-вывода +└── wwwroot/ + ├── app.css # Стили + ├── favicon.png # Иконка + ├── manifest.json # PWA-манифест + └── bootstrap/ # Bootstrap CSS +``` + +**Особенности:** +- Использует Blazor Static SSR (без интерактивности на клиенте) +- Страница отправляет POST-запрос с параметрами и получает готовый файл +- Поддерживает PWA для установки как приложения + +--- + +## 6. Конфигурация сети + +`BookGetterConfig` создаёт и настраивает `HttpClient`: +- **Базовый Handler**: поддержка cookies, автоматические редиректы +- **Прокси**: HTTP/HTTPS, SOCKS4, SOCKS5 +- **Flaresolverr**: перенаправление запросов через Flaresolverr для обхода Cloudflare +- **Таймаут**: настраиваемый (по умолчанию 120с) +- **Заголовки**: User-Agent, Referer и др. + +--- + +## 7. Основные паттерны + +| Паттерн | Применение | +|---------|-----------| +| **Factory Method** | `GetterFactory` — создание загрузчика по URL через рефлексию | +| **Strategy** | `BuilderBase` — разные форматы сборки через единый интерфейс | +| **Template Method** | `BuilderBase.Build()` → `BuildInternal()` — общая логика сохранения + специфичная сборка | +| **Abstract Factory** | `BuilderProvider` — выбор сборщика по строке формата | + +--- + +## 8. Сборка (Docker) + +Проект публикуется в два Docker-образа: +- `onlyfart/elib2ebookcli` — CLI версия +- `onlyfart/elib2ebookweb` — Web версия (Blazor) + +Оба собираются через GitHub Actions (см. `.github/workflows/dotnet.yml`). Для CLI используется **ReadyToRun + SingleFile** для portable-сборок без необходимости установки .NET Runtime. diff --git a/Core/Configs/AppSecrets.cs b/Core/Configs/AppSecrets.cs deleted file mode 100644 index c6e227aa..00000000 --- a/Core/Configs/AppSecrets.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Core.Configs; - -public static class AppSecrets -{ - public const string MyBookConsumerKey = "830968793b2a44c688400319f4a77231"; - public const string MyBookConsumerSecret = "HhaxXsMryrLz49R4"; - public const string MyBookIdentifier = "47b17a4c8231a28f06c6c836d0b5f6f2134cb74d"; - public const string MyBookDefaultAuthSecret = "fYFPndYhOTW6YxIJ"; - public const string MyBookDefaultAuthToken = "f8d32ef906664ed3a1525b7298aac461"; - - public const string LitnetSecret = "14a6579a984b3c6abecda6c2dfa83a64"; - - public const string LitresSecretKey = "AsAAfdV000-1kksn6591x:[}A{}<> - - - net10.0 - latestmajor - Core - 2.8.2 - true - - - - - - - - - - - - - - - - - - - Always - - - - - - External\EpubSharp.dll - - - - diff --git a/Core/Exceptions/Elib2EbookAuthException.cs b/Core/Exceptions/Elib2EbookAuthException.cs deleted file mode 100644 index 73972b7a..00000000 --- a/Core/Exceptions/Elib2EbookAuthException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Core.Exceptions; - -public class Elib2EbookAuthException : Elib2EbookException -{ - public Elib2EbookAuthException() - { - } - - public Elib2EbookAuthException(string message) : base(message) - { - } - - public Elib2EbookAuthException(string message, Exception innerException) : base(message, innerException) - { - } -} diff --git a/Core/Exceptions/Elib2EbookException.cs b/Core/Exceptions/Elib2EbookException.cs deleted file mode 100644 index c91627ec..00000000 --- a/Core/Exceptions/Elib2EbookException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Core.Exceptions; - -public class Elib2EbookException : Exception -{ - public Elib2EbookException() - { - } - - public Elib2EbookException(string message) : base(message) - { - } - - public Elib2EbookException(string message, Exception innerException) : base(message, innerException) - { - } -} diff --git a/Core/Exceptions/Elib2EbookFormatException.cs b/Core/Exceptions/Elib2EbookFormatException.cs deleted file mode 100644 index f240b015..00000000 --- a/Core/Exceptions/Elib2EbookFormatException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Core.Exceptions; - -public class Elib2EbookFormatException : Elib2EbookException -{ - public Elib2EbookFormatException() - { - } - - public Elib2EbookFormatException(string message) : base(message) - { - } - - public Elib2EbookFormatException(string message, Exception innerException) : base(message, innerException) - { - } -} diff --git a/Core/Exceptions/Elib2EbookParseException.cs b/Core/Exceptions/Elib2EbookParseException.cs deleted file mode 100644 index eef413a5..00000000 --- a/Core/Exceptions/Elib2EbookParseException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Core.Exceptions; - -public class Elib2EbookParseException : Elib2EbookException -{ - public Elib2EbookParseException() - { - } - - public Elib2EbookParseException(string message) : base(message) - { - } - - public Elib2EbookParseException(string message, Exception innerException) : base(message, innerException) - { - } -} diff --git a/Core/Extensions/HttpClientExtensions.cs b/Core/Extensions/HttpClientExtensions.cs deleted file mode 100644 index c997c9b6..00000000 --- a/Core/Extensions/HttpClientExtensions.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Core.Exceptions; -using HtmlAgilityPack; - -namespace Core.Extensions; - -public static class HttpClientExtensions -{ - private const int MAX_TRY_COUNT = 5; - private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(1); - - private static TimeSpan GetTimeout(TimeSpan errorTimeout) - { - return errorTimeout == default ? DefaultTimeout : errorTimeout; - } - - private static async Task SendWithTriesAsync( - Func> requestFunc, - TimeSpan errorTimeout) - { - Exception lastEx = null; - - for (var i = 0; i < MAX_TRY_COUNT; i++) - try - { - var response = await requestFunc(); - - if (response.StatusCode != HttpStatusCode.OK) - { - if (i == MAX_TRY_COUNT - 1) return response; - - response.Dispose(); - await Task.Delay(GetTimeout(errorTimeout)); - continue; - } - - return response; - } - catch (Exception ex) - { - lastEx = ex; - await Task.Delay(GetTimeout(errorTimeout)); - } - - if (lastEx != null) throw lastEx; - - return default; - } - - public static async Task GetWithTriesAsync(this HttpClient client, Uri url, - TimeSpan errorTimeout = default) - { - return await SendWithTriesAsync(() => client.GetAsync(url), errorTimeout); - } - - public static async Task SendWithTriesAsync(this HttpClient client, - Func message, TimeSpan errorTimeout = default) - { - return await SendWithTriesAsync(() => client.SendAsync(message()), errorTimeout); - } - - public static async Task PostWithTriesAsync(this HttpClient client, Uri url, - HttpContent content, TimeSpan errorTimeout = default) - { - return await SendWithTriesAsync(() => client.PostAsync(url, content), errorTimeout); - } - - public static async Task GetHtmlDocWithTriesAsync(this HttpClient client, Uri url, - Encoding encoding = null) - { - using var response = await client.GetWithTriesAsync(url); - return await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc(encoding)); - } - - public static async Task GetFromJsonWithTriesAsync(this HttpClient client, Uri url, - TimeSpan errorTimeout = default) - { - using var response = await client.GetWithTriesAsync(url, errorTimeout); - if (response is null) throw new Elib2EbookParseException($"Не удалось получить ответ от {url}"); - - if (response.StatusCode != HttpStatusCode.OK) - { - var errorBody = await SafeReadAsStringAsync(response.Content); - throw new Elib2EbookFormatException( - $"Ошибка запроса {url}. Код {(int)response.StatusCode} ({response.StatusCode}).{FormatSnippet(errorBody)}"); - } - - await using var stream = await response.Content.ReadAsStreamAsync(); - using var memoryStream = new MemoryStream(); - await stream.CopyToAsync(memoryStream); - - var payload = Encoding.UTF8.GetString(memoryStream.ToArray()); - if (string.IsNullOrWhiteSpace(payload)) throw new Elib2EbookParseException($"Ответ {url} пустой"); - - if (LooksLikeHtml(payload)) - throw new Elib2EbookParseException( - $"Сайт вернул HTML вместо JSON по адресу {url}. Возможно сработала защита (DDoS-Guard). Попробуйте увеличить параметры --delay/--timeout или настроить --flare."); - - try - { - return JsonSerializer.Deserialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web)) ?? - throw new Elib2EbookParseException($"Сайт вернул пустой JSON по адресу {url}"); - } - catch (JsonException ex) - { - throw new Elib2EbookFormatException( - $"Не удалось разобрать JSON ответа {url}. {ex.Message}. {FormatSnippet(payload)}", ex); - } - } - - public static async Task PostHtmlDocWithTriesAsync(this HttpClient client, Uri url, - HttpContent content, Encoding encoding = null) - { - using var response = await client.PostWithTriesAsync(url, content); - return await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc(encoding)); - } - - private static async Task SafeReadAsStringAsync(HttpContent content) - { - try - { - return await content.ReadAsStringAsync(); - } - catch - { - return string.Empty; - } - } - - private static bool LooksLikeHtml(string value) - { - return value.TrimStart().StartsWith('<'); - } - - private static string FormatSnippet(string value, int maxLength = 300) - { - if (string.IsNullOrWhiteSpace(value)) return string.Empty; - - var trimmed = value.Trim(); - if (LooksLikeHtml(trimmed)) return string.Empty; - - if (trimmed.Length > maxLength) trimmed = trimmed[..maxLength] + "…"; - - return $" Ответ сервера: {trimmed}"; - } -} diff --git a/Core/External/EpubSharp.deps.json b/Core/External/EpubSharp.deps.json deleted file mode 100644 index 7198bc52..00000000 --- a/Core/External/EpubSharp.deps.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "runtimeTarget": { - "name": ".NETCoreApp,Version=v10.0", - "signature": "" - }, - "compilationOptions": {}, - "targets": { - ".NETCoreApp,Version=v10.0": { - "EpubSharp/1.1.6-alpha.9": { - "runtime": { - "EpubSharp.dll": {} - } - } - } - }, - "libraries": { - "EpubSharp/1.1.6-alpha.9": { - "type": "project", - "serviceable": false, - "sha512": "" - } - } -} \ No newline at end of file diff --git a/Core/External/EpubSharp.dll b/Core/External/EpubSharp.dll deleted file mode 100644 index 60ec8cba..00000000 Binary files a/Core/External/EpubSharp.dll and /dev/null differ diff --git a/Core/External/EpubSharp.pdb b/Core/External/EpubSharp.pdb deleted file mode 100644 index 86a5171b..00000000 Binary files a/Core/External/EpubSharp.pdb and /dev/null differ diff --git a/Core/Logic/Builders/CbzBuilder.cs b/Core/Logic/Builders/CbzBuilder.cs deleted file mode 100644 index b83d7442..00000000 --- a/Core/Logic/Builders/CbzBuilder.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Threading.Tasks; -using Core.Configs; -using Core.Types.Book; -using Microsoft.Extensions.Logging; - -namespace Core.Logic.Builders; - -public class CbzBuilder(Options options, ILogger logger) : BuilderBase(options, logger) -{ - protected override string Extension => "cbz"; - - protected override async Task BuildInternal(Book book, string fileName) - { - if (File.Exists(fileName)) File.Delete(fileName); - - using var archive = ZipFile.Open(fileName, ZipArchiveMode.Create); - - var c = 0; - foreach (var chapter in book.Chapters) - { - if (chapter.Images is null) continue; - - foreach (var image in chapter.Images) - { - var image_name = $"{chapter.VolumeNumber}-{chapter.ChapterNumber}-{++c}{image.Extension}"; - var entry = archive.CreateEntry(image_name, CompressionLevel.Optimal); - await using var entryStream = entry.Open(); - await using var fileStream = image.GetStream(); - await fileStream.CopyToAsync(entryStream); - } - } - } - - protected override async Task SplitBuild(Book book, string fileName) - { - if (Options.SplitVolumes && Options.SplitChapters) - foreach (var volumeNumber in book.Volumes) - { - var volume_name = $"{fileName}.v{volumeNumber}.{Extension}"; - using var volume_archive = ZipFile.Open(volume_name, ZipArchiveMode.Create); - - foreach (var chapter in book.Chapters.Where(c => c.VolumeNumber == volumeNumber)) - { - if (chapter.Images is null) continue; - - var chapter_name = $"{fileName}.v{chapter.VolumeNumber}.c{chapter.ChapterNumber}.{Extension}"; - - using var chapter_archive = ZipFile.Open(chapter_name, ZipArchiveMode.Create); - - var c = 0; - foreach (var image in chapter.Images) - { - var image_name = $"{chapter.VolumeNumber}-{chapter.ChapterNumber}-{++c}{image.Extension}"; - var volume_entry = volume_archive.CreateEntry(image_name, CompressionLevel.Optimal); - var chapter_entry = chapter_archive.CreateEntry(image_name, CompressionLevel.Optimal); - await using var volume_entryStream = volume_entry.Open(); - await using var chapter_entryStream = chapter_entry.Open(); - await using var volume_fileStream = image.GetStream(); - await using var chapter_fileStream = image.GetStream(); - await volume_fileStream.CopyToAsync(volume_entryStream); - await chapter_fileStream.CopyToAsync(chapter_entryStream); - } - - Logger.LogInformation($"Книга {chapter_name} успешно сохранена"); - chapter_archive.Dispose(); - } - - Logger.LogInformation($"Книга {volume_name} успешно сохранена"); - volume_archive.Dispose(); - } - else if (Options.SplitVolumes) - foreach (var volumeNumber in book.Volumes) - { - var volume_name = $"{fileName}.v{volumeNumber}.{Extension}"; - - using var volume_archive = ZipFile.Open(volume_name, ZipArchiveMode.Create); - - var c = 0; - foreach (var chapter in book.Chapters.Where(c => c.VolumeNumber == volumeNumber)) - { - if (chapter.Images is null) continue; - foreach (var image in chapter.Images) - { - var image_name = $"{chapter.VolumeNumber}-{chapter.ChapterNumber}-{++c}{image.Extension}"; - var volume_entry = volume_archive.CreateEntry(image_name, CompressionLevel.Optimal); - await using var volume_entryStream = volume_entry.Open(); - await using var fileStream = image.GetStream(); - await fileStream.CopyToAsync(volume_entryStream); - } - } - - Logger.LogInformation($"Книга {volume_name} успешно сохранена"); - volume_archive.Dispose(); - } - else if (Options.SplitChapters) - foreach (var chapter in book.Chapters) - { - var chapter_name = $"{fileName}.v{chapter.VolumeNumber}.c{chapter.ChapterNumber}.{Extension}"; - - using var chapter_archive = ZipFile.Open(chapter_name, ZipArchiveMode.Create); - if (chapter.Images is null) continue; - var c = 0; - foreach (var image in chapter.Images) - { - var image_name = $"{chapter.VolumeNumber}-{chapter.ChapterNumber}-{++c}{image.Extension}"; - var chapter_entry = chapter_archive.CreateEntry(image_name, CompressionLevel.Optimal); - await using var chapter_entryStream = chapter_entry.Open(); - await using var fileStream = image.GetStream(); - await fileStream.CopyToAsync(chapter_entryStream); - } - - Logger.LogInformation($"Книга {chapter_name} успешно сохранена"); - chapter_archive.Dispose(); - } - else - await BuildInternal(book, fileName); - } -} diff --git a/Core/Logic/Builders/EpubSharpCompat.cs b/Core/Logic/Builders/EpubSharpCompat.cs deleted file mode 100644 index 07366596..00000000 --- a/Core/Logic/Builders/EpubSharpCompat.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Text.Json; -using EpubSharp; -using Microsoft.Extensions.Logging; - -namespace Core.Logic.Builders; - -internal static class EpubSharpCompat -{ - private const string ExpectedPdbMarker = "EpubSharp_Elib2Ebook"; - private const string CacheFileName = "epubsharp-origin.json"; - - private static readonly object CacheLock = new(); - private static OriginCacheEntry _originCache; - - private static readonly Lazy> TrySetSeriesUrlInvoker = - new(CreateTrySetSeriesUrlInvoker); - - private static readonly Lazy> TryAddNcxWarningPageInvoker = - new(CreateTryAddNcxWarningPageInvoker); - - public static bool TrySetSeriesUrlIfSupported(EpubWriter writer, string seriesUrl, ILogger logger) - { - if (writer == null) return false; - if (string.IsNullOrWhiteSpace(seriesUrl)) return false; - - if (!IsEpubSharpElib2EbookBuild(writer.GetType().Assembly, logger)) return false; - - var invoker = TrySetSeriesUrlInvoker.Value; - if (invoker == null) return false; - - try - { - return invoker(writer, seriesUrl); - } - catch (Exception ex) - { - logger?.LogDebug(ex, "EpubSharp TrySetSeriesUrl failed; skipping series url."); - return false; - } - } - - public static bool TryAddNcxWarningPageIfSupported(EpubWriter writer, string title, string xhtml, ILogger logger) - { - if (writer == null) return false; - if (string.IsNullOrWhiteSpace(title)) return false; - if (string.IsNullOrWhiteSpace(xhtml)) return false; - - var invoker = TryAddNcxWarningPageInvoker.Value; - if (invoker == null) return false; - - try - { - return invoker(writer, title, xhtml); - } - catch (Exception ex) - { - logger?.LogDebug(ex, "EpubSharp TryAddNcxWarningPage failed; skipping ncx warning."); - return false; - } - } - - private static Func CreateTrySetSeriesUrlInvoker() - { - var method = typeof(EpubWriter).GetMethod( - "TrySetSeriesUrl", - BindingFlags.Instance | BindingFlags.Public, - null, - [typeof(string)], - null); - - if (method == null) return null; - if (method.ReturnType != typeof(bool)) return null; - - try - { - return method.CreateDelegate>(); - } - catch - { - return null; - } - } - - private static Func CreateTryAddNcxWarningPageInvoker() - { - var method = typeof(EpubWriter).GetMethod( - "TryAddNcxWarningPage", - BindingFlags.Instance | BindingFlags.Public, - null, - [typeof(string), typeof(string)], - null); - - if (method == null) return null; - if (method.ReturnType != typeof(bool)) return null; - - try - { - return method.CreateDelegate>(); - } - catch - { - return null; - } - } - - private static bool IsEpubSharpElib2EbookBuild(Assembly epubSharpAssembly, ILogger logger) - { - var (mvid, size, dllPath) = GetAssemblyFingerprint(epubSharpAssembly); - if (string.IsNullOrWhiteSpace(dllPath)) return false; - - lock (CacheLock) - { - if (_originCache != null && - _originCache.Mvid == mvid && - _originCache.Size == size) - return _originCache.IsEpubSharpElib2Ebook; - - var cachePath = GetCachePath(); - var disk = TryReadCache(cachePath); - if (disk != null && disk.Mvid == mvid && disk.Size == size) - { - _originCache = disk; - return disk.IsEpubSharpElib2Ebook; - } - - var isExpected = ComputeIsExpectedBuild(dllPath); - var entry = new OriginCacheEntry - { - Mvid = mvid, - Size = size, - IsEpubSharpElib2Ebook = isExpected - }; - - _originCache = entry; - TryWriteCache(cachePath, entry, logger); - return isExpected; - } - } - - private static bool ComputeIsExpectedBuild(string dllPath) - { - try - { - var dir = Path.GetDirectoryName(dllPath); - if (string.IsNullOrWhiteSpace(dir)) return false; - - var pdbPath = Path.Combine(dir, "EpubSharp.pdb"); - if (!File.Exists(pdbPath)) return false; - - var bytes = File.ReadAllBytes(pdbPath); - return ContainsAscii(bytes, ExpectedPdbMarker); - } - catch - { - return false; - } - } - - private static (Guid mvid, long size, string path) GetAssemblyFingerprint(Assembly assembly) - { - try - { - var path = assembly.Location; - var size = new FileInfo(path).Length; - var mvid = typeof(EpubWriter).Module.ModuleVersionId; - return (mvid, size, path); - } - catch - { - return (Guid.Empty, 0, string.Empty); - } - } - - private static string GetCachePath() - { - var root = Environment.GetEnvironmentVariable("XDG_CACHE_HOME"); - if (string.IsNullOrWhiteSpace(root)) - root = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) - : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache"); - - return Path.Combine(root, "Elib2Ebook", CacheFileName); - } - - private static OriginCacheEntry TryReadCache(string cachePath) - { - try - { - if (!File.Exists(cachePath)) return null; - var json = File.ReadAllText(cachePath); - return JsonSerializer.Deserialize(json); - } - catch - { - return null; - } - } - - private static void TryWriteCache(string cachePath, OriginCacheEntry entry, ILogger logger) - { - try - { - var dir = Path.GetDirectoryName(cachePath); - if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir); - - var json = JsonSerializer.Serialize(entry); - File.WriteAllText(cachePath, json); - } - catch (Exception ex) - { - if (logger != null) logger.LogDebug(ex, "Failed to write EpubSharp origin cache."); - } - } - - private static bool ContainsAscii(byte[] haystack, string needle) - { - if (haystack.Length == 0) return false; - if (string.IsNullOrEmpty(needle)) return false; - - var n = needle.Length; - if (n > haystack.Length) return false; - - for (var i = 0; i <= haystack.Length - n; i++) - { - var ok = true; - for (var j = 0; j < n; j++) - if (haystack[i + j] != (byte)needle[j]) - { - ok = false; - break; - } - - if (ok) return true; - } - - return false; - } - - private sealed class OriginCacheEntry - { - public Guid Mvid { get; set; } - public long Size { get; set; } - public bool IsEpubSharpElib2Ebook { get; set; } - } -} diff --git a/Core/Logic/Getters/AuthorTodayGetter.cs b/Core/Logic/Getters/AuthorTodayGetter.cs deleted file mode 100644 index 80f26999..00000000 --- a/Core/Logic/Getters/AuthorTodayGetter.cs +++ /dev/null @@ -1,340 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Net.Http.Json; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Misc; -using Core.Types.AuthorToday; -using Core.Types.Book; -using Core.Types.Common; -using Microsoft.Extensions.Logging; - -namespace Core.Logic.Getters; - -public class AuthorTodayGetter(BookGetterConfig config) : GetterBase(config) -{ - private const string AT_CERT = AppSecrets.AuthorTodayCert; - - private bool _bypass; - private bool _singleFetchUsed; - protected override Uri SystemUrl => new("https://author.today/"); - - private Uri _apiUrl => new("https://api.author.today/"); - - /// - /// IP сайта author.today - /// - private Uri _systemIp => new("https://185.26.98.195/"); - - /// - /// IP сайта api.author.today - /// - private Uri _apiIp => new("https://185.26.98.195/"); - - private Uri ApiUrl => _bypass ? _apiIp : _apiUrl; - - private Uri SiteUrl => _bypass ? _systemIp : SystemUrl; - - private string UserId { get; set; } = string.Empty; - - private string hashedCert => Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(AT_CERT))).ToUpperInvariant(); - - protected override string GetId(Uri url) - { - return url.GetSegment(2); - } - - public override async Task Init() - { - Config.Client.DefaultRequestHeaders.Add("Accept", - "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); - Config.Client.DefaultRequestHeaders.Add("Accept-Language", "ru"); - Config.Client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br"); - Config.Client.DefaultRequestHeaders.Add("User-Agent", "okhttp/4.12.0 X_AT_API"); - Config.Client.DefaultRequestHeaders.Add("X-AT-Client", "android_1.8.013-GMS"); - Config.Client.DefaultRequestHeaders.Add("X-AT-Certificate", hashedCert); - - var response = await Config.Client.GetAsync(_apiUrl); - _bypass = response.StatusCode != HttpStatusCode.OK; - Config.Logger.LogInformation(_bypass - ? $"Сайт {_apiUrl} не доступен. Работаю через {_apiIp}" - : $"Сайт {_apiUrl} доступен. Работаю через него"); - - Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "guest"); - } - - public override async Task Authorize() - { - if (!Config.HasCredentials) return; - - const string directory = "ATCache"; - if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); - - var cachePath = $"{directory}/{Config.Options.Login.RemoveInvalidChars()}"; - - if (await TryAuthorizeFromCacheAsync(cachePath)) return; - - await AuthorizeByPasswordAsync(cachePath); - } - - private async Task GetUserIdAsync() - { - try - { - var response = await Config.Client.SendWithTriesAsync(() => - GetDefaultMessage(ApiUrl.MakeRelativeUri("/v1/account/current-user"), _apiUrl)); - - if (response.StatusCode != HttpStatusCode.OK) return null; - - var user = await response.Content.ReadFromJsonAsync(); - return user?.Id.ToString(); - } - catch (Exception ex) - { - Config.Logger.LogDebug(ex, "AuthorToday: не удалось получить идентификатор пользователя."); - return null; - } - } - - private async Task TryAuthorizeFromCacheAsync(string cachePath) - { - if (!File.Exists(cachePath)) return false; - - try - { - if (DateTime.Now - File.GetLastWriteTime(cachePath) >= TimeSpan.FromHours(24)) - { - File.Delete(cachePath); - return false; - } - - var json = await File.ReadAllTextAsync(cachePath); - var savedAuth = json.Deserialize(); - if (string.IsNullOrWhiteSpace(savedAuth?.Token)) return false; - - Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", savedAuth.Token); - - UserId = await GetUserIdAsync(); - if (!string.IsNullOrWhiteSpace(UserId)) - { - Config.Logger.LogInformation("AuthorToday: успешно авторизовались с помощью кэша"); - return true; - } - } - catch (Exception ex) - { - Config.Logger.LogDebug(ex, "AuthorToday: не удалось авторизоваться через кэш."); - try { File.Delete(cachePath); } catch { /* ignore */ } - } - - Config.Client.DefaultRequestHeaders.Authorization = null; - return false; - } - - private async Task AuthorizeByPasswordAsync(string cachePath) - { - var response = await Config.Client.SendAsync(GetDefaultMessage( - ApiUrl.MakeRelativeUri("/v1/account/login-by-password"), _apiUrl, - JsonContent.Create(new { Config.Options.Login, Config.Options.Password }))); - - if (response.StatusCode != HttpStatusCode.OK) - { - var err = await response.Content.ReadFromJsonAsync(); - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {err?.Message}"); - } - - var data = await response.Content.ReadFromJsonAsync(); - if (string.IsNullOrWhiteSpace(data?.Token)) - throw new Elib2EbookAuthException("Сайт не вернул token после успешного запроса авторизации."); - - Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", data.Token); - - UserId = await GetUserIdAsync(); - if (string.IsNullOrWhiteSpace(UserId)) - throw new Elib2EbookAuthException("Не удалось получить идентификатор пользователя после авторизации."); - - Config.Logger.LogInformation("Успешно авторизовались"); - - try - { - await File.WriteAllTextAsync(cachePath, JsonSerializer.Serialize(data)); - } - catch (Exception ex) - { - Config.Logger.LogDebug(ex, "AuthorToday: не удалось записать кэш авторизации."); - } - } - - public override async Task Get(Uri url) - { - url = SystemUrl.MakeRelativeUri($"/work/{GetId(url)}"); - var details = await GetBookDetails(GetId(url)); - - var book = new Book(url) - { - Cover = await GetCover(details), - Chapters = await FillChapters(details), - Title = details.Title, - Author = GetAuthor(details), - CoAuthors = GetCoAuthors(details), - Annotation = details.Annotation, - Seria = GetSeria(details) - }; - - await FillAdditional(book, details); - - return book; - } - - private async Task FillAdditional(Book book, AuthorTodayBookDetails details) - { - if (!Config.Options.HasAdditionalType(AdditionalTypeEnum.Images) || details.GalleryImages == null || - details.GalleryImages.Length == 0) return; - - Config.Logger.LogInformation("Загружаю дополнительные иллюстрации"); - foreach (var image in details.GalleryImages) - book.AdditionalFiles.Add(AdditionalTypeEnum.Images, await SaveImage(SystemUrl.MakeRelativeUri(image.Url))); - Config.Logger.LogInformation("Дополнительные иллюстрации загружены"); - } - - private HttpRequestMessage GetDefaultMessage(Uri uri, Uri host, HttpContent content = null) - { - var message = new HttpRequestMessage(content == null ? HttpMethod.Get : HttpMethod.Post, uri); - message.Content = content; - message.Version = Config.Client.DefaultRequestVersion; - - foreach (var header in Config.Client.DefaultRequestHeaders) message.Headers.Add(header.Key, header.Value); - - message.Headers.Host = host.Host; - - return message; - } - - private async Task GetBookDetails(string bookId) - { - var response = await Config.Client.SendWithTriesAsync(() => - GetDefaultMessage(ApiUrl.MakeRelativeUri($"/v1/work/{bookId}/details"), _apiUrl)); - if (response.StatusCode != HttpStatusCode.OK) throw new Elib2EbookParseException("Книга не найдена"); - - return await response.Content.ReadFromJsonAsync(); - } - - private Author GetAuthor(AuthorTodayBookDetails book) - { - return new Author(book.AuthorFio, SystemUrl.MakeRelativeUri($"/u/{book.AuthorUserName}/works")); - } - - private IEnumerable GetCoAuthors(AuthorTodayBookDetails book) - { - var result = new List(); - if (!string.IsNullOrWhiteSpace(book.CoAuthorFio)) - result.Add(new Author(book.CoAuthorFio, SystemUrl.MakeRelativeUri($"/u/{book.CoAuthorUserName}/works"))); - - return result; - } - - private Seria GetSeria(AuthorTodayBookDetails book) - { - if (!book.SeriesId.HasValue) return null; - - return new Seria - { - Name = book.SeriesTitle.Trim(), - Number = book.SeriesWorkNumber.HasValue ? book.SeriesWorkNumber.ToString() : string.Empty, - Url = SystemUrl.MakeRelativeUri($"/work/series/{book.SeriesId}") - }; - } - - private Task GetCover(AuthorTodayBookDetails book) - { - return !string.IsNullOrWhiteSpace(book.CoverUrl) - ? SaveImage(book.CoverUrl.AsUri()) - : Task.FromResult(null); - } - - protected override HttpRequestMessage GetImageRequestMessage(Uri uri) - { - if (uri.IsSameHost(SystemUrl) || uri.IsSameSubDomain(SystemUrl)) - return GetDefaultMessage(SiteUrl.MakeRelativeUri(uri.AbsolutePath), uri); - - return base.GetImageRequestMessage(uri); - } - - private async Task> FillChapters(AuthorTodayBookDetails book) - { - var result = new List(); - if (Config.Options.NoChapters) return result; - - foreach (var atChapter in await GetChapters(book)) - { - var title = atChapter.Title.ReplaceNewLine(); - var prefix = _singleFetchUsed ? "sf " : string.Empty; - Config.Logger.LogInformation($"{prefix}Загружаю главу {title.CoverQuotes()}"); - - var chapter = new Chapter - { - Title = title - }; - - if (atChapter.IsSuccessful) - { - var chapterDoc = atChapter.Decode(UserId, AT_CERT).AsHtmlDoc(); - chapter.Images = await GetImages(chapterDoc, SystemUrl); - chapter.Content = chapterDoc.DocumentNode.InnerHtml; - } - - result.Add(chapter); - } - - return result; - } - - private async Task> GetChapters(AuthorTodayBookDetails book) - { - var result = new List(); - - var fetchMode = Config.GetSetting("AT:FetchMode", "Batch"); - if (fetchMode.Equals("Single", StringComparison.OrdinalIgnoreCase)) - { - _singleFetchUsed = true; - foreach (var chapter in book.Chapters.Where(c => !c.IsDraft && c.isAvailable).OrderBy(c => c.SortOrder)) - { - var uri = ApiUrl.MakeRelativeUri($"/v1/work/{book.Id}/chapter/{chapter.Id}/text"); - Config.Logger.LogInformation($"Загружаю главу {chapter.Title.CoverQuotes()}"); - var response = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(uri, _apiUrl)); - var rchapter = await response.Content.ReadFromJsonAsync(); - if (rchapter != null && rchapter.Text != "") - { - Config.Logger.LogInformation($"Загружена глава {rchapter.Title.CoverQuotes()}"); - chapter.Text = rchapter.Text; - chapter.Key = rchapter.Key; - chapter.IsSuccessful = true; - result.Add(chapter); - } - } - } - else - { - foreach (var chunk in book.Chapters.Where(c => !c.IsDraft).OrderBy(c => c.SortOrder).Chunk(100)) - { - var ids = string.Join("&", chunk.Select((c, i) => $"ids[{i}]={c.Id}")); - var uri = ApiUrl.MakeRelativeUri($"/v1/work/{book.Id}/chapter/many-texts?{ids}"); - var response = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(uri, _apiUrl)); - var chapters = await response.Content.ReadFromJsonAsync(); - if (chapters != null) result.AddRange(chapters.Where(c => c.Code != "NotFound")); - } - } - - return SliceToc(result, c => c.Title); - } -} diff --git a/Core/Logic/Getters/BooksYandex/BooksYandexComicsGetter.cs b/Core/Logic/Getters/BooksYandex/BooksYandexComicsGetter.cs deleted file mode 100644 index d011eb41..00000000 --- a/Core/Logic/Getters/BooksYandex/BooksYandexComicsGetter.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Net.Http.Json; -using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.BookYandex; - -namespace Core.Logic.Getters.BooksYandex; - -public class BooksYandexComicsGetter(BookGetterConfig config) : BooksYandexGetterBase(config) -{ - protected override string[] Paths => ["comicbooks"]; - - protected override async Task> FillChapters(Book book, BooksYandexResponse response) - { - if (Config.Options.NoChapters || string.IsNullOrWhiteSpace(response.Comicbook?.UUID)) return []; - - var metadata = await Config.Client.GetFromJsonAsync( - $"https://api.bookmate.ru/api/v5/comicbooks/{response.Comicbook.UUID}/metadata.json"); - - var sb = new StringBuilder(); - - foreach (var page in metadata.Pages) sb.Append($""); - - var doc = sb.AsHtmlDoc(); - var chapter = new Chapter - { - Images = await GetImages(doc, SystemUrl), - Content = doc.DocumentNode.InnerHtml, - Title = response.Comicbook.Title - }; - - return [chapter]; - } -} diff --git a/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs b/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs deleted file mode 100644 index 2e44844d..00000000 --- a/Core/Logic/Getters/LibSocial/NewLibSocialGetterBase.cs +++ /dev/null @@ -1,360 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Net.Http.Json; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.SocialLib; -using HtmlAgilityPack; -using HtmlAgilityPack.CssSelectors.NetCore; -using Microsoft.Extensions.Logging; - -namespace Core.Logic.Getters.LibSocial; - -public abstract class NewLibSocialGetterBase(BookGetterConfig config) : GetterBase(config) -{ - private const string ALPHABET_BASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - private const string ALPHABET_CHALLENGE = ALPHABET_BASE + "-_"; - private static Uri AuthHost => new("https://auth.lib.social/"); - - private static Uri ApiHost => new("https://api.cdnlibs.org/"); - - private static Uri BaseImageHost => new("https://cover.imglib.info/"); - - protected virtual Uri ImagesHost => BaseImageHost; - - protected abstract int SiteId { get; } - - public override async Task Init() - { - await base.Init(); - - Config.Client.DefaultRequestHeaders.Remove("Site-Id"); - Config.Client.DefaultRequestHeaders.Add("Site-Id", SiteId.ToString()); - - try - { - var timeZoneId = TimeZoneInfo.Local.Id; - Config.Client.DefaultRequestHeaders.Remove("Client-Time-Zone"); - Config.Client.DefaultRequestHeaders.Add("Client-Time-Zone", timeZoneId); - } - catch (TimeZoneNotFoundException) - { - // Игнорируем: таймзона влияет только на аналитические метрики сайта - } - catch (InvalidTimeZoneException) - { - // Игнорируем: таймзона влияет только на аналитические метрики сайта - } - } - - private static string TrimForLog(string value, int maxLength = 300) - { - if (string.IsNullOrWhiteSpace(value)) return string.Empty; - - var trimmed = value.Trim(); - return trimmed.Length <= maxLength ? trimmed : trimmed[..maxLength] + "…"; - } - - private static string Challenge(string str) - { - Span bytesToHash = stackalloc byte[Encoding.UTF8.GetMaxByteCount(str.Length)]; - int byteCount = Encoding.UTF8.GetBytes(str, bytesToHash); - Span bytes = stackalloc byte[32]; - SHA256.HashData(bytesToHash[..byteCount], bytes); - - var sb = new StringBuilder(44); - - for (var t = 0; t < bytes.Length; t += 3) - { - sb.Append(ALPHABET_CHALLENGE[bytes[t] >> 2]); - sb.Append(ALPHABET_CHALLENGE[((bytes[t] & 3) << 4) | (bytes[t + 1] >> 4)]); - sb.Append(ALPHABET_CHALLENGE[ - ((bytes[t + 1] & 15) << 2) | ((t + 2 < bytes.Length ? bytes[t + 2] : 0) >> 6)]); - sb.Append(ALPHABET_CHALLENGE[(t + 2 < bytes.Length ? bytes[t + 2] : 0) & 63]); - } - - var result = sb.ToString(); - return (bytes.Length % 3) switch - { - 2 => result[..^1], - 1 => result[..^2], - _ => result - }; - } - - private static string GetRandom(int length) - { - return RandomNumberGenerator.GetString(ALPHABET_BASE, length); - } - - public override async Task Authorize() - { - if (string.IsNullOrEmpty(Config.Options.Token) && !Config.HasCredentials) return; - - if (!string.IsNullOrEmpty(Config.Options.Token)) - { - Config.Logger.LogInformation("Подставлен токен"); - Config.Client.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", Config.Options.Token); - return; - } - - var secret = GetRandom(128); - var state = GetRandom(40); - var redirectUri = SystemUrl.MakeRelativeUri("/ru/front/auth/oauth/callback"); - - var challenge = Challenge(secret); - var challengeUrl = - AuthHost.MakeRelativeUri( - $"/auth/oauth/authorize?scope=&client_id=1&response_type=code&redirect_uri={redirectUri}&state={state}&code_challenge={challenge}&code_challenge_method=S256&prompt=consent"); - - var csrfToken = await FetchCsrfTokenAsync(challengeUrl); - var postForm = await SubmitCredentialsAsync(challengeUrl, csrfToken); - var authCode = await ProcessAuthorizationConsentAsync(postForm); - await ExchangeTokenAsync(redirectUri, secret, authCode); - - Config.Logger.LogInformation("Успешно авторизовались"); - } - - private async Task FetchCsrfTokenAsync(Uri challengeUrl) - { - var loginForm = await Config.Client.GetHtmlDocWithTriesAsync(challengeUrl); - var tokenInput = loginForm.QuerySelector("input[name=_token]"); - if (tokenInput is null) - throw new Elib2EbookParseException( - "Не удалось получить csrf токен формы авторизации сайта (возможно, заблокировал DDoS-Guard)"); - return tokenInput.Attributes["value"].Value; - } - - private async Task SubmitCredentialsAsync(Uri challengeUrl, string csrfToken) - { - var payload = new Dictionary - { - { "_token", csrfToken }, - { "login", Config.Options.Login }, - { "password", Config.Options.Password } - }; - - await Config.Client.PostHtmlDocWithTriesAsync(AuthHost.MakeRelativeUri("/auth/login"), - new FormUrlEncodedContent(payload)); - var login = await Config.Client.GetHtmlDocWithTriesAsync(challengeUrl); - if (login.QuerySelector(".g-recaptcha") is not null) - throw new Elib2EbookParseException( - "Авторизация заблокирована капчей. Укажите --flare с адресом FlareSolverr"); - - var error = login.QuerySelector(".form-field__error"); - if (error is not null && !string.IsNullOrWhiteSpace(error.InnerText)) - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {error.InnerText.Trim()}"); - - var postForm = login.QuerySelector("form[method=post]"); - if (postForm is null) - throw new Elib2EbookAuthException( - "Не удалось получить форму подтверждения OAuth на сайте (проверьте логин/пароль)"); - - return postForm; - } - - private async Task ProcessAuthorizationConsentAsync(HtmlNode postForm) - { - var payload = postForm - .QuerySelectorAll("input[type=hidden]") - .ToDictionary(input => input.Attributes["name"].Value, input => input.Attributes["value"].Value); - - using var authorize = await Config.Client.PostAsync( - AuthHost.MakeRelativeUri(postForm.Attributes["action"].Value), new FormUrlEncodedContent(payload)); - if ((int)authorize.StatusCode == 419) - throw new Elib2EbookAuthException( - "Авторизация отклонена (код 419). Обычно это означает истекший CSRF токен или необходимость пройти капчу. Попробуйте указать --flare"); - if (authorize is null || authorize.RequestMessage?.RequestUri is null) - throw new Elib2EbookAuthException("Сайт не вернул код авторизации (проверьте логин/пароль)"); - - var authCode = authorize.RequestMessage.RequestUri.GetQueryParameter("code"); - if (string.IsNullOrWhiteSpace(authCode)) - throw new Elib2EbookAuthException("Сайт не вернул code после авторизации (проверьте логин/пароль)"); - - return authCode; - } - - private async Task ExchangeTokenAsync(Uri redirectUri, string secret, string authCode) - { - var tokenResponse = await Config.Client.PostAsync(ApiHost.MakeRelativeUri("/api/auth/oauth/token"), - JsonContent.Create(new - { - grant_type = "authorization_code", - client_id = 1, - redirect_uri = redirectUri, - code_verifier = secret, - code = authCode - })); - - if (tokenResponse is null) - throw new Elib2EbookAuthException("Не удалось получить ответ от сайта при запросе access token"); - - if (tokenResponse.StatusCode != HttpStatusCode.OK) - { - var errorBody = await tokenResponse.Content.ReadAsStringAsync(); - var snippet = string.IsNullOrWhiteSpace(errorBody) - ? string.Empty - : $" Ответ сервера: {TrimForLog(errorBody)}"; - throw new Elib2EbookAuthException( - $"Сайт не выдал access token. Код {(int)tokenResponse.StatusCode} ({tokenResponse.StatusCode}).{snippet}"); - } - - var token = await tokenResponse.Content.ReadFromJsonAsync(); - if (token is null || string.IsNullOrWhiteSpace(token.AccessToken)) - throw new Elib2EbookAuthException("Сайт не вернул access token. Проверьте корректность логина и пароля"); - - Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); - } - - protected override string GetId(Uri url) - { - var id = url.GetSegment(2); - return id is "book" or "read" or "manga" ? url.GetSegment(3) : id; - } - - public override async Task Get(Uri url) - { - var bid = url.GetQueryParameter("bid"); - var details = await GetBookDetails(url); - - var book = new Book(url) - { - SupportSplitVolumes = true, - SupportSplitChapters = true, - Cover = await GetCover(details), - Chapters = await FillChapters(details, bid), - Title = string.IsNullOrWhiteSpace(details.Data.RusName) ? details.Data.Name : details.Data.RusName, - Author = GetAuthor(details), - CoAuthors = GetCoAuthors(details), - Annotation = details.Data.Summary - }; - - return book; - } - - private async Task GetBookDetails(Uri url) - { - url = ApiHost - .AppendSegment("api/manga/" + GetId(url)) - .AppendQueryParameter("fields[]", "background") - .AppendQueryParameter("fields[]", "teams") - .AppendQueryParameter("fields[]", "authors") - .AppendQueryParameter("fields[]", "summary") - .AppendQueryParameter("fields[]", "chap_count"); - - var response = await Config.Client.GetWithTriesAsync(url); - if (response is null) - throw new Elib2EbookParseException("Не удалось получить ответ от сайта при загрузке информации о книге"); - - if (response.StatusCode != HttpStatusCode.OK) - throw new Elib2EbookParseException( - $"Ошибка загрузки информации о книге. Код {(int)response.StatusCode} ({response.StatusCode})."); - - return await response.Content.ReadFromJsonAsync(); - } - - private async Task> GetToc(RanobeLibBookDetails book, string bid) - { - var url = ApiHost.MakeRelativeUri($"/api/manga/{book.Data.SlugUrl}/chapters"); - - Config.Logger.LogInformation("Загружаю оглавление"); - - var response = await Config.Client.GetWithTriesAsync(url); - if (response is null) - throw new Elib2EbookParseException("Не удалось получить ответ от сайта при загрузке оглавления"); - - if (response.StatusCode != HttpStatusCode.OK) - throw new Elib2EbookParseException( - $"Ошибка загрузки оглавления. Код {(int)response.StatusCode} ({response.StatusCode})."); - - var result = await response.Content.ReadFromJsonAsync() - .ContinueWith(t => t.Result.Chapters); - if (!string.IsNullOrWhiteSpace(bid)) - result = result.Where(c => c.Branches.Any(b => b.BranchId.ToString() == bid)).ToList(); - - return SliceToc(result, c => c.Name); - } - - private Author GetAuthor(RanobeLibBookDetails details) - { - var author = details.Data.Authors.FirstOrDefault(); - return author is null - ? new Author("Ranobelib") - : new Author(author.Name, SystemUrl.MakeRelativeUri($"/ru/people/{author.SlugUrl}")); - } - - private IEnumerable GetCoAuthors(RanobeLibBookDetails details) - { - return details.Data.Authors - .Skip(1) - .Select(author => new Author(author.Name, SystemUrl.MakeRelativeUri($"/ru/people/{author.SlugUrl}"))) - .ToList(); - } - - private Task GetCover(RanobeLibBookDetails details) - { - return !string.IsNullOrWhiteSpace(details.Data.Cover.Default) - ? SaveImage(details.Data.Cover.Default.AsUri()) - : Task.FromResult(default(TempFile)); - } - - protected override HttpRequestMessage GetImageRequestMessage(Uri uri) - { - uri = uri.Host == BaseImageHost.Host ? uri : uri.ReplaceHost(ImagesHost.Host); - - var message = base.GetImageRequestMessage(uri); - message.Headers.Add("Referer", SystemUrl.ToString()); - return message; - } - - private async Task> FillChapters(RanobeLibBookDetails book, string bid) - { - var result = new List(); - if (Config.Options.NoChapters) return result; - - foreach (var socialChapter in await GetToc(book, bid)) - { - var title = socialChapter.Name.ReplaceNewLine(); - Config.Logger.LogInformation($"Загружаю главу {title.CoverQuotes()}"); - - var chapter = new Chapter - { - Title = title, - VolumeNumber = socialChapter.Volume, - ChapterNumber = socialChapter.Number - }; - - var chapterDoc = await GetChapter(book, socialChapter, bid); - chapter.Images = await GetImages(chapterDoc, ImagesHost); - chapter.Content = chapterDoc.DocumentNode.InnerHtml; - - result.Add(chapter); - } - - return result; - } - - private async Task GetChapter(RanobeLibBookDetails book, SocialLibBookChapter chapter, string bid) - { - var uri = ApiHost.MakeRelativeUri( - $"/api/manga/{book.Data.SlugUrl}/chapter?number={chapter.Number}&volume={chapter.Volume}"); - if (!string.IsNullOrWhiteSpace(bid)) uri = uri.AppendQueryParameter("branch_id", bid); - - var chapterResponse = - await Config.Client.GetFromJsonWithTriesAsync(uri, TimeSpan.FromSeconds(30)); - return ResponseToHtmlDoc(chapterResponse.Data); - } - - protected abstract HtmlDocument ResponseToHtmlDoc(SocialLibBookChapter chapterResponse); -} diff --git a/Core/Logic/Getters/LibSocial/RanobeLibGetter.cs b/Core/Logic/Getters/LibSocial/RanobeLibGetter.cs deleted file mode 100644 index e3f75ee5..00000000 --- a/Core/Logic/Getters/LibSocial/RanobeLibGetter.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.SocialLib; -using HtmlAgilityPack; -using Microsoft.Extensions.Logging; - -namespace Core.Logic.Getters.LibSocial; - -public class RanobeLibGetter(BookGetterConfig config) : NewLibSocialGetterBase(config) -{ - private static readonly Dictionary RecursiveTag = new() - { - { "paragraph", "p" }, - { "orderedList", "ol" }, - { "listItem", "li" }, - { "blockquote", "blockquote" } - }; - - private static readonly Dictionary InlineTag = new() - { - { "horizontalRule", "
" }, - { "hardBreak", "
" } - }; - - private static readonly Dictionary MarkTag = new() - { - { "italic", "i" }, - { "bold", "b" }, - { "underline", "u" } - }; - - protected override Uri SystemUrl => new("https://ranobelib.me/"); - - protected override int SiteId => 3; - - private StringBuilder AsHtml(SocialLibBookChapter chapterResponse, IEnumerable contents) - { - var sb = new StringBuilder(); - - foreach (var content in contents) - { - if (RecursiveTag.TryGetValue(content.Type, out var tag)) - { - sb.Append(AsHtml(chapterResponse, content.Content).ToString().CoverTag(tag)); - continue; - } - - if (InlineTag.TryGetValue(content.Type, out tag)) - { - sb.Append(tag); - continue; - } - - switch (content.Type) - { - case "text": - { - var text = content.Text.HtmlEncode(); - - foreach (var mark in content.Marks) - if (MarkTag.TryGetValue(mark.Type, out tag)) - text = text.CoverTag(tag); - else - Config.Logger.LogInformation($"Неизвестый тип форматирования {mark.Type}"); - - sb.Append(text); - continue; - } - case "image": - { - if (content.Attrs.TryGetValue("images", out var images)) - foreach (var image in images.Deserialize[]>()) - { - if (!image.TryGetValue("image", out var imageId)) continue; - - var attachment = chapterResponse.Attachments.FirstOrDefault(a => a.Name == imageId); - if (attachment is null) continue; - - sb.Append($""); - } - - continue; - } - default: - Config.Logger.LogInformation($"Неизвестый тип {content.Type}"); - break; - } - } - - return sb; - } - - protected override HtmlDocument ResponseToHtmlDoc(SocialLibBookChapter chapterResponse) - { - return chapterResponse.Content switch - { - JsonValue e => e.GetValue().AsHtmlDoc(), - JsonObject o => AsHtml(chapterResponse, o.Deserialize().Content).AsHtmlDoc(), - _ => throw new Elib2EbookParseException("Неизвестный тип") - }; - } -} diff --git a/Core/Logic/Getters/RanobeNovelsGetter.cs b/Core/Logic/Getters/RanobeNovelsGetter.cs deleted file mode 100644 index 1b203eb7..00000000 --- a/Core/Logic/Getters/RanobeNovelsGetter.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Json; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.WuxiaWorld; -using HtmlAgilityPack; -using HtmlAgilityPack.CssSelectors.NetCore; -using Microsoft.Extensions.Logging; - -namespace Core.Logic.Getters; - -public class RanobeNovelsGetter(BookGetterConfig config) : GetterBase(config) -{ - protected override Uri SystemUrl => new("https://ranobe-novels.ru/"); - - public override async Task Get(Uri url) - { - var bookId = GetId(url); - url = SystemUrl.MakeRelativeUri($"/ranobe/{bookId}/"); - var doc = await GetSafety(url); - - var book = new Book(url) - { - Cover = await GetCover(doc, url), - Chapters = await FillChapters(doc, url), - Title = doc.GetTextBySelector("h1"), - Author = new Author("ranobe-novels") - }; - - return book; - } - - private async Task> FillChapters(HtmlDocument doc, Uri url) - { - var result = new List(); - if (Config.Options.NoChapters) return result; - - var toc = await GetToc(doc); - if (toc.Count == 0) - { - doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/{GetId(url)}")); - toc = await GetToc(doc); - } - - foreach (var ranobeChapter in toc) - { - Config.Logger.LogInformation($"Загружаю главу {ranobeChapter.Title.CoverQuotes()}"); - var chapter = new Chapter(); - var chapterDoc = await GetChapter(ranobeChapter.Url); - chapter.Images = await GetImages(chapterDoc, url); - chapter.Content = chapterDoc.DocumentNode.InnerHtml; - chapter.Title = ranobeChapter.Title; - - result.Add(chapter); - } - - return result; - } - - private async Task GetChapter(Uri url) - { - var doc = await GetSafety(url); - return doc.QuerySelector("div.js-full-content").RemoveNodes("> :not(p)").InnerHtml.AsHtmlDoc(); - } - - private async Task> GetToc(HtmlDocument doc) - { - var catId = Regex.Match(doc.ParsedText, "data-cat=\"(?\\d+)\"").Groups["catId"].Value; - var result = new List(); - - foreach (var span in doc.QuerySelectorAll("ul.myUL span.caret[data-id]")) - { - var offset = (int.Parse(span.Attributes["data-id"].Value) - 1) * 100; - var payload = new FormUrlEncodedContent(new Dictionary - { - ["cat_id"] = catId, - ["offset"] = offset.ToString() - }); - - var post = await Config.Client.PostWithTriesAsync( - SystemUrl.MakeRelativeUri("/wp-content/themes/ranobe-novels/template-parts/post/menu-query.php"), - payload); - var toc = await post.Content.ReadFromJsonAsync(); - result.AddRange(toc.Select((a, i) => - new UrlChapter(SystemUrl.MakeRelativeUri(a.PostName), $"Глава {offset + i + 1}"))); - } - - return SliceToc(result, c => c.Title).ToList(); - } - - private Task GetCover(HtmlDocument doc, Uri uri) - { - var imagePath = doc.QuerySelector("meta[property=og:image]")?.Attributes["content"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); - } - - private async Task GetSafety(Uri url) - { - var response = await Config.Client.GetWithTriesAsync(url); - await Task.Delay(TimeSpan.FromSeconds(1)); - - while (response is null || response.StatusCode == HttpStatusCode.ServiceUnavailable) - { - Config.Logger.LogInformation("Получен бан от системы. Жду..."); - var errorTimeout = TimeSpan.FromSeconds(30); - response = await Config.Client.GetWithTriesAsync(url, errorTimeout); - await Task.Delay(errorTimeout); - } - - return await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc()); - } -} diff --git a/Core/Logic/Getters/Rulate/ErolateGetter.cs b/Core/Logic/Getters/Rulate/ErolateGetter.cs deleted file mode 100644 index 1461164f..00000000 --- a/Core/Logic/Getters/Rulate/ErolateGetter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using Core.Configs; - -namespace Core.Logic.Getters.Rulate; - -public class ErolateGetter(BookGetterConfig config) : RulateGetterBase(config) -{ - protected override Uri SystemUrl => new("https://erolate.com/"); - protected override string Mature => AppSecrets.ErolateMature; -} diff --git a/Core/Logic/Getters/Rulate/MlateGetter.cs b/Core/Logic/Getters/Rulate/MlateGetter.cs deleted file mode 100644 index 07880a68..00000000 --- a/Core/Logic/Getters/Rulate/MlateGetter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using Core.Configs; - -namespace Core.Logic.Getters.Rulate; - -public class MlateGetter(BookGetterConfig config) : RulateGetterBase(config) -{ - protected override Uri SystemUrl => new("https://mlate.ru/"); - protected override string Mature => AppSecrets.RulateMature; -} diff --git a/Core/Logic/Getters/Rulate/RulateGetter.cs b/Core/Logic/Getters/Rulate/RulateGetter.cs deleted file mode 100644 index ee499a49..00000000 --- a/Core/Logic/Getters/Rulate/RulateGetter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using Core.Configs; - -namespace Core.Logic.Getters.Rulate; - -public class RulateGetter(BookGetterConfig config) : RulateGetterBase(config) -{ - protected override Uri SystemUrl => new("https://tl.rulate.ru"); - protected override string Mature => AppSecrets.RulateMature; -} diff --git a/Core/Misc/GetterProvider.cs b/Core/Misc/GetterProvider.cs deleted file mode 100644 index f3bc546e..00000000 --- a/Core/Misc/GetterProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using Core.Configs; -using Core.Logic.Getters; - -namespace Core.Misc; - -public static class GetterProvider -{ - public static GetterBase Get(BookGetterConfig config, Uri url) - { - return Assembly.GetAssembly(typeof(GetterBase))!.GetTypes() - .Where(myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf(typeof(GetterBase))) - .Select(type => (GetterBase)Activator.CreateInstance(type, config)) - .FirstOrDefault(g => g!.IsSameUrl(url)) ?? - throw new ArgumentException($"Сайт {url.Host} не поддерживается"); - } -} diff --git a/Core/Types/AuthorToday/AuthorTodayBookDetails.cs b/Core/Types/AuthorToday/AuthorTodayBookDetails.cs deleted file mode 100644 index 03b27897..00000000 --- a/Core/Types/AuthorToday/AuthorTodayBookDetails.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.AuthorToday; - -public class AuthorTodayBookDetails -{ - [JsonPropertyName("annotation")] public string Annotation { get; set; } - - [JsonPropertyName("seriesId")] public long? SeriesId { get; set; } - - [JsonPropertyName("seriesWorkNumber")] public long? SeriesWorkNumber { get; set; } - - [JsonPropertyName("seriesTitle")] public string SeriesTitle { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("authorFIO")] public string AuthorFio { get; set; } - - [JsonPropertyName("coAuthorFIO")] public string CoAuthorFio { get; set; } - - [JsonPropertyName("coverUrl")] public string CoverUrl { get; set; } - - [JsonPropertyName("chapters")] public AuthorTodayChapter[] Chapters { get; set; } - - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("authorUserName")] public string AuthorUserName { get; set; } - - [JsonPropertyName("CoAuthorUserName")] public string CoAuthorUserName { get; set; } - - [JsonPropertyName("galleryImages")] public AuthorTodayImage[] GalleryImages { get; set; } -} diff --git a/Core/Types/AuthorToday/AuthorTodayChapter.cs b/Core/Types/AuthorToday/AuthorTodayChapter.cs deleted file mode 100644 index 33fb74c4..00000000 --- a/Core/Types/AuthorToday/AuthorTodayChapter.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json.Serialization; - -namespace Core.Types.AuthorToday; - -public class AuthorTodayChapter -{ - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("sortOrder")] public long SortOrder { get; set; } - - [JsonPropertyName("text")] public string Text { get; set; } - - [JsonPropertyName("key")] public string Key { get; set; } - - [JsonPropertyName("IsSuccessful")] public bool IsSuccessful { get; set; } - - [JsonPropertyName("IsDraft")] public bool IsDraft { get; set; } - - [JsonPropertyName("isAvailable")] public bool isAvailable { get; set; } - - [JsonPropertyName("code")] public string Code { get; set; } - - public string Decode(string userId, string cert) - { - var secret = - $"{string.Concat(Key.Reverse())}:{(string.IsNullOrWhiteSpace(userId) ? "Guest" : userId)}:{Core.Configs.AppSecrets.AuthorTodaySalt}:{cert}"; - var hashSecret = Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(secret))); - - using var aes = Aes.Create(); - const int IV_SHIFT = Core.Configs.AppSecrets.AesIvShift; - - var aesKey = Encoding.UTF8.GetBytes(hashSecret)[..IV_SHIFT]; - - aes.Key = aesKey; - aes.IV = aesKey; - - using var ms = new MemoryStream(Convert.FromBase64String(Text)); - using var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read); - - var output = new MemoryStream(); - cs.CopyTo(output); - - return Encoding.UTF8.GetString(output.ToArray()); - } -} diff --git a/Core/Types/AuthorToday/AuthorTodayImage.cs b/Core/Types/AuthorToday/AuthorTodayImage.cs deleted file mode 100644 index a2c27ece..00000000 --- a/Core/Types/AuthorToday/AuthorTodayImage.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.AuthorToday; - -public class AuthorTodayImage -{ - [JsonPropertyName("url")] public string Url { get; set; } -} diff --git a/Core/Types/AuthorToday/AuthorTodayUser.cs b/Core/Types/AuthorToday/AuthorTodayUser.cs deleted file mode 100644 index 0c88c898..00000000 --- a/Core/Types/AuthorToday/AuthorTodayUser.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.AuthorToday; - -public class AuthorTodayUser -{ - [JsonPropertyName("id")] public long Id { get; set; } -} diff --git a/Core/Types/Book/Chapter.cs b/Core/Types/Book/Chapter.cs deleted file mode 100644 index aa941d1a..00000000 --- a/Core/Types/Book/Chapter.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using Core.Types.Common; - -namespace Core.Types.Book; - -public class Chapter -{ - /// - /// Название части - /// - public string Title { get; set; } - - /// - /// Контент части - /// - public string Content { get; set; } - - /// - /// Номер тома - /// - public string VolumeNumber { get; set; } - - /// - /// Номер главы - /// - public string ChapterNumber { get; set; } - - /// - /// Изображения из части - /// - public IEnumerable Images { get; set; } = new List(); - - /// - /// Валидна ли часть - /// - public bool IsValid => !string.IsNullOrWhiteSpace(Content); -} diff --git a/Core/Types/BookYandex/BooksYandexAudioBitrate.cs b/Core/Types/BookYandex/BooksYandexAudioBitrate.cs deleted file mode 100644 index 0dd88efc..00000000 --- a/Core/Types/BookYandex/BooksYandexAudioBitrate.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public class BooksYandexAudioBitrate -{ - [JsonPropertyName("url")] public string Url { get; set; } -} diff --git a/Core/Types/BookYandex/BooksYandexAudioOffline.cs b/Core/Types/BookYandex/BooksYandexAudioOffline.cs deleted file mode 100644 index 1d08394b..00000000 --- a/Core/Types/BookYandex/BooksYandexAudioOffline.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public class BooksYandexAudioOffline -{ - [JsonPropertyName("max_bit_rate")] public BooksYandexAudioBitrate Max { get; set; } - - [JsonPropertyName("min_bit_rate")] public BooksYandexAudioBitrate Min { get; set; } -} diff --git a/Core/Types/BookYandex/BooksYandexAudioTrack.cs b/Core/Types/BookYandex/BooksYandexAudioTrack.cs deleted file mode 100644 index 9c3bf6de..00000000 --- a/Core/Types/BookYandex/BooksYandexAudioTrack.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public class BooksYandexAudioTrack -{ - [JsonPropertyName("offline")] public BooksYandexAudioOffline Offline { get; set; } -} diff --git a/Core/Types/BookYandex/BooksYandexAuthor.cs b/Core/Types/BookYandex/BooksYandexAuthor.cs deleted file mode 100644 index fdb745d9..00000000 --- a/Core/Types/BookYandex/BooksYandexAuthor.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public class BooksYandexAuthor -{ - [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("uuid")] public string Uuid { get; set; } -} diff --git a/Core/Types/BookYandex/BooksYandexBook.cs b/Core/Types/BookYandex/BooksYandexBook.cs deleted file mode 100644 index e48d2617..00000000 --- a/Core/Types/BookYandex/BooksYandexBook.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Linq; -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public abstract class BookmateBookBase -{ - [JsonPropertyName("uuid")] public string UUID { get; set; } - - [JsonPropertyName("annotation")] public string Annotation { get; set; } - - [JsonPropertyName("cover")] public BooksYandexCover Cover { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("language")] public string Language { get; set; } - - [JsonPropertyName("linked_book_uuids")] - public string[] LinkedBooks { get; set; } - - [JsonPropertyName("linked_audiobook_uuids")] - public string[] LinkedAudio { get; set; } - - [JsonPropertyName("source_type")] public string SourceType { get; set; } - - public abstract BooksYandexAuthor GetAuthor(); -} - -public class BooksYandexBook : BookmateBookBase -{ - [JsonPropertyName("authors_objects")] public BooksYandexAuthor[] AuthorsObjects { get; set; } - - public override BooksYandexAuthor GetAuthor() - { - return AuthorsObjects?.FirstOrDefault(); - } -} - -public class BookmateComic : BookmateBookBase -{ - [JsonPropertyName("authors")] public BooksYandexAuthor[] Authors { get; set; } - - public override BooksYandexAuthor GetAuthor() - { - return Authors?.FirstOrDefault(); - } -} diff --git a/Core/Types/BookYandex/BooksYandexComicContent.cs b/Core/Types/BookYandex/BooksYandexComicContent.cs deleted file mode 100644 index cfb5126d..00000000 --- a/Core/Types/BookYandex/BooksYandexComicContent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public class BooksYandexComicContent -{ - [JsonPropertyName("uris")] public BooksYandexUri Uri { get; set; } -} diff --git a/Core/Types/BookYandex/BooksYandexComicMetadata.cs b/Core/Types/BookYandex/BooksYandexComicMetadata.cs deleted file mode 100644 index ca5ee961..00000000 --- a/Core/Types/BookYandex/BooksYandexComicMetadata.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public class BooksYandexComicMetadata -{ - [JsonPropertyName("pages")] public BooksYandexComicPage[] Pages { get; set; } -} diff --git a/Core/Types/BookYandex/BooksYandexComicPage.cs b/Core/Types/BookYandex/BooksYandexComicPage.cs deleted file mode 100644 index 797c0c78..00000000 --- a/Core/Types/BookYandex/BooksYandexComicPage.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public class BooksYandexComicPage -{ - [JsonPropertyName("content")] public BooksYandexComicContent Content { get; set; } -} diff --git a/Core/Types/BookYandex/BooksYandexCover.cs b/Core/Types/BookYandex/BooksYandexCover.cs deleted file mode 100644 index 5cf57455..00000000 --- a/Core/Types/BookYandex/BooksYandexCover.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public class BooksYandexCover -{ - [JsonPropertyName("large")] public string Large { get; set; } - - [JsonPropertyName("small")] public string Small { get; set; } -} diff --git a/Core/Types/BookYandex/BooksYandexEpisode.cs b/Core/Types/BookYandex/BooksYandexEpisode.cs deleted file mode 100644 index e56ff4d3..00000000 --- a/Core/Types/BookYandex/BooksYandexEpisode.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public class BooksYandexEpisode -{ - [JsonPropertyName("uuid")] public string Uuid { get; set; } - - [JsonPropertyName("can_be_read")] public bool CanBeRead { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } -} diff --git a/Core/Types/BookYandex/BooksYandexEpisodes.cs b/Core/Types/BookYandex/BooksYandexEpisodes.cs deleted file mode 100644 index 5457bbeb..00000000 --- a/Core/Types/BookYandex/BooksYandexEpisodes.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public class BooksYandexEpisodes -{ - [JsonPropertyName("episodes")] public BooksYandexEpisode[] Episodes { get; set; } -} diff --git a/Core/Types/BookYandex/BooksYandexPlaylist.cs b/Core/Types/BookYandex/BooksYandexPlaylist.cs deleted file mode 100644 index b8c1416c..00000000 --- a/Core/Types/BookYandex/BooksYandexPlaylist.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public class BooksYandexPlaylist -{ - [JsonPropertyName("tracks")] public BooksYandexAudioTrack[] Tracks { get; set; } -} diff --git a/Core/Types/BookYandex/BooksYandexResponse.cs b/Core/Types/BookYandex/BooksYandexResponse.cs deleted file mode 100644 index 4e0f14b3..00000000 --- a/Core/Types/BookYandex/BooksYandexResponse.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public class BooksYandexResponse -{ - [JsonPropertyName("book")] public BooksYandexBook Book { get; set; } - - [JsonPropertyName("audiobook")] public BooksYandexBook AudioBook { get; set; } - - [JsonPropertyName("comicbook")] public BookmateComic Comicbook { get; set; } -} diff --git a/Core/Types/BookYandex/BooksYandexUri.cs b/Core/Types/BookYandex/BooksYandexUri.cs deleted file mode 100644 index 5ca243d3..00000000 --- a/Core/Types/BookYandex/BooksYandexUri.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.BookYandex; - -public class BooksYandexUri -{ - [JsonPropertyName("Image")] public string Image { get; set; } - - [JsonPropertyName("path")] public string Path { get; set; } -} diff --git a/Core/Types/Bookriver/BookRiverApiResponse.cs b/Core/Types/Bookriver/BookRiverApiResponse.cs deleted file mode 100644 index 7ab44b25..00000000 --- a/Core/Types/Bookriver/BookRiverApiResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Bookriver; - -public class BookRiverApiResponse -{ - [JsonPropertyName("data")] public T Data { get; set; } -} diff --git a/Core/Types/Bookriver/BookRiverAuthResponse.cs b/Core/Types/Bookriver/BookRiverAuthResponse.cs deleted file mode 100644 index 3e0b02e7..00000000 --- a/Core/Types/Bookriver/BookRiverAuthResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Bookriver; - -public class BookRiverAuthResponse -{ - [JsonPropertyName("token")] public string Token { get; set; } -} diff --git a/Core/Types/Bookriver/BookRiverChapter.cs b/Core/Types/Bookriver/BookRiverChapter.cs deleted file mode 100644 index 735617ff..00000000 --- a/Core/Types/Bookriver/BookRiverChapter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Bookriver; - -public class BookRiverChapter -{ - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("name")] public string Name { get; set; } -} diff --git a/Core/Types/Bookriver/BookRiverChapterContent.cs b/Core/Types/Bookriver/BookRiverChapterContent.cs deleted file mode 100644 index 350aae05..00000000 --- a/Core/Types/Bookriver/BookRiverChapterContent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Bookriver; - -public class BookRiverChapterContent -{ - [JsonPropertyName("content")] public string Content { get; set; } -} diff --git a/Core/Types/Bookriver/BookRiverCurrentBook.cs b/Core/Types/Bookriver/BookRiverCurrentBook.cs deleted file mode 100644 index 5c5d27ba..00000000 --- a/Core/Types/Bookriver/BookRiverCurrentBook.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Bookriver; - -public class BookRiverCurrentBook -{ - [JsonPropertyName("id")] public long Id { get; set; } -} diff --git a/Core/Types/Bookstab/BookstabApiResponse.cs b/Core/Types/Bookstab/BookstabApiResponse.cs deleted file mode 100644 index d6c77728..00000000 --- a/Core/Types/Bookstab/BookstabApiResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Bookstab; - -public class BookstabApiResponse -{ - [JsonPropertyName("book")] public BookstabBook Book { get; set; } - - [JsonPropertyName("chapter")] public BookstabChapter Chapter { get; set; } -} diff --git a/Core/Types/Bookstab/BookstabBook.cs b/Core/Types/Bookstab/BookstabBook.cs deleted file mode 100644 index d01cc902..00000000 --- a/Core/Types/Bookstab/BookstabBook.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Bookstab; - -public class BookstabBook -{ - [JsonPropertyName("image")] public string Image { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("excerpt")] public string Excerpt { get; set; } - - [JsonPropertyName("user")] public BooksnabUser User { get; set; } - - [JsonPropertyName("chapters_show")] public BookstabChapter[] ChaptersShow { get; set; } -} diff --git a/Core/Types/Bookstab/BookstabChapter.cs b/Core/Types/Bookstab/BookstabChapter.cs deleted file mode 100644 index 3d455b5f..00000000 --- a/Core/Types/Bookstab/BookstabChapter.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Bookstab; - -public class BookstabChapter -{ - [JsonPropertyName("id")] public int Id { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("body")] public string Body { get; set; } -} diff --git a/Core/Types/Bookstab/BookstabUser.cs b/Core/Types/Bookstab/BookstabUser.cs deleted file mode 100644 index ef62bd8c..00000000 --- a/Core/Types/Bookstab/BookstabUser.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Bookstab; - -public class BooksnabUser -{ - [JsonPropertyName("pseudonym")] public string Pseudonym { get; set; } - - [JsonPropertyName("name")] public string Name { get; set; } -} diff --git a/Core/Types/Boosty/BoostyApiResponse.cs b/Core/Types/Boosty/BoostyApiResponse.cs deleted file mode 100644 index db5f6ba3..00000000 --- a/Core/Types/Boosty/BoostyApiResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Boosty; - -public class BoostyApiResponse -{ - [JsonPropertyName("data")] public T Data { get; set; } - - [JsonPropertyName("extra")] public BoostyExtra Extra { get; set; } -} diff --git a/Core/Types/Boosty/BoostyExtra.cs b/Core/Types/Boosty/BoostyExtra.cs deleted file mode 100644 index d33cee9c..00000000 --- a/Core/Types/Boosty/BoostyExtra.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Boosty; - -public class BoostyExtra -{ - [JsonPropertyName("isLast")] public bool IsLast { get; set; } - - [JsonPropertyName("offset")] public string Offset { get; set; } -} diff --git a/Core/Types/Boosty/BoostyPost.cs b/Core/Types/Boosty/BoostyPost.cs deleted file mode 100644 index df2b8518..00000000 --- a/Core/Types/Boosty/BoostyPost.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Text.Json.Serialization; - -namespace Core.Types.Boosty; - -public class BoostyPost -{ - [JsonPropertyName("id")] public Guid Id { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("hasAccess")] public bool HasAccess { get; set; } -} diff --git a/Core/Types/DarkNovels/DarkNovelsAuthData.cs b/Core/Types/DarkNovels/DarkNovelsAuthData.cs deleted file mode 100644 index fb11e03a..00000000 --- a/Core/Types/DarkNovels/DarkNovelsAuthData.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.DarkNovels; - -public class DarkNovelsAuthResponse -{ - [JsonPropertyName("token")] public DarkNovelsAuthToken Token { get; set; } -} diff --git a/Core/Types/DarkNovels/DarkNovelsAuthToken.cs b/Core/Types/DarkNovels/DarkNovelsAuthToken.cs deleted file mode 100644 index f43ac4ca..00000000 --- a/Core/Types/DarkNovels/DarkNovelsAuthToken.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.DarkNovels; - -public class DarkNovelsAuthToken -{ - [JsonPropertyName("accessToken")] public string AccessToken { get; set; } -} diff --git a/Core/Types/DarkNovels/DarkNovelsChapter.cs b/Core/Types/DarkNovels/DarkNovelsChapter.cs deleted file mode 100644 index ebd9af05..00000000 --- a/Core/Types/DarkNovels/DarkNovelsChapter.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.DarkNovels; - -public class DarkNovelsChapter -{ - [JsonPropertyName("id")] public int Id { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("payed")] public int Payed { get; set; } -} diff --git a/Core/Types/DarkNovels/DarkNovelsData.cs b/Core/Types/DarkNovels/DarkNovelsData.cs deleted file mode 100644 index dc2e59eb..00000000 --- a/Core/Types/DarkNovels/DarkNovelsData.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.DarkNovels; - -public class DarkNovelsData -{ - [JsonPropertyName("status")] public string Status { get; set; } - - [JsonPropertyName("message")] public string Message { get; set; } - - [JsonPropertyName("data")] public T Data { get; set; } -} diff --git a/Core/Types/Dreame/DreameApiResponse.cs b/Core/Types/Dreame/DreameApiResponse.cs deleted file mode 100644 index 92a787b1..00000000 --- a/Core/Types/Dreame/DreameApiResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Dreame; - -public class DreameApiResponse -{ - [JsonPropertyName("data")] public T Data { get; set; } -} diff --git a/Core/Types/Dreame/DreameCatalog.cs b/Core/Types/Dreame/DreameCatalog.cs deleted file mode 100644 index c0f73a84..00000000 --- a/Core/Types/Dreame/DreameCatalog.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Dreame; - -public class DreameCatalog -{ - [JsonPropertyName("pager")] public DreamePager Pager { get; set; } -} diff --git a/Core/Types/Dreame/DreameChapter.cs b/Core/Types/Dreame/DreameChapter.cs deleted file mode 100644 index 06b06dd3..00000000 --- a/Core/Types/Dreame/DreameChapter.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Dreame; - -public class DreameChapter -{ - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("id")] public string Id { get; set; } - - [JsonPropertyName("content")] public string Content { get; set; } -} diff --git a/Core/Types/Dreame/DreameNovel.cs b/Core/Types/Dreame/DreameNovel.cs deleted file mode 100644 index 3062b18e..00000000 --- a/Core/Types/Dreame/DreameNovel.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Dreame; - -public class DreameNovel -{ - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("author_id")] public string AuthorId { get; set; } - - [JsonPropertyName("author_name")] public string AuthorName { get; set; } - - [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("descr")] public string Description { get; set; } - - [JsonPropertyName("language")] public string Language { get; set; } - - [JsonPropertyName("cover_url")] public string Cover { get; set; } -} diff --git a/Core/Types/Dreame/DreamePager.cs b/Core/Types/Dreame/DreamePager.cs deleted file mode 100644 index dfcfccd1..00000000 --- a/Core/Types/Dreame/DreamePager.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Core.Types.Dreame; - -public class DreamePager -{ - [JsonPropertyName("chap_list")] public List ChapterList { get; set; } -} diff --git a/Core/Types/FanFicus/FanFicusApiResponse.cs b/Core/Types/FanFicus/FanFicusApiResponse.cs deleted file mode 100644 index e668d16c..00000000 --- a/Core/Types/FanFicus/FanFicusApiResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.FanFicus; - -public class FanFicusApiResponse -{ - [JsonPropertyName("value")] public T Value { get; set; } -} diff --git a/Core/Types/FanFicus/FanFicusBook.cs b/Core/Types/FanFicus/FanFicusBook.cs deleted file mode 100644 index d2da3f03..00000000 --- a/Core/Types/FanFicus/FanFicusBook.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.FanFicus; - -public class FanFicusBook -{ - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("description")] public string Description { get; set; } - - [JsonPropertyName("creatorId")] public FanFicusCreator[] Creators { get; set; } - - [JsonPropertyName("images")] public FanFicusImage[] Images { get; set; } -} diff --git a/Core/Types/FanFicus/FanFicusChapter.cs b/Core/Types/FanFicus/FanFicusChapter.cs deleted file mode 100644 index ca2c2f78..00000000 --- a/Core/Types/FanFicus/FanFicusChapter.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.FanFicus; - -public class FanFicusChapter -{ - [JsonPropertyName("text")] public string Text { get; set; } -} diff --git a/Core/Types/FanFicus/FanFicusCreator.cs b/Core/Types/FanFicus/FanFicusCreator.cs deleted file mode 100644 index 8ea2962c..00000000 --- a/Core/Types/FanFicus/FanFicusCreator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.FanFicus; - -public class FanFicusCreator -{ - [JsonPropertyName("_id")] public string Id { get; set; } - - [JsonPropertyName("nickName")] public string NickName { get; set; } -} diff --git a/Core/Types/FanFicus/FanFicusImage.cs b/Core/Types/FanFicus/FanFicusImage.cs deleted file mode 100644 index b42d04e2..00000000 --- a/Core/Types/FanFicus/FanFicusImage.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.FanFicus; - -public class FanFicusImage -{ - [JsonPropertyName("url")] public string Url { get; set; } -} diff --git a/Core/Types/FanFicus/FanFicusPart.cs b/Core/Types/FanFicus/FanFicusPart.cs deleted file mode 100644 index e553d17f..00000000 --- a/Core/Types/FanFicus/FanFicusPart.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.FanFicus; - -public class FanFicusPart -{ - [JsonPropertyName("_id")] public string Id { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } -} diff --git a/Core/Types/FanFicus/FanFicusUser.cs b/Core/Types/FanFicus/FanFicusUser.cs deleted file mode 100644 index 8eff5e7e..00000000 --- a/Core/Types/FanFicus/FanFicusUser.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.FanFicus; - -public class FanFicusUser -{ - [JsonPropertyName("token")] public string Token { get; set; } -} diff --git a/Core/Types/Freedlit/FreedlitApiResponse.cs b/Core/Types/Freedlit/FreedlitApiResponse.cs deleted file mode 100644 index 4c849db6..00000000 --- a/Core/Types/Freedlit/FreedlitApiResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Freedlit; - -public class FreedlitApiResponse -{ - [JsonPropertyName("success")] public T Success { get; set; } -} diff --git a/Core/Types/Freedlit/FreedlitApp.cs b/Core/Types/Freedlit/FreedlitApp.cs deleted file mode 100644 index 4ad1add7..00000000 --- a/Core/Types/Freedlit/FreedlitApp.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Freedlit; - -public class FreedlitApp -{ - [JsonPropertyName("props")] public FreedlitProps Props { get; set; } -} diff --git a/Core/Types/Freedlit/FreedlitAuthError.cs b/Core/Types/Freedlit/FreedlitAuthError.cs deleted file mode 100644 index 62d91adc..00000000 --- a/Core/Types/Freedlit/FreedlitAuthError.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Freedlit; - -public class FreedlitAuthError -{ - [JsonPropertyName("email")] public string Email { get; set; } - - [JsonPropertyName("password")] public string[] Password { get; set; } -} diff --git a/Core/Types/Freedlit/FreedlitAuthor.cs b/Core/Types/Freedlit/FreedlitAuthor.cs deleted file mode 100644 index a5316187..00000000 --- a/Core/Types/Freedlit/FreedlitAuthor.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Freedlit; - -public class FreedlitAuthor -{ - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("user_link")] public string UserLink { get; set; } - - [JsonPropertyName("name")] public string Name { get; set; } -} diff --git a/Core/Types/Freedlit/FreedlitBook.cs b/Core/Types/Freedlit/FreedlitBook.cs deleted file mode 100644 index bef06ad7..00000000 --- a/Core/Types/Freedlit/FreedlitBook.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Freedlit; - -public class FreedlitBook -{ - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("cover")] public string Cover { get; set; } - - [JsonPropertyName("annotation")] public string Annotation { get; set; } - - [JsonPropertyName("language")] public string Language { get; set; } - - [JsonPropertyName("main_author")] public FreedlitAuthor MainAuthor { get; set; } -} diff --git a/Core/Types/Freedlit/FreedlitChapter.cs b/Core/Types/Freedlit/FreedlitChapter.cs deleted file mode 100644 index 63c1de48..00000000 --- a/Core/Types/Freedlit/FreedlitChapter.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Freedlit; - -public class FreedlitChapter -{ - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("header")] public string Header { get; set; } - - [JsonPropertyName("content")] public string Content { get; set; } -} diff --git a/Core/Types/Freedlit/FreedlitItemsContent.cs b/Core/Types/Freedlit/FreedlitItemsContent.cs deleted file mode 100644 index df1dfc1b..00000000 --- a/Core/Types/Freedlit/FreedlitItemsContent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Core.Types.Freedlit; - -public class FreedlitItemsContent -{ - [JsonPropertyName("items")] public List Items { get; set; } -} diff --git a/Core/Types/Freedlit/FreedlitProps.cs b/Core/Types/Freedlit/FreedlitProps.cs deleted file mode 100644 index d9a84d5e..00000000 --- a/Core/Types/Freedlit/FreedlitProps.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Freedlit; - -public class FreedlitProps -{ - [JsonPropertyName("book")] public FreedlitBook Book { get; set; } - - [JsonPropertyName("errors")] public TErr Errors { get; set; } -} diff --git a/Core/Types/Freedlit/FreedlitSuccessResponse.cs b/Core/Types/Freedlit/FreedlitSuccessResponse.cs deleted file mode 100644 index a6a458e4..00000000 --- a/Core/Types/Freedlit/FreedlitSuccessResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Core.Types.Freedlit; - -public class FreedlitSuccessResponse -{ - [JsonPropertyName("items")] public List Items { get; set; } -} diff --git a/Core/Types/HotNovelPub/HotNovelPubApiResponse.cs b/Core/Types/HotNovelPub/HotNovelPubApiResponse.cs deleted file mode 100644 index 1c14bb4e..00000000 --- a/Core/Types/HotNovelPub/HotNovelPubApiResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.HotNovelPub; - -public class HotNovelPubApiResponse -{ - [JsonPropertyName("data")] public T Data { get; set; } -} diff --git a/Core/Types/HotNovelPub/HotNovelPubAuthorize.cs b/Core/Types/HotNovelPub/HotNovelPubAuthorize.cs deleted file mode 100644 index 19df3a4b..00000000 --- a/Core/Types/HotNovelPub/HotNovelPubAuthorize.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.HotNovelPub; - -public class HotNovelPubAuthorize -{ - [JsonPropertyName("id")] public int Id { get; set; } - - [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("slug")] public string Slug { get; set; } -} diff --git a/Core/Types/HotNovelPub/HotNovelPubBook.cs b/Core/Types/HotNovelPub/HotNovelPubBook.cs deleted file mode 100644 index f26d2765..00000000 --- a/Core/Types/HotNovelPub/HotNovelPubBook.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.HotNovelPub; - -public class HotNovelPubBook -{ - [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("description")] public string Description { get; set; } - - [JsonPropertyName("authorize")] public HotNovelPubAuthorize Authorize { get; set; } - - [JsonPropertyName("image")] public string Image { get; set; } -} diff --git a/Core/Types/HotNovelPub/HotNovelPubBookResponse.cs b/Core/Types/HotNovelPub/HotNovelPubBookResponse.cs deleted file mode 100644 index f543efe0..00000000 --- a/Core/Types/HotNovelPub/HotNovelPubBookResponse.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Core.Types.HotNovelPub; - -public class HotNovelPubBookResponse -{ - [JsonPropertyName("book")] public HotNovelPubBook Book { get; set; } - - [JsonPropertyName("chapters")] public List Chapters { get; set; } -} diff --git a/Core/Types/HotNovelPub/HotNovelPubChapter.cs b/Core/Types/HotNovelPub/HotNovelPubChapter.cs deleted file mode 100644 index 211e4bc9..00000000 --- a/Core/Types/HotNovelPub/HotNovelPubChapter.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.HotNovelPub; - -public class HotNovelPubChapter -{ - [JsonPropertyName("id")] public int Id { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("slug")] public string Slug { get; set; } -} diff --git a/Core/Types/Librebook/LibrebookAuthResponse.cs b/Core/Types/Librebook/LibrebookAuthResponse.cs deleted file mode 100644 index f4984ede..00000000 --- a/Core/Types/Librebook/LibrebookAuthResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Librebook; - -public class LibrebookAuthResponse -{ - [JsonPropertyName("type")] public string Type { get; set; } - - [JsonPropertyName("text")] public string Text { get; set; } -} diff --git a/Core/Types/Litexit/LitexitChapter.cs b/Core/Types/Litexit/LitexitChapter.cs deleted file mode 100644 index 9d38d3b3..00000000 --- a/Core/Types/Litexit/LitexitChapter.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litexit; - -public class LitexitChapter -{ - [JsonPropertyName("id")] public int Id { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("text")] public string Text { get; set; } -} diff --git a/Core/Types/Litexit/LitexitUser.cs b/Core/Types/Litexit/LitexitUser.cs deleted file mode 100644 index b4a4706f..00000000 --- a/Core/Types/Litexit/LitexitUser.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litexit; - -public class LitexitUser -{ - [JsonPropertyName("id")] public int Id { get; set; } -} diff --git a/Core/Types/Litgorod/LitgorodAuthResponse.cs b/Core/Types/Litgorod/LitgorodAuthResponse.cs deleted file mode 100644 index 9be9dabc..00000000 --- a/Core/Types/Litgorod/LitgorodAuthResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litgorod; - -public class LitgorodAuthResponse -{ - [JsonPropertyName("message")] public string Message { get; set; } -} diff --git a/Core/Types/Litgorod/LitgorodBookPart.cs b/Core/Types/Litgorod/LitgorodBookPart.cs deleted file mode 100644 index 873c82a1..00000000 --- a/Core/Types/Litgorod/LitgorodBookPart.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litgorod; - -public class LitgorodBookPart -{ - [JsonPropertyName("text")] public string Text { get; set; } -} diff --git a/Core/Types/Litmarket/AuthResponse.cs b/Core/Types/Litmarket/AuthResponse.cs deleted file mode 100644 index ad28974b..00000000 --- a/Core/Types/Litmarket/AuthResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litmarket; - -public class AuthResponse -{ - [JsonPropertyName("success")] public bool Success { get; set; } -} diff --git a/Core/Types/Litmarket/Block.cs b/Core/Types/Litmarket/Block.cs deleted file mode 100644 index 0d07077e..00000000 --- a/Core/Types/Litmarket/Block.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litmarket; - -public class Block -{ - [JsonPropertyName("chunk")] public Chunk Chunk { get; set; } - - [JsonPropertyName("index")] public int Index { get; set; } -} diff --git a/Core/Types/Litmarket/Chunk.cs b/Core/Types/Litmarket/Chunk.cs deleted file mode 100644 index 0b825cbd..00000000 --- a/Core/Types/Litmarket/Chunk.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litmarket; - -public class Chunk -{ - [JsonPropertyName("mods")] public Mod[] Mods { get; set; } -} diff --git a/Core/Types/Litmarket/LBook.cs b/Core/Types/Litmarket/LBook.cs deleted file mode 100644 index 689f5f57..00000000 --- a/Core/Types/Litmarket/LBook.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litmarket; - -public class LBook -{ - [JsonPropertyName("ebookId")] public int EbookId { get; set; } -} diff --git a/Core/Types/Litmarket/Mod.cs b/Core/Types/Litmarket/Mod.cs deleted file mode 100644 index 29572b1b..00000000 --- a/Core/Types/Litmarket/Mod.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Core.Types.Litmarket; - -public class Mod -{ - [JsonPropertyName("type")] public string Type { get; set; } - - [JsonPropertyName("styles")] public string[] Styles { get; set; } - - [JsonPropertyName("text")] public string Text { get; set; } - - [JsonPropertyName("data")] public JsonElement Data { get; set; } - - [JsonPropertyName("mods")] public Mod[] Mods { get; set; } -} diff --git a/Core/Types/Litmarket/Response.cs b/Core/Types/Litmarket/Response.cs deleted file mode 100644 index fe518b08..00000000 --- a/Core/Types/Litmarket/Response.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litmarket; - -public class Response -{ - [JsonPropertyName("book")] public LBook Book { get; set; } - - [JsonPropertyName("tableOfContent")] public string Toc { get; set; } -} diff --git a/Core/Types/Litnet/LitnetAuthResponse.cs b/Core/Types/Litnet/LitnetAuthResponse.cs deleted file mode 100644 index f6ce395a..00000000 --- a/Core/Types/Litnet/LitnetAuthResponse.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litnet; - -public class LitnetAuthResponse -{ - [JsonPropertyName("error")] public string Error { get; set; } - - [JsonPropertyName("token")] public string Token { get; set; } - - [JsonPropertyName("device_id")] public string DeviceID { get; set; } -} diff --git a/Core/Types/Litnet/LitnetBookResponse.cs b/Core/Types/Litnet/LitnetBookResponse.cs deleted file mode 100644 index b341a2f5..00000000 --- a/Core/Types/Litnet/LitnetBookResponse.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litnet; - -public class LitnetBookResponse -{ - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("author_name")] public string AuthorName { get; set; } - - [JsonPropertyName("cover")] public string Cover { get; set; } - - [JsonPropertyName("annotation")] public string Annotation { get; set; } - - [JsonPropertyName("url")] public string Url { get; set; } - - [JsonPropertyName("author_id")] public long AuthorId { get; set; } - - [JsonPropertyName("lang")] public string Lang { get; set; } - - [JsonPropertyName("cycle_priority")] public int? CyclePriority { get; set; } - - [JsonPropertyName("adult_only")] public bool AdultOnly { get; set; } - - [JsonPropertyName("co_author_name")] public string CoAuthorName { get; set; } - - [JsonPropertyName("co_author")] public long? CoAuthorId { get; set; } -} diff --git a/Core/Types/Litnet/LitnetChapterResponse.cs b/Core/Types/Litnet/LitnetChapterResponse.cs deleted file mode 100644 index d88052db..00000000 --- a/Core/Types/Litnet/LitnetChapterResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litnet; - -public class LitnetChapterResponse -{ - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("text")] public string Text { get; set; } -} diff --git a/Core/Types/Litnet/LitnetContentsResponse.cs b/Core/Types/Litnet/LitnetContentsResponse.cs deleted file mode 100644 index d7e381f8..00000000 --- a/Core/Types/Litnet/LitnetContentsResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litnet; - -public class LitnetContentsResponse -{ - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } -} diff --git a/Core/Types/Litres/LitresPayload.cs b/Core/Types/Litres/LitresPayload.cs deleted file mode 100644 index 21492a00..00000000 --- a/Core/Types/Litres/LitresPayload.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json.Serialization; - -namespace Core.Types.Litres; - -public class LitresPayload -{ - [JsonPropertyName("sha")] public string Sha { get; set; } - - [JsonPropertyName("sid")] public string Sid { get; set; } - - [JsonPropertyName("uilang")] public string UiLang { get; set; } = "rus"; - - [JsonPropertyName("time")] public string Time { get; set; } - - [JsonPropertyName("mobile_app")] public string App { get; set; } - - [JsonPropertyName("requests")] public List Requests { get; set; } = new(); - - public static LitresPayload Create(DateTime date, string sid, string secret, string app) - { - var result = new LitresPayload - { - Time = date.ToString("yyyy-MM-ddTHH:mm:sszzzz"), - App = app, - Sid = sid - }; - - result.Sha = Sha256(result.Time + secret); - - return result; - } - - private static string Sha256(string str) - { - var bytes = Encoding.UTF8.GetBytes(str); - var crypto = SHA256.HashData(bytes); - - return Convert.ToHexString(crypto).ToLowerInvariant(); - } -} diff --git a/Core/Types/Litres/Requests/LitresAuthData.cs b/Core/Types/Litres/Requests/LitresAuthData.cs deleted file mode 100644 index e19fb8eb..00000000 --- a/Core/Types/Litres/Requests/LitresAuthData.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Requests; - -public class LitresAuthData -{ - [JsonPropertyName("login")] public string Login { get; set; } - - [JsonPropertyName("pwd")] public string Password { get; set; } -} diff --git a/Core/Types/Litres/Requests/LitresAuthRequest.cs b/Core/Types/Litres/Requests/LitresAuthRequest.cs deleted file mode 100644 index 2011fe80..00000000 --- a/Core/Types/Litres/Requests/LitresAuthRequest.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Core.Types.Litres.Requests; - -public class LitresAuthRequest : LitresRequestBase -{ - public LitresAuthRequest(string login, string password) - { - Func = "w_create_sid"; - Param.Login = login; - Param.Password = password; - } -} diff --git a/Core/Types/Litres/Requests/LitresBrowseArtsData.cs b/Core/Types/Litres/Requests/LitresBrowseArtsData.cs deleted file mode 100644 index e4540326..00000000 --- a/Core/Types/Litres/Requests/LitresBrowseArtsData.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Requests; - -public class LitresBrowseArtsData -{ - [JsonPropertyName("currency")] public string Currency { get; set; } - - [JsonPropertyName("anno")] public string Anno { get; set; } - - [JsonPropertyName("id")] public string[] Id { get; set; } -} diff --git a/Core/Types/Litres/Requests/LitresBrowseArtsRequest.cs b/Core/Types/Litres/Requests/LitresBrowseArtsRequest.cs deleted file mode 100644 index 2827d6ff..00000000 --- a/Core/Types/Litres/Requests/LitresBrowseArtsRequest.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Core.Types.Litres.Requests; - -public class LitresBrowseArtsRequest : LitresRequestBase -{ - public LitresBrowseArtsRequest(string[] id) - { - Func = "r_browse_arts"; - Param.Currency = "RUB"; - Param.Anno = "1"; - Param.Id = id; - } -} diff --git a/Core/Types/Litres/Requests/LitresRequestBase.cs b/Core/Types/Litres/Requests/LitresRequestBase.cs deleted file mode 100644 index 81ec054c..00000000 --- a/Core/Types/Litres/Requests/LitresRequestBase.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Requests; - -public abstract class LitresRequestBase where T : new() -{ - [JsonPropertyName("id")] public string Id { get; set; } = "data"; - - [JsonPropertyName("func")] public string Func { get; set; } - - [JsonPropertyName("param")] public T Param { get; } = new(); -} diff --git a/Core/Types/Litres/Response/LitresArt.cs b/Core/Types/Litres/Response/LitresArt.cs deleted file mode 100644 index d9c51e47..00000000 --- a/Core/Types/Litres/Response/LitresArt.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Response; - -public class LitresArt -{ - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("html_annotation")] public string Annotation { get; set; } - - [JsonPropertyName("persons")] public LitresPerson[] Persons { get; set; } = []; - - [JsonPropertyName("cover_url")] public string Cover { get; set; } - - [JsonPropertyName("series")] public LitresSeria[] Sequences { get; set; } = []; - - [JsonPropertyName("linked_arts")] public LitresArt[] LinkedArts { get; set; } - - [JsonPropertyName("art_type")] public LitresArtTypeEnum ArtType { get; set; } -} diff --git a/Core/Types/Litres/Response/LitresArtTypeEnum.cs b/Core/Types/Litres/Response/LitresArtTypeEnum.cs deleted file mode 100644 index ea5ddd0b..00000000 --- a/Core/Types/Litres/Response/LitresArtTypeEnum.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Core.Types.Litres.Response; - -public enum LitresArtTypeEnum -{ - Text = 0, - Audio = 1, - Pdf = 4 -} diff --git a/Core/Types/Litres/Response/LitresAuthResponseData.cs b/Core/Types/Litres/Response/LitresAuthResponseData.cs deleted file mode 100644 index 77c9ef5f..00000000 --- a/Core/Types/Litres/Response/LitresAuthResponseData.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Response; - -public class LitresAuthResponseData -{ - [JsonPropertyName("success")] public bool Success { get; set; } - - [JsonPropertyName("sid")] public string Sid { get; set; } - - [JsonPropertyName("error_message")] public string ErrorMessage { get; set; } -} diff --git a/Core/Types/Litres/Response/LitresFile.cs b/Core/Types/Litres/Response/LitresFile.cs deleted file mode 100644 index 7f0c54c5..00000000 --- a/Core/Types/Litres/Response/LitresFile.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Response; - -public class LitresFile -{ - [JsonPropertyName("extension")] public string Extension { get; set; } - - [JsonPropertyName("id")] public long Id { get; set; } -} diff --git a/Core/Types/Litres/Response/LitresFiles.cs b/Core/Types/Litres/Response/LitresFiles.cs deleted file mode 100644 index 97b99f60..00000000 --- a/Core/Types/Litres/Response/LitresFiles.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Response; - -public class LitresFiles -{ - [JsonPropertyName("files")] public LitresFile[] Files { get; set; } -} diff --git a/Core/Types/Litres/Response/LitresPartnerSubscriptions.cs b/Core/Types/Litres/Response/LitresPartnerSubscriptions.cs deleted file mode 100644 index 1d665297..00000000 --- a/Core/Types/Litres/Response/LitresPartnerSubscriptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Response; - -public class LitresPartnerSubscriptions -{ - [JsonPropertyName("subscriptions")] public LitresSubscription[] Subscriptions { get; set; } -} diff --git a/Core/Types/Litres/Response/LitresPerson.cs b/Core/Types/Litres/Response/LitresPerson.cs deleted file mode 100644 index 48aa9fdd..00000000 --- a/Core/Types/Litres/Response/LitresPerson.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Response; - -public class LitresPerson -{ - [JsonPropertyName("id")] public TId Id { get; set; } - - [JsonPropertyName("full_name")] public string FullName { get; set; } - - [JsonPropertyName("role")] public string Role { get; set; } - - [JsonPropertyName("url")] public string Url { get; set; } -} diff --git a/Core/Types/Litres/Response/LitresResponse.cs b/Core/Types/Litres/Response/LitresResponse.cs deleted file mode 100644 index e837d3ad..00000000 --- a/Core/Types/Litres/Response/LitresResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Response; - -public class LitresResponse -{ - [JsonPropertyName("data")] public T Data { get; set; } -} diff --git a/Core/Types/Litres/Response/LitresSeria.cs b/Core/Types/Litres/Response/LitresSeria.cs deleted file mode 100644 index 81e1a82c..00000000 --- a/Core/Types/Litres/Response/LitresSeria.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Response; - -public class LitresSeria -{ - [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("sequence_number")] public string SequenceNumber { get; set; } -} diff --git a/Core/Types/Litres/Response/LitresStaticResponse.cs b/Core/Types/Litres/Response/LitresStaticResponse.cs deleted file mode 100644 index bebc55b9..00000000 --- a/Core/Types/Litres/Response/LitresStaticResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Response; - -public class LitresStaticResponse -{ - [JsonPropertyName("payload")] public LitresResponse Payload { get; set; } -} diff --git a/Core/Types/Litres/Response/LitresSubscription.cs b/Core/Types/Litres/Response/LitresSubscription.cs deleted file mode 100644 index c1a4894a..00000000 --- a/Core/Types/Litres/Response/LitresSubscription.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Response; - -public class LitresSubscription -{ - [JsonPropertyName("is_active")] public bool IsActive { get; set; } - - [JsonPropertyName("host")] public string Host { get; set; } - - [JsonPropertyName("type")] public string Type { get; set; } -} diff --git a/Core/Types/Litres/Response/LitresWebAuthResponse.cs b/Core/Types/Litres/Response/LitresWebAuthResponse.cs deleted file mode 100644 index 60c2194c..00000000 --- a/Core/Types/Litres/Response/LitresWebAuthResponse.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litres.Response; - -public class LitresWebAuthPayloadData -{ - [JsonPropertyName("sid")] public string Sid { get; set; } -} - -public class LitresWebAuthPayload -{ - [JsonPropertyName("data")] public LitresWebAuthPayloadData Data { get; set; } -} - -public class LitresWebAuthResponse -{ - [JsonPropertyName("status")] public int Status { get; set; } - - [JsonPropertyName("payload")] public LitresWebAuthPayload Payload { get; set; } - - [JsonPropertyName("error")] public string Error { get; set; } = ""; -} diff --git a/Core/Types/Litsovet/LitsovetLoginResponse.cs b/Core/Types/Litsovet/LitsovetLoginResponse.cs deleted file mode 100644 index 9ce32185..00000000 --- a/Core/Types/Litsovet/LitsovetLoginResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Litsovet; - -public class LitsovetLoginResponse -{ - [JsonPropertyName("ok")] public int Ok { get; set; } -} diff --git a/Core/Types/MangaLib/MangaLibPg.cs b/Core/Types/MangaLib/MangaLibPg.cs deleted file mode 100644 index 7bf9f03c..00000000 --- a/Core/Types/MangaLib/MangaLibPg.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.MangaLib; - -public class MangaLibPg -{ - [JsonPropertyName("p")] public int P { get; set; } - - [JsonPropertyName("u")] public string U { get; set; } -} diff --git a/Core/Types/MyBook/MyBookAuth.cs b/Core/Types/MyBook/MyBookAuth.cs deleted file mode 100644 index 6c193e58..00000000 --- a/Core/Types/MyBook/MyBookAuth.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.MyBook; - -public class MyBookAuth -{ - [JsonPropertyName("token")] public string Token { get; set; } - - [JsonPropertyName("secret")] public string Secret { get; set; } -} diff --git a/Core/Types/MyBook/MyBookAuthor.cs b/Core/Types/MyBook/MyBookAuthor.cs deleted file mode 100644 index 8fcae065..00000000 --- a/Core/Types/MyBook/MyBookAuthor.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.MyBook; - -public class MyBookAuthor -{ - [JsonPropertyName("cover_name")] public string Name { get; set; } - - [JsonPropertyName("absolute_url")] public string Url { get; set; } -} diff --git a/Core/Types/MyBook/MyBookBook.cs b/Core/Types/MyBook/MyBookBook.cs deleted file mode 100644 index edd5eb80..00000000 --- a/Core/Types/MyBook/MyBookBook.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace Core.Types.MyBook; - -public class MyBookBook -{ - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("annotation")] public string Annotation { get; set; } - - [JsonPropertyName("bookfile")] public string BookFile { get; set; } - - [JsonPropertyName("default_cover")] public string Cover { get; set; } - - [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("authors")] public MyBookAuthor[] Authors { get; set; } - - [JsonPropertyName("type")] public string Type { get; set; } - - [JsonPropertyName("mapfiles")] public MyBookMapFile[] MapFiles { get; set; } - - [JsonPropertyName("connected_book")] public MyBookBook Connected { get; set; } - - [JsonPropertyName("bookfiles")] public JsonNode[] Files { get; set; } -} diff --git a/Core/Types/MyBook/MyBookFile.cs b/Core/Types/MyBook/MyBookFile.cs deleted file mode 100644 index ddc72f88..00000000 --- a/Core/Types/MyBook/MyBookFile.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.MyBook; - -public class MyBookFile -{ - [JsonPropertyName("url")] public string Url { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("order")] public int Order { get; set; } -} diff --git a/Core/Types/MyBook/MyBookMapFile.cs b/Core/Types/MyBook/MyBookMapFile.cs deleted file mode 100644 index 2c4c6656..00000000 --- a/Core/Types/MyBook/MyBookMapFile.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.MyBook; - -public class MyBookMapFile -{ - [JsonPropertyName("book")] public long Book { get; set; } -} diff --git a/Core/Types/Neobook/NeobookAttachment.cs b/Core/Types/Neobook/NeobookAttachment.cs deleted file mode 100644 index 8cbcc332..00000000 --- a/Core/Types/Neobook/NeobookAttachment.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Core.Types.Neobook; - -public class NeobookAttachment -{ - [JsonPropertyName("image")] public Dictionary Cover { get; set; } -} diff --git a/Core/Types/Neobook/NeobookAuth.cs b/Core/Types/Neobook/NeobookAuth.cs deleted file mode 100644 index aa425870..00000000 --- a/Core/Types/Neobook/NeobookAuth.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Neobook; - -public class NeobookAuth -{ - [JsonPropertyName("error")] public string Error { get; set; } - - [JsonPropertyName("login")] public NeobookLogin Login { get; set; } -} diff --git a/Core/Types/Neobook/NeobookBook.cs b/Core/Types/Neobook/NeobookBook.cs deleted file mode 100644 index dbe5a164..00000000 --- a/Core/Types/Neobook/NeobookBook.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Neobook; - -public class NeobookBook -{ - [JsonPropertyName("chapters")] public NeobookChapter[] Chapters { get; set; } - - [JsonPropertyName("active_chapter_index")] - public int ActiveChapterIndex { get; set; } -} diff --git a/Core/Types/Neobook/NeobookChapter.cs b/Core/Types/Neobook/NeobookChapter.cs deleted file mode 100644 index 9e391174..00000000 --- a/Core/Types/Neobook/NeobookChapter.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Neobook; - -public class NeobookChapter -{ - [JsonPropertyName("data")] public NeobookChapterData Data { get; set; } -} diff --git a/Core/Types/Neobook/NeobookChapterData.cs b/Core/Types/Neobook/NeobookChapterData.cs deleted file mode 100644 index 85e39cdf..00000000 --- a/Core/Types/Neobook/NeobookChapterData.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Neobook; - -public class NeobookChapterData -{ - [JsonPropertyName("html")] public string Html { get; set; } -} diff --git a/Core/Types/Neobook/NeobookLogin.cs b/Core/Types/Neobook/NeobookLogin.cs deleted file mode 100644 index 1d2eaffb..00000000 --- a/Core/Types/Neobook/NeobookLogin.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Neobook; - -public class NeobookLogin -{ - [JsonPropertyName("utoken")] public string Utoken { get; set; } - - [JsonPropertyName("uid")] public string Uid { get; set; } -} diff --git a/Core/Types/Neobook/NeobookPostData.cs b/Core/Types/Neobook/NeobookPostData.cs deleted file mode 100644 index fb6c85f3..00000000 --- a/Core/Types/Neobook/NeobookPostData.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Neobook; - -public class NeobookPostData -{ - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("text")] public string Text { get; set; } - - [JsonPropertyName("user")] public NeobookUser User { get; set; } - - [JsonPropertyName("chapters")] public NeobookTocChapter[] Chapters { get; set; } - - [JsonPropertyName("attachment")] public NeobookAttachment Attachment { get; set; } -} diff --git a/Core/Types/Neobook/NeobookTocChapter.cs b/Core/Types/Neobook/NeobookTocChapter.cs deleted file mode 100644 index e1540f65..00000000 --- a/Core/Types/Neobook/NeobookTocChapter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Neobook; - -public class NeobookTocChapter -{ - [JsonPropertyName("token")] public string Token { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } -} diff --git a/Core/Types/Neobook/NeobookUser.cs b/Core/Types/Neobook/NeobookUser.cs deleted file mode 100644 index 3aacfef7..00000000 --- a/Core/Types/Neobook/NeobookUser.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Neobook; - -public class NeobookUser -{ - [JsonPropertyName("username")] public string UserName { get; set; } - - [JsonPropertyName("firstname")] public string FirstName { get; set; } - - [JsonPropertyName("lastname")] public string LastName { get; set; } -} diff --git a/Core/Types/Ranobe/RanobeBook.cs b/Core/Types/Ranobe/RanobeBook.cs deleted file mode 100644 index a4946dad..00000000 --- a/Core/Types/Ranobe/RanobeBook.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Core.Types.Ranobe; - -public class RanobeBook -{ - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("author")] public string Author { get; set; } - - [JsonPropertyName("verticalImages")] public RanobeImage[] Images { get; set; } - - [JsonPropertyName("verticalImage")] public RanobeImage Image { get; set; } - - [JsonPropertyName("chapters")] public List Chapters { get; set; } -} diff --git a/Core/Types/Ranobe/RanobeChapter.cs b/Core/Types/Ranobe/RanobeChapter.cs deleted file mode 100644 index a2eb7fd4..00000000 --- a/Core/Types/Ranobe/RanobeChapter.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Ranobe; - -public class RanobeChapter -{ - [JsonPropertyName("content")] public RanobeChapterContent Content { get; set; } -} diff --git a/Core/Types/Ranobe/RanobeChapterContent.cs b/Core/Types/Ranobe/RanobeChapterContent.cs deleted file mode 100644 index ae84c90c..00000000 --- a/Core/Types/Ranobe/RanobeChapterContent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Ranobe; - -public class RanobeChapterContent -{ - [JsonPropertyName("text")] public string Text { get; set; } -} diff --git a/Core/Types/Ranobe/RanobeChapterShort.cs b/Core/Types/Ranobe/RanobeChapterShort.cs deleted file mode 100644 index eec594f9..00000000 --- a/Core/Types/Ranobe/RanobeChapterShort.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Ranobe; - -public class RanobeChapterShort -{ - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("url")] public string Url { get; set; } -} diff --git a/Core/Types/Ranobe/RanobeImage.cs b/Core/Types/Ranobe/RanobeImage.cs deleted file mode 100644 index 6f1e00c7..00000000 --- a/Core/Types/Ranobe/RanobeImage.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Ranobe; - -public class RanobeImage -{ - [JsonPropertyName("height")] public int Height { get; set; } - - [JsonPropertyName("url")] public string Url { get; set; } -} diff --git a/Core/Types/RanobeHub/RanobeHubApiResponse.cs b/Core/Types/RanobeHub/RanobeHubApiResponse.cs deleted file mode 100644 index 7f834b20..00000000 --- a/Core/Types/RanobeHub/RanobeHubApiResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.RanobeHub; - -public class RanobeHubApiResponse -{ - [JsonPropertyName("volumes")] public RanobeHubVolume[] Volumes { get; set; } -} diff --git a/Core/Types/RanobeHub/RanobeHubChapter.cs b/Core/Types/RanobeHub/RanobeHubChapter.cs deleted file mode 100644 index c92d9904..00000000 --- a/Core/Types/RanobeHub/RanobeHubChapter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.RanobeHub; - -public class RanobeHubChapter -{ - [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("url")] public string Url { get; set; } -} diff --git a/Core/Types/RanobeHub/RanobeHubVolume.cs b/Core/Types/RanobeHub/RanobeHubVolume.cs deleted file mode 100644 index d2131c9c..00000000 --- a/Core/Types/RanobeHub/RanobeHubVolume.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.RanobeHub; - -public class RanobeHubVolume -{ - [JsonPropertyName("chapters")] public RanobeHubChapter[] Chapters { get; set; } -} diff --git a/Core/Types/RanobeNovels/RanobeNovelsChapter.cs b/Core/Types/RanobeNovels/RanobeNovelsChapter.cs deleted file mode 100644 index f69cb4a5..00000000 --- a/Core/Types/RanobeNovels/RanobeNovelsChapter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.RanobeNovels; - -public class RanobeNovelsChapter -{ - [JsonPropertyName("post_name")] public string Name { get; set; } - - [JsonPropertyName("post_title")] public string Title { get; set; } -} diff --git a/Core/Types/RanobeOvh/RanobeOvhBranch.cs b/Core/Types/RanobeOvh/RanobeOvhBranch.cs deleted file mode 100644 index f9c1a255..00000000 --- a/Core/Types/RanobeOvh/RanobeOvhBranch.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.RanobeOvh; - -public class RanobeOvhBranch -{ - [JsonPropertyName("id")] public string Id { get; set; } - - [JsonPropertyName("translators")] public RanoveOvhTranslator[] Translators { get; set; } - - [JsonPropertyName("chaptersCount")] public long ChaptersCount { get; set; } -} diff --git a/Core/Types/RanobeOvh/RanobeOvhChapterFull.cs b/Core/Types/RanobeOvh/RanobeOvhChapterFull.cs deleted file mode 100644 index 3e4fd86a..00000000 --- a/Core/Types/RanobeOvh/RanobeOvhChapterFull.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace Core.Types.RanobeOvh; - -public class RanobeOvhChapterFull -{ - [JsonPropertyName("pages")] public RanobeOvhPage[] Pages { get; set; } - - [JsonPropertyName("content")] public JsonNode Content { get; set; } -} - -public class RanobeOvhChapterShort -{ - [JsonPropertyName("id")] public string Id { get; set; } - - [JsonPropertyName("number")] public decimal Number { get; set; } - - [JsonPropertyName("volume")] public int? Volume { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("branchId")] public string BranchId { get; set; } - - public string FullName - { - get - { - if (!Volume.HasValue) return Title; - - var shortName = $"Том {Volume}. Глава {(int)Number}"; - return string.IsNullOrWhiteSpace(Title) ? shortName : Title; - } - } -} - -public class RanobeOvhChapterContent -{ - [JsonPropertyName("type")] public string Type { get; set; } - - [JsonPropertyName("text")] public string Text { get; set; } - - [JsonPropertyName("marks")] public RanobeOvhChapterMark[] Marks { get; set; } = []; - - [JsonPropertyName("content")] public RanobeOvhChapterContent[] Content { get; set; } = []; - - [JsonPropertyName("attrs")] public Dictionary Attrs { get; set; } = new(); -} - -public class RanobeOvhChapterMark -{ - [JsonPropertyName("type")] public string Type { get; set; } -} diff --git a/Core/Types/RanobeOvh/RanobeOvhManga.cs b/Core/Types/RanobeOvh/RanobeOvhManga.cs deleted file mode 100644 index dc8013ef..00000000 --- a/Core/Types/RanobeOvh/RanobeOvhManga.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.RanobeOvh; - -public class RanobeOvhManga -{ - [JsonPropertyName("name")] public RanobeOvhMangaName Name { get; set; } - - [JsonPropertyName("poster")] public string Poster { get; set; } - - [JsonPropertyName("description")] public string Description { get; set; } -} diff --git a/Core/Types/RanobeOvh/RanobeOvhMangaName.cs b/Core/Types/RanobeOvh/RanobeOvhMangaName.cs deleted file mode 100644 index 6e3f66f6..00000000 --- a/Core/Types/RanobeOvh/RanobeOvhMangaName.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.RanobeOvh; - -public class RanobeOvhMangaName -{ - [JsonPropertyName("ru")] public string Ru { get; set; } -} diff --git a/Core/Types/RanobeOvh/RanobeOvhPage.cs b/Core/Types/RanobeOvh/RanobeOvhPage.cs deleted file mode 100644 index 4bf91bad..00000000 --- a/Core/Types/RanobeOvh/RanobeOvhPage.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.RanobeOvh; - -public class RanobeOvhPage -{ - [JsonPropertyName("id")] public string Id { get; set; } - - [JsonPropertyName("image")] public string Image { get; set; } -} diff --git a/Core/Types/RanobeOvh/RanoveOvhTranslator.cs b/Core/Types/RanobeOvh/RanoveOvhTranslator.cs deleted file mode 100644 index 4f2c853b..00000000 --- a/Core/Types/RanobeOvh/RanoveOvhTranslator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.RanobeOvh; - -public class RanoveOvhTranslator -{ - [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("slug")] public string Slug { get; set; } -} diff --git a/Core/Types/Ranobes/RanobesCookie.cs b/Core/Types/Ranobes/RanobesCookie.cs deleted file mode 100644 index 1603d3e8..00000000 --- a/Core/Types/Ranobes/RanobesCookie.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Ranobes; - -public class RanobesCookie -{ - [JsonPropertyName("cookie")] public string Cookie { get; set; } -} diff --git a/Core/Types/Renovels/RenovelsApiResponse.cs b/Core/Types/Renovels/RenovelsApiResponse.cs deleted file mode 100644 index 5053e6ec..00000000 --- a/Core/Types/Renovels/RenovelsApiResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Renovels; - -public class RenovelsApiResponse -{ - [JsonPropertyName("msg")] public string Message { get; set; } - - [JsonPropertyName("content")] public T Content { get; set; } -} diff --git a/Core/Types/Renovels/RenovelsAuthResponse.cs b/Core/Types/Renovels/RenovelsAuthResponse.cs deleted file mode 100644 index e5157d59..00000000 --- a/Core/Types/Renovels/RenovelsAuthResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Renovels; - -public class RenovelsAuthResponse -{ - [JsonPropertyName("access_token")] public string Token { get; set; } -} diff --git a/Core/Types/Renovels/RenovelsBranch.cs b/Core/Types/Renovels/RenovelsBranch.cs deleted file mode 100644 index 350b6c3f..00000000 --- a/Core/Types/Renovels/RenovelsBranch.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Renovels; - -public class RenovelsBranch -{ - [JsonPropertyName("id")] public int Id { get; set; } - - [JsonPropertyName("publishers")] public RenovelsPublisher[] Publishers { get; set; } -} diff --git a/Core/Types/Renovels/RenovelsChapter.cs b/Core/Types/Renovels/RenovelsChapter.cs deleted file mode 100644 index a0edf90c..00000000 --- a/Core/Types/Renovels/RenovelsChapter.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace Core.Types.Renovels; - -public class RenovelsChapter -{ - [JsonPropertyName("id")] public int Id { get; set; } - - [JsonPropertyName("chapter")] public string Chapter { get; set; } - - [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("tome")] public int Tome { get; set; } - - [JsonPropertyName("content")] public string Content { get; set; } - - [JsonPropertyName("is_paid")] public bool IsPaid { get; set; } - - [JsonPropertyName("is_bought")] public bool? IsBought { get; set; } - - [JsonPropertyName("pages")] public JsonArray Pages { get; set; } - - public string Title - { - get - { - var name = $"Том {Tome}. Глава {Chapter}"; - if (string.IsNullOrWhiteSpace(Name)) return name; - - return name + $" - {Name}"; - } - } -} diff --git a/Core/Types/Renovels/RenovelsContent.cs b/Core/Types/Renovels/RenovelsContent.cs deleted file mode 100644 index 4b49dbb0..00000000 --- a/Core/Types/Renovels/RenovelsContent.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Core.Types.Renovels; - -public class RenovelsContent -{ - [JsonPropertyName("cover")] public Dictionary Cover { get; set; } - - [JsonPropertyName("main_name")] public string MainName { get; set; } - - [JsonPropertyName("secondary_name")] public string SecondaryName { get; set; } - - [JsonPropertyName("another_name")] public string AnotherName { get; set; } - - [JsonPropertyName("description")] public string Description { get; set; } - - [JsonPropertyName("branches")] public RenovelsBranch[] Branches { get; set; } -} diff --git a/Core/Types/Renovels/RenovelsPage.cs b/Core/Types/Renovels/RenovelsPage.cs deleted file mode 100644 index eaa0c498..00000000 --- a/Core/Types/Renovels/RenovelsPage.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Renovels; - -public class RenovelsPage -{ - [JsonPropertyName("link")] public string Link { get; set; } -} diff --git a/Core/Types/Renovels/RenovelsPublisher.cs b/Core/Types/Renovels/RenovelsPublisher.cs deleted file mode 100644 index dbb794f5..00000000 --- a/Core/Types/Renovels/RenovelsPublisher.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Renovels; - -public class RenovelsPublisher -{ - [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("dir")] public string Dir { get; set; } -} diff --git a/Core/Types/Renovels/RenovelsTocResponse.cs b/Core/Types/Renovels/RenovelsTocResponse.cs deleted file mode 100644 index d3b194f7..00000000 --- a/Core/Types/Renovels/RenovelsTocResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Renovels; - -public class RenovelsTocResponse -{ - [JsonPropertyName("results")] public RenovelsChapter[] Results { get; set; } -} diff --git a/Core/Types/Rulate/RulateAuthResponse.cs b/Core/Types/Rulate/RulateAuthResponse.cs deleted file mode 100644 index 8d1c48c0..00000000 --- a/Core/Types/Rulate/RulateAuthResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Rulate; - -public class RulateAuthResponse -{ - [JsonPropertyName("error")] public string Error { get; set; } -} diff --git a/Core/Types/Rulate/RulateChapter.cs b/Core/Types/Rulate/RulateChapter.cs deleted file mode 100644 index ce64e866..00000000 --- a/Core/Types/Rulate/RulateChapter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Rulate; - -public class RulateChapter -{ - [JsonPropertyName("can_read")] public bool CanRead { get; set; } - - [JsonPropertyName("content")] public string Content { get; set; } -} diff --git a/Core/Types/SocialLib/LibSocialToken.cs b/Core/Types/SocialLib/LibSocialToken.cs deleted file mode 100644 index 01b67789..00000000 --- a/Core/Types/SocialLib/LibSocialToken.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.SocialLib; - -public class LibSocialToken -{ - [JsonPropertyName("access_token")] public string AccessToken { get; set; } -} diff --git a/Core/Types/SocialLib/RanobeLibBookDetail.cs b/Core/Types/SocialLib/RanobeLibBookDetail.cs deleted file mode 100644 index 504a54a0..00000000 --- a/Core/Types/SocialLib/RanobeLibBookDetail.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Core.Types.SocialLib; - -public class RanobeLibBookDetails -{ - [JsonPropertyName("data")] public RLBDData Data { get; set; } -} - -public class RLBDData -{ - public RLBDData(int? id) - { - Id = id; - } - - [JsonPropertyName("id")] public int? Id { get; set; } - - [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("rus_name")] public string RusName { get; set; } - - [JsonPropertyName("slug_url")] public string SlugUrl { get; set; } - - [JsonPropertyName("cover")] public RLBDCover Cover { get; set; } - - [JsonPropertyName("authors")] public List Authors { get; set; } - - [JsonPropertyName("summary")] public string Summary { get; set; } -} - -public class RLBDCover -{ - [JsonPropertyName("default")] public string Default { get; set; } -} - -public class RLBDAuthor -{ - [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("slug_url")] public string SlugUrl { get; set; } -} diff --git a/Core/Types/SocialLib/SocialLibBookChapters.cs b/Core/Types/SocialLib/SocialLibBookChapters.cs deleted file mode 100644 index 37394572..00000000 --- a/Core/Types/SocialLib/SocialLibBookChapters.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace Core.Types.SocialLib; - -public class SocialLibBookChapters -{ - [JsonPropertyName("data")] public List Chapters { get; set; } -} - -public class SocialLibBookChapterResponse -{ - [JsonPropertyName("data")] public SocialLibBookChapter Data { get; set; } -} - -public class SocialLibChapterBranch -{ - [JsonPropertyName("branch_id")] public long? BranchId { get; set; } -} - -public class SocialLibChapterAttachment -{ - [JsonPropertyName("url")] public string Url { get; set; } - - [JsonPropertyName("name")] public string Name { get; set; } -} - -public class SocialLibBookChapter -{ - [JsonPropertyName("volume")] public string Volume { get; set; } - - [JsonPropertyName("number")] public string Number { get; set; } - - [JsonPropertyName("branches")] public List Branches { get; set; } - - [JsonPropertyName("attachments")] public SocialLibChapterAttachment[] Attachments { get; set; } = []; - - [JsonPropertyName("pages")] public SocialLibPage[] Pages { get; set; } = []; - - [JsonPropertyName("content")] public JsonNode Content { get; set; } - - private string RawName { get; set; } - - [JsonPropertyName("name")] - public string Name - { - get - { - var name = $"Том {Volume} Глава {Number}"; - if (!string.IsNullOrWhiteSpace(RawName)) name += $" - {RawName}"; - - return name; - } - set => RawName = value; - } -} - -public class SocialLibChapterContent -{ - [JsonPropertyName("type")] public string Type { get; set; } - - [JsonPropertyName("text")] public string Text { get; set; } - - [JsonPropertyName("marks")] public SocialLibChapterMark[] Marks { get; set; } = []; - - [JsonPropertyName("content")] public SocialLibChapterContent[] Content { get; set; } = []; - - [JsonPropertyName("attrs")] public Dictionary Attrs { get; set; } = new(); -} - -public class SocialLibChapterMark -{ - [JsonPropertyName("type")] public string Type { get; set; } -} - -public class SocialLibPage -{ - [JsonPropertyName("url")] public string Url { get; set; } -} diff --git a/Core/Types/SocialLib/SocialLibChapter.cs b/Core/Types/SocialLib/SocialLibChapter.cs deleted file mode 100644 index 7473fe0a..00000000 --- a/Core/Types/SocialLib/SocialLibChapter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.SocialLib; - -public class SocialLibChapter -{ - [JsonPropertyName("chapter_number")] public string ChapterNumber { get; set; } - - [JsonPropertyName("chapter_volume")] public int ChapterVolume { get; set; } - - [JsonPropertyName("chapter_name")] public string ChapterName { get; set; } - - [JsonPropertyName("chapter_slug")] public string ChapterSlug { get; set; } - - [JsonPropertyName("branch_id")] public int? BranchId { get; set; } - - public string Name => $"Том {ChapterVolume} Глава {ChapterNumber} {ChapterName}".Trim(); -} diff --git a/Core/Types/SocialLib/SocialLibChapterOld.cs b/Core/Types/SocialLib/SocialLibChapterOld.cs deleted file mode 100644 index 039797af..00000000 --- a/Core/Types/SocialLib/SocialLibChapterOld.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.SocialLib; - -public class SocialLibChapterOld -{ - [JsonPropertyName("number")] public string ChapterNumber { get; set; } - - [JsonPropertyName("volume")] public string ChapterVolume { get; set; } - - [JsonPropertyName("name")] public string ChapterName { get; set; } - - [JsonPropertyName("slug")] public string ChapterSlug { get; set; } - - public string GetName() - { - return $"Том {ChapterVolume} Глава {ChapterNumber} {ChapterName}".Trim(); - } -} diff --git a/Core/Types/SocialLib/SocialLibChapters.cs b/Core/Types/SocialLib/SocialLibChapters.cs deleted file mode 100644 index 238c6f6a..00000000 --- a/Core/Types/SocialLib/SocialLibChapters.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Core.Types.SocialLib; - -public class SocialLibChapters -{ - [JsonPropertyName("list")] public List List { get; set; } -} diff --git a/Core/Types/SocialLib/User.cs b/Core/Types/SocialLib/User.cs deleted file mode 100644 index 5cc19a89..00000000 --- a/Core/Types/SocialLib/User.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.SocialLib; - -public class User -{ - [JsonPropertyName("id")] public long Id { get; set; } -} diff --git a/Core/Types/SocialLib/WindowData.cs b/Core/Types/SocialLib/WindowData.cs deleted file mode 100644 index a6cc2dff..00000000 --- a/Core/Types/SocialLib/WindowData.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.SocialLib; - -public class WindowData -{ - [JsonPropertyName("chapters")] public SocialLibChapters Chapters { get; set; } - - [JsonPropertyName("user")] public User User { get; set; } -} diff --git a/Core/Types/StrokiMts/StrokiMtsApiMultiResponse.cs b/Core/Types/StrokiMts/StrokiMtsApiMultiResponse.cs deleted file mode 100644 index 5c146992..00000000 --- a/Core/Types/StrokiMts/StrokiMtsApiMultiResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Core.Types.StrokiMts; - -public class StrokiMtsApiMultiResponse -{ - [JsonPropertyName("items")] public List Items { get; set; } -} diff --git a/Core/Types/StrokiMts/StrokiMtsApiResponse.cs b/Core/Types/StrokiMts/StrokiMtsApiResponse.cs deleted file mode 100644 index 219c82fd..00000000 --- a/Core/Types/StrokiMts/StrokiMtsApiResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.StrokiMts; - -public class StrokiMtsApiResponse -{ - [JsonPropertyName("data")] public T Data { get; set; } -} diff --git a/Core/Types/StrokiMts/StrokiMtsAuthor.cs b/Core/Types/StrokiMts/StrokiMtsAuthor.cs deleted file mode 100644 index 3e91280d..00000000 --- a/Core/Types/StrokiMts/StrokiMtsAuthor.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.StrokiMts; - -public class StrokiMtsAuthor -{ - [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("friendlyUrl")] public string FriendlyUrl { get; set; } -} diff --git a/Core/Types/StrokiMts/StrokiMtsBookItem.cs b/Core/Types/StrokiMts/StrokiMtsBookItem.cs deleted file mode 100644 index 8294daa8..00000000 --- a/Core/Types/StrokiMts/StrokiMtsBookItem.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Core.Types.StrokiMts; - -public class StrokiMtsBookItem -{ - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("annotation")] public string Annotation { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } - - [JsonPropertyName("imageUrl")] public Dictionary ImageUrl { get; set; } - - [JsonPropertyName("authors")] public List Authors { get; set; } - - [JsonPropertyName("contentInfo")] public StrokiMtsContentInfo ContentInfo { get; set; } -} diff --git a/Core/Types/StrokiMts/StrokiMtsContentInfo.cs b/Core/Types/StrokiMts/StrokiMtsContentInfo.cs deleted file mode 100644 index b838d12f..00000000 --- a/Core/Types/StrokiMts/StrokiMtsContentInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.StrokiMts; - -public class StrokiMtsContentInfo -{ - [JsonPropertyName("fileType")] public string FileType { get; set; } -} diff --git a/Core/Types/StrokiMts/StrokiMtsFile.cs b/Core/Types/StrokiMts/StrokiMtsFile.cs deleted file mode 100644 index f2da4ce2..00000000 --- a/Core/Types/StrokiMts/StrokiMtsFile.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.StrokiMts; - -public class StrokiMtsFile -{ - [JsonPropertyName("fileId")] public long FileId { get; set; } -} diff --git a/Core/Types/StrokiMts/StrokiMtsFileUrl.cs b/Core/Types/StrokiMts/StrokiMtsFileUrl.cs deleted file mode 100644 index b111fca1..00000000 --- a/Core/Types/StrokiMts/StrokiMtsFileUrl.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.StrokiMts; - -public class StrokiMtsFileUrl -{ - [JsonPropertyName("url")] public string Url { get; set; } -} diff --git a/Core/Types/StrokiMts/StrokiMtsFiles.cs b/Core/Types/StrokiMts/StrokiMtsFiles.cs deleted file mode 100644 index 01fa6236..00000000 --- a/Core/Types/StrokiMts/StrokiMtsFiles.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.StrokiMts; - -public class StrokiMtsFiles -{ - [JsonPropertyName("preview")] public StrokiMtsFile Preview { get; set; } - - [JsonPropertyName("full")] public StrokiMtsFile[] Full { get; set; } -} diff --git a/Core/Types/StrokiMts/StrokiMtsMultiItem.cs b/Core/Types/StrokiMts/StrokiMtsMultiItem.cs deleted file mode 100644 index 5d2502a8..00000000 --- a/Core/Types/StrokiMts/StrokiMtsMultiItem.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.StrokiMts; - -public class StrokiMtsMultiItem -{ - [JsonPropertyName("textBook")] public StrokiMtsBookItem TextBook { get; set; } - - [JsonPropertyName("audioBook")] public StrokiMtsBookItem AudioBook { get; set; } - - [JsonPropertyName("comics")] public StrokiMtsBookItem ComicBook { get; set; } - - [JsonPropertyName("pressa")] public StrokiMtsBookItem PressBook { get; set; } -} diff --git a/Core/Types/Wattpad/WattpadInfo.cs b/Core/Types/Wattpad/WattpadInfo.cs deleted file mode 100644 index 1ae50bf8..00000000 --- a/Core/Types/Wattpad/WattpadInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Wattpad; - -public class WattpadInfo -{ - [JsonPropertyName("user")] public WattpadUser User { get; set; } - - [JsonPropertyName("cover")] public string Cover { get; set; } - - [JsonPropertyName("parts")] public WattpadPart[] Parts { get; set; } - - [JsonPropertyName("description")] public string Description { get; set; } - - [JsonPropertyName("title")] public string Title { get; set; } -} diff --git a/Core/Types/Wattpad/WattpadMeta.cs b/Core/Types/Wattpad/WattpadMeta.cs deleted file mode 100644 index 13f106f6..00000000 --- a/Core/Types/Wattpad/WattpadMeta.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Core.Types.Wattpad; - -public class WattpadMeta -{ - public string StoryId { get; set; } -} diff --git a/Core/Types/Wattpad/WattpadPart.cs b/Core/Types/Wattpad/WattpadPart.cs deleted file mode 100644 index 1143d8e7..00000000 --- a/Core/Types/Wattpad/WattpadPart.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Text.Json.Serialization; -using Core.Extensions; - -namespace Core.Types.Wattpad; - -public class WattpadPart -{ - [JsonPropertyName("id")] public long Id { get; set; } - - [JsonPropertyName("title")] public string Title { private get; set; } - - public string FullName => Title.Replace("\r", " ").CollapseWhitespace().Trim(); - - public Uri Url => new($"https://www.wattpad.com/apiv2/storytext?id={Id}"); -} diff --git a/Core/Types/Wattpad/WattpadUser.cs b/Core/Types/Wattpad/WattpadUser.cs deleted file mode 100644 index f5990de3..00000000 --- a/Core/Types/Wattpad/WattpadUser.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.Wattpad; - -public class WattpadUser -{ - [JsonPropertyName("name")] public string Name { get; set; } -} diff --git a/Core/Types/WuxiaWorld/WuxiaWorldToc.cs b/Core/Types/WuxiaWorld/WuxiaWorldToc.cs deleted file mode 100644 index c814037c..00000000 --- a/Core/Types/WuxiaWorld/WuxiaWorldToc.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Core.Types.WuxiaWorld; - -public class WuxiaWorldToc -{ - [JsonPropertyName("post_name")] public string PostName { get; set; } -} diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..7df3bb1c --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,9 @@ + + + net10.0 + disable + enable + latestmajor + 2.9.1 + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..a5056fbf --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,27 @@ + + + true + false + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Elib2Ebook.sln b/Elib2Ebook.sln index 134c4c5b..43d01ad4 100644 --- a/Elib2Ebook.sln +++ b/Elib2Ebook.sln @@ -1,28 +1,1067 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{3576A033-7429-4DE3-BBE3-CD703A4564ED}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2EbookWeb", "Elib2EbookWeb\Elib2EbookWeb.csproj", "{C3CCD71A-37D0-4665-A628-AC6185313A4C}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{D12CC161-970E-46FB-8C3E-DE2A78304283}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + Directory.Packages.props = Directory.Packages.props + EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2EbookCli", "Elib2EbookCli\Elib2EbookCli.csproj", "{04A5E9A6-9EA9-4771-9766-1241F8341A9B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2EbookWeb", "src\Elib2EbookWeb\Elib2EbookWeb.csproj", "{C3CCD71A-37D0-4665-A628-AC6185313A4C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2EbookCli", "src\Elib2EbookCli\Elib2EbookCli.csproj", "{04A5E9A6-9EA9-4771-9766-1241F8341A9B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0C88DD14-F956-CE84-757C-A364CCF449FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.DomainServices.Tests", "tests\Elib2Ebook.DomainServices.Tests\Elib2Ebook.DomainServices.Tests.csproj", "{97B6CA29-9704-4A44-8879-E7015403E743}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExternalServices", "ExternalServices", "{EC9257F5-F59F-4F7C-B316-A153DCB9501C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Acomics", "src\ExternalServices\Elib2Ebook.ExternalServices.Acomics\Elib2Ebook.ExternalServices.Acomics.csproj", "{4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.AuthorToday", "src\ExternalServices\Elib2Ebook.ExternalServices.AuthorToday\Elib2Ebook.ExternalServices.AuthorToday.csproj", "{898F86C5-1886-41BE-97DD-46A182261678}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.BookInBook", "src\ExternalServices\Elib2Ebook.ExternalServices.BookInBook\Elib2Ebook.ExternalServices.BookInBook.csproj", "{88836B48-12C3-4CC7-A73E-0245970589CB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.BookRiver", "src\ExternalServices\Elib2Ebook.ExternalServices.BookRiver\Elib2Ebook.ExternalServices.BookRiver.csproj", "{297D901A-F3D0-4653-B4EB-CBD3D4402612}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Bookinist", "src\ExternalServices\Elib2Ebook.ExternalServices.Bookinist\Elib2Ebook.ExternalServices.Bookinist.csproj", "{82456D25-7753-4D1C-9ADC-450948B7FAC0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.BooksYandex", "src\ExternalServices\Elib2Ebook.ExternalServices.BooksYandex\Elib2Ebook.ExternalServices.BooksYandex.csproj", "{E5408C31-60C9-4A60-89D9-840C8C9E5E2F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Bookstab", "src\ExternalServices\Elib2Ebook.ExternalServices.Bookstab\Elib2Ebook.ExternalServices.Bookstab.csproj", "{79094B35-C3C6-4332-8A79-6E3D32582059}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Bookstime", "src\ExternalServices\Elib2Ebook.ExternalServices.Bookstime\Elib2Ebook.ExternalServices.Bookstime.csproj", "{7311835B-6CE8-470E-AFB4-B76424A86096}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Bookuruk", "src\ExternalServices\Elib2Ebook.ExternalServices.Bookuruk\Elib2Ebook.ExternalServices.Bookuruk.csproj", "{04F673E8-6A3D-4A60-A829-1B851FDB5A4C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Boosty", "src\ExternalServices\Elib2Ebook.ExternalServices.Boosty\Elib2Ebook.ExternalServices.Boosty.csproj", "{1826ECD1-D177-4AEF-B821-8B65535CAA46}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Boovel", "src\ExternalServices\Elib2Ebook.ExternalServices.Boovel\Elib2Ebook.ExternalServices.Boovel.csproj", "{C6D9607E-AB00-4E29-A8A9-8F378B2D755B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.DarkNovels", "src\ExternalServices\Elib2Ebook.ExternalServices.DarkNovels\Elib2Ebook.ExternalServices.DarkNovels.csproj", "{19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Desu", "src\ExternalServices\Elib2Ebook.ExternalServices.Desu\Elib2Ebook.ExternalServices.Desu.csproj", "{745684DE-BABB-4BAA-BC36-DA3029E8C99F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Dreame", "src\ExternalServices\Elib2Ebook.ExternalServices.Dreame\Elib2Ebook.ExternalServices.Dreame.csproj", "{F58C0BE6-D91A-42F7-A221-4A882911E61C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.FanFicus", "src\ExternalServices\Elib2Ebook.ExternalServices.FanFicus\Elib2Ebook.ExternalServices.FanFicus.csproj", "{5F1D96A7-8252-4D60-AE6E-A66C045A8A92}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Fb2Top", "src\ExternalServices\Elib2Ebook.ExternalServices.Fb2Top\Elib2Ebook.ExternalServices.Fb2Top.csproj", "{AEDD4AE8-A831-46EE-AF49-EAC3183D5971}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Ficbook", "src\ExternalServices\Elib2Ebook.ExternalServices.Ficbook\Elib2Ebook.ExternalServices.Ficbook.csproj", "{F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.FictionBook", "src\ExternalServices\Elib2Ebook.ExternalServices.FictionBook\Elib2Ebook.ExternalServices.FictionBook.csproj", "{D509243A-E5AA-430D-B47E-8ADC5DA31125}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Freedlit", "src\ExternalServices\Elib2Ebook.ExternalServices.Freedlit\Elib2Ebook.ExternalServices.Freedlit.csproj", "{7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Freedom", "src\ExternalServices\Elib2Ebook.ExternalServices.Freedom\Elib2Ebook.ExternalServices.Freedom.csproj", "{443B5C89-0904-4AE8-B48F-1E92709FBEF3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.HogwartsNet", "src\ExternalServices\Elib2Ebook.ExternalServices.HogwartsNet\Elib2Ebook.ExternalServices.HogwartsNet.csproj", "{48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.HotNovelPub", "src\ExternalServices\Elib2Ebook.ExternalServices.HotNovelPub\Elib2Ebook.ExternalServices.HotNovelPub.csproj", "{EF1817C6-1FEB-4185-94F3-6BDB9ED1660A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.HubBook", "src\ExternalServices\Elib2Ebook.ExternalServices.HubBook\Elib2Ebook.ExternalServices.HubBook.csproj", "{1EF56286-3151-4D13-B662-C23CBE7148FE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.IGram", "src\ExternalServices\Elib2Ebook.ExternalServices.IGram\Elib2Ebook.ExternalServices.IGram.csproj", "{5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Jaomix", "src\ExternalServices\Elib2Ebook.ExternalServices.Jaomix\Elib2Ebook.ExternalServices.Jaomix.csproj", "{F743054C-0D7F-4352-9DBD-365C4CFA19C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.LibSocial", "src\ExternalServices\Elib2Ebook.ExternalServices.LibSocial\Elib2Ebook.ExternalServices.LibSocial.csproj", "{509EA8AB-5717-4953-87FC-F3EDD041F200}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Libbox", "src\ExternalServices\Elib2Ebook.ExternalServices.Libbox\Elib2Ebook.ExternalServices.Libbox.csproj", "{A9677606-014A-43A4-83D1-7871D1547862}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Libking", "src\ExternalServices\Elib2Ebook.ExternalServices.Libking\Elib2Ebook.ExternalServices.Libking.csproj", "{42B441C3-43AD-4D8F-96E0-547D7257FB04}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Librebook", "src\ExternalServices\Elib2Ebook.ExternalServices.Librebook\Elib2Ebook.ExternalServices.Librebook.csproj", "{DD4CFE16-3F58-4AA0-A995-312AFC55F0B3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Libst", "src\ExternalServices\Elib2Ebook.ExternalServices.Libst\Elib2Ebook.ExternalServices.Libst.csproj", "{CC31B699-7B15-4685-8E00-01E2DE6CA88B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.LitLife", "src\ExternalServices\Elib2Ebook.ExternalServices.LitLife\Elib2Ebook.ExternalServices.LitLife.csproj", "{ED59B9E2-82EC-4984-9A27-E9DFF3B13B60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Litgorod", "src\ExternalServices\Elib2Ebook.ExternalServices.Litgorod\Elib2Ebook.ExternalServices.Litgorod.csproj", "{6FC8F397-8F3D-4300-A8B3-6F76B68C959B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Litmarket", "src\ExternalServices\Elib2Ebook.ExternalServices.Litmarket\Elib2Ebook.ExternalServices.Litmarket.csproj", "{7B9E64D1-70C9-4462-BE0B-A965A2825919}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Litmir", "src\ExternalServices\Elib2Ebook.ExternalServices.Litmir\Elib2Ebook.ExternalServices.Litmir.csproj", "{82D6EBAF-B0B6-4744-A556-E5667DF823B7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Litnet", "src\ExternalServices\Elib2Ebook.ExternalServices.Litnet\Elib2Ebook.ExternalServices.Litnet.csproj", "{410366DF-4121-44DE-923D-218ACBA2F8CC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Litres", "src\ExternalServices\Elib2Ebook.ExternalServices.Litres\Elib2Ebook.ExternalServices.Litres.csproj", "{A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Litsovet", "src\ExternalServices\Elib2Ebook.ExternalServices.Litsovet\Elib2Ebook.ExternalServices.Litsovet.csproj", "{656422FD-47DA-4F06-AB76-57FF1F77B040}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.MangaMammy", "src\ExternalServices\Elib2Ebook.ExternalServices.MangaMammy\Elib2Ebook.ExternalServices.MangaMammy.csproj", "{502B8111-F428-4584-AE44-D1AD3B4F16B5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.MirKnig", "src\ExternalServices\Elib2Ebook.ExternalServices.MirKnig\Elib2Ebook.ExternalServices.MirKnig.csproj", "{174CF08D-E13D-4904-90C0-CC740C9DE086}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.MyBook", "src\ExternalServices\Elib2Ebook.ExternalServices.MyBook\Elib2Ebook.ExternalServices.MyBook.csproj", "{853E3B54-DB8A-4A82-8565-E6103130DA3C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Neobook", "src\ExternalServices\Elib2Ebook.ExternalServices.Neobook\Elib2Ebook.ExternalServices.Neobook.csproj", "{B8DC7FD1-782F-4246-B096-836D82B29A03}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.NovelTranslate", "src\ExternalServices\Elib2Ebook.ExternalServices.NovelTranslate\Elib2Ebook.ExternalServices.NovelTranslate.csproj", "{8AE239F7-A0B1-459A-AF6F-FB1501544C2B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Novelhall", "src\ExternalServices\Elib2Ebook.ExternalServices.Novelhall\Elib2Ebook.ExternalServices.Novelhall.csproj", "{FB7DB850-11DB-4F7B-9003-13C145442C73}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Novelxo", "src\ExternalServices\Elib2Ebook.ExternalServices.Novelxo\Elib2Ebook.ExternalServices.Novelxo.csproj", "{23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.OnlineKnigi", "src\ExternalServices\Elib2Ebook.ExternalServices.OnlineKnigi\Elib2Ebook.ExternalServices.OnlineKnigi.csproj", "{DF2456D1-0CFF-4213-9E8F-D666DAEA2E54}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Prodaman", "src\ExternalServices\Elib2Ebook.ExternalServices.Prodaman\Elib2Ebook.ExternalServices.Prodaman.csproj", "{1980F468-08B9-4159-B63D-4197EE6FC83B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Ranobe", "src\ExternalServices\Elib2Ebook.ExternalServices.Ranobe\Elib2Ebook.ExternalServices.Ranobe.csproj", "{957FCED2-6B1E-490E-97B4-2B123E565785}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.RanobeHub", "src\ExternalServices\Elib2Ebook.ExternalServices.RanobeHub\Elib2Ebook.ExternalServices.RanobeHub.csproj", "{C67F2919-4530-4EAF-B1A1-CCED621BCCFE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.RanobeOvh", "src\ExternalServices\Elib2Ebook.ExternalServices.RanobeOvh\Elib2Ebook.ExternalServices.RanobeOvh.csproj", "{8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.RanobesCom", "src\ExternalServices\Elib2Ebook.ExternalServices.RanobesCom\Elib2Ebook.ExternalServices.RanobesCom.csproj", "{035C2D5E-18DD-4CFC-98E2-0A8BF69A1511}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.ReadManga", "src\ExternalServices\Elib2Ebook.ExternalServices.ReadManga\Elib2Ebook.ExternalServices.ReadManga.csproj", "{2FAEDBBC-6F75-46AC-A281-F507412EDF6A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Readli", "src\ExternalServices\Elib2Ebook.ExternalServices.Readli\Elib2Ebook.ExternalServices.Readli.csproj", "{F00538BE-2AF8-4388-BF7C-59934838B7BD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Renovels", "src\ExternalServices\Elib2Ebook.ExternalServices.Renovels\Elib2Ebook.ExternalServices.Renovels.csproj", "{06A50D0C-EAF4-4599-83A9-B23C0756ADF5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Romfant", "src\ExternalServices\Elib2Ebook.ExternalServices.Romfant\Elib2Ebook.ExternalServices.Romfant.csproj", "{86DEDD76-E85D-44C9-BDF6-7D3299F0AED7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.RoyalRoad", "src\ExternalServices\Elib2Ebook.ExternalServices.RoyalRoad\Elib2Ebook.ExternalServices.RoyalRoad.csproj", "{994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Rulate", "src\ExternalServices\Elib2Ebook.ExternalServices.Rulate\Elib2Ebook.ExternalServices.Rulate.csproj", "{8F625A63-FFCD-4F8F-8C5C-97252728AA8B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Samlib", "src\ExternalServices\Elib2Ebook.ExternalServices.Samlib\Elib2Ebook.ExternalServices.Samlib.csproj", "{BC735304-7981-4854-A9EE-00FD55943BF4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.StrokiMts", "src\ExternalServices\Elib2Ebook.ExternalServices.StrokiMts\Elib2Ebook.ExternalServices.StrokiMts.csproj", "{EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.TopLiba", "src\ExternalServices\Elib2Ebook.ExternalServices.TopLiba\Elib2Ebook.ExternalServices.TopLiba.csproj", "{E6717F55-79BC-494A-84B1-215491824F20}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.TwilightRussia", "src\ExternalServices\Elib2Ebook.ExternalServices.TwilightRussia\Elib2Ebook.ExternalServices.TwilightRussia.csproj", "{7CA07B2A-F612-47F0-8F4F-E7771BF9DED1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.WattpadCom", "src\ExternalServices\Elib2Ebook.ExternalServices.WattpadCom\Elib2Ebook.ExternalServices.WattpadCom.csproj", "{FA121F56-04BB-46A1-B8C1-0C282295584B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.WuxiaWorld", "src\ExternalServices\Elib2Ebook.ExternalServices.WuxiaWorld\Elib2Ebook.ExternalServices.WuxiaWorld.csproj", "{3C0A98D5-DAFF-4FF1-9A59-F7409CB66754}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.Younettranslate", "src\ExternalServices\Elib2Ebook.ExternalServices.Younettranslate\Elib2Ebook.ExternalServices.Younettranslate.csproj", "{46B6E125-FF47-4453-9172-5DF695687FDF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.Domain", "src\Elib2Ebook.Domain\Elib2Ebook.Domain.csproj", "{07A39741-8656-488A-964C-D8387A1925DF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.DomainServices", "src\Elib2Ebook.DomainServices\Elib2Ebook.DomainServices.csproj", "{76924193-4339-4BC8-8C0B-CA5F0A5047FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elib2Ebook.ExternalServices.WattpadRu", "src\ExternalServices\Elib2Ebook.ExternalServices.WattpadRu\Elib2Ebook.ExternalServices.WattpadRu.csproj", "{8042BF60-B374-4C40-BB55-7405736A711A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {3576A033-7429-4DE3-BBE3-CD703A4564ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3576A033-7429-4DE3-BBE3-CD703A4564ED}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3576A033-7429-4DE3-BBE3-CD703A4564ED}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3576A033-7429-4DE3-BBE3-CD703A4564ED}.Release|Any CPU.Build.0 = Release|Any CPU {C3CCD71A-37D0-4665-A628-AC6185313A4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C3CCD71A-37D0-4665-A628-AC6185313A4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3CCD71A-37D0-4665-A628-AC6185313A4C}.Debug|x64.ActiveCfg = Debug|Any CPU + {C3CCD71A-37D0-4665-A628-AC6185313A4C}.Debug|x64.Build.0 = Debug|Any CPU + {C3CCD71A-37D0-4665-A628-AC6185313A4C}.Debug|x86.ActiveCfg = Debug|Any CPU + {C3CCD71A-37D0-4665-A628-AC6185313A4C}.Debug|x86.Build.0 = Debug|Any CPU {C3CCD71A-37D0-4665-A628-AC6185313A4C}.Release|Any CPU.ActiveCfg = Release|Any CPU {C3CCD71A-37D0-4665-A628-AC6185313A4C}.Release|Any CPU.Build.0 = Release|Any CPU + {C3CCD71A-37D0-4665-A628-AC6185313A4C}.Release|x64.ActiveCfg = Release|Any CPU + {C3CCD71A-37D0-4665-A628-AC6185313A4C}.Release|x64.Build.0 = Release|Any CPU + {C3CCD71A-37D0-4665-A628-AC6185313A4C}.Release|x86.ActiveCfg = Release|Any CPU + {C3CCD71A-37D0-4665-A628-AC6185313A4C}.Release|x86.Build.0 = Release|Any CPU {04A5E9A6-9EA9-4771-9766-1241F8341A9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {04A5E9A6-9EA9-4771-9766-1241F8341A9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04A5E9A6-9EA9-4771-9766-1241F8341A9B}.Debug|x64.ActiveCfg = Debug|Any CPU + {04A5E9A6-9EA9-4771-9766-1241F8341A9B}.Debug|x64.Build.0 = Debug|Any CPU + {04A5E9A6-9EA9-4771-9766-1241F8341A9B}.Debug|x86.ActiveCfg = Debug|Any CPU + {04A5E9A6-9EA9-4771-9766-1241F8341A9B}.Debug|x86.Build.0 = Debug|Any CPU {04A5E9A6-9EA9-4771-9766-1241F8341A9B}.Release|Any CPU.ActiveCfg = Release|Any CPU {04A5E9A6-9EA9-4771-9766-1241F8341A9B}.Release|Any CPU.Build.0 = Release|Any CPU + {04A5E9A6-9EA9-4771-9766-1241F8341A9B}.Release|x64.ActiveCfg = Release|Any CPU + {04A5E9A6-9EA9-4771-9766-1241F8341A9B}.Release|x64.Build.0 = Release|Any CPU + {04A5E9A6-9EA9-4771-9766-1241F8341A9B}.Release|x86.ActiveCfg = Release|Any CPU + {04A5E9A6-9EA9-4771-9766-1241F8341A9B}.Release|x86.Build.0 = Release|Any CPU + {97B6CA29-9704-4A44-8879-E7015403E743}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97B6CA29-9704-4A44-8879-E7015403E743}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97B6CA29-9704-4A44-8879-E7015403E743}.Debug|x64.ActiveCfg = Debug|Any CPU + {97B6CA29-9704-4A44-8879-E7015403E743}.Debug|x64.Build.0 = Debug|Any CPU + {97B6CA29-9704-4A44-8879-E7015403E743}.Debug|x86.ActiveCfg = Debug|Any CPU + {97B6CA29-9704-4A44-8879-E7015403E743}.Debug|x86.Build.0 = Debug|Any CPU + {97B6CA29-9704-4A44-8879-E7015403E743}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97B6CA29-9704-4A44-8879-E7015403E743}.Release|Any CPU.Build.0 = Release|Any CPU + {97B6CA29-9704-4A44-8879-E7015403E743}.Release|x64.ActiveCfg = Release|Any CPU + {97B6CA29-9704-4A44-8879-E7015403E743}.Release|x64.Build.0 = Release|Any CPU + {97B6CA29-9704-4A44-8879-E7015403E743}.Release|x86.ActiveCfg = Release|Any CPU + {97B6CA29-9704-4A44-8879-E7015403E743}.Release|x86.Build.0 = Release|Any CPU + {4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8}.Debug|x64.ActiveCfg = Debug|Any CPU + {4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8}.Debug|x64.Build.0 = Debug|Any CPU + {4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8}.Debug|x86.ActiveCfg = Debug|Any CPU + {4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8}.Debug|x86.Build.0 = Debug|Any CPU + {4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8}.Release|Any CPU.Build.0 = Release|Any CPU + {4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8}.Release|x64.ActiveCfg = Release|Any CPU + {4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8}.Release|x64.Build.0 = Release|Any CPU + {4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8}.Release|x86.ActiveCfg = Release|Any CPU + {4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8}.Release|x86.Build.0 = Release|Any CPU + {898F86C5-1886-41BE-97DD-46A182261678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {898F86C5-1886-41BE-97DD-46A182261678}.Debug|Any CPU.Build.0 = Debug|Any CPU + {898F86C5-1886-41BE-97DD-46A182261678}.Debug|x64.ActiveCfg = Debug|Any CPU + {898F86C5-1886-41BE-97DD-46A182261678}.Debug|x64.Build.0 = Debug|Any CPU + {898F86C5-1886-41BE-97DD-46A182261678}.Debug|x86.ActiveCfg = Debug|Any CPU + {898F86C5-1886-41BE-97DD-46A182261678}.Debug|x86.Build.0 = Debug|Any CPU + {898F86C5-1886-41BE-97DD-46A182261678}.Release|Any CPU.ActiveCfg = Release|Any CPU + {898F86C5-1886-41BE-97DD-46A182261678}.Release|Any CPU.Build.0 = Release|Any CPU + {898F86C5-1886-41BE-97DD-46A182261678}.Release|x64.ActiveCfg = Release|Any CPU + {898F86C5-1886-41BE-97DD-46A182261678}.Release|x64.Build.0 = Release|Any CPU + {898F86C5-1886-41BE-97DD-46A182261678}.Release|x86.ActiveCfg = Release|Any CPU + {898F86C5-1886-41BE-97DD-46A182261678}.Release|x86.Build.0 = Release|Any CPU + {88836B48-12C3-4CC7-A73E-0245970589CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88836B48-12C3-4CC7-A73E-0245970589CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88836B48-12C3-4CC7-A73E-0245970589CB}.Debug|x64.ActiveCfg = Debug|Any CPU + {88836B48-12C3-4CC7-A73E-0245970589CB}.Debug|x64.Build.0 = Debug|Any CPU + {88836B48-12C3-4CC7-A73E-0245970589CB}.Debug|x86.ActiveCfg = Debug|Any CPU + {88836B48-12C3-4CC7-A73E-0245970589CB}.Debug|x86.Build.0 = Debug|Any CPU + {88836B48-12C3-4CC7-A73E-0245970589CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88836B48-12C3-4CC7-A73E-0245970589CB}.Release|Any CPU.Build.0 = Release|Any CPU + {88836B48-12C3-4CC7-A73E-0245970589CB}.Release|x64.ActiveCfg = Release|Any CPU + {88836B48-12C3-4CC7-A73E-0245970589CB}.Release|x64.Build.0 = Release|Any CPU + {88836B48-12C3-4CC7-A73E-0245970589CB}.Release|x86.ActiveCfg = Release|Any CPU + {88836B48-12C3-4CC7-A73E-0245970589CB}.Release|x86.Build.0 = Release|Any CPU + {297D901A-F3D0-4653-B4EB-CBD3D4402612}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {297D901A-F3D0-4653-B4EB-CBD3D4402612}.Debug|Any CPU.Build.0 = Debug|Any CPU + {297D901A-F3D0-4653-B4EB-CBD3D4402612}.Debug|x64.ActiveCfg = Debug|Any CPU + {297D901A-F3D0-4653-B4EB-CBD3D4402612}.Debug|x64.Build.0 = Debug|Any CPU + {297D901A-F3D0-4653-B4EB-CBD3D4402612}.Debug|x86.ActiveCfg = Debug|Any CPU + {297D901A-F3D0-4653-B4EB-CBD3D4402612}.Debug|x86.Build.0 = Debug|Any CPU + {297D901A-F3D0-4653-B4EB-CBD3D4402612}.Release|Any CPU.ActiveCfg = Release|Any CPU + {297D901A-F3D0-4653-B4EB-CBD3D4402612}.Release|Any CPU.Build.0 = Release|Any CPU + {297D901A-F3D0-4653-B4EB-CBD3D4402612}.Release|x64.ActiveCfg = Release|Any CPU + {297D901A-F3D0-4653-B4EB-CBD3D4402612}.Release|x64.Build.0 = Release|Any CPU + {297D901A-F3D0-4653-B4EB-CBD3D4402612}.Release|x86.ActiveCfg = Release|Any CPU + {297D901A-F3D0-4653-B4EB-CBD3D4402612}.Release|x86.Build.0 = Release|Any CPU + {82456D25-7753-4D1C-9ADC-450948B7FAC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82456D25-7753-4D1C-9ADC-450948B7FAC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82456D25-7753-4D1C-9ADC-450948B7FAC0}.Debug|x64.ActiveCfg = Debug|Any CPU + {82456D25-7753-4D1C-9ADC-450948B7FAC0}.Debug|x64.Build.0 = Debug|Any CPU + {82456D25-7753-4D1C-9ADC-450948B7FAC0}.Debug|x86.ActiveCfg = Debug|Any CPU + {82456D25-7753-4D1C-9ADC-450948B7FAC0}.Debug|x86.Build.0 = Debug|Any CPU + {82456D25-7753-4D1C-9ADC-450948B7FAC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82456D25-7753-4D1C-9ADC-450948B7FAC0}.Release|Any CPU.Build.0 = Release|Any CPU + {82456D25-7753-4D1C-9ADC-450948B7FAC0}.Release|x64.ActiveCfg = Release|Any CPU + {82456D25-7753-4D1C-9ADC-450948B7FAC0}.Release|x64.Build.0 = Release|Any CPU + {82456D25-7753-4D1C-9ADC-450948B7FAC0}.Release|x86.ActiveCfg = Release|Any CPU + {82456D25-7753-4D1C-9ADC-450948B7FAC0}.Release|x86.Build.0 = Release|Any CPU + {E5408C31-60C9-4A60-89D9-840C8C9E5E2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5408C31-60C9-4A60-89D9-840C8C9E5E2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5408C31-60C9-4A60-89D9-840C8C9E5E2F}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5408C31-60C9-4A60-89D9-840C8C9E5E2F}.Debug|x64.Build.0 = Debug|Any CPU + {E5408C31-60C9-4A60-89D9-840C8C9E5E2F}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5408C31-60C9-4A60-89D9-840C8C9E5E2F}.Debug|x86.Build.0 = Debug|Any CPU + {E5408C31-60C9-4A60-89D9-840C8C9E5E2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5408C31-60C9-4A60-89D9-840C8C9E5E2F}.Release|Any CPU.Build.0 = Release|Any CPU + {E5408C31-60C9-4A60-89D9-840C8C9E5E2F}.Release|x64.ActiveCfg = Release|Any CPU + {E5408C31-60C9-4A60-89D9-840C8C9E5E2F}.Release|x64.Build.0 = Release|Any CPU + {E5408C31-60C9-4A60-89D9-840C8C9E5E2F}.Release|x86.ActiveCfg = Release|Any CPU + {E5408C31-60C9-4A60-89D9-840C8C9E5E2F}.Release|x86.Build.0 = Release|Any CPU + {79094B35-C3C6-4332-8A79-6E3D32582059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79094B35-C3C6-4332-8A79-6E3D32582059}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79094B35-C3C6-4332-8A79-6E3D32582059}.Debug|x64.ActiveCfg = Debug|Any CPU + {79094B35-C3C6-4332-8A79-6E3D32582059}.Debug|x64.Build.0 = Debug|Any CPU + {79094B35-C3C6-4332-8A79-6E3D32582059}.Debug|x86.ActiveCfg = Debug|Any CPU + {79094B35-C3C6-4332-8A79-6E3D32582059}.Debug|x86.Build.0 = Debug|Any CPU + {79094B35-C3C6-4332-8A79-6E3D32582059}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79094B35-C3C6-4332-8A79-6E3D32582059}.Release|Any CPU.Build.0 = Release|Any CPU + {79094B35-C3C6-4332-8A79-6E3D32582059}.Release|x64.ActiveCfg = Release|Any CPU + {79094B35-C3C6-4332-8A79-6E3D32582059}.Release|x64.Build.0 = Release|Any CPU + {79094B35-C3C6-4332-8A79-6E3D32582059}.Release|x86.ActiveCfg = Release|Any CPU + {79094B35-C3C6-4332-8A79-6E3D32582059}.Release|x86.Build.0 = Release|Any CPU + {7311835B-6CE8-470E-AFB4-B76424A86096}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7311835B-6CE8-470E-AFB4-B76424A86096}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7311835B-6CE8-470E-AFB4-B76424A86096}.Debug|x64.ActiveCfg = Debug|Any CPU + {7311835B-6CE8-470E-AFB4-B76424A86096}.Debug|x64.Build.0 = Debug|Any CPU + {7311835B-6CE8-470E-AFB4-B76424A86096}.Debug|x86.ActiveCfg = Debug|Any CPU + {7311835B-6CE8-470E-AFB4-B76424A86096}.Debug|x86.Build.0 = Debug|Any CPU + {7311835B-6CE8-470E-AFB4-B76424A86096}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7311835B-6CE8-470E-AFB4-B76424A86096}.Release|Any CPU.Build.0 = Release|Any CPU + {7311835B-6CE8-470E-AFB4-B76424A86096}.Release|x64.ActiveCfg = Release|Any CPU + {7311835B-6CE8-470E-AFB4-B76424A86096}.Release|x64.Build.0 = Release|Any CPU + {7311835B-6CE8-470E-AFB4-B76424A86096}.Release|x86.ActiveCfg = Release|Any CPU + {7311835B-6CE8-470E-AFB4-B76424A86096}.Release|x86.Build.0 = Release|Any CPU + {04F673E8-6A3D-4A60-A829-1B851FDB5A4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04F673E8-6A3D-4A60-A829-1B851FDB5A4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04F673E8-6A3D-4A60-A829-1B851FDB5A4C}.Debug|x64.ActiveCfg = Debug|Any CPU + {04F673E8-6A3D-4A60-A829-1B851FDB5A4C}.Debug|x64.Build.0 = Debug|Any CPU + {04F673E8-6A3D-4A60-A829-1B851FDB5A4C}.Debug|x86.ActiveCfg = Debug|Any CPU + {04F673E8-6A3D-4A60-A829-1B851FDB5A4C}.Debug|x86.Build.0 = Debug|Any CPU + {04F673E8-6A3D-4A60-A829-1B851FDB5A4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04F673E8-6A3D-4A60-A829-1B851FDB5A4C}.Release|Any CPU.Build.0 = Release|Any CPU + {04F673E8-6A3D-4A60-A829-1B851FDB5A4C}.Release|x64.ActiveCfg = Release|Any CPU + {04F673E8-6A3D-4A60-A829-1B851FDB5A4C}.Release|x64.Build.0 = Release|Any CPU + {04F673E8-6A3D-4A60-A829-1B851FDB5A4C}.Release|x86.ActiveCfg = Release|Any CPU + {04F673E8-6A3D-4A60-A829-1B851FDB5A4C}.Release|x86.Build.0 = Release|Any CPU + {1826ECD1-D177-4AEF-B821-8B65535CAA46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1826ECD1-D177-4AEF-B821-8B65535CAA46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1826ECD1-D177-4AEF-B821-8B65535CAA46}.Debug|x64.ActiveCfg = Debug|Any CPU + {1826ECD1-D177-4AEF-B821-8B65535CAA46}.Debug|x64.Build.0 = Debug|Any CPU + {1826ECD1-D177-4AEF-B821-8B65535CAA46}.Debug|x86.ActiveCfg = Debug|Any CPU + {1826ECD1-D177-4AEF-B821-8B65535CAA46}.Debug|x86.Build.0 = Debug|Any CPU + {1826ECD1-D177-4AEF-B821-8B65535CAA46}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1826ECD1-D177-4AEF-B821-8B65535CAA46}.Release|Any CPU.Build.0 = Release|Any CPU + {1826ECD1-D177-4AEF-B821-8B65535CAA46}.Release|x64.ActiveCfg = Release|Any CPU + {1826ECD1-D177-4AEF-B821-8B65535CAA46}.Release|x64.Build.0 = Release|Any CPU + {1826ECD1-D177-4AEF-B821-8B65535CAA46}.Release|x86.ActiveCfg = Release|Any CPU + {1826ECD1-D177-4AEF-B821-8B65535CAA46}.Release|x86.Build.0 = Release|Any CPU + {C6D9607E-AB00-4E29-A8A9-8F378B2D755B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6D9607E-AB00-4E29-A8A9-8F378B2D755B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6D9607E-AB00-4E29-A8A9-8F378B2D755B}.Debug|x64.ActiveCfg = Debug|Any CPU + {C6D9607E-AB00-4E29-A8A9-8F378B2D755B}.Debug|x64.Build.0 = Debug|Any CPU + {C6D9607E-AB00-4E29-A8A9-8F378B2D755B}.Debug|x86.ActiveCfg = Debug|Any CPU + {C6D9607E-AB00-4E29-A8A9-8F378B2D755B}.Debug|x86.Build.0 = Debug|Any CPU + {C6D9607E-AB00-4E29-A8A9-8F378B2D755B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6D9607E-AB00-4E29-A8A9-8F378B2D755B}.Release|Any CPU.Build.0 = Release|Any CPU + {C6D9607E-AB00-4E29-A8A9-8F378B2D755B}.Release|x64.ActiveCfg = Release|Any CPU + {C6D9607E-AB00-4E29-A8A9-8F378B2D755B}.Release|x64.Build.0 = Release|Any CPU + {C6D9607E-AB00-4E29-A8A9-8F378B2D755B}.Release|x86.ActiveCfg = Release|Any CPU + {C6D9607E-AB00-4E29-A8A9-8F378B2D755B}.Release|x86.Build.0 = Release|Any CPU + {19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD}.Debug|x64.ActiveCfg = Debug|Any CPU + {19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD}.Debug|x64.Build.0 = Debug|Any CPU + {19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD}.Debug|x86.ActiveCfg = Debug|Any CPU + {19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD}.Debug|x86.Build.0 = Debug|Any CPU + {19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD}.Release|Any CPU.Build.0 = Release|Any CPU + {19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD}.Release|x64.ActiveCfg = Release|Any CPU + {19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD}.Release|x64.Build.0 = Release|Any CPU + {19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD}.Release|x86.ActiveCfg = Release|Any CPU + {19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD}.Release|x86.Build.0 = Release|Any CPU + {745684DE-BABB-4BAA-BC36-DA3029E8C99F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {745684DE-BABB-4BAA-BC36-DA3029E8C99F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {745684DE-BABB-4BAA-BC36-DA3029E8C99F}.Debug|x64.ActiveCfg = Debug|Any CPU + {745684DE-BABB-4BAA-BC36-DA3029E8C99F}.Debug|x64.Build.0 = Debug|Any CPU + {745684DE-BABB-4BAA-BC36-DA3029E8C99F}.Debug|x86.ActiveCfg = Debug|Any CPU + {745684DE-BABB-4BAA-BC36-DA3029E8C99F}.Debug|x86.Build.0 = Debug|Any CPU + {745684DE-BABB-4BAA-BC36-DA3029E8C99F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {745684DE-BABB-4BAA-BC36-DA3029E8C99F}.Release|Any CPU.Build.0 = Release|Any CPU + {745684DE-BABB-4BAA-BC36-DA3029E8C99F}.Release|x64.ActiveCfg = Release|Any CPU + {745684DE-BABB-4BAA-BC36-DA3029E8C99F}.Release|x64.Build.0 = Release|Any CPU + {745684DE-BABB-4BAA-BC36-DA3029E8C99F}.Release|x86.ActiveCfg = Release|Any CPU + {745684DE-BABB-4BAA-BC36-DA3029E8C99F}.Release|x86.Build.0 = Release|Any CPU + {F58C0BE6-D91A-42F7-A221-4A882911E61C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F58C0BE6-D91A-42F7-A221-4A882911E61C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F58C0BE6-D91A-42F7-A221-4A882911E61C}.Debug|x64.ActiveCfg = Debug|Any CPU + {F58C0BE6-D91A-42F7-A221-4A882911E61C}.Debug|x64.Build.0 = Debug|Any CPU + {F58C0BE6-D91A-42F7-A221-4A882911E61C}.Debug|x86.ActiveCfg = Debug|Any CPU + {F58C0BE6-D91A-42F7-A221-4A882911E61C}.Debug|x86.Build.0 = Debug|Any CPU + {F58C0BE6-D91A-42F7-A221-4A882911E61C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F58C0BE6-D91A-42F7-A221-4A882911E61C}.Release|Any CPU.Build.0 = Release|Any CPU + {F58C0BE6-D91A-42F7-A221-4A882911E61C}.Release|x64.ActiveCfg = Release|Any CPU + {F58C0BE6-D91A-42F7-A221-4A882911E61C}.Release|x64.Build.0 = Release|Any CPU + {F58C0BE6-D91A-42F7-A221-4A882911E61C}.Release|x86.ActiveCfg = Release|Any CPU + {F58C0BE6-D91A-42F7-A221-4A882911E61C}.Release|x86.Build.0 = Release|Any CPU + {5F1D96A7-8252-4D60-AE6E-A66C045A8A92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F1D96A7-8252-4D60-AE6E-A66C045A8A92}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F1D96A7-8252-4D60-AE6E-A66C045A8A92}.Debug|x64.ActiveCfg = Debug|Any CPU + {5F1D96A7-8252-4D60-AE6E-A66C045A8A92}.Debug|x64.Build.0 = Debug|Any CPU + {5F1D96A7-8252-4D60-AE6E-A66C045A8A92}.Debug|x86.ActiveCfg = Debug|Any CPU + {5F1D96A7-8252-4D60-AE6E-A66C045A8A92}.Debug|x86.Build.0 = Debug|Any CPU + {5F1D96A7-8252-4D60-AE6E-A66C045A8A92}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F1D96A7-8252-4D60-AE6E-A66C045A8A92}.Release|Any CPU.Build.0 = Release|Any CPU + {5F1D96A7-8252-4D60-AE6E-A66C045A8A92}.Release|x64.ActiveCfg = Release|Any CPU + {5F1D96A7-8252-4D60-AE6E-A66C045A8A92}.Release|x64.Build.0 = Release|Any CPU + {5F1D96A7-8252-4D60-AE6E-A66C045A8A92}.Release|x86.ActiveCfg = Release|Any CPU + {5F1D96A7-8252-4D60-AE6E-A66C045A8A92}.Release|x86.Build.0 = Release|Any CPU + {AEDD4AE8-A831-46EE-AF49-EAC3183D5971}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEDD4AE8-A831-46EE-AF49-EAC3183D5971}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEDD4AE8-A831-46EE-AF49-EAC3183D5971}.Debug|x64.ActiveCfg = Debug|Any CPU + {AEDD4AE8-A831-46EE-AF49-EAC3183D5971}.Debug|x64.Build.0 = Debug|Any CPU + {AEDD4AE8-A831-46EE-AF49-EAC3183D5971}.Debug|x86.ActiveCfg = Debug|Any CPU + {AEDD4AE8-A831-46EE-AF49-EAC3183D5971}.Debug|x86.Build.0 = Debug|Any CPU + {AEDD4AE8-A831-46EE-AF49-EAC3183D5971}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEDD4AE8-A831-46EE-AF49-EAC3183D5971}.Release|Any CPU.Build.0 = Release|Any CPU + {AEDD4AE8-A831-46EE-AF49-EAC3183D5971}.Release|x64.ActiveCfg = Release|Any CPU + {AEDD4AE8-A831-46EE-AF49-EAC3183D5971}.Release|x64.Build.0 = Release|Any CPU + {AEDD4AE8-A831-46EE-AF49-EAC3183D5971}.Release|x86.ActiveCfg = Release|Any CPU + {AEDD4AE8-A831-46EE-AF49-EAC3183D5971}.Release|x86.Build.0 = Release|Any CPU + {F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889}.Debug|x64.ActiveCfg = Debug|Any CPU + {F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889}.Debug|x64.Build.0 = Debug|Any CPU + {F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889}.Debug|x86.ActiveCfg = Debug|Any CPU + {F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889}.Debug|x86.Build.0 = Debug|Any CPU + {F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889}.Release|Any CPU.Build.0 = Release|Any CPU + {F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889}.Release|x64.ActiveCfg = Release|Any CPU + {F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889}.Release|x64.Build.0 = Release|Any CPU + {F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889}.Release|x86.ActiveCfg = Release|Any CPU + {F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889}.Release|x86.Build.0 = Release|Any CPU + {D509243A-E5AA-430D-B47E-8ADC5DA31125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D509243A-E5AA-430D-B47E-8ADC5DA31125}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D509243A-E5AA-430D-B47E-8ADC5DA31125}.Debug|x64.ActiveCfg = Debug|Any CPU + {D509243A-E5AA-430D-B47E-8ADC5DA31125}.Debug|x64.Build.0 = Debug|Any CPU + {D509243A-E5AA-430D-B47E-8ADC5DA31125}.Debug|x86.ActiveCfg = Debug|Any CPU + {D509243A-E5AA-430D-B47E-8ADC5DA31125}.Debug|x86.Build.0 = Debug|Any CPU + {D509243A-E5AA-430D-B47E-8ADC5DA31125}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D509243A-E5AA-430D-B47E-8ADC5DA31125}.Release|Any CPU.Build.0 = Release|Any CPU + {D509243A-E5AA-430D-B47E-8ADC5DA31125}.Release|x64.ActiveCfg = Release|Any CPU + {D509243A-E5AA-430D-B47E-8ADC5DA31125}.Release|x64.Build.0 = Release|Any CPU + {D509243A-E5AA-430D-B47E-8ADC5DA31125}.Release|x86.ActiveCfg = Release|Any CPU + {D509243A-E5AA-430D-B47E-8ADC5DA31125}.Release|x86.Build.0 = Release|Any CPU + {7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC}.Debug|x64.ActiveCfg = Debug|Any CPU + {7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC}.Debug|x64.Build.0 = Debug|Any CPU + {7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC}.Debug|x86.Build.0 = Debug|Any CPU + {7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC}.Release|Any CPU.Build.0 = Release|Any CPU + {7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC}.Release|x64.ActiveCfg = Release|Any CPU + {7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC}.Release|x64.Build.0 = Release|Any CPU + {7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC}.Release|x86.ActiveCfg = Release|Any CPU + {7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC}.Release|x86.Build.0 = Release|Any CPU + {443B5C89-0904-4AE8-B48F-1E92709FBEF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {443B5C89-0904-4AE8-B48F-1E92709FBEF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {443B5C89-0904-4AE8-B48F-1E92709FBEF3}.Debug|x64.ActiveCfg = Debug|Any CPU + {443B5C89-0904-4AE8-B48F-1E92709FBEF3}.Debug|x64.Build.0 = Debug|Any CPU + {443B5C89-0904-4AE8-B48F-1E92709FBEF3}.Debug|x86.ActiveCfg = Debug|Any CPU + {443B5C89-0904-4AE8-B48F-1E92709FBEF3}.Debug|x86.Build.0 = Debug|Any CPU + {443B5C89-0904-4AE8-B48F-1E92709FBEF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {443B5C89-0904-4AE8-B48F-1E92709FBEF3}.Release|Any CPU.Build.0 = Release|Any CPU + {443B5C89-0904-4AE8-B48F-1E92709FBEF3}.Release|x64.ActiveCfg = Release|Any CPU + {443B5C89-0904-4AE8-B48F-1E92709FBEF3}.Release|x64.Build.0 = Release|Any CPU + {443B5C89-0904-4AE8-B48F-1E92709FBEF3}.Release|x86.ActiveCfg = Release|Any CPU + {443B5C89-0904-4AE8-B48F-1E92709FBEF3}.Release|x86.Build.0 = Release|Any CPU + {48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8}.Debug|x64.ActiveCfg = Debug|Any CPU + {48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8}.Debug|x64.Build.0 = Debug|Any CPU + {48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8}.Debug|x86.ActiveCfg = Debug|Any CPU + {48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8}.Debug|x86.Build.0 = Debug|Any CPU + {48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8}.Release|Any CPU.Build.0 = Release|Any CPU + {48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8}.Release|x64.ActiveCfg = Release|Any CPU + {48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8}.Release|x64.Build.0 = Release|Any CPU + {48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8}.Release|x86.ActiveCfg = Release|Any CPU + {48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8}.Release|x86.Build.0 = Release|Any CPU + {EF1817C6-1FEB-4185-94F3-6BDB9ED1660A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF1817C6-1FEB-4185-94F3-6BDB9ED1660A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF1817C6-1FEB-4185-94F3-6BDB9ED1660A}.Debug|x64.ActiveCfg = Debug|Any CPU + {EF1817C6-1FEB-4185-94F3-6BDB9ED1660A}.Debug|x64.Build.0 = Debug|Any CPU + {EF1817C6-1FEB-4185-94F3-6BDB9ED1660A}.Debug|x86.ActiveCfg = Debug|Any CPU + {EF1817C6-1FEB-4185-94F3-6BDB9ED1660A}.Debug|x86.Build.0 = Debug|Any CPU + {EF1817C6-1FEB-4185-94F3-6BDB9ED1660A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF1817C6-1FEB-4185-94F3-6BDB9ED1660A}.Release|Any CPU.Build.0 = Release|Any CPU + {EF1817C6-1FEB-4185-94F3-6BDB9ED1660A}.Release|x64.ActiveCfg = Release|Any CPU + {EF1817C6-1FEB-4185-94F3-6BDB9ED1660A}.Release|x64.Build.0 = Release|Any CPU + {EF1817C6-1FEB-4185-94F3-6BDB9ED1660A}.Release|x86.ActiveCfg = Release|Any CPU + {EF1817C6-1FEB-4185-94F3-6BDB9ED1660A}.Release|x86.Build.0 = Release|Any CPU + {1EF56286-3151-4D13-B662-C23CBE7148FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1EF56286-3151-4D13-B662-C23CBE7148FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1EF56286-3151-4D13-B662-C23CBE7148FE}.Debug|x64.ActiveCfg = Debug|Any CPU + {1EF56286-3151-4D13-B662-C23CBE7148FE}.Debug|x64.Build.0 = Debug|Any CPU + {1EF56286-3151-4D13-B662-C23CBE7148FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {1EF56286-3151-4D13-B662-C23CBE7148FE}.Debug|x86.Build.0 = Debug|Any CPU + {1EF56286-3151-4D13-B662-C23CBE7148FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1EF56286-3151-4D13-B662-C23CBE7148FE}.Release|Any CPU.Build.0 = Release|Any CPU + {1EF56286-3151-4D13-B662-C23CBE7148FE}.Release|x64.ActiveCfg = Release|Any CPU + {1EF56286-3151-4D13-B662-C23CBE7148FE}.Release|x64.Build.0 = Release|Any CPU + {1EF56286-3151-4D13-B662-C23CBE7148FE}.Release|x86.ActiveCfg = Release|Any CPU + {1EF56286-3151-4D13-B662-C23CBE7148FE}.Release|x86.Build.0 = Release|Any CPU + {5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA}.Debug|x64.ActiveCfg = Debug|Any CPU + {5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA}.Debug|x64.Build.0 = Debug|Any CPU + {5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA}.Debug|x86.ActiveCfg = Debug|Any CPU + {5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA}.Debug|x86.Build.0 = Debug|Any CPU + {5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA}.Release|Any CPU.Build.0 = Release|Any CPU + {5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA}.Release|x64.ActiveCfg = Release|Any CPU + {5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA}.Release|x64.Build.0 = Release|Any CPU + {5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA}.Release|x86.ActiveCfg = Release|Any CPU + {5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA}.Release|x86.Build.0 = Release|Any CPU + {F743054C-0D7F-4352-9DBD-365C4CFA19C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F743054C-0D7F-4352-9DBD-365C4CFA19C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F743054C-0D7F-4352-9DBD-365C4CFA19C6}.Debug|x64.ActiveCfg = Debug|Any CPU + {F743054C-0D7F-4352-9DBD-365C4CFA19C6}.Debug|x64.Build.0 = Debug|Any CPU + {F743054C-0D7F-4352-9DBD-365C4CFA19C6}.Debug|x86.ActiveCfg = Debug|Any CPU + {F743054C-0D7F-4352-9DBD-365C4CFA19C6}.Debug|x86.Build.0 = Debug|Any CPU + {F743054C-0D7F-4352-9DBD-365C4CFA19C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F743054C-0D7F-4352-9DBD-365C4CFA19C6}.Release|Any CPU.Build.0 = Release|Any CPU + {F743054C-0D7F-4352-9DBD-365C4CFA19C6}.Release|x64.ActiveCfg = Release|Any CPU + {F743054C-0D7F-4352-9DBD-365C4CFA19C6}.Release|x64.Build.0 = Release|Any CPU + {F743054C-0D7F-4352-9DBD-365C4CFA19C6}.Release|x86.ActiveCfg = Release|Any CPU + {F743054C-0D7F-4352-9DBD-365C4CFA19C6}.Release|x86.Build.0 = Release|Any CPU + {509EA8AB-5717-4953-87FC-F3EDD041F200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {509EA8AB-5717-4953-87FC-F3EDD041F200}.Debug|Any CPU.Build.0 = Debug|Any CPU + {509EA8AB-5717-4953-87FC-F3EDD041F200}.Debug|x64.ActiveCfg = Debug|Any CPU + {509EA8AB-5717-4953-87FC-F3EDD041F200}.Debug|x64.Build.0 = Debug|Any CPU + {509EA8AB-5717-4953-87FC-F3EDD041F200}.Debug|x86.ActiveCfg = Debug|Any CPU + {509EA8AB-5717-4953-87FC-F3EDD041F200}.Debug|x86.Build.0 = Debug|Any CPU + {509EA8AB-5717-4953-87FC-F3EDD041F200}.Release|Any CPU.ActiveCfg = Release|Any CPU + {509EA8AB-5717-4953-87FC-F3EDD041F200}.Release|Any CPU.Build.0 = Release|Any CPU + {509EA8AB-5717-4953-87FC-F3EDD041F200}.Release|x64.ActiveCfg = Release|Any CPU + {509EA8AB-5717-4953-87FC-F3EDD041F200}.Release|x64.Build.0 = Release|Any CPU + {509EA8AB-5717-4953-87FC-F3EDD041F200}.Release|x86.ActiveCfg = Release|Any CPU + {509EA8AB-5717-4953-87FC-F3EDD041F200}.Release|x86.Build.0 = Release|Any CPU + {A9677606-014A-43A4-83D1-7871D1547862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9677606-014A-43A4-83D1-7871D1547862}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9677606-014A-43A4-83D1-7871D1547862}.Debug|x64.ActiveCfg = Debug|Any CPU + {A9677606-014A-43A4-83D1-7871D1547862}.Debug|x64.Build.0 = Debug|Any CPU + {A9677606-014A-43A4-83D1-7871D1547862}.Debug|x86.ActiveCfg = Debug|Any CPU + {A9677606-014A-43A4-83D1-7871D1547862}.Debug|x86.Build.0 = Debug|Any CPU + {A9677606-014A-43A4-83D1-7871D1547862}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9677606-014A-43A4-83D1-7871D1547862}.Release|Any CPU.Build.0 = Release|Any CPU + {A9677606-014A-43A4-83D1-7871D1547862}.Release|x64.ActiveCfg = Release|Any CPU + {A9677606-014A-43A4-83D1-7871D1547862}.Release|x64.Build.0 = Release|Any CPU + {A9677606-014A-43A4-83D1-7871D1547862}.Release|x86.ActiveCfg = Release|Any CPU + {A9677606-014A-43A4-83D1-7871D1547862}.Release|x86.Build.0 = Release|Any CPU + {42B441C3-43AD-4D8F-96E0-547D7257FB04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42B441C3-43AD-4D8F-96E0-547D7257FB04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42B441C3-43AD-4D8F-96E0-547D7257FB04}.Debug|x64.ActiveCfg = Debug|Any CPU + {42B441C3-43AD-4D8F-96E0-547D7257FB04}.Debug|x64.Build.0 = Debug|Any CPU + {42B441C3-43AD-4D8F-96E0-547D7257FB04}.Debug|x86.ActiveCfg = Debug|Any CPU + {42B441C3-43AD-4D8F-96E0-547D7257FB04}.Debug|x86.Build.0 = Debug|Any CPU + {42B441C3-43AD-4D8F-96E0-547D7257FB04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42B441C3-43AD-4D8F-96E0-547D7257FB04}.Release|Any CPU.Build.0 = Release|Any CPU + {42B441C3-43AD-4D8F-96E0-547D7257FB04}.Release|x64.ActiveCfg = Release|Any CPU + {42B441C3-43AD-4D8F-96E0-547D7257FB04}.Release|x64.Build.0 = Release|Any CPU + {42B441C3-43AD-4D8F-96E0-547D7257FB04}.Release|x86.ActiveCfg = Release|Any CPU + {42B441C3-43AD-4D8F-96E0-547D7257FB04}.Release|x86.Build.0 = Release|Any CPU + {DD4CFE16-3F58-4AA0-A995-312AFC55F0B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD4CFE16-3F58-4AA0-A995-312AFC55F0B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD4CFE16-3F58-4AA0-A995-312AFC55F0B3}.Debug|x64.ActiveCfg = Debug|Any CPU + {DD4CFE16-3F58-4AA0-A995-312AFC55F0B3}.Debug|x64.Build.0 = Debug|Any CPU + {DD4CFE16-3F58-4AA0-A995-312AFC55F0B3}.Debug|x86.ActiveCfg = Debug|Any CPU + {DD4CFE16-3F58-4AA0-A995-312AFC55F0B3}.Debug|x86.Build.0 = Debug|Any CPU + {DD4CFE16-3F58-4AA0-A995-312AFC55F0B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD4CFE16-3F58-4AA0-A995-312AFC55F0B3}.Release|Any CPU.Build.0 = Release|Any CPU + {DD4CFE16-3F58-4AA0-A995-312AFC55F0B3}.Release|x64.ActiveCfg = Release|Any CPU + {DD4CFE16-3F58-4AA0-A995-312AFC55F0B3}.Release|x64.Build.0 = Release|Any CPU + {DD4CFE16-3F58-4AA0-A995-312AFC55F0B3}.Release|x86.ActiveCfg = Release|Any CPU + {DD4CFE16-3F58-4AA0-A995-312AFC55F0B3}.Release|x86.Build.0 = Release|Any CPU + {CC31B699-7B15-4685-8E00-01E2DE6CA88B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC31B699-7B15-4685-8E00-01E2DE6CA88B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC31B699-7B15-4685-8E00-01E2DE6CA88B}.Debug|x64.ActiveCfg = Debug|Any CPU + {CC31B699-7B15-4685-8E00-01E2DE6CA88B}.Debug|x64.Build.0 = Debug|Any CPU + {CC31B699-7B15-4685-8E00-01E2DE6CA88B}.Debug|x86.ActiveCfg = Debug|Any CPU + {CC31B699-7B15-4685-8E00-01E2DE6CA88B}.Debug|x86.Build.0 = Debug|Any CPU + {CC31B699-7B15-4685-8E00-01E2DE6CA88B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC31B699-7B15-4685-8E00-01E2DE6CA88B}.Release|Any CPU.Build.0 = Release|Any CPU + {CC31B699-7B15-4685-8E00-01E2DE6CA88B}.Release|x64.ActiveCfg = Release|Any CPU + {CC31B699-7B15-4685-8E00-01E2DE6CA88B}.Release|x64.Build.0 = Release|Any CPU + {CC31B699-7B15-4685-8E00-01E2DE6CA88B}.Release|x86.ActiveCfg = Release|Any CPU + {CC31B699-7B15-4685-8E00-01E2DE6CA88B}.Release|x86.Build.0 = Release|Any CPU + {ED59B9E2-82EC-4984-9A27-E9DFF3B13B60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED59B9E2-82EC-4984-9A27-E9DFF3B13B60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED59B9E2-82EC-4984-9A27-E9DFF3B13B60}.Debug|x64.ActiveCfg = Debug|Any CPU + {ED59B9E2-82EC-4984-9A27-E9DFF3B13B60}.Debug|x64.Build.0 = Debug|Any CPU + {ED59B9E2-82EC-4984-9A27-E9DFF3B13B60}.Debug|x86.ActiveCfg = Debug|Any CPU + {ED59B9E2-82EC-4984-9A27-E9DFF3B13B60}.Debug|x86.Build.0 = Debug|Any CPU + {ED59B9E2-82EC-4984-9A27-E9DFF3B13B60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED59B9E2-82EC-4984-9A27-E9DFF3B13B60}.Release|Any CPU.Build.0 = Release|Any CPU + {ED59B9E2-82EC-4984-9A27-E9DFF3B13B60}.Release|x64.ActiveCfg = Release|Any CPU + {ED59B9E2-82EC-4984-9A27-E9DFF3B13B60}.Release|x64.Build.0 = Release|Any CPU + {ED59B9E2-82EC-4984-9A27-E9DFF3B13B60}.Release|x86.ActiveCfg = Release|Any CPU + {ED59B9E2-82EC-4984-9A27-E9DFF3B13B60}.Release|x86.Build.0 = Release|Any CPU + {6FC8F397-8F3D-4300-A8B3-6F76B68C959B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FC8F397-8F3D-4300-A8B3-6F76B68C959B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FC8F397-8F3D-4300-A8B3-6F76B68C959B}.Debug|x64.ActiveCfg = Debug|Any CPU + {6FC8F397-8F3D-4300-A8B3-6F76B68C959B}.Debug|x64.Build.0 = Debug|Any CPU + {6FC8F397-8F3D-4300-A8B3-6F76B68C959B}.Debug|x86.ActiveCfg = Debug|Any CPU + {6FC8F397-8F3D-4300-A8B3-6F76B68C959B}.Debug|x86.Build.0 = Debug|Any CPU + {6FC8F397-8F3D-4300-A8B3-6F76B68C959B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FC8F397-8F3D-4300-A8B3-6F76B68C959B}.Release|Any CPU.Build.0 = Release|Any CPU + {6FC8F397-8F3D-4300-A8B3-6F76B68C959B}.Release|x64.ActiveCfg = Release|Any CPU + {6FC8F397-8F3D-4300-A8B3-6F76B68C959B}.Release|x64.Build.0 = Release|Any CPU + {6FC8F397-8F3D-4300-A8B3-6F76B68C959B}.Release|x86.ActiveCfg = Release|Any CPU + {6FC8F397-8F3D-4300-A8B3-6F76B68C959B}.Release|x86.Build.0 = Release|Any CPU + {7B9E64D1-70C9-4462-BE0B-A965A2825919}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B9E64D1-70C9-4462-BE0B-A965A2825919}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B9E64D1-70C9-4462-BE0B-A965A2825919}.Debug|x64.ActiveCfg = Debug|Any CPU + {7B9E64D1-70C9-4462-BE0B-A965A2825919}.Debug|x64.Build.0 = Debug|Any CPU + {7B9E64D1-70C9-4462-BE0B-A965A2825919}.Debug|x86.ActiveCfg = Debug|Any CPU + {7B9E64D1-70C9-4462-BE0B-A965A2825919}.Debug|x86.Build.0 = Debug|Any CPU + {7B9E64D1-70C9-4462-BE0B-A965A2825919}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B9E64D1-70C9-4462-BE0B-A965A2825919}.Release|Any CPU.Build.0 = Release|Any CPU + {7B9E64D1-70C9-4462-BE0B-A965A2825919}.Release|x64.ActiveCfg = Release|Any CPU + {7B9E64D1-70C9-4462-BE0B-A965A2825919}.Release|x64.Build.0 = Release|Any CPU + {7B9E64D1-70C9-4462-BE0B-A965A2825919}.Release|x86.ActiveCfg = Release|Any CPU + {7B9E64D1-70C9-4462-BE0B-A965A2825919}.Release|x86.Build.0 = Release|Any CPU + {82D6EBAF-B0B6-4744-A556-E5667DF823B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82D6EBAF-B0B6-4744-A556-E5667DF823B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82D6EBAF-B0B6-4744-A556-E5667DF823B7}.Debug|x64.ActiveCfg = Debug|Any CPU + {82D6EBAF-B0B6-4744-A556-E5667DF823B7}.Debug|x64.Build.0 = Debug|Any CPU + {82D6EBAF-B0B6-4744-A556-E5667DF823B7}.Debug|x86.ActiveCfg = Debug|Any CPU + {82D6EBAF-B0B6-4744-A556-E5667DF823B7}.Debug|x86.Build.0 = Debug|Any CPU + {82D6EBAF-B0B6-4744-A556-E5667DF823B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82D6EBAF-B0B6-4744-A556-E5667DF823B7}.Release|Any CPU.Build.0 = Release|Any CPU + {82D6EBAF-B0B6-4744-A556-E5667DF823B7}.Release|x64.ActiveCfg = Release|Any CPU + {82D6EBAF-B0B6-4744-A556-E5667DF823B7}.Release|x64.Build.0 = Release|Any CPU + {82D6EBAF-B0B6-4744-A556-E5667DF823B7}.Release|x86.ActiveCfg = Release|Any CPU + {82D6EBAF-B0B6-4744-A556-E5667DF823B7}.Release|x86.Build.0 = Release|Any CPU + {410366DF-4121-44DE-923D-218ACBA2F8CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {410366DF-4121-44DE-923D-218ACBA2F8CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {410366DF-4121-44DE-923D-218ACBA2F8CC}.Debug|x64.ActiveCfg = Debug|Any CPU + {410366DF-4121-44DE-923D-218ACBA2F8CC}.Debug|x64.Build.0 = Debug|Any CPU + {410366DF-4121-44DE-923D-218ACBA2F8CC}.Debug|x86.ActiveCfg = Debug|Any CPU + {410366DF-4121-44DE-923D-218ACBA2F8CC}.Debug|x86.Build.0 = Debug|Any CPU + {410366DF-4121-44DE-923D-218ACBA2F8CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {410366DF-4121-44DE-923D-218ACBA2F8CC}.Release|Any CPU.Build.0 = Release|Any CPU + {410366DF-4121-44DE-923D-218ACBA2F8CC}.Release|x64.ActiveCfg = Release|Any CPU + {410366DF-4121-44DE-923D-218ACBA2F8CC}.Release|x64.Build.0 = Release|Any CPU + {410366DF-4121-44DE-923D-218ACBA2F8CC}.Release|x86.ActiveCfg = Release|Any CPU + {410366DF-4121-44DE-923D-218ACBA2F8CC}.Release|x86.Build.0 = Release|Any CPU + {A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E}.Debug|x64.ActiveCfg = Debug|Any CPU + {A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E}.Debug|x64.Build.0 = Debug|Any CPU + {A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E}.Debug|x86.ActiveCfg = Debug|Any CPU + {A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E}.Debug|x86.Build.0 = Debug|Any CPU + {A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E}.Release|Any CPU.Build.0 = Release|Any CPU + {A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E}.Release|x64.ActiveCfg = Release|Any CPU + {A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E}.Release|x64.Build.0 = Release|Any CPU + {A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E}.Release|x86.ActiveCfg = Release|Any CPU + {A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E}.Release|x86.Build.0 = Release|Any CPU + {656422FD-47DA-4F06-AB76-57FF1F77B040}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {656422FD-47DA-4F06-AB76-57FF1F77B040}.Debug|Any CPU.Build.0 = Debug|Any CPU + {656422FD-47DA-4F06-AB76-57FF1F77B040}.Debug|x64.ActiveCfg = Debug|Any CPU + {656422FD-47DA-4F06-AB76-57FF1F77B040}.Debug|x64.Build.0 = Debug|Any CPU + {656422FD-47DA-4F06-AB76-57FF1F77B040}.Debug|x86.ActiveCfg = Debug|Any CPU + {656422FD-47DA-4F06-AB76-57FF1F77B040}.Debug|x86.Build.0 = Debug|Any CPU + {656422FD-47DA-4F06-AB76-57FF1F77B040}.Release|Any CPU.ActiveCfg = Release|Any CPU + {656422FD-47DA-4F06-AB76-57FF1F77B040}.Release|Any CPU.Build.0 = Release|Any CPU + {656422FD-47DA-4F06-AB76-57FF1F77B040}.Release|x64.ActiveCfg = Release|Any CPU + {656422FD-47DA-4F06-AB76-57FF1F77B040}.Release|x64.Build.0 = Release|Any CPU + {656422FD-47DA-4F06-AB76-57FF1F77B040}.Release|x86.ActiveCfg = Release|Any CPU + {656422FD-47DA-4F06-AB76-57FF1F77B040}.Release|x86.Build.0 = Release|Any CPU + {502B8111-F428-4584-AE44-D1AD3B4F16B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {502B8111-F428-4584-AE44-D1AD3B4F16B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {502B8111-F428-4584-AE44-D1AD3B4F16B5}.Debug|x64.ActiveCfg = Debug|Any CPU + {502B8111-F428-4584-AE44-D1AD3B4F16B5}.Debug|x64.Build.0 = Debug|Any CPU + {502B8111-F428-4584-AE44-D1AD3B4F16B5}.Debug|x86.ActiveCfg = Debug|Any CPU + {502B8111-F428-4584-AE44-D1AD3B4F16B5}.Debug|x86.Build.0 = Debug|Any CPU + {502B8111-F428-4584-AE44-D1AD3B4F16B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {502B8111-F428-4584-AE44-D1AD3B4F16B5}.Release|Any CPU.Build.0 = Release|Any CPU + {502B8111-F428-4584-AE44-D1AD3B4F16B5}.Release|x64.ActiveCfg = Release|Any CPU + {502B8111-F428-4584-AE44-D1AD3B4F16B5}.Release|x64.Build.0 = Release|Any CPU + {502B8111-F428-4584-AE44-D1AD3B4F16B5}.Release|x86.ActiveCfg = Release|Any CPU + {502B8111-F428-4584-AE44-D1AD3B4F16B5}.Release|x86.Build.0 = Release|Any CPU + {174CF08D-E13D-4904-90C0-CC740C9DE086}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {174CF08D-E13D-4904-90C0-CC740C9DE086}.Debug|Any CPU.Build.0 = Debug|Any CPU + {174CF08D-E13D-4904-90C0-CC740C9DE086}.Debug|x64.ActiveCfg = Debug|Any CPU + {174CF08D-E13D-4904-90C0-CC740C9DE086}.Debug|x64.Build.0 = Debug|Any CPU + {174CF08D-E13D-4904-90C0-CC740C9DE086}.Debug|x86.ActiveCfg = Debug|Any CPU + {174CF08D-E13D-4904-90C0-CC740C9DE086}.Debug|x86.Build.0 = Debug|Any CPU + {174CF08D-E13D-4904-90C0-CC740C9DE086}.Release|Any CPU.ActiveCfg = Release|Any CPU + {174CF08D-E13D-4904-90C0-CC740C9DE086}.Release|Any CPU.Build.0 = Release|Any CPU + {174CF08D-E13D-4904-90C0-CC740C9DE086}.Release|x64.ActiveCfg = Release|Any CPU + {174CF08D-E13D-4904-90C0-CC740C9DE086}.Release|x64.Build.0 = Release|Any CPU + {174CF08D-E13D-4904-90C0-CC740C9DE086}.Release|x86.ActiveCfg = Release|Any CPU + {174CF08D-E13D-4904-90C0-CC740C9DE086}.Release|x86.Build.0 = Release|Any CPU + {853E3B54-DB8A-4A82-8565-E6103130DA3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {853E3B54-DB8A-4A82-8565-E6103130DA3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {853E3B54-DB8A-4A82-8565-E6103130DA3C}.Debug|x64.ActiveCfg = Debug|Any CPU + {853E3B54-DB8A-4A82-8565-E6103130DA3C}.Debug|x64.Build.0 = Debug|Any CPU + {853E3B54-DB8A-4A82-8565-E6103130DA3C}.Debug|x86.ActiveCfg = Debug|Any CPU + {853E3B54-DB8A-4A82-8565-E6103130DA3C}.Debug|x86.Build.0 = Debug|Any CPU + {853E3B54-DB8A-4A82-8565-E6103130DA3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {853E3B54-DB8A-4A82-8565-E6103130DA3C}.Release|Any CPU.Build.0 = Release|Any CPU + {853E3B54-DB8A-4A82-8565-E6103130DA3C}.Release|x64.ActiveCfg = Release|Any CPU + {853E3B54-DB8A-4A82-8565-E6103130DA3C}.Release|x64.Build.0 = Release|Any CPU + {853E3B54-DB8A-4A82-8565-E6103130DA3C}.Release|x86.ActiveCfg = Release|Any CPU + {853E3B54-DB8A-4A82-8565-E6103130DA3C}.Release|x86.Build.0 = Release|Any CPU + {B8DC7FD1-782F-4246-B096-836D82B29A03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8DC7FD1-782F-4246-B096-836D82B29A03}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8DC7FD1-782F-4246-B096-836D82B29A03}.Debug|x64.ActiveCfg = Debug|Any CPU + {B8DC7FD1-782F-4246-B096-836D82B29A03}.Debug|x64.Build.0 = Debug|Any CPU + {B8DC7FD1-782F-4246-B096-836D82B29A03}.Debug|x86.ActiveCfg = Debug|Any CPU + {B8DC7FD1-782F-4246-B096-836D82B29A03}.Debug|x86.Build.0 = Debug|Any CPU + {B8DC7FD1-782F-4246-B096-836D82B29A03}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8DC7FD1-782F-4246-B096-836D82B29A03}.Release|Any CPU.Build.0 = Release|Any CPU + {B8DC7FD1-782F-4246-B096-836D82B29A03}.Release|x64.ActiveCfg = Release|Any CPU + {B8DC7FD1-782F-4246-B096-836D82B29A03}.Release|x64.Build.0 = Release|Any CPU + {B8DC7FD1-782F-4246-B096-836D82B29A03}.Release|x86.ActiveCfg = Release|Any CPU + {B8DC7FD1-782F-4246-B096-836D82B29A03}.Release|x86.Build.0 = Release|Any CPU + {8AE239F7-A0B1-459A-AF6F-FB1501544C2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8AE239F7-A0B1-459A-AF6F-FB1501544C2B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8AE239F7-A0B1-459A-AF6F-FB1501544C2B}.Debug|x64.ActiveCfg = Debug|Any CPU + {8AE239F7-A0B1-459A-AF6F-FB1501544C2B}.Debug|x64.Build.0 = Debug|Any CPU + {8AE239F7-A0B1-459A-AF6F-FB1501544C2B}.Debug|x86.ActiveCfg = Debug|Any CPU + {8AE239F7-A0B1-459A-AF6F-FB1501544C2B}.Debug|x86.Build.0 = Debug|Any CPU + {8AE239F7-A0B1-459A-AF6F-FB1501544C2B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8AE239F7-A0B1-459A-AF6F-FB1501544C2B}.Release|Any CPU.Build.0 = Release|Any CPU + {8AE239F7-A0B1-459A-AF6F-FB1501544C2B}.Release|x64.ActiveCfg = Release|Any CPU + {8AE239F7-A0B1-459A-AF6F-FB1501544C2B}.Release|x64.Build.0 = Release|Any CPU + {8AE239F7-A0B1-459A-AF6F-FB1501544C2B}.Release|x86.ActiveCfg = Release|Any CPU + {8AE239F7-A0B1-459A-AF6F-FB1501544C2B}.Release|x86.Build.0 = Release|Any CPU + {FB7DB850-11DB-4F7B-9003-13C145442C73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB7DB850-11DB-4F7B-9003-13C145442C73}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB7DB850-11DB-4F7B-9003-13C145442C73}.Debug|x64.ActiveCfg = Debug|Any CPU + {FB7DB850-11DB-4F7B-9003-13C145442C73}.Debug|x64.Build.0 = Debug|Any CPU + {FB7DB850-11DB-4F7B-9003-13C145442C73}.Debug|x86.ActiveCfg = Debug|Any CPU + {FB7DB850-11DB-4F7B-9003-13C145442C73}.Debug|x86.Build.0 = Debug|Any CPU + {FB7DB850-11DB-4F7B-9003-13C145442C73}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB7DB850-11DB-4F7B-9003-13C145442C73}.Release|Any CPU.Build.0 = Release|Any CPU + {FB7DB850-11DB-4F7B-9003-13C145442C73}.Release|x64.ActiveCfg = Release|Any CPU + {FB7DB850-11DB-4F7B-9003-13C145442C73}.Release|x64.Build.0 = Release|Any CPU + {FB7DB850-11DB-4F7B-9003-13C145442C73}.Release|x86.ActiveCfg = Release|Any CPU + {FB7DB850-11DB-4F7B-9003-13C145442C73}.Release|x86.Build.0 = Release|Any CPU + {23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62}.Debug|x64.ActiveCfg = Debug|Any CPU + {23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62}.Debug|x64.Build.0 = Debug|Any CPU + {23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62}.Debug|x86.ActiveCfg = Debug|Any CPU + {23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62}.Debug|x86.Build.0 = Debug|Any CPU + {23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62}.Release|Any CPU.Build.0 = Release|Any CPU + {23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62}.Release|x64.ActiveCfg = Release|Any CPU + {23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62}.Release|x64.Build.0 = Release|Any CPU + {23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62}.Release|x86.ActiveCfg = Release|Any CPU + {23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62}.Release|x86.Build.0 = Release|Any CPU + {DF2456D1-0CFF-4213-9E8F-D666DAEA2E54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF2456D1-0CFF-4213-9E8F-D666DAEA2E54}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF2456D1-0CFF-4213-9E8F-D666DAEA2E54}.Debug|x64.ActiveCfg = Debug|Any CPU + {DF2456D1-0CFF-4213-9E8F-D666DAEA2E54}.Debug|x64.Build.0 = Debug|Any CPU + {DF2456D1-0CFF-4213-9E8F-D666DAEA2E54}.Debug|x86.ActiveCfg = Debug|Any CPU + {DF2456D1-0CFF-4213-9E8F-D666DAEA2E54}.Debug|x86.Build.0 = Debug|Any CPU + {DF2456D1-0CFF-4213-9E8F-D666DAEA2E54}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF2456D1-0CFF-4213-9E8F-D666DAEA2E54}.Release|Any CPU.Build.0 = Release|Any CPU + {DF2456D1-0CFF-4213-9E8F-D666DAEA2E54}.Release|x64.ActiveCfg = Release|Any CPU + {DF2456D1-0CFF-4213-9E8F-D666DAEA2E54}.Release|x64.Build.0 = Release|Any CPU + {DF2456D1-0CFF-4213-9E8F-D666DAEA2E54}.Release|x86.ActiveCfg = Release|Any CPU + {DF2456D1-0CFF-4213-9E8F-D666DAEA2E54}.Release|x86.Build.0 = Release|Any CPU + {1980F468-08B9-4159-B63D-4197EE6FC83B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1980F468-08B9-4159-B63D-4197EE6FC83B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1980F468-08B9-4159-B63D-4197EE6FC83B}.Debug|x64.ActiveCfg = Debug|Any CPU + {1980F468-08B9-4159-B63D-4197EE6FC83B}.Debug|x64.Build.0 = Debug|Any CPU + {1980F468-08B9-4159-B63D-4197EE6FC83B}.Debug|x86.ActiveCfg = Debug|Any CPU + {1980F468-08B9-4159-B63D-4197EE6FC83B}.Debug|x86.Build.0 = Debug|Any CPU + {1980F468-08B9-4159-B63D-4197EE6FC83B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1980F468-08B9-4159-B63D-4197EE6FC83B}.Release|Any CPU.Build.0 = Release|Any CPU + {1980F468-08B9-4159-B63D-4197EE6FC83B}.Release|x64.ActiveCfg = Release|Any CPU + {1980F468-08B9-4159-B63D-4197EE6FC83B}.Release|x64.Build.0 = Release|Any CPU + {1980F468-08B9-4159-B63D-4197EE6FC83B}.Release|x86.ActiveCfg = Release|Any CPU + {1980F468-08B9-4159-B63D-4197EE6FC83B}.Release|x86.Build.0 = Release|Any CPU + {957FCED2-6B1E-490E-97B4-2B123E565785}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {957FCED2-6B1E-490E-97B4-2B123E565785}.Debug|Any CPU.Build.0 = Debug|Any CPU + {957FCED2-6B1E-490E-97B4-2B123E565785}.Debug|x64.ActiveCfg = Debug|Any CPU + {957FCED2-6B1E-490E-97B4-2B123E565785}.Debug|x64.Build.0 = Debug|Any CPU + {957FCED2-6B1E-490E-97B4-2B123E565785}.Debug|x86.ActiveCfg = Debug|Any CPU + {957FCED2-6B1E-490E-97B4-2B123E565785}.Debug|x86.Build.0 = Debug|Any CPU + {957FCED2-6B1E-490E-97B4-2B123E565785}.Release|Any CPU.ActiveCfg = Release|Any CPU + {957FCED2-6B1E-490E-97B4-2B123E565785}.Release|Any CPU.Build.0 = Release|Any CPU + {957FCED2-6B1E-490E-97B4-2B123E565785}.Release|x64.ActiveCfg = Release|Any CPU + {957FCED2-6B1E-490E-97B4-2B123E565785}.Release|x64.Build.0 = Release|Any CPU + {957FCED2-6B1E-490E-97B4-2B123E565785}.Release|x86.ActiveCfg = Release|Any CPU + {957FCED2-6B1E-490E-97B4-2B123E565785}.Release|x86.Build.0 = Release|Any CPU + {C67F2919-4530-4EAF-B1A1-CCED621BCCFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C67F2919-4530-4EAF-B1A1-CCED621BCCFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C67F2919-4530-4EAF-B1A1-CCED621BCCFE}.Debug|x64.ActiveCfg = Debug|Any CPU + {C67F2919-4530-4EAF-B1A1-CCED621BCCFE}.Debug|x64.Build.0 = Debug|Any CPU + {C67F2919-4530-4EAF-B1A1-CCED621BCCFE}.Debug|x86.ActiveCfg = Debug|Any CPU + {C67F2919-4530-4EAF-B1A1-CCED621BCCFE}.Debug|x86.Build.0 = Debug|Any CPU + {C67F2919-4530-4EAF-B1A1-CCED621BCCFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C67F2919-4530-4EAF-B1A1-CCED621BCCFE}.Release|Any CPU.Build.0 = Release|Any CPU + {C67F2919-4530-4EAF-B1A1-CCED621BCCFE}.Release|x64.ActiveCfg = Release|Any CPU + {C67F2919-4530-4EAF-B1A1-CCED621BCCFE}.Release|x64.Build.0 = Release|Any CPU + {C67F2919-4530-4EAF-B1A1-CCED621BCCFE}.Release|x86.ActiveCfg = Release|Any CPU + {C67F2919-4530-4EAF-B1A1-CCED621BCCFE}.Release|x86.Build.0 = Release|Any CPU + {8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E}.Debug|x64.ActiveCfg = Debug|Any CPU + {8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E}.Debug|x64.Build.0 = Debug|Any CPU + {8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E}.Debug|x86.ActiveCfg = Debug|Any CPU + {8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E}.Debug|x86.Build.0 = Debug|Any CPU + {8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E}.Release|Any CPU.Build.0 = Release|Any CPU + {8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E}.Release|x64.ActiveCfg = Release|Any CPU + {8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E}.Release|x64.Build.0 = Release|Any CPU + {8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E}.Release|x86.ActiveCfg = Release|Any CPU + {8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E}.Release|x86.Build.0 = Release|Any CPU + {035C2D5E-18DD-4CFC-98E2-0A8BF69A1511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {035C2D5E-18DD-4CFC-98E2-0A8BF69A1511}.Debug|Any CPU.Build.0 = Debug|Any CPU + {035C2D5E-18DD-4CFC-98E2-0A8BF69A1511}.Debug|x64.ActiveCfg = Debug|Any CPU + {035C2D5E-18DD-4CFC-98E2-0A8BF69A1511}.Debug|x64.Build.0 = Debug|Any CPU + {035C2D5E-18DD-4CFC-98E2-0A8BF69A1511}.Debug|x86.ActiveCfg = Debug|Any CPU + {035C2D5E-18DD-4CFC-98E2-0A8BF69A1511}.Debug|x86.Build.0 = Debug|Any CPU + {035C2D5E-18DD-4CFC-98E2-0A8BF69A1511}.Release|Any CPU.ActiveCfg = Release|Any CPU + {035C2D5E-18DD-4CFC-98E2-0A8BF69A1511}.Release|Any CPU.Build.0 = Release|Any CPU + {035C2D5E-18DD-4CFC-98E2-0A8BF69A1511}.Release|x64.ActiveCfg = Release|Any CPU + {035C2D5E-18DD-4CFC-98E2-0A8BF69A1511}.Release|x64.Build.0 = Release|Any CPU + {035C2D5E-18DD-4CFC-98E2-0A8BF69A1511}.Release|x86.ActiveCfg = Release|Any CPU + {035C2D5E-18DD-4CFC-98E2-0A8BF69A1511}.Release|x86.Build.0 = Release|Any CPU + {2FAEDBBC-6F75-46AC-A281-F507412EDF6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2FAEDBBC-6F75-46AC-A281-F507412EDF6A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2FAEDBBC-6F75-46AC-A281-F507412EDF6A}.Debug|x64.ActiveCfg = Debug|Any CPU + {2FAEDBBC-6F75-46AC-A281-F507412EDF6A}.Debug|x64.Build.0 = Debug|Any CPU + {2FAEDBBC-6F75-46AC-A281-F507412EDF6A}.Debug|x86.ActiveCfg = Debug|Any CPU + {2FAEDBBC-6F75-46AC-A281-F507412EDF6A}.Debug|x86.Build.0 = Debug|Any CPU + {2FAEDBBC-6F75-46AC-A281-F507412EDF6A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2FAEDBBC-6F75-46AC-A281-F507412EDF6A}.Release|Any CPU.Build.0 = Release|Any CPU + {2FAEDBBC-6F75-46AC-A281-F507412EDF6A}.Release|x64.ActiveCfg = Release|Any CPU + {2FAEDBBC-6F75-46AC-A281-F507412EDF6A}.Release|x64.Build.0 = Release|Any CPU + {2FAEDBBC-6F75-46AC-A281-F507412EDF6A}.Release|x86.ActiveCfg = Release|Any CPU + {2FAEDBBC-6F75-46AC-A281-F507412EDF6A}.Release|x86.Build.0 = Release|Any CPU + {F00538BE-2AF8-4388-BF7C-59934838B7BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F00538BE-2AF8-4388-BF7C-59934838B7BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F00538BE-2AF8-4388-BF7C-59934838B7BD}.Debug|x64.ActiveCfg = Debug|Any CPU + {F00538BE-2AF8-4388-BF7C-59934838B7BD}.Debug|x64.Build.0 = Debug|Any CPU + {F00538BE-2AF8-4388-BF7C-59934838B7BD}.Debug|x86.ActiveCfg = Debug|Any CPU + {F00538BE-2AF8-4388-BF7C-59934838B7BD}.Debug|x86.Build.0 = Debug|Any CPU + {F00538BE-2AF8-4388-BF7C-59934838B7BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F00538BE-2AF8-4388-BF7C-59934838B7BD}.Release|Any CPU.Build.0 = Release|Any CPU + {F00538BE-2AF8-4388-BF7C-59934838B7BD}.Release|x64.ActiveCfg = Release|Any CPU + {F00538BE-2AF8-4388-BF7C-59934838B7BD}.Release|x64.Build.0 = Release|Any CPU + {F00538BE-2AF8-4388-BF7C-59934838B7BD}.Release|x86.ActiveCfg = Release|Any CPU + {F00538BE-2AF8-4388-BF7C-59934838B7BD}.Release|x86.Build.0 = Release|Any CPU + {06A50D0C-EAF4-4599-83A9-B23C0756ADF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06A50D0C-EAF4-4599-83A9-B23C0756ADF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06A50D0C-EAF4-4599-83A9-B23C0756ADF5}.Debug|x64.ActiveCfg = Debug|Any CPU + {06A50D0C-EAF4-4599-83A9-B23C0756ADF5}.Debug|x64.Build.0 = Debug|Any CPU + {06A50D0C-EAF4-4599-83A9-B23C0756ADF5}.Debug|x86.ActiveCfg = Debug|Any CPU + {06A50D0C-EAF4-4599-83A9-B23C0756ADF5}.Debug|x86.Build.0 = Debug|Any CPU + {06A50D0C-EAF4-4599-83A9-B23C0756ADF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06A50D0C-EAF4-4599-83A9-B23C0756ADF5}.Release|Any CPU.Build.0 = Release|Any CPU + {06A50D0C-EAF4-4599-83A9-B23C0756ADF5}.Release|x64.ActiveCfg = Release|Any CPU + {06A50D0C-EAF4-4599-83A9-B23C0756ADF5}.Release|x64.Build.0 = Release|Any CPU + {06A50D0C-EAF4-4599-83A9-B23C0756ADF5}.Release|x86.ActiveCfg = Release|Any CPU + {06A50D0C-EAF4-4599-83A9-B23C0756ADF5}.Release|x86.Build.0 = Release|Any CPU + {86DEDD76-E85D-44C9-BDF6-7D3299F0AED7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {86DEDD76-E85D-44C9-BDF6-7D3299F0AED7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86DEDD76-E85D-44C9-BDF6-7D3299F0AED7}.Debug|x64.ActiveCfg = Debug|Any CPU + {86DEDD76-E85D-44C9-BDF6-7D3299F0AED7}.Debug|x64.Build.0 = Debug|Any CPU + {86DEDD76-E85D-44C9-BDF6-7D3299F0AED7}.Debug|x86.ActiveCfg = Debug|Any CPU + {86DEDD76-E85D-44C9-BDF6-7D3299F0AED7}.Debug|x86.Build.0 = Debug|Any CPU + {86DEDD76-E85D-44C9-BDF6-7D3299F0AED7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {86DEDD76-E85D-44C9-BDF6-7D3299F0AED7}.Release|Any CPU.Build.0 = Release|Any CPU + {86DEDD76-E85D-44C9-BDF6-7D3299F0AED7}.Release|x64.ActiveCfg = Release|Any CPU + {86DEDD76-E85D-44C9-BDF6-7D3299F0AED7}.Release|x64.Build.0 = Release|Any CPU + {86DEDD76-E85D-44C9-BDF6-7D3299F0AED7}.Release|x86.ActiveCfg = Release|Any CPU + {86DEDD76-E85D-44C9-BDF6-7D3299F0AED7}.Release|x86.Build.0 = Release|Any CPU + {994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9}.Debug|x64.ActiveCfg = Debug|Any CPU + {994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9}.Debug|x64.Build.0 = Debug|Any CPU + {994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9}.Debug|x86.ActiveCfg = Debug|Any CPU + {994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9}.Debug|x86.Build.0 = Debug|Any CPU + {994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9}.Release|Any CPU.Build.0 = Release|Any CPU + {994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9}.Release|x64.ActiveCfg = Release|Any CPU + {994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9}.Release|x64.Build.0 = Release|Any CPU + {994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9}.Release|x86.ActiveCfg = Release|Any CPU + {994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9}.Release|x86.Build.0 = Release|Any CPU + {8F625A63-FFCD-4F8F-8C5C-97252728AA8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F625A63-FFCD-4F8F-8C5C-97252728AA8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F625A63-FFCD-4F8F-8C5C-97252728AA8B}.Debug|x64.ActiveCfg = Debug|Any CPU + {8F625A63-FFCD-4F8F-8C5C-97252728AA8B}.Debug|x64.Build.0 = Debug|Any CPU + {8F625A63-FFCD-4F8F-8C5C-97252728AA8B}.Debug|x86.ActiveCfg = Debug|Any CPU + {8F625A63-FFCD-4F8F-8C5C-97252728AA8B}.Debug|x86.Build.0 = Debug|Any CPU + {8F625A63-FFCD-4F8F-8C5C-97252728AA8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F625A63-FFCD-4F8F-8C5C-97252728AA8B}.Release|Any CPU.Build.0 = Release|Any CPU + {8F625A63-FFCD-4F8F-8C5C-97252728AA8B}.Release|x64.ActiveCfg = Release|Any CPU + {8F625A63-FFCD-4F8F-8C5C-97252728AA8B}.Release|x64.Build.0 = Release|Any CPU + {8F625A63-FFCD-4F8F-8C5C-97252728AA8B}.Release|x86.ActiveCfg = Release|Any CPU + {8F625A63-FFCD-4F8F-8C5C-97252728AA8B}.Release|x86.Build.0 = Release|Any CPU + {BC735304-7981-4854-A9EE-00FD55943BF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC735304-7981-4854-A9EE-00FD55943BF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC735304-7981-4854-A9EE-00FD55943BF4}.Debug|x64.ActiveCfg = Debug|Any CPU + {BC735304-7981-4854-A9EE-00FD55943BF4}.Debug|x64.Build.0 = Debug|Any CPU + {BC735304-7981-4854-A9EE-00FD55943BF4}.Debug|x86.ActiveCfg = Debug|Any CPU + {BC735304-7981-4854-A9EE-00FD55943BF4}.Debug|x86.Build.0 = Debug|Any CPU + {BC735304-7981-4854-A9EE-00FD55943BF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC735304-7981-4854-A9EE-00FD55943BF4}.Release|Any CPU.Build.0 = Release|Any CPU + {BC735304-7981-4854-A9EE-00FD55943BF4}.Release|x64.ActiveCfg = Release|Any CPU + {BC735304-7981-4854-A9EE-00FD55943BF4}.Release|x64.Build.0 = Release|Any CPU + {BC735304-7981-4854-A9EE-00FD55943BF4}.Release|x86.ActiveCfg = Release|Any CPU + {BC735304-7981-4854-A9EE-00FD55943BF4}.Release|x86.Build.0 = Release|Any CPU + {EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80}.Debug|x64.ActiveCfg = Debug|Any CPU + {EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80}.Debug|x64.Build.0 = Debug|Any CPU + {EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80}.Debug|x86.ActiveCfg = Debug|Any CPU + {EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80}.Debug|x86.Build.0 = Debug|Any CPU + {EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80}.Release|Any CPU.Build.0 = Release|Any CPU + {EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80}.Release|x64.ActiveCfg = Release|Any CPU + {EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80}.Release|x64.Build.0 = Release|Any CPU + {EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80}.Release|x86.ActiveCfg = Release|Any CPU + {EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80}.Release|x86.Build.0 = Release|Any CPU + {E6717F55-79BC-494A-84B1-215491824F20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6717F55-79BC-494A-84B1-215491824F20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6717F55-79BC-494A-84B1-215491824F20}.Debug|x64.ActiveCfg = Debug|Any CPU + {E6717F55-79BC-494A-84B1-215491824F20}.Debug|x64.Build.0 = Debug|Any CPU + {E6717F55-79BC-494A-84B1-215491824F20}.Debug|x86.ActiveCfg = Debug|Any CPU + {E6717F55-79BC-494A-84B1-215491824F20}.Debug|x86.Build.0 = Debug|Any CPU + {E6717F55-79BC-494A-84B1-215491824F20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6717F55-79BC-494A-84B1-215491824F20}.Release|Any CPU.Build.0 = Release|Any CPU + {E6717F55-79BC-494A-84B1-215491824F20}.Release|x64.ActiveCfg = Release|Any CPU + {E6717F55-79BC-494A-84B1-215491824F20}.Release|x64.Build.0 = Release|Any CPU + {E6717F55-79BC-494A-84B1-215491824F20}.Release|x86.ActiveCfg = Release|Any CPU + {E6717F55-79BC-494A-84B1-215491824F20}.Release|x86.Build.0 = Release|Any CPU + {7CA07B2A-F612-47F0-8F4F-E7771BF9DED1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CA07B2A-F612-47F0-8F4F-E7771BF9DED1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CA07B2A-F612-47F0-8F4F-E7771BF9DED1}.Debug|x64.ActiveCfg = Debug|Any CPU + {7CA07B2A-F612-47F0-8F4F-E7771BF9DED1}.Debug|x64.Build.0 = Debug|Any CPU + {7CA07B2A-F612-47F0-8F4F-E7771BF9DED1}.Debug|x86.ActiveCfg = Debug|Any CPU + {7CA07B2A-F612-47F0-8F4F-E7771BF9DED1}.Debug|x86.Build.0 = Debug|Any CPU + {7CA07B2A-F612-47F0-8F4F-E7771BF9DED1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CA07B2A-F612-47F0-8F4F-E7771BF9DED1}.Release|Any CPU.Build.0 = Release|Any CPU + {7CA07B2A-F612-47F0-8F4F-E7771BF9DED1}.Release|x64.ActiveCfg = Release|Any CPU + {7CA07B2A-F612-47F0-8F4F-E7771BF9DED1}.Release|x64.Build.0 = Release|Any CPU + {7CA07B2A-F612-47F0-8F4F-E7771BF9DED1}.Release|x86.ActiveCfg = Release|Any CPU + {7CA07B2A-F612-47F0-8F4F-E7771BF9DED1}.Release|x86.Build.0 = Release|Any CPU + {FA121F56-04BB-46A1-B8C1-0C282295584B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA121F56-04BB-46A1-B8C1-0C282295584B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA121F56-04BB-46A1-B8C1-0C282295584B}.Debug|x64.ActiveCfg = Debug|Any CPU + {FA121F56-04BB-46A1-B8C1-0C282295584B}.Debug|x64.Build.0 = Debug|Any CPU + {FA121F56-04BB-46A1-B8C1-0C282295584B}.Debug|x86.ActiveCfg = Debug|Any CPU + {FA121F56-04BB-46A1-B8C1-0C282295584B}.Debug|x86.Build.0 = Debug|Any CPU + {FA121F56-04BB-46A1-B8C1-0C282295584B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA121F56-04BB-46A1-B8C1-0C282295584B}.Release|Any CPU.Build.0 = Release|Any CPU + {FA121F56-04BB-46A1-B8C1-0C282295584B}.Release|x64.ActiveCfg = Release|Any CPU + {FA121F56-04BB-46A1-B8C1-0C282295584B}.Release|x64.Build.0 = Release|Any CPU + {FA121F56-04BB-46A1-B8C1-0C282295584B}.Release|x86.ActiveCfg = Release|Any CPU + {FA121F56-04BB-46A1-B8C1-0C282295584B}.Release|x86.Build.0 = Release|Any CPU + {3C0A98D5-DAFF-4FF1-9A59-F7409CB66754}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C0A98D5-DAFF-4FF1-9A59-F7409CB66754}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C0A98D5-DAFF-4FF1-9A59-F7409CB66754}.Debug|x64.ActiveCfg = Debug|Any CPU + {3C0A98D5-DAFF-4FF1-9A59-F7409CB66754}.Debug|x64.Build.0 = Debug|Any CPU + {3C0A98D5-DAFF-4FF1-9A59-F7409CB66754}.Debug|x86.ActiveCfg = Debug|Any CPU + {3C0A98D5-DAFF-4FF1-9A59-F7409CB66754}.Debug|x86.Build.0 = Debug|Any CPU + {3C0A98D5-DAFF-4FF1-9A59-F7409CB66754}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C0A98D5-DAFF-4FF1-9A59-F7409CB66754}.Release|Any CPU.Build.0 = Release|Any CPU + {3C0A98D5-DAFF-4FF1-9A59-F7409CB66754}.Release|x64.ActiveCfg = Release|Any CPU + {3C0A98D5-DAFF-4FF1-9A59-F7409CB66754}.Release|x64.Build.0 = Release|Any CPU + {3C0A98D5-DAFF-4FF1-9A59-F7409CB66754}.Release|x86.ActiveCfg = Release|Any CPU + {3C0A98D5-DAFF-4FF1-9A59-F7409CB66754}.Release|x86.Build.0 = Release|Any CPU + {46B6E125-FF47-4453-9172-5DF695687FDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46B6E125-FF47-4453-9172-5DF695687FDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46B6E125-FF47-4453-9172-5DF695687FDF}.Debug|x64.ActiveCfg = Debug|Any CPU + {46B6E125-FF47-4453-9172-5DF695687FDF}.Debug|x64.Build.0 = Debug|Any CPU + {46B6E125-FF47-4453-9172-5DF695687FDF}.Debug|x86.ActiveCfg = Debug|Any CPU + {46B6E125-FF47-4453-9172-5DF695687FDF}.Debug|x86.Build.0 = Debug|Any CPU + {46B6E125-FF47-4453-9172-5DF695687FDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46B6E125-FF47-4453-9172-5DF695687FDF}.Release|Any CPU.Build.0 = Release|Any CPU + {46B6E125-FF47-4453-9172-5DF695687FDF}.Release|x64.ActiveCfg = Release|Any CPU + {46B6E125-FF47-4453-9172-5DF695687FDF}.Release|x64.Build.0 = Release|Any CPU + {46B6E125-FF47-4453-9172-5DF695687FDF}.Release|x86.ActiveCfg = Release|Any CPU + {46B6E125-FF47-4453-9172-5DF695687FDF}.Release|x86.Build.0 = Release|Any CPU + {07A39741-8656-488A-964C-D8387A1925DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07A39741-8656-488A-964C-D8387A1925DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07A39741-8656-488A-964C-D8387A1925DF}.Debug|x64.ActiveCfg = Debug|Any CPU + {07A39741-8656-488A-964C-D8387A1925DF}.Debug|x64.Build.0 = Debug|Any CPU + {07A39741-8656-488A-964C-D8387A1925DF}.Debug|x86.ActiveCfg = Debug|Any CPU + {07A39741-8656-488A-964C-D8387A1925DF}.Debug|x86.Build.0 = Debug|Any CPU + {07A39741-8656-488A-964C-D8387A1925DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07A39741-8656-488A-964C-D8387A1925DF}.Release|Any CPU.Build.0 = Release|Any CPU + {07A39741-8656-488A-964C-D8387A1925DF}.Release|x64.ActiveCfg = Release|Any CPU + {07A39741-8656-488A-964C-D8387A1925DF}.Release|x64.Build.0 = Release|Any CPU + {07A39741-8656-488A-964C-D8387A1925DF}.Release|x86.ActiveCfg = Release|Any CPU + {07A39741-8656-488A-964C-D8387A1925DF}.Release|x86.Build.0 = Release|Any CPU + {76924193-4339-4BC8-8C0B-CA5F0A5047FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76924193-4339-4BC8-8C0B-CA5F0A5047FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76924193-4339-4BC8-8C0B-CA5F0A5047FB}.Debug|x64.ActiveCfg = Debug|Any CPU + {76924193-4339-4BC8-8C0B-CA5F0A5047FB}.Debug|x64.Build.0 = Debug|Any CPU + {76924193-4339-4BC8-8C0B-CA5F0A5047FB}.Debug|x86.ActiveCfg = Debug|Any CPU + {76924193-4339-4BC8-8C0B-CA5F0A5047FB}.Debug|x86.Build.0 = Debug|Any CPU + {76924193-4339-4BC8-8C0B-CA5F0A5047FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76924193-4339-4BC8-8C0B-CA5F0A5047FB}.Release|Any CPU.Build.0 = Release|Any CPU + {76924193-4339-4BC8-8C0B-CA5F0A5047FB}.Release|x64.ActiveCfg = Release|Any CPU + {76924193-4339-4BC8-8C0B-CA5F0A5047FB}.Release|x64.Build.0 = Release|Any CPU + {76924193-4339-4BC8-8C0B-CA5F0A5047FB}.Release|x86.ActiveCfg = Release|Any CPU + {76924193-4339-4BC8-8C0B-CA5F0A5047FB}.Release|x86.Build.0 = Release|Any CPU + {8042BF60-B374-4C40-BB55-7405736A711A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8042BF60-B374-4C40-BB55-7405736A711A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8042BF60-B374-4C40-BB55-7405736A711A}.Debug|x64.ActiveCfg = Debug|Any CPU + {8042BF60-B374-4C40-BB55-7405736A711A}.Debug|x64.Build.0 = Debug|Any CPU + {8042BF60-B374-4C40-BB55-7405736A711A}.Debug|x86.ActiveCfg = Debug|Any CPU + {8042BF60-B374-4C40-BB55-7405736A711A}.Debug|x86.Build.0 = Debug|Any CPU + {8042BF60-B374-4C40-BB55-7405736A711A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8042BF60-B374-4C40-BB55-7405736A711A}.Release|Any CPU.Build.0 = Release|Any CPU + {8042BF60-B374-4C40-BB55-7405736A711A}.Release|x64.ActiveCfg = Release|Any CPU + {8042BF60-B374-4C40-BB55-7405736A711A}.Release|x64.Build.0 = Release|Any CPU + {8042BF60-B374-4C40-BB55-7405736A711A}.Release|x86.ActiveCfg = Release|Any CPU + {8042BF60-B374-4C40-BB55-7405736A711A}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C3CCD71A-37D0-4665-A628-AC6185313A4C} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + {04A5E9A6-9EA9-4771-9766-1241F8341A9B} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + {97B6CA29-9704-4A44-8879-E7015403E743} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {EC9257F5-F59F-4F7C-B316-A153DCB9501C} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + {4FBF6A86-DE1B-45E3-A1CE-B1271E927DF8} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {898F86C5-1886-41BE-97DD-46A182261678} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {88836B48-12C3-4CC7-A73E-0245970589CB} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {297D901A-F3D0-4653-B4EB-CBD3D4402612} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {82456D25-7753-4D1C-9ADC-450948B7FAC0} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {E5408C31-60C9-4A60-89D9-840C8C9E5E2F} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {79094B35-C3C6-4332-8A79-6E3D32582059} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {7311835B-6CE8-470E-AFB4-B76424A86096} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {04F673E8-6A3D-4A60-A829-1B851FDB5A4C} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {1826ECD1-D177-4AEF-B821-8B65535CAA46} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {C6D9607E-AB00-4E29-A8A9-8F378B2D755B} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {19462AF9-35D7-43C8-AFA1-39E2FD1C7EBD} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {745684DE-BABB-4BAA-BC36-DA3029E8C99F} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {F58C0BE6-D91A-42F7-A221-4A882911E61C} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {5F1D96A7-8252-4D60-AE6E-A66C045A8A92} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {AEDD4AE8-A831-46EE-AF49-EAC3183D5971} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {F2387E3F-8B78-4FAC-BDAA-A3E2EBC4D889} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {D509243A-E5AA-430D-B47E-8ADC5DA31125} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {7C8AA7A4-E72E-4A21-BC13-D7BA5A2219DC} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {443B5C89-0904-4AE8-B48F-1E92709FBEF3} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {48EA13C9-7FBC-4DCA-85CF-CCB1493A73C8} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {EF1817C6-1FEB-4185-94F3-6BDB9ED1660A} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {1EF56286-3151-4D13-B662-C23CBE7148FE} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {5FF3DBB0-B465-46C5-ADB7-CE8998A6E7BA} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {F743054C-0D7F-4352-9DBD-365C4CFA19C6} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {509EA8AB-5717-4953-87FC-F3EDD041F200} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {A9677606-014A-43A4-83D1-7871D1547862} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {42B441C3-43AD-4D8F-96E0-547D7257FB04} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {DD4CFE16-3F58-4AA0-A995-312AFC55F0B3} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {CC31B699-7B15-4685-8E00-01E2DE6CA88B} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {ED59B9E2-82EC-4984-9A27-E9DFF3B13B60} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {6FC8F397-8F3D-4300-A8B3-6F76B68C959B} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {7B9E64D1-70C9-4462-BE0B-A965A2825919} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {82D6EBAF-B0B6-4744-A556-E5667DF823B7} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {410366DF-4121-44DE-923D-218ACBA2F8CC} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {A8E066A4-00E7-4209-BB1E-0BD9A2FBAC8E} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {656422FD-47DA-4F06-AB76-57FF1F77B040} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {502B8111-F428-4584-AE44-D1AD3B4F16B5} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {174CF08D-E13D-4904-90C0-CC740C9DE086} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {853E3B54-DB8A-4A82-8565-E6103130DA3C} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {B8DC7FD1-782F-4246-B096-836D82B29A03} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {8AE239F7-A0B1-459A-AF6F-FB1501544C2B} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {FB7DB850-11DB-4F7B-9003-13C145442C73} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {23BEB12C-DE15-4B88-96F6-5DBDE2E9BA62} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {DF2456D1-0CFF-4213-9E8F-D666DAEA2E54} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {1980F468-08B9-4159-B63D-4197EE6FC83B} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {957FCED2-6B1E-490E-97B4-2B123E565785} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {C67F2919-4530-4EAF-B1A1-CCED621BCCFE} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {8CE6E5B6-9BE6-4E8E-B17A-A9CA0148842E} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {035C2D5E-18DD-4CFC-98E2-0A8BF69A1511} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {2FAEDBBC-6F75-46AC-A281-F507412EDF6A} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {F00538BE-2AF8-4388-BF7C-59934838B7BD} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {06A50D0C-EAF4-4599-83A9-B23C0756ADF5} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {86DEDD76-E85D-44C9-BDF6-7D3299F0AED7} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {994DA72D-7712-4AF4-A1FC-6E53CFD9ACD9} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {8F625A63-FFCD-4F8F-8C5C-97252728AA8B} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {BC735304-7981-4854-A9EE-00FD55943BF4} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {EF4C2EAF-8CF8-4E43-BBAA-9A2017C43B80} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {E6717F55-79BC-494A-84B1-215491824F20} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {7CA07B2A-F612-47F0-8F4F-E7771BF9DED1} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {FA121F56-04BB-46A1-B8C1-0C282295584B} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {3C0A98D5-DAFF-4FF1-9A59-F7409CB66754} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {46B6E125-FF47-4453-9172-5DF695687FDF} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} + {07A39741-8656-488A-964C-D8387A1925DF} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + {76924193-4339-4BC8-8C0B-CA5F0A5047FB} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + {8042BF60-B374-4C40-BB55-7405736A711A} = {EC9257F5-F59F-4F7C-B316-A153DCB9501C} EndGlobalSection EndGlobal diff --git a/Elib2EbookCli/Dockerfile.disabled b/Elib2EbookCli/Dockerfile.disabled deleted file mode 100644 index 030092aa..00000000 --- a/Elib2EbookCli/Dockerfile.disabled +++ /dev/null @@ -1,11 +0,0 @@ -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build-env -WORKDIR /app - -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -FROM mcr.microsoft.com/dotnet/aspnet:9.0 -WORKDIR /app -COPY --from=build-env /app/publish . -ENTRYPOINT ["dotnet", "Elib2EbookCli.dll"] \ No newline at end of file diff --git a/Elib2EbookCli/Elib2EbookCli.csproj b/Elib2EbookCli/Elib2EbookCli.csproj deleted file mode 100644 index d8a2dce9..00000000 --- a/Elib2EbookCli/Elib2EbookCli.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net10.0 - enable - enable - - - - - - - - - - - - - .dockerignore - - - - diff --git a/Elib2EbookWeb/Components/App.razor b/Elib2EbookWeb/Components/App.razor deleted file mode 100644 index cf995804..00000000 --- a/Elib2EbookWeb/Components/App.razor +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Elib2EbookWeb/Components/BookForm.razor b/Elib2EbookWeb/Components/BookForm.razor deleted file mode 100644 index 2c7f20d3..00000000 --- a/Elib2EbookWeb/Components/BookForm.razor +++ /dev/null @@ -1,160 +0,0 @@ -@using Core.Configs -@using Core.Extensions -@using Elib2EbookWeb.Misc -@inject IJSRuntime JsRuntime -@inject ISnackbar Snackbar - - - - - - - - - - - - - - - - - - - @if (_startEndCSwitcher) - { -
- - -
- } - else - { -
- - -
- } - - - По номеру - По названию - -
-
- - - @if (_onGenerate) - { - - @if (_onGenerate) - { - @if (!string.IsNullOrWhiteSpace(_log)) - { - @_log - } - else - { - - } - } - - } - else - { - - - Скачать - - - Скачать без картинок - - - } - - -@code { - private readonly Options _options = new(); - private string _customOptionsText = string.Empty; - private ActionLogger? _logger; - private bool _onGenerate; - private string? _log; - private bool _startEndCSwitcher; - - private bool _isShow; - private InputType _passwordInput = InputType.Password; - private string _passwordInputIcon = Icons.Material.Filled.VisibilityOff; - - [Parameter] public required Func OnClick { get; set; } - - void ShowPassword() - { - if (_isShow) - { - _isShow = false; - _passwordInputIcon = Icons.Material.Filled.VisibilityOff; - _passwordInput = InputType.Password; - } - else - { - _isShow = true; - _passwordInputIcon = Icons.Material.Filled.Visibility; - _passwordInput = InputType.Text; - } - } - - private async Task WithImages() - { - _options.NoImage = false; - await Download(); - } - - private async Task NoImages() - { - _options.NoImage = true; - await Download(); - } - - private async Task Download() - { - if (_options.Url == null || !_options.Url.Any() || string.IsNullOrWhiteSpace(_options.Url.First())) - { - Snackbar.Add("Укажите ссылку на книгу", Severity.Error); - return; - } - - if (_options.Format == null || !_options.Format.Any()) - { - Snackbar.Add("Выберите формат", Severity.Error); - return; - } - - _options.CustomOptions = string.IsNullOrWhiteSpace(_customOptionsText) - ? Array.Empty() - : _customOptionsText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) - .Select(line => line.Trim()) - .Where(line => !string.IsNullOrWhiteSpace(line)) - .ToList(); - - _onGenerate = true; - _logger = new ActionLogger(str => - { - _log = str.Crop(300); - InvokeAsync(StateHasChanged); - }); - - await OnClick(_options, _logger); - _onGenerate = false; - } - - private void OnSwitch(bool obj) - { - _startEndCSwitcher = obj; - - _options.Start = default; - _options.End = default; - _options.StartName = default; - _options.EndName = default; - } - -} diff --git a/Elib2EbookWeb/Components/Layout/MainLayout.razor b/Elib2EbookWeb/Components/Layout/MainLayout.razor deleted file mode 100644 index 2e0f862d..00000000 --- a/Elib2EbookWeb/Components/Layout/MainLayout.razor +++ /dev/null @@ -1,27 +0,0 @@ -@inherits LayoutComponentBase - - - - - -
-
- - Elib2Ebook - - - - - - -
- @Body -
-
-
- -
- An unhandled error has occurred. - Reload - 🗙 -
diff --git a/Elib2EbookWeb/Components/Layout/MainLayout.razor.css b/Elib2EbookWeb/Components/Layout/MainLayout.razor.css deleted file mode 100644 index 9c4a0f37..00000000 --- a/Elib2EbookWeb/Components/Layout/MainLayout.razor.css +++ /dev/null @@ -1,96 +0,0 @@ -.page { - position: relative; - display: flex; - flex-direction: column; -} - -main { - flex: 1; -} - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - -.top-row { - background-color: #f7f7f7; - border-bottom: 1px solid #d6d5d5; - justify-content: flex-end; - height: 3.5rem; - display: flex; - align-items: center; -} - - .top-row ::deep a, .top-row ::deep .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - text-decoration: none; - } - - .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { - text-decoration: underline; - } - - .top-row ::deep a:first-child { - overflow: hidden; - text-overflow: ellipsis; - } - -@media (max-width: 640.98px) { - .top-row { - justify-content: space-between; - } - - .top-row ::deep a, .top-row ::deep .btn-link { - margin-left: 0; - } -} - -@media (min-width: 641px) { - .page { - flex-direction: row; - } - - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } - - .top-row { - position: sticky; - top: 0; - z-index: 1; - } - - .top-row.auth ::deep a:first-child { - flex: 1; - text-align: right; - width: 0; - } - - .top-row, article { - padding-left: 2rem !important; - padding-right: 1.5rem !important; - } -} - -#blazor-error-ui { - background: lightyellow; - bottom: 0; - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; - width: 100%; - z-index: 1000; -} - - #blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; - } \ No newline at end of file diff --git a/Elib2EbookWeb/Components/PWA.razor b/Elib2EbookWeb/Components/PWA.razor deleted file mode 100644 index a074810d..00000000 --- a/Elib2EbookWeb/Components/PWA.razor +++ /dev/null @@ -1,22 +0,0 @@ -Установить приложение - - diff --git a/Elib2EbookWeb/Components/Pages/Error.razor b/Elib2EbookWeb/Components/Pages/Error.razor deleted file mode 100644 index 2199bfd7..00000000 --- a/Elib2EbookWeb/Components/Pages/Error.razor +++ /dev/null @@ -1,39 +0,0 @@ -@page "/Error" -@using System.Diagnostics - -Error - -

Error.

-

An error occurred while processing your request.

- -@if (ShowRequestId) -{ -

- Request ID: @RequestId -

-} - -

Development Mode

-

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

-

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

- -@code{ - - [CascadingParameter] private HttpContext? HttpContext { get; set; } - - private string? RequestId { get; set; } - private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - - protected override void OnInitialized() - { - RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; - } - -} diff --git a/Elib2EbookWeb/Components/ShortBook.razor b/Elib2EbookWeb/Components/ShortBook.razor deleted file mode 100644 index 9c01d33d..00000000 --- a/Elib2EbookWeb/Components/ShortBook.razor +++ /dev/null @@ -1,77 +0,0 @@ -@using Core.Types.Book - - - - - - - - - @Book.Title - - - @if (Book.Author != default) - { - @if (Book.Author.Url == default) - { - Автор: @Book.Author.Name - } - else - { - Автор: @Book.Author.Name - } - } - - @if (Book.Seria != default) - { - @if (Book.Seria?.Url == default) - { - Серия: @Seria - } - else - { - Серия: @Seria - } - } - - @if (!string.IsNullOrWhiteSpace(Book.Annotation)) - { - @((MarkupString)Book.Annotation) - } - - - - - -@code { - [Parameter] public required Book Book { get; set; } - - private string? _cover; - - private string? _coverPath; - - private string Seria => Book.Seria?.Name + (string.IsNullOrWhiteSpace(Book.Seria?.Number) ? string.Empty : $" #{Book.Seria.Number}"); - - protected override Task OnInitializedAsync() - { - InitCover(); - return base.OnInitializedAsync(); - } - - protected override Task OnParametersSetAsync() - { - if (Book.Cover?.FilePath != _coverPath) - { - InitCover(); - } - - return base.OnParametersSetAsync(); - } - - private void InitCover() - { - _cover = Book.Cover != default && Book.Cover.Content.Length > 0 ? $"data:image/png;base64, {Convert.ToBase64String(Book.Cover.Content)}" : _cover; - _coverPath = Book.Cover?.FilePath; - } - -} diff --git a/Elib2EbookWeb/Dockerfile.disabled b/Elib2EbookWeb/Dockerfile.disabled deleted file mode 100644 index 3ef9cf8d..00000000 --- a/Elib2EbookWeb/Dockerfile.disabled +++ /dev/null @@ -1,13 +0,0 @@ -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build-env -WORKDIR /app -EXPOSE 8080 -EXPOSE 8081 - -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish /p:UseAppHost=false - -FROM mcr.microsoft.com/dotnet/aspnet:9.0 -WORKDIR /app -COPY --from=build-env /app/publish . -ENTRYPOINT ["dotnet", "Elib2EbookWeb.dll"] \ No newline at end of file diff --git a/Elib2EbookWeb/Elib2EbookWeb.csproj b/Elib2EbookWeb/Elib2EbookWeb.csproj deleted file mode 100644 index 07cf4f34..00000000 --- a/Elib2EbookWeb/Elib2EbookWeb.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - net10.0 - enable - enable - Linux - - - - - - - - - - - - - .dockerignore - - - PreserveNewest - - - - diff --git a/Elib2EbookWeb/Misc/SbWriter.cs b/Elib2EbookWeb/Misc/SbWriter.cs deleted file mode 100644 index 595bec78..00000000 --- a/Elib2EbookWeb/Misc/SbWriter.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Elib2EbookWeb.Misc; - -public class ActionLogger : ILogger -{ - private readonly Action _action; - - public ActionLogger(Action action) - { - _action = action; - } - - public IDisposable? BeginScope(TState state) where TState : notnull - { - return default!; - } - - public bool IsEnabled(LogLevel logLevel) - { - return true; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, - Func formatter) - { - _action($"{formatter(state, exception)}"); - } -} diff --git a/Elib2EbookWeb/Properties/launchSettings.json b/Elib2EbookWeb/Properties/launchSettings.json deleted file mode 100644 index 364efbdc..00000000 --- a/Elib2EbookWeb/Properties/launchSettings.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:41898", - "sslPort": 44383 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:5271", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:7188;http://localhost:5271", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } - } diff --git a/Elib2EbookWeb/wwwroot/app.css b/Elib2EbookWeb/wwwroot/app.css deleted file mode 100644 index 5dfd85f2..00000000 --- a/Elib2EbookWeb/wwwroot/app.css +++ /dev/null @@ -1,58 +0,0 @@ -html, body { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -a, .btn-link { - color: #006bb7; -} - -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { - box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; -} - -.content { - padding-top: 1.1rem; -} - -h1:focus { - outline: none; -} - -.valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; -} - -.invalid { - outline: 1px solid #e50000; -} - -.validation-message { - color: #e50000; -} - -.blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; - padding: 1rem 1rem 1rem 3.7rem; - color: white; -} - - .blazor-error-boundary::after { - content: "An error has occurred." - } - -.darker-border-checkbox.form-check-input { - border-color: #929292; -} - -.autoscrollable-wrapper { - overflow: auto; - max-height: 100%; - display: flex; - flex-direction: column-reverse; -} \ No newline at end of file diff --git a/Elib2EbookWeb/wwwroot/bootstrap/bootstrap.min.css b/Elib2EbookWeb/wwwroot/bootstrap/bootstrap.min.css deleted file mode 100644 index 02ae65b5..00000000 --- a/Elib2EbookWeb/wwwroot/bootstrap/bootstrap.min.css +++ /dev/null @@ -1,7 +0,0 @@ -@charset "UTF-8";/*! - * Bootstrap v5.1.0 (https://getbootstrap.com/) - * Copyright 2011-2021 The Bootstrap Authors - * Copyright 2011-2021 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-rgb:33,37,41;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/Elib2EbookWeb/wwwroot/bootstrap/bootstrap.min.css.map b/Elib2EbookWeb/wwwroot/bootstrap/bootstrap.min.css.map deleted file mode 100644 index afcd9e33..00000000 --- a/Elib2EbookWeb/wwwroot/bootstrap/bootstrap.min.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Bootstrap v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n// scss-docs-start import-stack\n// Configuration\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"utilities\";\n\n// Layout & components\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"containers\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"accordion\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"alert\";\n@import \"progress\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"offcanvas\";\n@import \"placeholders\";\n\n// Helpers\n@import \"helpers\";\n\n// Utilities\n@import \"utilities/api\";\n// scss-docs-end import-stack\n",":root {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$variable-prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$variable-prefix}#{$color}-rgb: #{$value};\n }\n\n --#{$variable-prefix}white-rgb: #{to-rgb($white)};\n --#{$variable-prefix}black-rgb: #{to-rgb($black)};\n --#{$variable-prefix}body-rgb: #{to-rgb($body-color)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$variable-prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$variable-prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$variable-prefix}gradient: #{$gradient};\n\n // Root and body\n // stylelint-disable custom-property-empty-line-before\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$variable-prefix}root-font-size: #{$font-size-root};\n }\n --#{$variable-prefix}body-font-family: #{$font-family-base};\n --#{$variable-prefix}body-font-size: #{$font-size-base};\n --#{$variable-prefix}body-font-weight: #{$font-weight-base};\n --#{$variable-prefix}body-line-height: #{$line-height-base};\n --#{$variable-prefix}body-color: #{$body-color};\n @if $body-text-align != null {\n --#{$variable-prefix}body-text-align: #{$body-text-align};\n }\n --#{$variable-prefix}body-bg: #{$body-bg};\n // scss-docs-end root-body-variables\n // stylelint-enable custom-property-empty-line-before\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n font-size: var(--#{$variable-prefix}-root-font-size);\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$variable-prefix}body-font-family);\n @include font-size(var(--#{$variable-prefix}body-font-size));\n font-weight: var(--#{$variable-prefix}body-font-weight);\n line-height: var(--#{$variable-prefix}body-line-height);\n color: var(--#{$variable-prefix}body-color);\n text-align: var(--#{$variable-prefix}body-text-align);\n background-color: var(--#{$variable-prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n background-color: currentColor;\n border: 0;\n opacity: $hr-opacity;\n}\n\nhr:not([size]) {\n height: $hr-height; // 2\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

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

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

+ + + +
+ +
+ @if (_showRange) + { +
+
+ + +
+ + @if (_startEndCSwitcher) + { +
+
+ +
+
+
+ +
+
+
+ } + else + { +
+
+ +
+
+
+ +
+
+
+ } +
+ } +
+
+ + +
+ @if (_onGenerate) + { +
+
+ @if (!string.IsNullOrWhiteSpace(_log)) + { + @_log + } + else + { +
+ } +
+
+ } + else + { +
+ +
+ + @if (_showMenu) + { +
+ +
+ } +
+
+ } +
+ + +@code { + private readonly Options _options = new(); + private readonly List _formats = []; + private ActionLogger _logger; + private bool _onGenerate; + private string _log; + private bool _startEndCSwitcher; + private bool _isShow; + private bool _showMenu; + private bool _showAuth; + private bool _showRange; + private string _url; + private string _startNumStr = ""; + private string _endNumStr = ""; + + [Parameter] + public required Func OnClick { get; set; } + + private string GetChipClass(string format) + => _formats.Contains(format) ? "format-card-selected" : ""; + + private bool IsFormatSelected(string format) + => _formats.Contains(format); + + private string GetPasswordType() + => _isShow ? "text" : "password"; + + private string GetRadioClass(bool switcher) + => switcher == _startEndCSwitcher ? "radio-selected" : ""; + + private string GetCircleClass(bool switcher) + => switcher == _startEndCSwitcher ? "circle-selected" : ""; + + private void ToggleFormat(string format) + { + if (_formats.Contains(format)) + _formats.Remove(format); + else + _formats.Add(format); + } + + private void ShowPassword() + { + _isShow = !_isShow; + } + + private void ToggleAuth() + { + _showAuth = !_showAuth; + } + + private void ToggleRange() + { + _showRange = !_showRange; + } + + private void ToggleMenu() + { + _showMenu = !_showMenu; + } + + private async Task WithImages() + { + _showMenu = false; + _options.NoImage = false; + await Download(); + } + + private async Task NoImages() + { + _showMenu = false; + _options.NoImage = true; + await Download(); + } + + private async Task Download() + { + if (string.IsNullOrWhiteSpace(_url)) + { + Snackbar.Add("Укажите ссылку на книгу", Severity.Error); + return; + } + + if (_formats.Count == 0) + { + Snackbar.Add("Выберите формат", Severity.Error); + return; + } + + _options.Format = _formats; + _options.Url = [_url]; + + if (int.TryParse(_startNumStr, out var startNum)) + _options.Start = startNum; + if (int.TryParse(_endNumStr, out var endNum)) + _options.End = endNum; + + _onGenerate = true; + _showMenu = false; + _logger = new ActionLogger(str => + { + _log = str.Crop(300); + InvokeAsync(StateHasChanged); + }); + + await OnClick(_options, _logger); + _onGenerate = false; + } + + private void OnSwitch(bool value) + { + _startEndCSwitcher = value; + _options.Start = null; + _options.End = null; + _options.StartName = null; + _options.EndName = null; + _startNumStr = ""; + _endNumStr = ""; + } +} diff --git a/src/Elib2EbookWeb/Components/Layout/MainLayout.razor b/src/Elib2EbookWeb/Components/Layout/MainLayout.razor new file mode 100644 index 00000000..aec76d43 --- /dev/null +++ b/src/Elib2EbookWeb/Components/Layout/MainLayout.razor @@ -0,0 +1,238 @@ +@inherits LayoutComponentBase + + + + + +
+
+
+
+ + + + + Elib2Ebook +
+ +
+ + + + + + +
+
+
+ +
+ +
+
+ @Body +
+
+ +
+ +
+
+ +
+ Произошла ошибка. + Перезагрузить + 🗙 +
+ +@code { + private bool _mobileNavOpen; + private bool _darkMode; + private string _themeCycleMode = "system"; // "light" | "dark" | "system" + + [Inject] + private IJSRuntime Js { get; set; } + + private MudTheme _theme = new() + { + PaletteLight = new PaletteLight + { + Primary = "#0071e3", + Secondary = "#86868b", + Tertiary = "#34c759", + Surface = "#ffffff", + Background = "#f5f5f7", + AppbarBackground = "#ffffff", + DrawerBackground = "#ffffff", + DrawerText = "#1d1d1f", + AppbarText = "#1d1d1f", + TextPrimary = "#1d1d1f", + TextSecondary = "#6e6e73", + ActionDefault = "#6e6e73", + ActionDisabled = "#c7c7cc", + Divider = "#e5e5ea", + Error = "#ff3b30", + Info = "#0071e3", + Success = "#34c759", + Warning = "#ff9f0a", + TableLines = "#e5e5ea", + LinesDefault = "#e5e5ea", + LinesInputs = "#e5e5ea", + Dark = "#1d1d1f", + DarkDarken = "#000000", + }, + PaletteDark = new PaletteDark + { + Primary = "#2997ff", + Secondary = "#98989d", + Tertiary = "#32d74b", + Surface = "#1c1c1e", + Background = "#000000", + AppbarBackground = "#1c1c1e", + DrawerBackground = "#1c1c1e", + DrawerText = "#f5f5f7", + AppbarText = "#f5f5f7", + TextPrimary = "#f5f5f7", + TextSecondary = "#a1a1a6", + ActionDefault = "#a1a1a6", + ActionDisabled = "#48484a", + Divider = "#38383a", + Error = "#ff453a", + Info = "#2997ff", + Success = "#32d74b", + Warning = "#ff9f0a", + TableLines = "#38383a", + LinesDefault = "#38383a", + LinesInputs = "#38383a", + Dark = "#f5f5f7", + DarkDarken = "#ffffff", + } + }; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + // Read the effective theme computed by app.js (which applied any + // saved localStorage choice) and mirror it into MudBlazor. + var current = await Js.InvokeAsync("elibMotion.getThemeMode"); + _darkMode = current == "dark"; + _themeCycleMode = current == "light" || current == "dark" ? current : "system"; + StateHasChanged(); + } + } + + private async Task CycleTheme() + { + // light -> dark -> system -> light + _themeCycleMode = _themeCycleMode switch + { + "light" => "dark", + "dark" => "system", + _ => "light" + }; + + if (_themeCycleMode == "system") + { + // Follow the OS again: clear the stored choice and recompute. + await Js.InvokeVoidAsync("elibMotion.setThemeMode", "system"); + var current = await Js.InvokeAsync("elibMotion.getThemeMode"); + _darkMode = current == "dark"; + } + else + { + _darkMode = _themeCycleMode == "dark"; + var mode = _darkMode ? "dark" : "light"; + await Js.InvokeVoidAsync("elibMotion.setThemeMode", mode); + } + } + + // Two-way callback from MudThemeProvider — keep our field in sync. + private void OnIsDarkModeChanged(bool value) + { + _darkMode = value; + } + + private async Task ToggleMobileNav() + { + _mobileNavOpen = !_mobileNavOpen; + await AnimateNav(); + } + + private async Task CloseMobileNav() + { + if (!_mobileNavOpen) return; + _mobileNavOpen = false; + await AnimateNav(); + } + + private async Task AnimateNav() + { + await Js.InvokeVoidAsync("elibMotion.drawer", _mobileNavOpen, 280); + } +} diff --git a/src/Elib2EbookWeb/Components/Layout/MainLayout.razor.css b/src/Elib2EbookWeb/Components/Layout/MainLayout.razor.css new file mode 100644 index 00000000..bc0cc090 --- /dev/null +++ b/src/Elib2EbookWeb/Components/Layout/MainLayout.razor.css @@ -0,0 +1,326 @@ +/* ========================================================================== + MainLayout — Apple-style scoped layout styles + -------------------------------------------------------------------------- + Global design tokens live in wwwroot/app.css. This file holds the scoped + component styles for the shell (header, nav, drawer, footer). + ========================================================================== */ + +/* The app-shell layout: column so the footer sits at the bottom and the + sticky header stays on top. */ +.app-layout { + display: flex; + flex-direction: column; + min-height: 100vh; + background: var(--bg); +} + +/* --- Header (translucent floating chrome) --- */ +.app-header { + position: sticky; + top: 0; + z-index: 100; + background: var(--header-bg); + -webkit-backdrop-filter: var(--header-blur); + backdrop-filter: var(--header-blur); + border-bottom: 1px solid var(--border); +} + +.app-header-content { + max-width: 980px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + height: 56px; + padding: 0 20px; + gap: 12px; +} + +.app-header-left { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.app-header-right { + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; +} + +/* --- Desktop nav (hidden on mobile, replaced by spring drawer) --- */ +.app-nav { + display: flex; + align-items: center; + gap: 2px; +} + +.app-nav-link { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + font-size: 14px; + font-weight: 500; + letter-spacing: -0.01em; + color: var(--text-2); + text-decoration: none; + border-radius: var(--radius-s); + transition: background 0.18s ease, color 0.18s ease; + white-space: nowrap; +} +.app-nav-link svg { + opacity: 0.7; +} +.app-nav-link:hover { + color: var(--text); + background: var(--fill-hover); +} +.app-nav-link:active { + background: var(--fill-active); +} +.app-nav-link.active { + color: var(--text); + background: var(--fill); +} +.app-nav-link.active svg { + opacity: 1; + color: var(--accent); +} + +.app-logo-icon { + width: 28px; + height: 28px; + color: var(--accent); + flex-shrink: 0; +} + +.app-title { + font-size: 19px !important; + font-weight: 600 !important; + letter-spacing: -0.018em; + color: var(--text); + text-decoration: none !important; + white-space: nowrap; + display: inline-flex; + align-items: center; +} +.app-title:hover { + text-decoration: none !important; +} + +/* Header action icons (github + theme) — ~20px glyph in a 40px hit target, + optically centered. Nav icons stay 18px to align with text. */ +.github-btn svg { width: 20px; height: 20px; } +.theme-toggle svg { width: 20px; height: 20px; } +.app-nav-link svg { width: 18px; height: 18px; flex-shrink: 0; } +.hamburger-btn svg { width: 22px; height: 22px; } + +.github-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 50%; + color: var(--text-2); + transition: transform 0.24s var(--motion-snappy), background 0.18s ease; +} +.github-btn:hover { + background: var(--fill-hover); +} +.github-btn:active { + transform: scale(0.88); +} + +/* --- Main --- */ +.app-main { + flex: 1; + display: flex; + justify-content: center; +} + +.app-content { + width: 100%; + max-width: 980px; + padding: 32px 20px 64px; +} + +/* --- Footer --- */ +.app-footer { + border-top: 1px solid var(--border); + background: var(--surface); +} +.app-footer-content { + max-width: 980px; + margin: 0 auto; + padding: 24px 20px; + display: flex; + flex-direction: column; + gap: 16px; + font-size: 13px; + color: var(--text-2); +} +.app-footer-links { + display: flex; + gap: 20px; + flex-wrap: wrap; +} +.app-footer-link { + font-size: 13px; + color: var(--text-2); + text-decoration: none; + transition: color 0.15s ease; +} +.app-footer-link:hover { + color: var(--text); + text-decoration: none; +} +.app-footer-info p { + margin: 0; + line-height: 1.5; + color: var(--text-2); +} +.app-footer-copy { + margin-top: 4px !important; + font-size: 12px; + opacity: 0.7; +} + +/* --- Hamburger (mobile) --- */ +.hamburger-btn { + display: none; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: none; + border-radius: 50%; + background: transparent; + color: var(--text); + cursor: pointer; + padding: 0; + transition: background 0.18s ease, transform 0.24s var(--motion-snappy); + flex-shrink: 0; +} +.hamburger-btn:hover { + background: var(--fill-hover); +} +.hamburger-btn:active { + transform: scale(0.9); +} + +.mobile-overlay { + display: none; +} + +/* ========================================================================== + MOBILE — spring drawer + -------------------------------------------------------------------------- + The single #app-nav element doubles as the desktop row and the mobile + drawer. On small screens it becomes a fixed panel parked off-screen left + (translateX(-100%)) and is moved to 0 by a damped spring in app.js. + No CSS transition on transform — springs are interruptible and + velocity-aware, so the panel can be grabbed and reversed mid-flight. + ========================================================================== */ +@media (max-width: 640px) { + .app-header-content { + padding: 0 12px; + } + + .app-content { + padding: 20px 16px 48px; + } + + .hamburger-btn { + display: flex; + } + + .app-nav { + position: fixed; + top: 0; + left: 0; + width: 280px; + height: 100vh; + flex-direction: column; + align-items: stretch; + gap: 4px; + padding: 64px 10px 20px; + background: var(--surface); + box-shadow: var(--elevation-3); + z-index: 200; + overflow-y: auto; + display: flex; + transform: translateX(-100%); + will-change: transform; + border-right: 1px solid var(--border); + } + + .app-nav .app-nav-link { + width: 100%; + padding: 12px 16px; + gap: 12px; + border-radius: var(--radius-s); + font-size: 17px; + } + + .mobile-overlay { + display: block; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--scrim); + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + z-index: 150; + opacity: 0; + transition: opacity 0.25s ease; + pointer-events: none; + } + .mobile-overlay.show { + opacity: 1; + pointer-events: auto; + } + + .app-footer-content { + padding: 20px 16px; + } + + .app-footer-links { + flex-direction: column; + gap: 10px; + } +} + +/* ========================================================================== + ACCESSIBILITY + ========================================================================== */ +@media (prefers-reduced-motion: reduce) { + .github-btn:active, + .hamburger-btn:active, + .theme-toggle:active { + transform: none; + } +} + +@media (prefers-reduced-transparency: reduce) { + .app-header, + .mobile-overlay { + background: var(--surface) !important; + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; + } +} + +@media (prefers-contrast: more) { + .app-header { + border-bottom-color: var(--border-strong); + } + .app-footer { + border-top-color: var(--border-strong); + } +} diff --git a/src/Elib2EbookWeb/Components/Pages/Error.razor b/src/Elib2EbookWeb/Components/Pages/Error.razor new file mode 100644 index 00000000..e410323b --- /dev/null +++ b/src/Elib2EbookWeb/Components/Pages/Error.razor @@ -0,0 +1,31 @@ +@page "/Error" +@using System.Diagnostics + +Ошибка + +
+
+

Произошла ошибка

+

При обработке вашего запроса возникла непредвиденная ошибка.

+ + @if (ShowRequestId) + { +

+ ID запроса: @RequestId +

+ } +
+
+ +@code{ + + [CascadingParameter] + private HttpContext HttpContext { get; set; } + + private string RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() + => RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; + +} diff --git a/src/Elib2EbookWeb/Components/Pages/Features.razor b/src/Elib2EbookWeb/Components/Pages/Features.razor new file mode 100644 index 00000000..2e5e04a5 --- /dev/null +++ b/src/Elib2EbookWeb/Components/Pages/Features.razor @@ -0,0 +1,90 @@ +@page "/features" + +Возможности — Elib2Ebook + +
+
+

Возможности

+

Всё, что нужно для комфортного чтения в любом формате

+
+ +
+
+
+ + + +
+

Множество форматов

+

+ EPUB, FB2, CBZ, TXT — скачивайте книги в удобном формате для любого устройства. + Поддержка всех популярных форматов электронных книг. +

+
+ +
+
+ + + +
+

Авторизация

+

+ Поддержка авторизации на сайтах с платным контентом. + Введите логин и пароль — всё остальное сделаем мы. +

+
+ +
+
+ + + +
+

Выбор глав

+

+ Скачивайте выборочные главы по номерам или названиям. + Не нужно ждать, пока обработается вся книга целиком. +

+
+ +
+
+ + + +
+

Изображения

+

+ Поддержка изображений в книгах. Можно скачать с картинками + или без — выбор остаётся за вами. +

+
+ +
+
+ + + +
+

Бесплатно

+

+ Полностью бесплатный open-source инструмент. + Никаких ограничений, подписок и скрытых платежей. +

+
+ +
+
+ + + +
+

Настраиваемо

+

+ Гибкие настройки: выбор формата, диапазона глав, + авторизации и многое другое под ваши задачи. +

+
+
+
diff --git a/Elib2EbookWeb/Components/Pages/Home.razor b/src/Elib2EbookWeb/Components/Pages/Home.razor similarity index 76% rename from Elib2EbookWeb/Components/Pages/Home.razor rename to src/Elib2EbookWeb/Components/Pages/Home.razor index dac4fb24..130e83ce 100644 --- a/Elib2EbookWeb/Components/Pages/Home.razor +++ b/src/Elib2EbookWeb/Components/Pages/Home.razor @@ -1,32 +1,27 @@ @page "/" -@using Core.Configs -@using Core.Extensions -@using Core.Misc -@using Core.Misc.TempFolder -@using Core.Types.Book -@rendermode InteractiveServer +@using Elib2Ebook.Domain.Book +@using Elib2Ebook.DomainServices.Configs +@using Elib2Ebook.DomainServices.Extensions +@using Elib2Ebook.DomainServices.Misc +@using Elib2Ebook.DomainServices.Misc.TempFolder @inject IJSRuntime JsRuntime @inject ISnackbar Snackbar Elib2Ebook - - - - - +
+ +
-@if (_shortBook != default) +@if (_shortBook != null) { - - - - - +
+ +
} @code { - private Book? _shortBook; + private Book _shortBook; private async Task OnClick(Options options, ILogger logger) { @@ -38,7 +33,7 @@ try { using var getterConfig = BookGetterConfig.GetDefault(options, logger); - using var getter = GetterProvider.Get(getterConfig, options.Url.First().AsUri()); + using var getter = GetterFactory.Get(getterConfig, options.Url.First().AsUri()); await getter.Init(); await getter.Authorize(); @@ -82,11 +77,13 @@ logger.LogInformation(ex.Message); Snackbar.Add(ex.Message, Severity.Error); } + finally + { + tempSave.DeleteForce(); + } } - private string GetBookName(string url, Book? book) - { - return book?.Title ?? url; - } + private static string GetBookName(string url, Book book) + => book?.Title ?? url; } diff --git a/src/Elib2EbookWeb/Components/Pages/Supported.razor b/src/Elib2EbookWeb/Components/Pages/Supported.razor new file mode 100644 index 00000000..53536582 --- /dev/null +++ b/src/Elib2EbookWeb/Components/Pages/Supported.razor @@ -0,0 +1,156 @@ +@page "/supported" + +Поддерживаемые сайты — Elib2Ebook + +
+
+

Поддерживаемые сайты

+

82 сайта, с которых можно скачивать книги

+
+ + + +
+ @foreach (var site in FilteredSites) + { +
+
+ @switch (site.Icon) + { + case "book": + + + + break; + case "globe": + + + + break; + case "manga": + + + + break; + } +
+
+ @site.Name + @site.Url +
+
+ } + @if (FilteredSites.Count == 0) + { +
+ + + +

Ничего не найдено

+
+ } +
+
+ +@code { + private string _search = string.Empty; + + private IReadOnlyList FilteredSites => + string.IsNullOrWhiteSpace(_search) + ? Sites + : Sites.Where(s => s.Name.Contains(_search, StringComparison.OrdinalIgnoreCase) || + s.Url.Contains(_search, StringComparison.OrdinalIgnoreCase)).ToList(); + + private static readonly IReadOnlyList Sites = new List + { + new("Acomics", "https://acomics.ru", "manga"), + new("Author.Today", "https://author.today", "globe"), + new("Bigliba", "https://bigliba.com", "book"), + new("Bllate", "https://bllate.org", "book"), + new("Book in Book", "https://bookinbook.ru", "book"), + new("BookHamster", "https://bookhamster.ru", "book"), + new("Bookinist", "https://bookinist.pw", "book"), + new("Booknet", "https://booknet.com", "book"), + new("Booknet.ua", "https://booknet.ua", "book"), + new("BookRiver", "https://bookriver.ru", "book"), + new("Bookstab", "https://bookstab.ru", "book"), + new("BooksTime", "https://bookstime.ru", "book"), + new("Bookuruk", "https://bookuruk.com", "book"), + new("Boosty", "https://boosty.to", "globe"), + new("Boovel", "https://boovell.ru", "book"), + new("DarkNovels", "https://dark-novels.ru", "book"), + new("Desu", "https://desu.me", "manga"), + new("Dreame", "https://dreame.com", "book"), + new("Erolate", "https://erolate.com", "book"), + new("EzNovels", "https://eznovels.com", "book"), + new("FanFicus", "https://fanficus.com", "book"), + new("Fb2Top", "https://fb2.top", "book"), + new("Ficbook", "https://ficbook.net", "book"), + new("FictionBook", "https://fictionbook.ru", "book"), + new("Freedlit", "https://freedlit.space", "book"), + new("Freedom", "https://ifreedom.su", "book"), + new("HentaiLib", "https://hentailib.me", "manga"), + new("HogwartsNet", "https://hogwartsnet.ru", "book"), + new("HotNovelPub", "https://hotnovelpub.com", "book"), + new("HubBook", "https://hub-book.com", "book"), + new("iGram", "https://i-gram.ru", "globe"), + new("Jaomix", "https://jaomix.ru", "book"), + new("LadyLib", "https://ladylib.top", "book"), + new("LaNovels", "https://lanovels.com", "book"), + new("Libbox", "https://libbox.ru", "book"), + new("LibKing", "https://libking.ru", "book"), + new("Librebook", "https://librebook.me", "book"), + new("Libst", "https://libst.ru", "book"), + new("LightNovelDaily", "https://lightnoveldaily.com", "book"), + new("Litgorod", "https://litgorod.ru", "book"), + new("LitLife", "https://litlife.club", "book"), + new("Litmarket", "https://litmarket.ru", "book"), + new("Litmir", "https://litmir.me", "book"), + new("Litnet", "https://litnet.com", "book"), + new("Litres", "https://www.litres.ru", "book"), + new("Litsovet", "https://litsovet.ru", "book"), + new("MangaLib", "https://mangalib.me", "manga"), + new("MangaLib.org", "https://mangalib.org", "manga"), + new("Manga Mammy", "https://mangamammy.ru", "manga"), + new("MangaOvh", "https://manga.ovh", "manga"), + new("MirKnig", "https://mir-knig.com", "book"), + new("Mlate", "https://mlate.ru", "book"), + new("MyBook", "https://mybook.ru", "book"), + new("Neobook", "http://neobook.org", "book"), + new("Novelhall", "https://www.novelhall.com", "book"), + new("NovelOvh", "https://novel.ovh", "book"), + new("NovelTranslate", "https://noveltranslate.com", "book"), + new("Novelxo", "https://novelxo.com", "book"), + new("Novelxo (RU)", "https://ru.novelxo.com", "book"), + new("OnlineKnigi", "https://online-knigi.com.ua", "book"), + new("Prodaman", "https://prodaman.ru", "book"), + new("Ranobe", "https://ранобэ.рф", "book"), + new("RanobeHub", "https://ranobehub.org", "book"), + new("RanobeLib", "https://ranobelib.me", "book"), + new("Ranobes", "https://ranobes.com", "book"), + new("Readli", "https://readli.net", "book"), + new("ReadManga", "https://readmanga.live", "manga"), + new("ReManga", "https://remanga.org", "manga"), + new("Renovels", "https://renovels.org", "book"), + new("Romfant", "https://romfant.ru", "book"), + new("RoyalRoad", "https://royalroad.com", "book"), + new("Rulate", "https://tl.rulate.ru", "book"), + new("Samlib", "http://samlib.ru", "book"), + new("SlashLib", "https://v2.slashlib.me", "book"), + new("Stroki MTS", "https://stroki.mts.ru", "book"), + new("TopLiba", "https://topliba.com", "book"), + new("Twilight Russia", "https://twilightrussia.ru", "book"), + new("Wattpad", "https://wattpad.com", "book"), + new("Wattpad (RU)", "https://watt-pad.ru", "book"), + new("WuxiaWorld", "https://wuxiaworld.ru", "book"), + new("Yandex Books", "https://books.yandex.ru", "book"), + new("Younettranslate", "https://younettranslate.com", "book"), + }; + + private record SiteInfo(string Name, string Url, string Icon); +} diff --git a/Elib2EbookWeb/Components/Routes.razor b/src/Elib2EbookWeb/Components/Routes.razor similarity index 58% rename from Elib2EbookWeb/Components/Routes.razor rename to src/Elib2EbookWeb/Components/Routes.razor index 14eab61b..7bd06db6 100644 --- a/Elib2EbookWeb/Components/Routes.razor +++ b/src/Elib2EbookWeb/Components/Routes.razor @@ -1,7 +1,8 @@ -@using Elib2EbookWeb.Components.Layout +@rendermode InteractiveServer + - + diff --git a/src/Elib2EbookWeb/Components/ShortBook.razor b/src/Elib2EbookWeb/Components/ShortBook.razor new file mode 100644 index 00000000..ecb08c98 --- /dev/null +++ b/src/Elib2EbookWeb/Components/ShortBook.razor @@ -0,0 +1,82 @@ +@using Elib2Ebook.Domain.Book + +
+
+ Обложка +
+
+ + @Book.Title + + + @if (Book.Author != null) + { +
+ Автор: + @if (Book.Author.Url == null) + { + @Book.Author.Name + } + else + { + @Book.Author.Name + } +
+ } + + @if (Book.Seria != null) + { +
+ Серия: + @if (Book.Seria?.Url == null) + { + @Seria + } + else + { + @Seria + } +
+ } + + @if (!string.IsNullOrWhiteSpace(Book.Annotation)) + { +
@((MarkupString)Book.Annotation)
+ } +
+
+ +@code { + + [Parameter] + public required Book Book { get; set; } + + private string _cover; + + private string _coverPath; + + private string Seria => Book.Seria?.Name + (string.IsNullOrWhiteSpace(Book.Seria?.Number) ? string.Empty : $" #{Book.Seria.Number}"); + + protected override Task OnInitializedAsync() + { + InitCover(); + return base.OnInitializedAsync(); + } + + protected override Task OnParametersSetAsync() + { + if (Book.Cover?.FilePath != _coverPath) + { + InitCover(); + } + + return base.OnParametersSetAsync(); + } + + private void InitCover() + { + _cover = Book.Cover != null && Book.Cover.Content.Length > 0 ? $"data:image/png;base64, {Convert.ToBase64String(Book.Cover.Content)}" : _cover; + _coverPath = Book.Cover?.FilePath; + } + +} diff --git a/Elib2EbookWeb/Components/_Imports.razor b/src/Elib2EbookWeb/Components/_Imports.razor similarity index 95% rename from Elib2EbookWeb/Components/_Imports.razor rename to src/Elib2EbookWeb/Components/_Imports.razor index a4fed926..ab21d4f2 100644 --- a/Elib2EbookWeb/Components/_Imports.razor +++ b/src/Elib2EbookWeb/Components/_Imports.razor @@ -8,4 +8,4 @@ @using Microsoft.JSInterop @using Elib2EbookWeb @using Elib2EbookWeb.Components -@using MudBlazor +@using MudBlazor \ No newline at end of file diff --git a/src/Elib2EbookWeb/Dockerfile b/src/Elib2EbookWeb/Dockerfile new file mode 100644 index 00000000..0fc2ba9c --- /dev/null +++ b/src/Elib2EbookWeb/Dockerfile @@ -0,0 +1,13 @@ +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build-env +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +COPY . . +RUN dotnet restore src/Elib2EbookWeb/Elib2EbookWeb.csproj +RUN dotnet publish src/Elib2EbookWeb/Elib2EbookWeb.csproj -c Release -o /app/publish /p:UseAppHost=false + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build-env /app/publish . +ENTRYPOINT ["dotnet", "Elib2EbookWeb.dll"] diff --git a/src/Elib2EbookWeb/Elib2EbookWeb.csproj b/src/Elib2EbookWeb/Elib2EbookWeb.csproj new file mode 100644 index 00000000..f62ec2b6 --- /dev/null +++ b/src/Elib2EbookWeb/Elib2EbookWeb.csproj @@ -0,0 +1,21 @@ + + + + + + + + + + + + .dockerignore + + + PreserveNewest + + + PreserveNewest + + + diff --git a/src/Elib2EbookWeb/Misc/SbWriter.cs b/src/Elib2EbookWeb/Misc/SbWriter.cs new file mode 100644 index 00000000..99e9522d --- /dev/null +++ b/src/Elib2EbookWeb/Misc/SbWriter.cs @@ -0,0 +1,22 @@ +namespace Elib2EbookWeb.Misc; + +public class ActionLogger : ILogger +{ + private readonly Action _action; + + public ActionLogger(Action action) + { + _action = action; + } + + public IDisposable BeginScope(TState state) where TState : notnull + => null!; + + public bool IsEnabled(LogLevel logLevel) + => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + _action($"{formatter(state, exception)}"); + } +} diff --git a/Elib2EbookWeb/Program.cs b/src/Elib2EbookWeb/Program.cs similarity index 94% rename from Elib2EbookWeb/Program.cs rename to src/Elib2EbookWeb/Program.cs index 28850e2a..90cb2701 100644 --- a/Elib2EbookWeb/Program.cs +++ b/src/Elib2EbookWeb/Program.cs @@ -24,7 +24,7 @@ // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { - app.UseExceptionHandler("/Error", true); + app.UseExceptionHandler("/Error", createScopeForErrors: true); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } diff --git a/src/Elib2EbookWeb/Properties/launchSettings.json b/src/Elib2EbookWeb/Properties/launchSettings.json new file mode 100644 index 00000000..e0d10891 --- /dev/null +++ b/src/Elib2EbookWeb/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:41898", + "sslPort": 44383 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5271", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7188;http://localhost:5271", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Elib2EbookWeb/appsettings.Development.json b/src/Elib2EbookWeb/appsettings.Development.json similarity index 100% rename from Elib2EbookWeb/appsettings.Development.json rename to src/Elib2EbookWeb/appsettings.Development.json diff --git a/Elib2EbookWeb/appsettings.json b/src/Elib2EbookWeb/appsettings.json similarity index 100% rename from Elib2EbookWeb/appsettings.json rename to src/Elib2EbookWeb/appsettings.json diff --git a/src/Elib2EbookWeb/wwwroot/app.css b/src/Elib2EbookWeb/wwwroot/app.css new file mode 100644 index 00000000..bbb13095 --- /dev/null +++ b/src/Elib2EbookWeb/wwwroot/app.css @@ -0,0 +1,1781 @@ +/* ========================================================================== + Elib2Ebook — Apple Design System + -------------------------------------------------------------------------- + Styled after Apple/Apple-style platforms (WWDC "Designing Fluid + Interfaces", system typography, translucent materials, hardware-agnostic + Dark Mode via prefers-color-scheme). + + Principles applied here: + • System font with optical sizing — tracking/leading tuned per text size. + • Translucent floating chrome (backdrop-filter) — content scrolls under it. + • Depth = elevation (soft layered shadows), never harsh outlines. + • Materials get heavier with size; a modal pairs with a dimming scrim. + • Reduced-motion / reduced-transparency / reduced-contrast all respected. + ========================================================================== */ + +/* -------------------------------------------------------------------------- + Design tokens — Light + Dark (system-driven) + -------------------------------------------------------------------------- */ +:root { + --font-system: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", Roboto, system-ui, sans-serif; + --font-mono: ui-monospace, "SF Mono", "Roboto Mono", Menlo, monospace; + + color-scheme: light; + + /* Radius — Apple corners: hard on small, rounder as surfaces grow */ + --radius-xs: 6px; + --radius-s: 10px; + --radius-m: 14px; + --radius-l: 20px; + --radius-xl: 28px; + + /* Motion — Apple-style damped spring defaults (response in seconds). + 1.0 damping = critically damped (no bounce) for general UI. */ + --motion-spring: cubic-bezier(0.2, 0.8, 0.2, 1); + --motion-snappy: cubic-bezier(0.25, 0.8, 0.25, 1); + + --elevation-1: 0 1px 2px rgba(0, 0, 0, 0.04), 0 2px 8px rgba(0, 0, 0, 0.04); + --elevation-2: 0 4px 12px rgba(0, 0, 0, 0.06), 0 8px 24px rgba(0, 0, 0, 0.06); + --elevation-3: 0 12px 32px rgba(0, 0, 0, 0.10), 0 4px 12px rgba(0, 0, 0, 0.06); +} + +/* Light theme */ +:root { + --bg: #f5f5f7; /* Apple light gray */ + --surface: #ffffff; + --surface-2: #f5f5f7; /* tinted fill for fields */ + --surface-3: #e9e9eb; /* pressed / raised tint */ + --fill: rgba(120, 120, 128, 0.12); /* label-colored translucent fill */ + --fill-hover: rgba(120, 120, 128, 0.18); + --fill-active: rgba(120, 120, 128, 0.24); + --text: #1d1d1f; + --text-2: #6e6e73; + --text-3: #86868b; + --border: rgba(0, 0, 0, 0.10); + --border-strong: rgba(0, 0, 0, 0.16); + --accent: #0071e3; /* Apple blue */ + --accent-hover: #0077ed; + --accent-active: #006edb; + --on-accent: #ffffff; + --accent-soft: rgba(0, 113, 227, 0.12); + --accent-soft-2: rgba(0, 113, 227, 0.16); + --danger: #ff3b30; + --success: #34c759; + --warning: #ff9f0a; + --scrim: rgba(0, 0, 0, 0.32); + --header-blur: saturate(180%) blur(20px); + --header-bg: rgba(245, 245, 247, 0.72); + --shadow-color: rgba(0, 0, 0, 0.12); +} + +/* Dark theme (Apple-style dark grays) — applied via data-theme on , + so it respects an explicit user toggle in addition to following the system + preference (the JS in app.js sets data-theme from localStorage or the OS). */ +:root[data-theme="dark"] { + color-scheme: dark; + --bg: #000000; + --surface: #1c1c1e; + --surface-2: #2c2c2e; + --surface-3: #3a3a3c; + --fill: rgba(120, 120, 128, 0.24); + --fill-hover: rgba(120, 120, 128, 0.32); + --fill-active: rgba(120, 120, 128, 0.42); + --text: #f5f5f7; + --text-2: #a1a1a6; + --text-3: #6e6e73; + --border: rgba(255, 255, 255, 0.12); + --border-strong: rgba(255, 255, 255, 0.20); + --accent: #2997ff; + --accent-hover: #47a6ff; + --accent-active: #1d87ff; + --on-accent: #000000; + --accent-soft: rgba(41, 151, 255, 0.18); + --accent-soft-2: rgba(41, 151, 255, 0.26); + --danger: #ff453a; + --success: #32d74b; + --warning: #ff9f0a; + --scrim: rgba(0, 0, 0, 0.60); + --header-bg: rgba(28, 28, 30, 0.72); + --header-blur: saturate(180%) blur(20px); + --shadow-color: rgba(0, 0, 0, 0.5); +} + +/* -------------------------------------------------------------------------- + Base + -------------------------------------------------------------------------- */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: var(--font-system); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-optical-sizing: auto; +} + +body { + margin: 0; + padding: 0; + background-color: var(--bg); + color: var(--text); + min-height: 100vh; + line-height: 1.47059; + letter-spacing: -0.022em; /* Apple body tracking */ + font-size: 17px; + transition: background-color 0.3s ease, color 0.3s ease; +} + +h1, h2, h3 { + color: var(--text); + font-weight: 600; + letter-spacing: -0.022em; +} + +h1 { + font-size: clamp(2.25rem, 6vw, 3rem); + line-height: 1.08349; + letter-spacing: -0.022em; +} + +h2 { + font-size: clamp(1.75rem, 4vw, 2.5rem); + line-height: 1.1; + letter-spacing: -0.022em; +} + +h3 { + font-size: 1.25rem; + line-height: 1.3; +} + +p { + color: var(--text-2); +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* Scrollbar — subtle, matching the material */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: var(--text-3); + border-radius: 6px; + border: 2px solid transparent; + background-clip: content-box; +} +::-webkit-scrollbar-thumb:hover { + background: var(--text-2); + background-clip: content-box; +} + +/* Focus — Apple uses blue ring, offset */ +:focus-visible { + outline: none; + border-radius: var(--radius-xs); + box-shadow: 0 0 0 4px var(--accent-soft-2); +} + +::selection { + background: var(--accent-soft-2); +} + +/* -------------------------------------------------------------------------- + App layout (header / main / footer) + -------------------------------------------------------------------------- */ +.app-layout { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.app-header { + position: sticky; + top: 0; + z-index: 100; + /* Translucent floating chrome — content scrolls underneath. Heavier on + grey in dark, brighter top edge via border. */ + background: var(--header-bg); + -webkit-backdrop-filter: var(--header-blur); + backdrop-filter: var(--header-blur); + border-bottom: 1px solid var(--border); +} + +.app-header-content { + max-width: 980px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + height: 56px; + padding: 0 20px; + gap: 12px; +} + +.app-header-left { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.app-header-right { + display: flex; + align-items: center; + flex-shrink: 0; +} + +.app-logo-icon { + width: 28px; + height: 28px; + color: var(--accent); +} + +.app-title { + font-size: 21px !important; + font-weight: 600 !important; + letter-spacing: -0.022em; + color: var(--text); + text-decoration: none !important; + white-space: nowrap; +} + +.app-title:hover { + text-decoration: none !important; +} + +.github-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 50%; + color: var(--text-2); + transition: transform 0.24s var(--motion-snappy), background 0.18s ease; + /* Instant press feedback, spring-like scale */ +} +.github-btn:hover { + background: var(--fill-hover); +} +.github-btn:active { + transform: scale(0.88); +} + +/* Theme toggle (light/dark) — same circular chrome as github-btn, shows the + opposite icon (sun when dark, moon when light). Spring-like press feedback. */ +.theme-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: none; + border-radius: 50%; + background: transparent; + color: var(--text-2); + cursor: pointer; + padding: 0; + transition: transform 0.24s var(--motion-snappy), background 0.18s ease, color 0.18s ease; +} +.theme-toggle:hover { + background: var(--fill-hover); + color: var(--text); +} +.theme-toggle:active { + transform: scale(0.88); +} +.theme-toggle svg { + transition: transform 0.3s var(--motion-snappy); +} + +/* Nav (desktop) */ +.app-nav { + display: flex; + align-items: center; + gap: 2px; +} + +.app-nav-link { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + font-size: 14px; + font-weight: 500; + letter-spacing: -0.01em; + color: var(--text-2); + text-decoration: none; + border-radius: var(--radius-s); + transition: background 0.18s ease, color 0.18s ease; + white-space: nowrap; +} +.app-nav-link svg { + opacity: 0.7; +} +.app-nav-link:hover { + color: var(--text); + background: var(--fill-hover); +} +.app-nav-link:active { + background: var(--fill-active); +} +.app-nav-link.active { + color: var(--text); + background: var(--fill); +} +.app-nav-link.active svg { + opacity: 1; + color: var(--accent); +} + +/* Main / content width — Apple uses ~980px for max readability */ +.app-main { + flex: 1; + display: flex; + justify-content: center; +} + +.app-content { + width: 100%; + max-width: 980px; + padding: 32px 20px 64px; +} + +/* Footer — simple Apple-like bottom bar */ +.app-footer { + border-top: 1px solid var(--border); + background: var(--surface); +} + +.app-footer-content { + max-width: 980px; + margin: 0 auto; + padding: 24px 20px; + display: flex; + flex-direction: column; + gap: 16px; + font-size: 13px; + color: var(--text-2); +} + +.app-footer-links { + display: flex; + gap: 20px; + flex-wrap: wrap; +} + +.app-footer-link { + font-size: 13px; + color: var(--text-2); + text-decoration: none; + transition: color 0.15s ease; +} +.app-footer-link:hover { + color: var(--text); + text-decoration: none; +} + +.app-footer-info p { + margin: 0; + line-height: 1.5; + color: var(--text-2); +} + +.app-footer-copy { + margin-top: 4px !important; + font-size: 12px; + opacity: 0.7; +} + +/* Hamburger button (mobile) */ +.hamburger-btn { + display: none; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: none; + border-radius: 50%; + background: transparent; + color: var(--text); + cursor: pointer; + padding: 0; + transition: background 0.18s ease, transform 0.24s var(--motion-snappy); + flex-shrink: 0; +} +.hamburger-btn:hover { + background: var(--fill-hover); +} +.hamburger-btn:active { + transform: scale(0.9); +} + +.mobile-overlay { + display: none; +} + +/* ========================================================================== + HOME PAGE — Hero + ========================================================================== */ +.home-page { + display: flex; + flex-direction: column; + gap: 28px; +} + +.home-hero { + text-align: center; + padding: 32px 0 12px; +} + +.home-badge { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 6px 14px; + border-radius: 100px; + background: var(--accent-soft); + color: var(--accent); + font-size: 13px; + font-weight: 600; + letter-spacing: -0.01em; + margin-bottom: 22px; +} +.home-badge svg { + width: 13px; + height: 13px; +} + +.home-hero h1 { + margin: 0 0 14px; + font-size: clamp(2.5rem, 7vw, 3.75rem); + line-height: 1.05; + letter-spacing: -0.015em; +} + +.home-hero p { + font-size: clamp(1.125rem, 3vw, 1.375rem); + color: var(--text-2); + margin: 0 auto; + max-width: 560px; + line-height: 1.45; + letter-spacing: 0; +} + +.home-hero .home-hero-accent { + color: var(--accent); +} + +/* Home stats strip */ +.home-stats { + display: flex; + gap: 14px; + justify-content: center; + flex-wrap: wrap; + padding: 8px 0; +} + +.home-stat { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + padding: 22px 26px; + background: var(--surface); + border-radius: var(--radius-l); + box-shadow: var(--elevation-1); + border: 1px solid transparent; + min-width: 132px; + text-align: left; + transition: transform 0.24s var(--motion-snappy), box-shadow 0.24s ease; +} +.home-stat:hover { + transform: translateY(-2px); + box-shadow: var(--elevation-2); +} + +.home-stat-value { + font-size: 2rem; + font-weight: 600; + letter-spacing: -0.022em; + color: var(--accent); + line-height: 1.1; +} + +.home-stat-label { + font-size: 13px; + color: var(--text-2); + text-align: left; + letter-spacing: 0; + line-height: 1.35; +} + +.home-book-result { + margin-top: 8px; +} + +/* ========================================================================== + FORM — BookForm (segmented + cards, Apple style) + ========================================================================== */ +.book-form { + display: flex; + flex-direction: column; + gap: 0; + background: var(--surface); + border-radius: var(--radius-l); + box-shadow: var(--elevation-2); + overflow: hidden; + border: 1px solid transparent; +} + +.form-section { + padding: 24px; + position: relative; +} + +/* Hairline separators, softer than solid lines */ +.form-section + .form-section::before { + content: ''; + position: absolute; + top: 0; + left: 24px; + right: 24px; + height: 1px; + background: var(--border); +} + +/* Apple-style section label */ +.google-label { + display: block; + font-size: 13px; + font-weight: 600; + color: var(--text-2); + margin-bottom: 10px; + letter-spacing: -0.01em; + line-height: 1.4; + text-transform: none; +} + +/* Text field (grouped background) */ +.google-textfield { + position: relative; + width: 100%; +} + +.google-input { + width: 100%; + padding: 12px 14px; + font-family: var(--font-system); + font-size: 17px; + letter-spacing: 0; + color: var(--text); + background: var(--fill); + border: none; + border-radius: var(--radius-s); + outline: none; + transition: background 0.18s ease, box-shadow 0.18s ease, transform 0.12s ease; + -webkit-appearance: none; + appearance: none; +} +.google-input:hover { + background: var(--fill-hover); +} +.google-input:focus { + background: var(--surface); + box-shadow: inset 0 0 0 1.5px var(--accent); +} +.google-input::placeholder { + color: var(--text-3); + opacity: 1; +} + +.google-input-underline { + display: none; +} + +/* Segmented control (chapter range switch) */ +.radio-group { + display: flex; + gap: 0; + background: var(--fill); + border-radius: var(--radius-s); + padding: 2px; + margin-bottom: 14px; +} + +.google-radio { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + flex: 1; + padding: 6px 12px; + font-family: var(--font-system); + font-size: 13px; + font-weight: 500; + color: var(--text-2); + background: transparent; + border: none; + border-radius: var(--radius-xs); + cursor: pointer; + transition: background 0.18s ease, color 0.18s ease, transform 0.12s ease, box-shadow 0.18s ease; + user-select: none; +} +.google-radio:hover { + color: var(--text); +} +.google-radio:active { + transform: scale(0.98); +} + +.radio-selected { + background: var(--surface); + color: var(--text); + box-shadow: var(--elevation-1); +} +.radio-selected:hover { + color: var(--text); +} + +.radio-circle { + width: 16px; + height: 16px; + border-radius: 50%; + border: 2px solid var(--text-3); + transition: all 0.18s ease; + display: inline-flex; + align-items: center; + justify-content: center; +} +.circle-selected { + border-color: var(--accent); + border-width: 5px; +} + +/* Range fields side by side */ +.range-fields { + display: flex; + gap: 12px; +} +.range-field { + flex: 1; +} + +/* Auth group */ +/* --- Auth: collapsible Apple-style disclosure --- */ +.auth-header { + display: flex; + align-items: center; + width: 100%; + gap: 10px; + padding: 2px 0; + background: none; + border: none; + cursor: pointer; + text-align: left; + -webkit-tap-highlight-color: transparent; +} + +.auth-header-icon { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 8px; + background: var(--fill); + color: var(--accent); + flex-shrink: 0; +} + +.auth-header-label { + font-size: 15px; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--text); + flex: 0 0 auto; +} + +.auth-header-hint { + font-size: 12px; + color: var(--text-3); + padding: 2px 8px; + border-radius: 100px; + background: var(--fill); +} + +.auth-header-chevron { + margin-left: auto; + color: var(--text-2); + flex-shrink: 0; + transition: transform 0.28s var(--motion-spring); +} +.auth-header-chevron.open { + transform: rotate(180deg); +} + +/* Smooth expand/collapse via grid-template-rows trick (no layout thrash). */ +.auth-body { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.32s var(--motion-snappy); + overflow: hidden; +} +.auth-body.open { + grid-template-rows: 1fr; +} +.auth-body > * { + min-height: 0; + overflow: hidden; +} +.auth-body .auth-group { + padding-top: 14px; +} +.auth-body .range-body { + padding-top: 14px; +} +.auth-body .range-body .radio-group { + margin-bottom: 14px; +} + +.auth-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.auth-input-wrapper { + display: flex; + align-items: center; + background: var(--fill); + border-radius: var(--radius-s); + border: 1.5px solid transparent; + transition: background 0.18s ease, border-color 0.18s ease; +} +.auth-input-wrapper:hover { + background: var(--fill-hover); +} +.auth-input-wrapper:focus-within { + background: var(--surface); + border-color: var(--accent); +} + +.auth-icon { + flex-shrink: 0; + color: var(--text-2); + margin-left: 16px; + margin-right: 10px; + transition: color 0.18s ease; +} +.auth-input-wrapper:focus-within .auth-icon { + color: var(--accent); +} + +.auth-input { + flex: 1; + padding: 12px 12px 12px 0; + font-family: var(--font-system); + font-size: 17px; + letter-spacing: 0; + color: var(--text); + background: transparent; + border: none; + outline: none; + -webkit-appearance: none; +} +/* The wrapper owns the focus ring (border) — suppress the global :focus-visible + box-shadow ring on the bare input so it never overlaps the text/toggle. */ +.auth-input:focus-visible { + outline: none; + box-shadow: none; +} +.auth-input::placeholder { + color: var(--text-3); + opacity: 1; +} + +.auth-password-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + margin-right: 6px; + background: none; + border: none; + border-radius: 50%; + cursor: pointer; + color: var(--text-2); + padding: 0; + transition: background 0.18s ease, color 0.18s ease, transform 0.12s ease; + flex-shrink: 0; +} +.auth-password-toggle:hover { + background: var(--fill-hover); + color: var(--text); +} +.auth-password-toggle:active { + transform: scale(0.9); +} + +/* Format picker — Apple-style selection cards */ +.format-cards { + display: flex; + gap: 12px; + flex-wrap: wrap; + justify-content: center; +} + +.format-card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + width: 120px; + padding: 14px 10px 12px; + font-family: var(--font-system); + color: var(--text); + background: var(--surface-2); + border: 1px solid transparent; + border-radius: var(--radius-m); + cursor: pointer; + transition: background 0.18s ease, transform 0.24s var(--motion-snappy), box-shadow 0.24s ease, border-color 0.18s ease; + user-select: none; +} +.format-card:hover { + background: var(--fill-hover); +} +.format-card:active { + transform: scale(0.96); +} + +.format-card-selected { + background: var(--accent-soft); + border-color: var(--accent); +} +.format-card-selected:hover { + background: var(--accent-soft-2); +} + +.format-card-icon { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: var(--radius-s); + background: var(--fill); + color: var(--accent); + transition: background 0.18s ease, color 0.18s ease, transform 0.24s var(--motion-snappy); +} +.format-card-icon svg { + width: 22px; + height: 22px; +} +.format-card:hover .format-card-icon { + transform: scale(1.05); +} +.format-card-selected .format-card-icon { + background: var(--accent); + color: var(--on-accent); +} + +.format-card-label { + font-size: 15px; + font-weight: 600; + letter-spacing: -0.01em; + line-height: 1.2; +} + +.format-card-desc { + font-size: 11px; + color: var(--text-2); + line-height: 1.3; + text-align: center; + letter-spacing: 0; +} +.format-card-selected .format-card-desc { + color: var(--text); +} + +.format-card-check { + position: absolute; + top: 8px; + right: 8px; + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--accent); + color: var(--on-accent); +} + +/* Buttons — Apple pill/rounded */ +.google-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 10px 22px; + font-family: var(--font-system); + font-size: 16px; + font-weight: 500; + letter-spacing: -0.01em; + border: none; + border-radius: var(--radius-s); + cursor: pointer; + transition: background 0.18s ease, transform 0.24s var(--motion-snappy), box-shadow 0.24s ease, opacity 0.18s ease; + user-select: none; + text-decoration: none; + line-height: 1.25; + min-height: 44px; +} + +.google-button-primary { + background: var(--accent); + color: var(--on-accent); +} +.google-button-primary:hover { + background: var(--accent-hover); +} +.google-button-primary:active { + background: var(--accent-active); + transform: scale(0.98); +} + +.google-button-disabled { + background: var(--fill); + color: var(--text-2); + cursor: default; + opacity: 0.8; + pointer-events: none; +} + +.full-width { + width: 100%; +} + +.button-content { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + overflow: hidden; + max-width: 100%; +} + +.button-log { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + font-size: 14px; +} + +.button-group { + display: flex; + align-items: stretch; + gap: 0; + width: 100%; +} +.button-group .google-button-primary { + flex: 1; + border-radius: var(--radius-s) 0 0 var(--radius-s); +} + +.split-menu { + position: relative; +} +.split-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + min-height: 44px; + background: var(--accent); + color: var(--on-accent); + border: none; + border-left: 1px solid rgba(128, 128, 128, 0.3); + border-radius: 0 var(--radius-s) var(--radius-s) 0; + cursor: pointer; + transition: background 0.18s ease, transform 0.24s var(--motion-snappy); +} +.split-toggle:hover { + background: var(--accent-hover); +} +.split-toggle:active { + transform: scale(0.94); +} +.split-toggle svg { + transition: transform 0.24s var(--motion-snappy); +} +.split-menu.open .split-toggle svg { + transform: rotate(180deg); +} + +.split-dropdown { + position: absolute; + bottom: calc(100% + 8px); + right: 0; + background: var(--surface); + border-radius: var(--radius-m); + box-shadow: var(--elevation-2); + z-index: 200; + min-width: 220px; + overflow: hidden; + border: 1px solid var(--border); + /* Materialize: animate in with slight raise + fade (spring-driven scale) */ + transform-origin: bottom right; + animation: menu-in 0.28s var(--motion-snappy) both; +} +@keyframes menu-in { + from { opacity: 0; transform: scale(0.96) translateY(4px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +.dropdown-item { + display: flex; + align-items: center; + padding: 12px 16px; + font-family: var(--font-system); + font-size: 15px; + color: var(--text); + cursor: pointer; + transition: background 0.15s ease; + white-space: nowrap; +} +.dropdown-item svg { + margin-right: 12px; + color: var(--text-2); +} +.dropdown-item:hover { + background: var(--fill-hover); +} +.dropdown-item:active { + background: var(--fill-active); +} + +/* Spinner — accent, springy */ +.google-spinner { + width: 20px; + height: 20px; + border: 2px solid var(--fill-active); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.7s linear infinite; +} +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ========================================================================== + SHORT BOOK CARD + ========================================================================== */ +.book-card { + display: flex; + gap: 24px; + padding: 24px; + background: var(--surface); + border-radius: var(--radius-l); + box-shadow: var(--elevation-1); + border: 1px solid transparent; + transition: box-shadow 0.3s ease, transform 0.3s var(--motion-snappy); +} +.book-card:hover { + box-shadow: var(--elevation-2); +} + +.book-card-cover { + width: 140px; + min-width: 140px; + height: 210px; + border-radius: var(--radius-m); + overflow: hidden; + background: var(--surface-2); + box-shadow: var(--elevation-1); +} +.book-card-cover img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.book-card-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.book-card-title { + font-size: 1.5rem; + font-weight: 600; + letter-spacing: -0.022em; + color: var(--text); + line-height: 1.2; + text-decoration: none; + display: block; +} +.book-card-title:hover { + color: var(--accent); + text-decoration: none; +} + +.book-card-meta { + font-size: 15px; + color: var(--text-2); + line-height: 1.4; +} +.book-card-meta a { + color: var(--accent); +} + +.book-card-annotation { + font-size: 15px; + color: var(--text); + line-height: 1.55; + margin-top: 6px; + white-space: pre-line; + max-height: 160px; + overflow-y: auto; + letter-spacing: 0; +} + +/* ========================================================================== + FEATURES PAGE — card grid + ========================================================================== */ +.features-page { + display: flex; + flex-direction: column; + gap: 32px; + padding: 8px 0; +} + +.features-header { + text-align: center; + padding: 16px 0; +} + +.features-title { + font-size: clamp(2.25rem, 6vw, 3rem); + font-weight: 600; + letter-spacing: -0.022em; + color: var(--text); + margin: 0 0 12px; + line-height: 1.05; +} + +.features-subtitle { + font-size: clamp(1.125rem, 3vw, 1.375rem); + color: var(--text-2); + margin: 0; + line-height: 1.4; +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; +} + +.feature-card { + display: flex; + flex-direction: column; + gap: 12px; + padding: 28px 24px; + background: var(--surface); + border-radius: var(--radius-l); + box-shadow: var(--elevation-1); + border: 1px solid transparent; + transition: transform 0.3s var(--motion-snappy), box-shadow 0.3s ease; +} +.feature-card:hover { + transform: translateY(-3px); + box-shadow: var(--elevation-2); +} + +.feature-icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 56px; + height: 56px; + border-radius: var(--radius-m); + background: var(--accent-soft); + color: var(--accent); + margin-bottom: 4px; + transition: transform 0.3s var(--motion-snappy); +} +.feature-card:hover .feature-icon-wrapper { + transform: scale(1.06); +} + +.feature-card-title { + font-size: 1.25rem; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--text); + margin: 0; + line-height: 1.25; +} + +.feature-card-desc { + font-size: 15px; + color: var(--text-2); + margin: 0; + line-height: 1.5; + letter-spacing: 0; +} + +/* ========================================================================== + SUPPORTED SITES PAGE + ========================================================================== */ +.supported-page { + display: flex; + flex-direction: column; + gap: 24px; + padding: 8px 0; +} + +.supported-header { + text-align: center; + padding: 16px 0; +} + +.supported-title { + font-size: clamp(2.25rem, 6vw, 3rem); + font-weight: 600; + letter-spacing: -0.022em; + color: var(--text); + margin: 0 0 12px; + line-height: 1.05; +} + +.supported-subtitle { + font-size: clamp(1.125rem, 3vw, 1.375rem); + color: var(--text-2); + margin: 0; + line-height: 1.4; +} + +.supported-search { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + background: var(--fill); + border-radius: var(--radius-s); + max-width: 360px; + margin: 0 auto; + width: 100%; + transition: background 0.18s ease, box-shadow 0.18s ease; + -webkit-appearance: none; +} +.supported-search:hover { + background: var(--fill-hover); +} +.supported-search:focus-within { + background: var(--surface); + box-shadow: inset 0 0 0 1.5px var(--accent); +} + +.search-icon { + flex-shrink: 0; + color: var(--text-2); +} + +.supported-input { + flex: 1; + font-family: var(--font-system); + font-size: 16px; + color: var(--text); + background: transparent; + border: none; + outline: none; + padding: 4px 0; + letter-spacing: 0; +} +.supported-input::placeholder { + color: var(--text-3); + opacity: 1; +} + +.supported-sites { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 10px; +} + +.site-card { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 16px; + background: var(--surface); + border-radius: var(--radius-m); + box-shadow: var(--elevation-1); + border: 1px solid transparent; + transition: transform 0.24s var(--motion-snappy), box-shadow 0.24s ease; +} +.site-card:hover { + transform: translateY(-2px); + box-shadow: var(--elevation-2); +} +.site-card:active { + transform: scale(0.985); +} + +.site-card-icon { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: var(--radius-s); + background: var(--accent-soft); + color: var(--accent); + flex-shrink: 0; +} +.site-card-icon svg { + width: 20px; + height: 20px; +} + +.site-card-info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.site-card-name { + font-size: 16px; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--text); + line-height: 1.25; +} + +.site-card-url { + font-size: 13px; + color: var(--text-3); + text-decoration: none; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + letter-spacing: 0; +} +.site-card-url:hover { + color: var(--accent); + text-decoration: none; +} + +.supported-empty { + grid-column: 1 / -1; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 56px 0; + color: var(--text-2); +} +.supported-empty svg { + color: var(--text-3); +} +.supported-empty p { + margin: 0; + font-size: 17px; + color: var(--text-2); +} + +/* ========================================================================== + ERROR PAGE + ========================================================================== */ +.error-page { + display: flex; + justify-content: center; + padding-top: 64px; +} + +.error-card { + background: var(--surface); + border-radius: var(--radius-l); + padding: 40px 32px; + max-width: 440px; + width: 100%; + text-align: center; + box-shadow: var(--elevation-2); + border: 1px solid transparent; +} + +.error-title { + font-size: 1.75rem; + font-weight: 600; + letter-spacing: -0.022em; + color: var(--text); + margin: 0 0 12px; +} + +.error-description { + font-size: 17px; + color: var(--text-2); + margin: 0 0 12px; + line-height: 1.5; + letter-spacing: 0; +} + +.error-details { + font-size: 14px; + color: var(--text-2); + margin: 0; +} +.error-details code { + background: var(--fill); + color: var(--text); + padding: 3px 10px; + border-radius: var(--radius-xs); + font-family: var(--font-mono); + font-size: 13px; +} + +/* ========================================================================== + BLAZOR ERROR UI + LOADING BAR + ========================================================================== */ +#blazor-error-ui { + background: var(--danger); + color: #fff; + bottom: 0; + box-shadow: 0 -2px 16px var(--shadow-color); + display: none; + left: 0; + padding: 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; + font-size: 15px; + line-height: 1.4; +} +#blazor-error-ui .reload { + color: #fff; + margin-left: 0.5rem; + text-decoration: underline; + cursor: pointer; +} +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 50%; + transform: translateY(-50%); +} + +#blazor-loading { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 3px; + background: transparent; + z-index: 9999; + overflow: hidden; +} +#blazor-loading::after { + content: ''; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 40%; + background: linear-gradient(90deg, transparent, var(--accent), transparent); + animation: loading-slide 1.2s ease-in-out infinite; + border-radius: 2px; +} +@keyframes loading-slide { + 0% { left: -40%; } + 100% { left: 100%; } +} + +/* ========================================================================== + RESPONSIVE + ========================================================================== */ +@media (max-width: 640px) { + .app-header-content { + padding: 0 12px; + } + + .app-content { + padding: 20px 16px 48px; + } + + .home-hero { + padding: 16px 0 4px; + } + + .home-stats { + gap: 10px; + } + + .home-stat { + padding: 16px 20px; + min-width: 0; + flex: 1; + max-width: none; + } + + .home-stat-value { + font-size: 1.5rem; + } + + .features-grid { + grid-template-columns: 1fr; + } + + .features-page { + gap: 24px; + } + + .feature-card { + padding: 24px 20px; + } + + .supported-sites { + grid-template-columns: 1fr; + } + + .supported-page { + gap: 16px; + } + + .form-section { + padding: 20px 16px; + } + + .format-cards { + gap: 8px; + } + + .format-card { + width: calc(50% - 4px); + min-width: 0; + padding: 16px 8px 14px; + } + + .format-card-icon { + width: 44px; + height: 44px; + } + .format-card-icon svg { + width: 24px; + height: 24px; + } + + .range-fields { + flex-direction: column; + gap: 10px; + } + + .radio-group { + flex-direction: column; + gap: 2px; + } + + .google-radio { + padding: 8px 12px; + } + + .book-card { + flex-direction: column; + gap: 16px; + padding: 20px; + } + + .book-card-cover { + width: 120px; + min-width: 120px; + height: 180px; + } + + .book-card-title { + font-size: 1.25rem; + } + + .book-card-annotation { + max-height: 120px; + } + + .split-dropdown { + right: 0; + left: auto; + min-width: 190px; + } + + .error-page { + padding-top: 32px; + } + + .error-card { + padding: 28px 20px; + } + + .app-footer-content { + padding: 20px 16px; + } + + .app-footer-links { + flex-direction: column; + gap: 10px; + } + + /* full-size inputs on small screens prevent iOS zoom */ + .google-input, + .auth-input { + font-size: 17px; + } + + /* Mobile nav: slide-in drawer, spring-driven in JavaScript */ + .app-nav { + display: none; + } + + .hamburger-btn { + display: flex; + } + + .app-nav-mobile { + position: fixed; + top: 56px; + left: 0; + width: 280px; + height: calc(100vh - 56px); + flex-direction: column; + gap: 4px; + padding: 8px; + background: var(--surface); + border-right: 1px solid var(--border); + box-shadow: var(--elevation-3); + z-index: 200; + overflow-y: auto; + display: flex; + will-change: transform; + /* Transform driven by spring in app.js (interruptible). */ + } + + .app-nav-mobile .app-nav-link { + width: 100%; + padding: 12px 16px; + gap: 12px; + border-radius: var(--radius-s); + font-size: 17px; + } + + .mobile-overlay { + display: block; + position: fixed; + top: 56px; + left: 0; + width: 100%; + height: calc(100vh - 56px); + background: var(--scrim); + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + z-index: 150; + opacity: 0; + transition: opacity 0.25s ease; + } + .mobile-overlay.show { + opacity: 1; + } +} + +@media (max-width: 380px) { + .format-card { + width: 100%; + } + + .format-cards { + gap: 6px; + } + + .auth-password-toggle { + width: 32px; + height: 32px; + } + + .book-card-cover { + width: 100px; + min-width: 100px; + height: 150px; + } + + .split-toggle { + width: 42px; + } +} + +/* ========================================================================== + ACCESSIBILITY — Reduced motion / transparency / contrast + ========================================================================== */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + scroll-behavior: auto !important; + } + + .format-card:active, + .google-button-primary:active, + .split-toggle:active, + .google-radio:active, + .site-card:active, + .dropdown-item:active, + .home-stat:hover, + .feature-card:hover, + .site-card:hover, + .book-card:hover { + transform: none; + } +} + +@media (prefers-reduced-transparency: reduce) { + .app-header, + .mobile-overlay { + background: var(--surface) !important; + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; + } +} + +@media (prefers-contrast: more) { + .app-header, + .book-card, + .feature-card, + .site-card, + .home-stat, + .error-card, + .book-form { + border: 1px solid var(--border-strong); + } +} diff --git a/src/Elib2EbookWeb/wwwroot/app.js b/src/Elib2EbookWeb/wwwroot/app.js new file mode 100644 index 00000000..558860b2 --- /dev/null +++ b/src/Elib2EbookWeb/wwwroot/app.js @@ -0,0 +1,189 @@ +/* Elib2Ebook — Apple-style fluid motion. + * + * A tiny damped-spring engine for interruptible UI motion, in the spirit of + * the "Designing Fluid Interfaces" approach: every animation starts from the + * current on-screen value, inherits velocity, and can be grabbed and reversed + * at any instant. Springs are inherently interruptible and velocity-aware. + * + * We map Apple's two parameters directly: + * damping = damping ratio (1.0 critically damped, <1.0 bounce) + * response = time to reach the target in seconds (lower = snappier) + */ +(function () { + const state = new WeakMap(); + const reduced = window.matchMedia('(prefers-reduced-motion: reduce)'); + const reducedTransparency = window.matchMedia('(prefers-reduced-transparency: reduce)'); + + // Critically/under-damped spring -> stiffness + damping coefficient. + function springParams(damping, response) { + const w0 = (2 * Math.PI) / response; // natural angular frequency + const stiffness = w0 * w0; + const dampingC = 2 * damping * w0; // zeta * 2 * sqrt(k) + return { stiffness, dampingC }; + } + + // Current numeric value of a CSS length property ("0px" -> 0). + function read(el, prop) { + return parseFloat(getComputedStyle(el)[prop]) || 0; + } + + /** + * Animate a numeric CSS property with a damped spring to `target`. + * Interruptible: calling `go` again on the same element re-targets from the + * live on-screen value and carries the current velocity (no "brick wall"). + * + * @param {HTMLElement} el + * @param {string} prop e.g. 'left', 'top' + * @param {number} target final value in `unit` + * @param {object} [opts] { damping, response, unit, onDone } + */ + function go(el, prop, target, opts = {}) { + const damping = opts.damping ?? 1.0; + const response = opts.response ?? 0.32; + const unit = opts.unit ?? 'px'; + const { stiffness, dampingC } = springParams(damping, response); + + // Reduced motion: no springing — just land. Feedback, not vestibular motion. + if (reduced.matches) { + el.style[prop] = target + unit; + opts.onDone && opts.onDone(); + return; + } + + let s = state.get(el); + if (!s || s.raf === null) { + s = { x: read(el, prop), v: 0 }; + } + if (s.raf) cancelAnimationFrame(s.raf); + + const set = (v) => { el.style[prop] = v + unit; }; + let prev = performance.now(); + + function frame(now) { + const dt = Math.min((now - prev) / 1000, 1 / 30); + prev = now; + const a = -stiffness * (s.x - target) - dampingC * s.v; + s.v += a * dt; + s.x += s.v * dt; + set(s.x); + + if (Math.abs(s.x - target) < 0.3 && Math.abs(s.v) < 0.5) { + set(target); + s.raf = null; + opts.onDone && opts.onDone(); + return; + } + s.raf = requestAnimationFrame(frame); + } + s.raf = requestAnimationFrame(frame); + state.set(el, s); + } + + // Read the current translateX of an element (fallback to 0). + function readTranslateX(el) { + const t = getComputedStyle(el).transform; + if (!t || t === 'none') return 0; + const m = t.match(/matrix\(([^)]+)\)/); + if (m) { + const parts = m[1].split(', ').map(Number); + return parts[4] || 0; + } + return 0; + } + + // Spring a transform: translateX from current live value to `target` px. + function springX(el, target, opts = {}) { + const damping = opts.damping ?? 1.0; + const response = opts.response ?? 0.32; + const { stiffness, dampingC } = springParams(damping, response); + + if (reduced.matches) { + el.style.transform = 'translateX(' + target + 'px)'; + opts.onDone && opts.onDone(); + return; + } + + let s = state.get(el); + if (!s || s.raf === null) { + s = { x: readTranslateX(el), v: 0 }; + } + if (s.raf) cancelAnimationFrame(s.raf); + + let prev = performance.now(); + function frame(now) { + const dt = Math.min((now - prev) / 1000, 1 / 30); + prev = now; + const a = -stiffness * (s.x - target) - dampingC * s.v; + s.v += a * dt; + s.x += s.v * dt; + el.style.transform = 'translateX(' + s.x + 'px)'; + if (Math.abs(s.x - target) < 0.3 && Math.abs(s.v) < 0.5) { + el.style.transform = 'translateX(' + target + 'px)'; + s.raf = null; + opts.onDone && opts.onDone(); + return; + } + s.raf = requestAnimationFrame(frame); + } + s.raf = requestAnimationFrame(frame); + state.set(el, s); + } + + // --- Mobile drawer (interruptible spring slide) ------------------------- + // The #app-nav element is the same element as the desktop row; on mobile + // it becomes a fixed drawer parked off-screen (translateX(-width)). We + // spring the transform between closed (-width px) and open (0px). + function drawer(open, width = 280) { + const el = document.getElementById('app-nav'); + if (!el) return; + const target = open ? 0 : -width; + springX(el, target, { damping: 1.0, response: 0.32 }); + el.classList.toggle('open', open); + // Overlay + const overlay = el.parentElement && el.parentElement.querySelector('.mobile-overlay'); + if (overlay) overlay.classList.toggle('show', open); + } + + // --- Theme (light/dark) ---------------------------------------------- + // "light" | "dark" | "system". An explicit user choice is persisted in + // localStorage (elib-theme). While "system" (default), the + // data-theme attribute follows prefers-color-scheme; a manual choice pins + // it. Setting data-theme on is read by the CSS variables in app.css + // and by the default overridden in the head. + const THEME_KEY = 'elib-theme'; + function currentSystemDark() { + return window.matchMedia('(prefers-color-scheme: dark)').matches; + } + function applyTheme(pref) { + const theme = pref === 'light' || pref === 'dark' ? pref : (currentSystemDark() ? 'dark' : 'light'); + const root = document.documentElement; + root.setAttribute('data-theme', theme); + // Keep native controls (scrollbars, inputs) matching the palette. + root.style.colorScheme = theme; + } + function initTheme() { + const saved = (() => { try { return localStorage.getItem(THEME_KEY); } catch (e) { return null; } })(); + applyTheme(saved === 'light' || saved === 'dark' ? saved : 'system'); + if (saved === 'light' || saved === 'dark') return; // pinned by the user + // No explicit choice → follow the system, live. + const mq = window.matchMedia('(prefers-color-scheme: dark)'); + const onChange = () => applyTheme('system'); + if (mq.addEventListener) mq.addEventListener('change', onChange); + else mq.addListener(onChange); + } + // Called by Blazor (MainLayout). + function setThemeMode(mode) { + if (mode === 'light' || mode === 'dark') { + try { localStorage.setItem(THEME_KEY, mode); } catch (e) {} + applyTheme(mode); + } else { + try { localStorage.removeItem(THEME_KEY); } catch (e) {} + applyTheme('system'); + } + } + // Let Blazor query the current effective theme if needed. + function getThemeMode() { return document.documentElement.getAttribute('data-theme'); } + + window.elibMotion = { go, drawer, springX, setThemeMode, getThemeMode }; + initTheme(); +})(); diff --git a/Elib2EbookWeb/wwwroot/favicon.png b/src/Elib2EbookWeb/wwwroot/favicon.png similarity index 100% rename from Elib2EbookWeb/wwwroot/favicon.png rename to src/Elib2EbookWeb/wwwroot/favicon.png diff --git a/Core/Logic/Getters/AcomicsGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Acomics/AcomicsGetter.cs similarity index 86% rename from Core/Logic/Getters/AcomicsGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Acomics/AcomicsGetter.cs index c9394bd4..4c715369 100644 --- a/Core/Logic/Getters/AcomicsGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Acomics/AcomicsGetter.cs @@ -1,26 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Acomics; public class AcomicsGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://acomics.ru"); protected override string GetId(Uri url) - { - return url.GetSegment(1); - } + => url.GetSegment(1); public override async Task Init() { @@ -57,7 +52,10 @@ private static Author GetAuthor(HtmlDocument doc, Uri url) private async Task> FillChapters(HtmlDocument doc, string bookId, string title, Uri url) { - if (Config.Options.NoChapters) return []; + if (Config.Options.NoChapters) + { + return []; + } var chapter = new Chapter { @@ -79,6 +77,9 @@ private async Task> FillChapters(HtmlDocument doc, string b chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; - return new[] { chapter }; + return + [ + chapter, + ]; } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Acomics/Elib2Ebook.ExternalServices.Acomics.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Acomics/Elib2Ebook.ExternalServices.Acomics.csproj new file mode 100644 index 00000000..42353cea --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Acomics/Elib2Ebook.ExternalServices.Acomics.csproj @@ -0,0 +1,8 @@ + + + net10.0 + + + + + diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/AuthorTodayGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/AuthorTodayGetter.cs new file mode 100644 index 00000000..18f13907 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/AuthorTodayGetter.cs @@ -0,0 +1,259 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Security.Cryptography; +using System.Text; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.AuthorToday.Types; +using Microsoft.Extensions.Logging; + +namespace Elib2Ebook.ExternalServices.AuthorToday; + +public class AuthorTodayGetter(BookGetterConfig config) : GetterBase(config) +{ + protected override Uri SystemUrl => new("https://author.today/"); + + private Uri _apiUrl => new("https://api.author.today/"); + + /// + /// IP сайта author.today + /// + private Uri _systemIp => new("https://185.26.98.195/"); + + /// + /// IP сайта api.author.today + /// + private Uri _apiIp => new("https://185.26.98.195/"); + + private bool _bypass; + + private Uri ApiUrl => _bypass ? _apiIp : _apiUrl; + + private Uri SiteUrl => _bypass ? _systemIp : SystemUrl; + + private string UserId { get; set; } = string.Empty; + + protected override string GetId(Uri url) + => url.GetSegment(2); + + private const string AT_CERT = "rmy8LDkVZR+pSXopxUte7hFbA6I="; + + private string hashedCert => Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(AT_CERT))).ToUpper(); + + public override async Task Init() + { + Config.Client.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); + Config.Client.DefaultRequestHeaders.Add("Accept-Language", "ru"); + Config.Client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br"); + Config.Client.DefaultRequestHeaders.Add("User-Agent", "okhttp/4.12.0 X_AT_API"); + Config.Client.DefaultRequestHeaders.Add("X-AT-Client", "android_1.8.013-GMS"); + Config.Client.DefaultRequestHeaders.Add("X-AT-Certificate", hashedCert); + + var response = await Config.Client.GetAsync(_apiUrl); + _bypass = response.StatusCode != HttpStatusCode.OK; + Config.Logger.LogInformation( + _bypass ? + $"Сайт {_apiUrl} не доступен. Работаю через {_apiIp}" : + $"Сайт {_apiUrl} доступен. Работаю через него"); + + Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "guest"); + } + + public override async Task Authorize() + { + if (!Config.HasCredentials) + { + return; + } + + var response = await Config.Client.SendAsync( + GetDefaultMessage( + ApiUrl.MakeRelativeUri("/v1/account/login-by-password"), + _apiUrl, + JsonContent.Create( + new + { + Config.Options.Login, Config.Options.Password + }))); + + var data = await response.Content.ReadFromJsonAsync(); + + if (!string.IsNullOrWhiteSpace(data?.Token)) + { + Config.Logger.LogInformation("Успешно авторизовались"); + Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", data.Token); + + response = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(ApiUrl.MakeRelativeUri("/v1/account/current-user"), _apiUrl)); + var user = await response.Content.ReadFromJsonAsync(); + UserId = user!.Id.ToString(); + } + else + { + throw new Exception($"Не удалось авторизоваться. {data?.Message}"); + } + } + + public override async Task Get(Uri url) + { + url = SystemUrl.MakeRelativeUri($"/work/{GetId(url)}"); + var details = await GetBookDetails(GetId(url)); + + var book = new Book(url) + { + Cover = await GetCover(details), + Chapters = await FillChapters(details), + Title = details.Title, + Author = GetAuthor(details), + CoAuthors = GetCoAuthors(details), + Annotation = details.Annotation, + Seria = GetSeria(details) + }; + + await FillAdditional(book, details); + + return book; + } + + private async Task FillAdditional(Book book, BookDetails details) + { + if (!Config.Options.HasAdditionalType(AdditionalTypeEnum.Images) || details.GalleryImages == null || details.GalleryImages.Length == 0) + { + return; + } + + Config.Logger.LogInformation("Загружаю дополнительные иллюстрации"); + foreach (var image in details.GalleryImages) + { + book.AdditionalFiles.Add(AdditionalTypeEnum.Images, await SaveImage(SystemUrl.MakeRelativeUri(image.Url))); + } + + Config.Logger.LogInformation("Дополнительные иллюстрации загружены"); + } + + private HttpRequestMessage GetDefaultMessage(Uri uri, Uri host, HttpContent content = null) + { + var message = new HttpRequestMessage(content == null ? HttpMethod.Get : HttpMethod.Post, uri); + message.Content = content; + message.Version = Config.Client.DefaultRequestVersion; + + foreach (var header in Config.Client.DefaultRequestHeaders) + { + message.Headers.Add(header.Key, header.Value); + } + + message.Headers.Host = host.Host; + + return message; + } + + private async Task GetBookDetails(string bookId) + { + var response = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(ApiUrl.MakeRelativeUri($"/v1/work/{bookId}/details"), _apiUrl)); + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception("Книга не найдена"); + } + + return await response.Content.ReadFromJsonAsync(); + } + + private Author GetAuthor(BookDetails book) + { + return new Author(book.AuthorFio, SystemUrl.MakeRelativeUri($"/u/{book.AuthorUserName}/works")); + } + + private IEnumerable GetCoAuthors(BookDetails book) + { + var result = new List(); + if (!string.IsNullOrWhiteSpace(book.CoAuthorFio)) + { + result.Add(new Author(book.CoAuthorFio, SystemUrl.MakeRelativeUri($"/u/{book.CoAuthorUserName}/works"))); + } + + return result; + } + + private Seria GetSeria(BookDetails book) + { + if (!book.SeriesId.HasValue) + { + return null; + } + + return new Seria + { + Name = book.SeriesTitle.Trim(), + Number = book.SeriesWorkNumber.HasValue ? book.SeriesWorkNumber.ToString() : string.Empty, + Url = SystemUrl.MakeRelativeUri($"/work/series/{book.SeriesId}") + }; + } + + private Task GetCover(BookDetails book) + { + return !string.IsNullOrWhiteSpace(book.CoverUrl) ? SaveImage(book.CoverUrl.AsUri()) : Task.FromResult(default(TempFile)); + } + + protected override HttpRequestMessage GetImageRequestMessage(Uri uri) + { + if (uri.IsSameHost(SystemUrl) || uri.IsSameSubDomain(SystemUrl)) + { + return GetDefaultMessage(SiteUrl.MakeRelativeUri(uri.AbsolutePath), uri); + } + + return base.GetImageRequestMessage(uri); + } + + private async Task> FillChapters(BookDetails book) + { + var result = new List(); + if (Config.Options.NoChapters) + { + return result; + } + + foreach (var atChapter in await GetChapters(book)) + { + var title = atChapter.Title.ReplaceNewLine(); + Config.Logger.LogInformation($"Загружаю главу {title.CoverQuotes()}"); + + var chapter = new Chapter + { + Title = title + }; + + if (atChapter.IsSuccessful) + { + var chapterDoc = atChapter.Decode(UserId, AT_CERT).AsHtmlDoc(); + chapter.Images = await GetImages(chapterDoc, SystemUrl); + chapter.Content = chapterDoc.DocumentNode.InnerHtml; + } + + result.Add(chapter); + } + + return result; + } + + private async Task> GetChapters(BookDetails book) + { + var result = new List(); + + foreach (var chunk in book.Chapters.Where(c => !c.IsDraft).OrderBy(c => c.SortOrder).Chunk(100)) + { + var ids = string.Join("&", chunk.Select((c, i) => $"ids[{i}]={c.Id}")); + var uri = ApiUrl.MakeRelativeUri($"/v1/work/{book.Id}/chapter/many-texts?{ids}"); + var response = await Config.Client.SendWithTriesAsync(() => GetDefaultMessage(uri, _apiUrl)); + var chapters = await response.Content.ReadFromJsonAsync(); + if (chapters != null) + { + result.AddRange(chapters.Where(c => c.Code != "NotFound")); + } + } + + return SliceToc(result, c => c.Title); + } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Elib2Ebook.ExternalServices.AuthorToday.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Elib2Ebook.ExternalServices.AuthorToday.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Elib2Ebook.ExternalServices.AuthorToday.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Types/AuthorToday/AuthorTodayAuthResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/AuthResponse.cs similarity index 68% rename from Core/Types/AuthorToday/AuthorTodayAuthResponse.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/AuthResponse.cs index 5be99357..49165fc3 100644 --- a/Core/Types/AuthorToday/AuthorTodayAuthResponse.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/AuthResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.AuthorToday; +namespace Elib2Ebook.ExternalServices.AuthorToday.Types; -public class AuthorTodayAuthResponse +internal class AuthResponse { [JsonPropertyName("message")] public string Message { get; set; } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/AuthorTodayChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/AuthorTodayChapter.cs new file mode 100644 index 00000000..70f417fc --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/AuthorTodayChapter.cs @@ -0,0 +1,54 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.AuthorToday.Types; + +internal class AuthorTodayChapter +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("sortOrder")] + public long SortOrder { get; set; } + + [JsonPropertyName("text")] + public string Text { get; set; } + + [JsonPropertyName("key")] + public string Key { get; set; } + + [JsonPropertyName("IsSuccessful")] + public bool IsSuccessful { get; set; } + + [JsonPropertyName("IsDraft")] + public bool IsDraft { get; set; } + + [JsonPropertyName("code")] + public string Code { get; set; } + + public string Decode(string userId, string cert) + { + var secret = $"{string.Concat(Key.Reverse())}:{(string.IsNullOrWhiteSpace(userId) ? "Guest" : userId)}:{"FjPg]{2+$8JRvv~("}:{cert}"; + var hashSecret = Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(secret))); + + using var aes = Aes.Create(); + const int IV_SHIFT = 16; + + var aesKey = Encoding.UTF8.GetBytes(hashSecret)[..IV_SHIFT]; + + aes.Key = aesKey; + aes.IV = aesKey; + + using var ms = new MemoryStream(Convert.FromBase64String(Text)); + using var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read); + + var output = new MemoryStream(); + cs.CopyTo(output); + + return Encoding.UTF8.GetString(output.ToArray()); + } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/BookDetails.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/BookDetails.cs new file mode 100644 index 00000000..8d84696e --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/BookDetails.cs @@ -0,0 +1,45 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.AuthorToday.Types; + +internal class BookDetails +{ + [JsonPropertyName("annotation")] + public string Annotation { get; set; } + + [JsonPropertyName("seriesId")] + public long? SeriesId { get; set; } + + [JsonPropertyName("seriesWorkNumber")] + public long? SeriesWorkNumber { get; set; } + + [JsonPropertyName("seriesTitle")] + public string SeriesTitle { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("authorFIO")] + public string AuthorFio { get; set; } + + [JsonPropertyName("coAuthorFIO")] + public string CoAuthorFio { get; set; } + + [JsonPropertyName("coverUrl")] + public string CoverUrl { get; set; } + + [JsonPropertyName("chapters")] + public AuthorTodayChapter[] Chapters { get; set; } + + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("authorUserName")] + public string AuthorUserName { get; set; } + + [JsonPropertyName("CoAuthorUserName")] + public string CoAuthorUserName { get; set; } + + [JsonPropertyName("galleryImages")] + public Image[] GalleryImages { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/Image.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/Image.cs new file mode 100644 index 00000000..597f541f --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/Image.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.AuthorToday.Types; + +internal class Image +{ + [JsonPropertyName("url")] + public string Url { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/User.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/User.cs new file mode 100644 index 00000000..12624ec2 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.AuthorToday/Types/User.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.AuthorToday.Types; + +internal class User +{ + [JsonPropertyName("id")] + public long Id { get; set; } +} diff --git a/Core/Logic/Getters/BookInBookGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BookInBook/BookInBookGetter.cs similarity index 72% rename from Core/Logic/Getters/BookInBookGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.BookInBook/BookInBookGetter.cs index 3185135a..1d20e202 100644 --- a/Core/Logic/Getters/BookInBookGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BookInBook/BookInBookGetter.cs @@ -1,27 +1,21 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.BookInBook; public class BookInBookGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://bookinbook.ru/"); protected override string GetId(Uri url) - { - return url.GetQueryParameter("id"); - } + => url.GetQueryParameter("id"); public override async Task Get(Uri url) { @@ -44,7 +38,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(HtmlDocument doc, Uri uri) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var bookChapter in GetToc(doc, uri)) { @@ -69,10 +66,12 @@ private async Task GetChapter(Uri url) for (var i = 1;; i++) { - var uri = SystemUrl.MakeRelativeUri( - $"read?id={url.GetQueryParameter("id")}&chapter={url.GetQueryParameter("chapter")}&page={i}"); + var uri = SystemUrl.MakeRelativeUri($"read?id={url.GetQueryParameter("id")}&chapter={url.GetQueryParameter("chapter")}&page={i}"); using var response = await Config.Client.GetWithTriesAsync(uri, TimeSpan.FromMilliseconds(100)); - if (response is null) return sb.AsHtmlDoc(); + if (response == null) + { + return sb.AsHtmlDoc(); + } var doc = await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc()); var text = doc.GetTextBySelector("#PAGE_TEXT").Deserialize().HtmlDecode(); @@ -81,9 +80,15 @@ private async Task GetChapter(Uri url) while (true) { var line = await sr.ReadLineAsync(); - if (line == null) break; + if (line == null) + { + break; + } - if (string.IsNullOrWhiteSpace(line)) continue; + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } sb.Append(line.CoverTag("p")); } @@ -92,29 +97,30 @@ private async Task GetChapter(Uri url) private IEnumerable GetToc(HtmlDocument doc, Uri url) { - var urlChapters = doc.QuerySelectorAll("a.chapters-form__chapter").Select(a => - new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())).ToList(); + var urlChapters = doc.QuerySelectorAll("a.chapters-form__chapter").Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) + .ToList(); + return SliceToc(urlChapters, c => c.Title); } private static Seria GetSeria(HtmlDocument doc) { var a = doc.QuerySelector("span.series-info__name"); - if (a is not null) + if (a != null) + { return new Seria { Name = a.GetText() }; + } - return default; + return null; } private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("img.book-cover__image")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private static Author GetAuthor(HtmlDocument doc, Uri uri) diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BookInBook/Elib2Ebook.ExternalServices.BookInBook.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.BookInBook/Elib2Ebook.ExternalServices.BookInBook.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BookInBook/Elib2Ebook.ExternalServices.BookInBook.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/BookRiverGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/BookRiverGetter.cs similarity index 70% rename from Core/Logic/Getters/BookRiverGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/BookRiverGetter.cs index bf4e3180..9127eeff 100644 --- a/Core/Logic/Getters/BookRiverGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/BookRiverGetter.cs @@ -1,48 +1,44 @@ -using System; -using System.Collections.Generic; using System.Net; -using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Bookriver; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.BookRiver.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.BookRiver; public class BookriverGetter(BookGetterConfig config) : GetterBase(config) { - private string _token; protected override Uri SystemUrl => new("https://bookriver.ru/"); private Uri _apiUrl => new($"https://api.{SystemUrl.Host}/"); + private string _token; + protected override string GetId(Uri url) - { - return url.GetSegment(2); - } + => url.GetSegment(2); public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var payload = new { - email = Config.Options.Login, - password = Config.Options.Password, - rememberMe = 1 + email = Config.Options.Login, password = Config.Options.Password, rememberMe = 1 }; using var post = await Config.Client.PostAsJsonAsync(_apiUrl.MakeRelativeUri("/api/v1/auth/login"), payload); - var data = await post.Content.ReadFromJsonAsync(); + var data = await post.Content.ReadFromJsonAsync(); if (!string.IsNullOrWhiteSpace(data.Token)) { Config.Logger.LogInformation("Успешно авторизовались"); @@ -51,7 +47,7 @@ public override async Task Authorize() } else { - throw new Elib2EbookAuthException("Не удалось авторизоваться"); + throw new Exception("Не удалось авторизоваться"); } } @@ -83,23 +79,30 @@ private static Author GetAuthor(HtmlDocument doc, Uri url) private static Seria GetSeria(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("a[href^=/series/]"); - if (a is null) return default; + if (a == null) + { + return null; + } var span = a.QuerySelector("+ span"); - if (span is null) return default; + if (span == null) + { + return null; + } return new Seria { - Name = a.GetText()[5..].Trim(), - Number = span.GetText().Trim().Trim('#'), - Url = url.MakeRelativeUri(a.Attributes["href"].Value) + Name = a.GetText()[5..].Trim(), Number = span.GetText().Trim().Trim('#'), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; } private async Task> FillChapters(Uri uri, string bookId) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } var internalId = await GetInternalBookId(bookId); @@ -112,7 +115,7 @@ private async Task> FillChapters(Uri uri, string bookId) }; var doc = await GetChapter(bookChapter.Id); - if (doc is not null) + if (doc != null) { chapter.Images = await GetImages(doc, uri); chapter.Content = doc.DocumentNode.InnerHtml; @@ -129,9 +132,14 @@ protected virtual HttpRequestMessage GetMessage(Uri uri) var message = new HttpRequestMessage(HttpMethod.Get, uri); message.Version = Config.Client.DefaultRequestVersion; if (!string.IsNullOrWhiteSpace(_token)) + { message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token); + } - foreach (var header in Config.Client.DefaultRequestHeaders) message.Headers.Add(header.Key, header.Value); + foreach (var header in Config.Client.DefaultRequestHeaders) + { + message.Headers.Add(header.Key, header.Value); + } return message; } @@ -158,29 +166,26 @@ private static T GetNextData(HtmlDocument doc, string node) private async Task GetChapter(long bookChapterId) { - var response = - await Config.Client.SendAsync( - GetMessage(_apiUrl.MakeRelativeUri($"/api/v1/books/chapter/text/{bookChapterId}"))); - if (response.StatusCode == HttpStatusCode.Forbidden) return default; + var response = await Config.Client.SendAsync(GetMessage(_apiUrl.MakeRelativeUri($"/api/v1/books/chapter/text/{bookChapterId}"))); + if (response.StatusCode == HttpStatusCode.Forbidden) + { + return null; + } var content = await response.Content.ReadAsStringAsync(); - return content.Deserialize>().Data.Content.AsHtmlDoc(); + return content.Deserialize>().Data.Content.AsHtmlDoc(); } private async Task> GetToc(string bookId) { - var response = - await Config.Client.GetWithTriesAsync( - _apiUrl.MakeRelativeUri($"/api/v1/books/chapters/text/published?bookId={bookId}")); + var response = await Config.Client.GetWithTriesAsync(_apiUrl.MakeRelativeUri($"/api/v1/books/chapters/text/published?bookId={bookId}")); var content = await response.Content.ReadAsStringAsync(); - return SliceToc(content.Deserialize>().Data, c => c.Name); + return SliceToc(content.Deserialize>().Data, c => c.Name); } private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("img[itemprop=contentUrl]")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Elib2Ebook.ExternalServices.BookRiver.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Elib2Ebook.ExternalServices.BookRiver.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Elib2Ebook.ExternalServices.BookRiver.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/ApiResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/ApiResponse.cs new file mode 100644 index 00000000..447e88a8 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/ApiResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BookRiver.Types; + +internal class ApiResponse +{ + [JsonPropertyName("data")] + public T Data { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/AuthResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/AuthResponse.cs new file mode 100644 index 00000000..edaf131d --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/AuthResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BookRiver.Types; + +internal class AuthResponse +{ + [JsonPropertyName("token")] + public string Token { get; set; } +} diff --git a/Core/Types/Bookriver/BookRiverBook.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/BookRiverBook.cs similarity index 62% rename from Core/Types/Bookriver/BookRiverBook.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/BookRiverBook.cs index 2b7af786..bbea5ec3 100644 --- a/Core/Types/Bookriver/BookRiverBook.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/BookRiverBook.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Bookriver; +namespace Elib2Ebook.ExternalServices.BookRiver.Types; -public class BookRiverBook +internal class BookRiverBook { [JsonPropertyName("currentGraphqlBook")] public BookRiverCurrentBook Book { get; set; } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/BookRiverChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/BookRiverChapter.cs new file mode 100644 index 00000000..83c350b2 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/BookRiverChapter.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BookRiver.Types; + +internal class BookRiverChapter +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/BookRiverCurrentBook.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/BookRiverCurrentBook.cs new file mode 100644 index 00000000..49290ac1 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/BookRiverCurrentBook.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BookRiver.Types; + +internal class BookRiverCurrentBook +{ + [JsonPropertyName("id")] + public long Id { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/ChapterContent.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/ChapterContent.cs new file mode 100644 index 00000000..27d913c5 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BookRiver/Types/ChapterContent.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BookRiver.Types; + +internal class ChapterContent +{ + [JsonPropertyName("content")] + public string Content { get; set; } +} diff --git a/Core/Logic/Getters/BookinistGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookinist/BookinistGetter.cs similarity index 70% rename from Core/Logic/Getters/BookinistGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Bookinist/BookinistGetter.cs index 80944538..654bdb49 100644 --- a/Core/Logic/Getters/BookinistGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookinist/BookinistGetter.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Bookinist; public class BookinistGetter(BookGetterConfig config) : GetterBase(config) { @@ -23,10 +20,7 @@ public override async Task Get(Uri url) var book = new Book(url) { - Cover = await GetCover(doc, url), - Chapters = await FillChapters(doc, url), - Title = doc.GetTextBySelector("title"), - Author = new Author("Bookinist") + Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("title"), Author = new Author("Bookinist") }; return book; @@ -35,7 +29,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var bookChapter in GetToc(doc, url)) { @@ -58,9 +55,7 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("meta[property='og:image']")?.Attributes["content"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private Task GetChapter(UrlChapter bookChapter) @@ -71,8 +66,7 @@ private Task GetChapter(UrlChapter bookChapter) private IEnumerable GetToc(HtmlDocument doc, Uri url) { var bookId = GetId(url); - var result = doc.QuerySelectorAll("ul.menu-toc li a").Skip(1).Select((a, i) => - new UrlChapter(SystemUrl.MakeRelativeUri($"/bookpage/{bookId}/{i + 1}"), a.GetText())).ToList(); + var result = doc.QuerySelectorAll("ul.menu-toc li a").Skip(1).Select((a, i) => new UrlChapter(SystemUrl.MakeRelativeUri($"/bookpage/{bookId}/{i + 1}"), a.GetText())).ToList(); return SliceToc(result, c => c.Title); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Bookinist/Elib2Ebook.ExternalServices.Bookinist.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookinist/Elib2Ebook.ExternalServices.Bookinist.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookinist/Elib2Ebook.ExternalServices.Bookinist.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Elib2Ebook.ExternalServices.BooksYandex.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Elib2Ebook.ExternalServices.BooksYandex.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Elib2Ebook.ExternalServices.BooksYandex.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/BooksYandex/BooksYandexBooksGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Getters/BooksYandexBooksGetter.cs similarity index 63% rename from Core/Logic/Getters/BooksYandex/BooksYandexBooksGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Getters/BooksYandexBooksGetter.cs index ef44b7c2..4e47f775 100644 --- a/Core/Logic/Getters/BooksYandex/BooksYandexBooksGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Getters/BooksYandexBooksGetter.cs @@ -1,20 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Http.Json; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Misc; -using Core.Types.Book; -using Core.Types.BookYandex; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.ExternalServices.BooksYandex.Types; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.BooksYandex; +namespace Elib2Ebook.ExternalServices.BooksYandex.Getters; public class BooksYandexBooksGetter(BookGetterConfig config) : BooksYandexGetterBase(config) { @@ -23,23 +16,35 @@ public class BooksYandexBooksGetter(BookGetterConfig config) : BooksYandexGetter protected override async Task> FillChapters(Book book, BooksYandexResponse response) { if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Audio)) + { book.AdditionalFiles.Add(AdditionalTypeEnum.Audio, await GetAudio(response)); + } var id = response.Book?.UUID ?? response.AudioBook?.LinkedBooks?.FirstOrDefault(); - if (string.IsNullOrWhiteSpace(id)) return []; + if (string.IsNullOrWhiteSpace(id)) + { + return []; + } - if (response.Book?.SourceType == "serial") return await GetFromSerial(book, id); + if (response.Book?.SourceType == "serial") + { + return await GetFromSerial(book, id); + } var requestUri = $"https://api.bookmate.ru/api/v5/books/{id}/content/v4".AsUri(); var epubResponse = await Config.Client.GetAsync(requestUri); if (epubResponse.StatusCode == HttpStatusCode.OK) { - var epubFile = await TempFile.Create(requestUri, Config.TempFolder.Path, + var epubFile = await TempFile.Create( + requestUri, + Config.TempFolder.Path, epubResponse.Content.Headers.ContentDisposition.FileName.Trim('\"'), await epubResponse.Content.ReadAsStreamAsync()); - if (epubFile is not null && Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) + if (epubFile != null && Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) + { book.AdditionalFiles.Add(AdditionalTypeEnum.Books, epubFile); + } return await FillChaptersFromEpub(epubFile); } @@ -59,22 +64,27 @@ private async Task> GetFromSerial(Book book, string id) var requestUri = $"https://api.bookmate.ru/api/v5/books/{episode.Uuid}/content/v4".AsUri(); var epubResponse = await Config.Client.GetAsync(requestUri); - if (epubResponse.StatusCode != HttpStatusCode.OK) continue; + if (epubResponse.StatusCode != HttpStatusCode.OK) + { + continue; + } - var epubFile = await TempFile.Create(requestUri, Config.TempFolder.Path, + var epubFile = await TempFile.Create( + requestUri, + Config.TempFolder.Path, epubResponse.Content.Headers.ContentDisposition.FileName.Trim('\"'), await epubResponse.Content.ReadAsStreamAsync()); - if (epubFile is not null && Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) + if (epubFile != null && Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) + { book.AdditionalFiles.Add(AdditionalTypeEnum.Books, epubFile); + } var episodeChapters = await FillChaptersFromEpub(epubFile); var chapter = new Chapter { - Title = episode.Title, - Content = string.Join(Environment.NewLine, episodeChapters.Select(c => c.Content)), - Images = episodeChapters.SelectMany(c => c.Images) + Title = episode.Title, Content = string.Join(Environment.NewLine, episodeChapters.Select(c => c.Content)), Images = episodeChapters.SelectMany(c => c.Images) }; result.Add(chapter); @@ -91,9 +101,11 @@ private async Task> GetEpisodes(string id) for (var i = 1;; i++) { - var response = await Config.Client.GetFromJsonAsync( - $"https://api.bookmate.ru/api/v5/books/{id}/episodes".AsUri().AppendQueryParameter("page", i)); - if (response.Episodes.Length == 0) break; + var response = await Config.Client.GetFromJsonAsync($"https://api.bookmate.ru/api/v5/books/{id}/episodes".AsUri().AppendQueryParameter("page", i)); + if (response.Episodes.Length == 0) + { + break; + } result.AddRange(response.Episodes); } @@ -103,7 +115,9 @@ private async Task> GetEpisodes(string id) catch (HttpRequestException ex) { if (ex.StatusCode == HttpStatusCode.Unauthorized) - throw new Elib2EbookAuthException("Авторизационный токен невалиден. Требуется обновление"); + { + throw new Exception("Авторизационный токен невалиден. Требуется обновление"); + } throw; } @@ -113,11 +127,17 @@ private async Task> GetAudio(BooksYandexResponse bookResponse) { var result = new List(); var id = bookResponse.AudioBook?.UUID ?? bookResponse.Book?.LinkedAudio?.FirstOrDefault(); - if (string.IsNullOrWhiteSpace(id)) return result; + if (string.IsNullOrWhiteSpace(id)) + { + return result; + } var playlist = await GetPlayList(id); - if (playlist.Tracks.Length == 0) return result; + if (playlist.Tracks.Length == 0) + { + return result; + } for (var i = 0; i < playlist.Tracks.Length; i++) { @@ -126,8 +146,7 @@ private async Task> GetAudio(BooksYandexResponse bookResponse) Config.Logger.LogInformation($"Загружаю аудиоверсию {i + 1}/{playlist.Tracks.Length} {url}"); var response = await Config.Client.GetWithTriesAsync(url.AsUri()); - result.Add(await TempFile.Create(url.AsUri(), Config.TempFolder.Path, $"{i}_{url.AsUri().GetFileName()}", - await response.Content.ReadAsStreamAsync())); + result.Add(await TempFile.Create(url.AsUri(), Config.TempFolder.Path, $"{i}_{url.AsUri().GetFileName()}", await response.Content.ReadAsStreamAsync())); Config.Logger.LogInformation($"Аудиоверсия {i + 1}/{playlist.Tracks.Length} {url} загружена"); } @@ -138,13 +157,14 @@ private async Task GetPlayList(string id) { try { - return await Config.Client.GetFromJsonAsync( - $"https://api.bookmate.ru/api/v5/audiobooks/{id}/playlists.json".AsUri()); + return await Config.Client.GetFromJsonAsync($"https://api.bookmate.ru/api/v5/audiobooks/{id}/playlists.json".AsUri()); } catch (HttpRequestException ex) { if (ex.StatusCode == HttpStatusCode.Unauthorized) - throw new Elib2EbookAuthException("Авторизационный токен невалиден. Требуется обновление"); + { + throw new Exception("Авторизационный токен невалиден. Требуется обновление"); + } throw; } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Getters/BooksYandexComicsGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Getters/BooksYandexComicsGetter.cs new file mode 100644 index 00000000..da0e3491 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Getters/BooksYandexComicsGetter.cs @@ -0,0 +1,38 @@ +using System.Net.Http.Json; +using System.Text; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.ExternalServices.BooksYandex.Types; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Getters; + +public class BooksYandexComicsGetter(BookGetterConfig config) : BooksYandexGetterBase(config) +{ + protected override string[] Paths => ["comicbooks"]; + + protected override async Task> FillChapters(Book book, BooksYandexResponse response) + { + if (Config.Options.NoChapters || string.IsNullOrWhiteSpace(response.Comicbook?.UUID)) + { + return []; + } + + var metadata = await Config.Client.GetFromJsonAsync($"https://api.bookmate.ru/api/v5/comicbooks/{response.Comicbook.UUID}/metadata.json"); + + var sb = new StringBuilder(); + + foreach (var page in metadata.Pages) + { + sb.Append($""); + } + + var doc = sb.AsHtmlDoc(); + var chapter = new Chapter + { + Images = await GetImages(doc, SystemUrl), Content = doc.DocumentNode.InnerHtml, Title = response.Comicbook.Title + }; + + return [chapter]; + } +} diff --git a/Core/Logic/Getters/BooksYandex/BooksYandexGetterBase.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Getters/BooksYandexGetterBase.cs similarity index 65% rename from Core/Logic/Getters/BooksYandex/BooksYandexGetterBase.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Getters/BooksYandexGetterBase.cs index 38b9d130..71ad4a37 100644 --- a/Core/Logic/Getters/BooksYandex/BooksYandexGetterBase.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Getters/BooksYandexGetterBase.cs @@ -1,18 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Http.Json; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.BookYandex; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.BooksYandex.Types; -namespace Core.Logic.Getters.BooksYandex; +namespace Elib2Ebook.ExternalServices.BooksYandex.Getters; public abstract class BooksYandexGetterBase(BookGetterConfig config) : GetterBase(config) { @@ -27,14 +22,16 @@ protected override string GetId(Uri url) public override bool IsSameUrl(Uri url) { - return base.IsSameUrl(url) && Paths.Any(path => - string.Equals(url.GetSegment(1), path, StringComparison.InvariantCultureIgnoreCase)); + return base.IsSameUrl(url) && Paths.Any(path => string.Equals(url.GetSegment(1), path, StringComparison.InvariantCultureIgnoreCase)); } public override Task Authorize() { var token = Config.Options.Login ?? Config.Options.Password; - if (string.IsNullOrWhiteSpace(token)) throw new Elib2EbookAuthException("Не указан авторизационный token."); + if (string.IsNullOrWhiteSpace(token)) + { + throw new Exception("Не указан авторизационный token."); + } Config.Client.DefaultRequestHeaders.Add("auth-token", token); return Task.CompletedTask; @@ -48,8 +45,7 @@ public override async Task Get(Uri url) url = SystemUrl.MakeRelativeUri($"/{path}/{id}"); var bookResponse = await GetBookResponse(path, id); - var details = (BookmateBookBase)bookResponse.Book ?? - (BookmateBookBase)bookResponse.AudioBook ?? bookResponse.Comicbook; + var details = (BookmateBookBase)bookResponse.Book ?? (BookmateBookBase)bookResponse.AudioBook ?? bookResponse.Comicbook; var book = new Book(url) { @@ -57,7 +53,7 @@ public override async Task Get(Uri url) Title = details.Title, Author = GetAuthor(details), Annotation = details.Annotation, - Lang = details.Language + Lang = details.Language, }; book.Chapters = await FillChapters(book, bookResponse); @@ -76,16 +72,19 @@ private async Task GetBookResponse(string path, string id) { var response = await Config.Client.GetAsync($"https://api.bookmate.ru/api/v5/{path}/{id}".AsUri()); if (response.StatusCode != HttpStatusCode.OK) + { response = await Config.Client.GetAsync($"https://api.bookmate.ru/api/v5/books/{id}".AsUri()); + } var booksYandexResponse = await response.Content.ReadFromJsonAsync(); if (booksYandexResponse.AudioBook?.LinkedBooks?.Length > 0) { - var linkedResponse = await Config.Client.GetAsync( - $"https://api.bookmate.ru/api/v5/books/{booksYandexResponse.AudioBook.LinkedBooks[0]}".AsUri()); + var linkedResponse = await Config.Client.GetAsync($"https://api.bookmate.ru/api/v5/books/{booksYandexResponse.AudioBook.LinkedBooks[0]}".AsUri()); if (linkedResponse.StatusCode == HttpStatusCode.OK) + { return await linkedResponse.Content.ReadFromJsonAsync(); + } } return booksYandexResponse; @@ -93,7 +92,9 @@ private async Task GetBookResponse(string path, string id) catch (HttpRequestException ex) { if (ex.StatusCode == HttpStatusCode.Unauthorized) - throw new Elib2EbookAuthException("Авторизационный токен невалиден. Требуется обновление"); + { + throw new Exception("Авторизационный токен невалиден. Требуется обновление"); + } throw; } @@ -102,16 +103,12 @@ private async Task GetBookResponse(string path, string id) private Task GetCover(BookmateBookBase book) { var url = book.Cover.Large ?? book.Cover.Small; - return !string.IsNullOrWhiteSpace(url) - ? SaveImage(SystemUrl.MakeRelativeUri(url)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(url) ? SaveImage(SystemUrl.MakeRelativeUri(url)) : Task.FromResult(default(TempFile)); } private Author GetAuthor(BookmateBookBase book) { var author = book.GetAuthor(); - return author is null - ? new Author("BooksYandex") - : new Author(author.Name, SystemUrl.MakeRelativeUri($"/authors/{author.Uuid}")); + return author == null ? new Author("BooksYandex") : new Author(author.Name, SystemUrl.MakeRelativeUri($"/authors/{author.Uuid}")); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexAudioBitrate.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexAudioBitrate.cs new file mode 100644 index 00000000..b4e556ef --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexAudioBitrate.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +internal class BooksYandexAudioBitrate +{ + [JsonPropertyName("url")] + public string Url { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexAudioOffline.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexAudioOffline.cs new file mode 100644 index 00000000..aa002924 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexAudioOffline.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +internal class BooksYandexAudioOffline +{ + [JsonPropertyName("max_bit_rate")] + public BooksYandexAudioBitrate Max { get; set; } + + [JsonPropertyName("min_bit_rate")] + public BooksYandexAudioBitrate Min { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexAudioTrack.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexAudioTrack.cs new file mode 100644 index 00000000..09282bc0 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexAudioTrack.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +internal class BooksYandexAudioTrack +{ + [JsonPropertyName("offline")] + public BooksYandexAudioOffline Offline { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexAuthor.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexAuthor.cs new file mode 100644 index 00000000..991057e5 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexAuthor.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +public class BooksYandexAuthor +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("uuid")] + public string Uuid { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexBook.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexBook.cs new file mode 100644 index 00000000..cbf80d1c --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexBook.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +public abstract class BookmateBookBase +{ + [JsonPropertyName("uuid")] + public string UUID { get; set; } + + [JsonPropertyName("annotation")] + public string Annotation { get; set; } + + [JsonPropertyName("cover")] + public BooksYandexCover Cover { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("language")] + public string Language { get; set; } + + [JsonPropertyName("linked_book_uuids")] + public string[] LinkedBooks { get; set; } + + [JsonPropertyName("linked_audiobook_uuids")] + public string[] LinkedAudio { get; set; } + + [JsonPropertyName("source_type")] + public string SourceType { get; set; } + + public abstract BooksYandexAuthor GetAuthor(); +} + +public class BooksYandexBook : BookmateBookBase +{ + [JsonPropertyName("authors_objects")] + public BooksYandexAuthor[] AuthorsObjects { get; set; } + + public override BooksYandexAuthor GetAuthor() + { + return AuthorsObjects?.FirstOrDefault(); + } +} + +public class BookmateComic : BookmateBookBase +{ + [JsonPropertyName("authors")] + public BooksYandexAuthor[] Authors { get; set; } + + public override BooksYandexAuthor GetAuthor() + { + return Authors?.FirstOrDefault(); + } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexComicContent.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexComicContent.cs new file mode 100644 index 00000000..5c3d3d00 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexComicContent.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +internal class BooksYandexComicContent +{ + [JsonPropertyName("uris")] + public BooksYandexUri Uri { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexComicMetadata.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexComicMetadata.cs new file mode 100644 index 00000000..a7f0eb7b --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexComicMetadata.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +internal class BooksYandexComicMetadata +{ + [JsonPropertyName("pages")] + public BooksYandexComicPage[] Pages { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexComicPage.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexComicPage.cs new file mode 100644 index 00000000..80d87b96 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexComicPage.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +internal class BooksYandexComicPage +{ + [JsonPropertyName("content")] + public BooksYandexComicContent Content { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexCover.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexCover.cs new file mode 100644 index 00000000..f2b4e730 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexCover.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +public class BooksYandexCover +{ + [JsonPropertyName("large")] + public string Large { get; set; } + + [JsonPropertyName("small")] + public string Small { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexEpisode.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexEpisode.cs new file mode 100644 index 00000000..9e29c626 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexEpisode.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +internal class BooksYandexEpisode +{ + [JsonPropertyName("uuid")] + public string Uuid { get; set; } + + [JsonPropertyName("can_be_read")] + public bool CanBeRead { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexEpisodes.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexEpisodes.cs new file mode 100644 index 00000000..e39cb51c --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexEpisodes.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +internal class BooksYandexEpisodes +{ + [JsonPropertyName("episodes")] + public BooksYandexEpisode[] Episodes { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexPlaylist.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexPlaylist.cs new file mode 100644 index 00000000..55278e8f --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexPlaylist.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +internal class BooksYandexPlaylist +{ + [JsonPropertyName("tracks")] + public BooksYandexAudioTrack[] Tracks { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexResponse.cs new file mode 100644 index 00000000..024e8995 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexResponse.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +public class BooksYandexResponse +{ + [JsonPropertyName("book")] + public BooksYandexBook Book { get; set; } + + [JsonPropertyName("audiobook")] + public BooksYandexBook AudioBook { get; set; } + + [JsonPropertyName("comicbook")] + public BookmateComic Comicbook { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexUri.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexUri.cs new file mode 100644 index 00000000..177b79ff --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.BooksYandex/Types/BooksYandexUri.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.BooksYandex.Types; + +internal class BooksYandexUri +{ + [JsonPropertyName("Image")] + public string Image { get; set; } + + [JsonPropertyName("path")] + public string Path { get; set; } +} diff --git a/Core/Logic/Getters/BookstabGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/BookstabGetter.cs similarity index 61% rename from Core/Logic/Getters/BookstabGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/BookstabGetter.cs index 4d42a2d2..8016de19 100644 --- a/Core/Logic/Getters/BookstabGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/BookstabGetter.cs @@ -1,18 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Http.Json; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Bookstab; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Bookstab.Types; using HtmlAgilityPack; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Bookstab; public class BookstabGetter(BookGetterConfig config) : GetterBase(config) { @@ -21,17 +18,14 @@ public class BookstabGetter(BookGetterConfig config) : GetterBase(config) private Uri _apiUrl => new($"https://api.{SystemUrl.Host}/"); protected override string GetId(Uri url) - { - return url.GetSegment(2); - } + => url.GetSegment(2); public override async Task Get(Uri url) { var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/book/{bookId}"); - using var response = - await Config.Client.GetWithTriesAsync(_apiUrl.MakeRelativeUri($"/api/reader-get/{bookId}")); - var data = await response.Content.ReadFromJsonAsync(); + using var response = await Config.Client.GetWithTriesAsync(_apiUrl.MakeRelativeUri($"/api/reader-get/{bookId}")); + var data = await response.Content.ReadFromJsonAsync(); var book = new Book(url) { @@ -45,23 +39,25 @@ public override async Task Get(Uri url) return book; } - private Author GetAuthor(BookstabApiResponse book) + private Author GetAuthor(ApiResponse book) { return new Author(book.Book.User.Pseudonym, SystemUrl.MakeRelativeUri($"/user/{book.Book.User.Name}")); } private static string GetAnnotation(BookstabBook book) { - return string.IsNullOrWhiteSpace(book.Excerpt) - ? string.Empty - : string.Join("", - book.Excerpt.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim().CoverTag("p"))); + return string.IsNullOrWhiteSpace(book.Excerpt) ? + string.Empty : + string.Join("", book.Excerpt.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim().CoverTag("p"))); } - private async Task> FillChapters(BookstabApiResponse response, Uri uri, string bookId) + private async Task> FillChapters(ApiResponse response, Uri uri, string bookId) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var bookChapter in SliceToc(response.Book.ChaptersShow, c => c.Title)) { @@ -74,7 +70,7 @@ private async Task> FillChapters(BookstabApiResponse respon var doc = await GetChapter(bookChapter.Id, bookId); - if (doc is not null) + if (doc != null) { chapter.Images = await GetImages(doc, uri); chapter.Content = doc.DocumentNode.InnerHtml; @@ -90,8 +86,7 @@ private async Task GetChapter(int chapterId, string bookId) { while (true) { - using var response = - await Config.Client.GetAsync(_apiUrl.MakeRelativeUri($"/api/reader-get/{bookId}/{chapterId}")); + using var response = await Config.Client.GetAsync(_apiUrl.MakeRelativeUri($"/api/reader-get/{bookId}/{chapterId}")); if (response.StatusCode == HttpStatusCode.Forbidden) { Config.Logger.LogInformation("Очень много запросов. Подождем..."); @@ -99,16 +94,14 @@ private async Task GetChapter(int chapterId, string bookId) continue; } - var data = await response.Content.ReadFromJsonAsync(); - return string.IsNullOrWhiteSpace(data?.Chapter.Body) ? default : data.Chapter.Body.AsHtmlDoc(); + var data = await response.Content.ReadFromJsonAsync(); + return string.IsNullOrWhiteSpace(data?.Chapter.Body) ? null : data.Chapter.Body.AsHtmlDoc(); } } - private Task GetCover(BookstabApiResponse response) + private Task GetCover(ApiResponse response) { var imagePath = response.Book.Image; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(_apiUrl.MakeRelativeUri($"/storage/{imagePath}")) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(_apiUrl.MakeRelativeUri($"/storage/{imagePath}")) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Elib2Ebook.ExternalServices.Bookstab.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Elib2Ebook.ExternalServices.Bookstab.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Elib2Ebook.ExternalServices.Bookstab.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Types/ApiResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Types/ApiResponse.cs new file mode 100644 index 00000000..f3227c63 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Types/ApiResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Bookstab.Types; + +internal class ApiResponse +{ + [JsonPropertyName("book")] + public BookstabBook Book { get; set; } + + [JsonPropertyName("chapter")] + public BookstabChapter Chapter { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Types/BookstabBook.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Types/BookstabBook.cs new file mode 100644 index 00000000..e5a96f5b --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Types/BookstabBook.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Bookstab.Types; + +internal class BookstabBook +{ + [JsonPropertyName("image")] + public string Image { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("excerpt")] + public string Excerpt { get; set; } + + [JsonPropertyName("user")] + public BooksnabUser User { get; set; } + + [JsonPropertyName("chapters_show")] + public BookstabChapter[] ChaptersShow { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Types/BookstabChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Types/BookstabChapter.cs new file mode 100644 index 00000000..136634e4 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Types/BookstabChapter.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Bookstab.Types; + +internal class BookstabChapter +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("body")] + public string Body { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Types/User.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Types/User.cs new file mode 100644 index 00000000..94912b5f --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstab/Types/User.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Bookstab.Types; + +internal class BooksnabUser +{ + [JsonPropertyName("pseudonym")] + public string Pseudonym { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } +} diff --git a/Core/Logic/Getters/BookstimeGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstime/BookstimeGetter.cs similarity index 78% rename from Core/Logic/Getters/BookstimeGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Bookstime/BookstimeGetter.cs index 541cf787..1532295b 100644 --- a/Core/Logic/Getters/BookstimeGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstime/BookstimeGetter.cs @@ -1,22 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Http.Json; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Bookstime; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Bookstime.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Bookstime; public class BookstimeGetter(BookGetterConfig config) : GetterBase(config) { @@ -37,28 +32,39 @@ public override async Task Init() public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } using var post = await Config.Client.PostAsync(SystemUrl, GenerateAuthData()); if (post.StatusCode != HttpStatusCode.OK) { var content = await post.Content.ReadAsStringAsync(); - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {content}"); + throw new Exception($"Не удалось авторизоваться. {content}"); } - var response = await post.Content.ReadFromJsonAsync(); + var response = await post.Content.ReadFromJsonAsync(); if (string.IsNullOrWhiteSpace(response.XOctoberErrorMessage)) + { Config.Logger.LogInformation("Успешно авторизовались"); + } else - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {response.XOctoberErrorMessage}"); + { + throw new Exception($"Не удалось авторизоваться. {response.XOctoberErrorMessage}"); + } } private FormUrlEncodedContent GenerateAuthData() { var payload = new Dictionary { - { "email", Config.Options.Login }, - { "password", Config.Options.Password } + { + "email", Config.Options.Login + }, + { + "password", Config.Options.Password + }, }; return new FormUrlEncodedContent(payload); @@ -100,7 +106,10 @@ private IEnumerable GetToc(HtmlDocument doc, Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in GetToc(doc, url)) { @@ -111,7 +120,7 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) }; var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, urlChapter.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -127,7 +136,10 @@ private async Task GetChapter(UrlChapter urlChapter) { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); var reader = doc.QuerySelector("div.reader__body"); - if (reader is null) return default; + if (reader == null) + { + return null; + } var text = new StringBuilder(); text.Append(reader.RemoveNodes("h2.ui-text-head--3-em, iframe").InnerHtml); @@ -151,8 +163,6 @@ private async Task GetChapter(UrlChapter urlChapter) private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("div[itemprop=image] img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstime/Elib2Ebook.ExternalServices.Bookstime.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstime/Elib2Ebook.ExternalServices.Bookstime.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstime/Elib2Ebook.ExternalServices.Bookstime.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Types/Bookstime/BookstimeAuthResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstime/Types/AuthResponse.cs similarity index 63% rename from Core/Types/Bookstime/BookstimeAuthResponse.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Bookstime/Types/AuthResponse.cs index 305d02e6..ae5c2d61 100644 --- a/Core/Types/Bookstime/BookstimeAuthResponse.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookstime/Types/AuthResponse.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Bookstime; +namespace Elib2Ebook.ExternalServices.Bookstime.Types; -public class BookstimeAuthResponse +internal class AuthResponse { [JsonPropertyName("X_OCTOBER_ERROR_MESSAGE")] public string XOctoberErrorMessage { get; set; } diff --git a/Core/Logic/Getters/BookurukGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookuruk/BookurukGetter.cs similarity index 72% rename from Core/Logic/Getters/BookurukGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Bookuruk/BookurukGetter.cs index cb05b293..ca37a923 100644 --- a/Core/Logic/Getters/BookurukGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookuruk/BookurukGetter.cs @@ -1,18 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Bookuruk; public class BookurukGetter(BookGetterConfig config) : GetterBase(config) { @@ -25,27 +20,39 @@ protected override string GetId(Uri url) public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl); var token = doc.QuerySelector("#loginpopup input[name=_csrf-shop]").Attributes["value"].Value; - doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/login"), - GenerateAuthData(token)); + doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/login"), GenerateAuthData(token)); var alert = doc.QuerySelector("#infoMess")?.GetText(); if (string.IsNullOrWhiteSpace(alert)) + { Config.Logger.LogInformation("Успешно авторизовались"); + } else - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {alert}"); + { + throw new Exception($"Не удалось авторизоваться. {alert}"); + } } private FormUrlEncodedContent GenerateAuthData(string token) { var payload = new Dictionary { - { "_csrf-shop", token }, - { "email", Config.Options.Login }, - { "password", Config.Options.Password } + { + "_csrf-shop", token + }, + { + "email", Config.Options.Login + }, + { + "password", Config.Options.Password + }, }; return new FormUrlEncodedContent(payload); @@ -73,14 +80,17 @@ private Author GetAuthor(HtmlDocument doc) { var a = doc.QuerySelector("a.bookcard_author + a"); var href = a.Attributes["href"]?.Value; - return string.IsNullOrWhiteSpace(href) - ? new Author(a.GetText()) - : new Author(a.GetText(), SystemUrl.MakeRelativeUri(href)); + return string.IsNullOrWhiteSpace(href) ? + new Author(a.GetText()) : + new Author(a.GetText(), SystemUrl.MakeRelativeUri(href)); } private async Task GetMainUrl(Uri url) { - if (url.GetSegment(1) == "book") return url; + if (url.GetSegment(1) == "book") + { + return url; + } var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); return url.MakeRelativeUri(doc.QuerySelector("div.content.aside-right li a").Attributes["href"].Value); @@ -88,9 +98,7 @@ private async Task GetMainUrl(Uri url) private async Task> GetToc(HtmlDocument doc, Uri url) { - var internalId = SystemUrl - .MakeRelativeUri(doc.QuerySelectorAll("div.content.aside-right li a")[1].Attributes["href"].Value) - .GetSegment(2); + var internalId = SystemUrl.MakeRelativeUri(doc.QuerySelectorAll("div.content.aside-right li a")[1].Attributes["href"].Value).GetSegment(2); doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/book/chapters/{internalId}")); var result = doc @@ -104,7 +112,10 @@ private async Task> GetToc(HtmlDocument doc, Uri url) private async Task> FillChapters(HtmlDocument doc, Uri uri) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in await GetToc(doc, uri)) { @@ -116,7 +127,7 @@ private async Task> FillChapters(HtmlDocument doc, Uri uri) Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); var chapterDoc = await GetChapter(urlChapter.Url); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, urlChapter.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -137,8 +148,6 @@ private async Task GetChapter(Uri url) private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("div.single-prew__img img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Bookuruk/Elib2Ebook.ExternalServices.Bookuruk.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookuruk/Elib2Ebook.ExternalServices.Bookuruk.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Bookuruk/Elib2Ebook.ExternalServices.Bookuruk.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/BoostyGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/BoostyGetter.cs similarity index 78% rename from Core/Logic/Getters/BoostyGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/BoostyGetter.cs index 473116b9..8bbd2ba1 100644 --- a/Core/Logic/Getters/BoostyGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/BoostyGetter.cs @@ -1,17 +1,15 @@ -using System; -using System.Collections.Generic; using System.Net.Http.Json; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Boosty; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Boosty.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Boosty; public class BoostyGetter(BookGetterConfig config) : GetterBase(config) { @@ -36,7 +34,7 @@ public override async Task Get(Uri url) Chapters = await FillChapters(url, bookId), Title = "Exclusive content on Boosty", Author = GetAuthor(doc, url), - Annotation = doc.QuerySelector("div.AboutAuthor_content_HprOc")?.InnerHtml + Annotation = doc.QuerySelector("div.AboutAuthor_content_HprOc")?.InnerHtml, }; return book; @@ -61,7 +59,7 @@ private async Task> FillChapters(Uri uri, string bookId) }; var doc = await GetChapter(post, bookId); - if (doc is not null) + if (doc != null) { chapter.Images = await GetImages(doc, uri); chapter.Content = doc.DocumentNode.InnerHtml; @@ -75,21 +73,30 @@ private async Task> FillChapters(Uri uri, string bookId) private async Task GetChapter(BoostyPost post, string bookId) { - if (!post.HasAccess) return default; + if (!post.HasAccess) + { + return null; + } var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/{bookId}/posts/{post.Id}")); doc = doc.QuerySelector("article.Post_content_ETkyT").InnerHtml.AsHtmlDoc().RemoveNodes("h1"); foreach (var img in doc.QuerySelectorAll("img")) + { if (img.ParentNode.Name == "a") { img.ParentNode.Name = "div"; img.ParentNode.Attributes.RemoveAll(); } + } foreach (var div in doc.QuerySelectorAll("div.BlockRenderer_markup_Wtipg")) + { if (string.IsNullOrWhiteSpace(div.InnerHtml)) + { div.InnerHtml = "
"; + } + } return doc; } @@ -98,15 +105,15 @@ private async Task> GetToc(string bookId) { var result = new List(); - var response = await Config.Client.GetWithTriesAsync(ApiUrl.MakeRelativeUri($"/v1/blog/{bookId}/post/") - .AppendQueryParameter("limit", 10)); + var response = await Config.Client.GetWithTriesAsync(ApiUrl.MakeRelativeUri($"/v1/blog/{bookId}/post/").AppendQueryParameter("limit", 10)); var content = await response.Content.ReadFromJsonAsync>(); result.AddRange(content.Data); while (!content.Extra.IsLast) { - response = await Config.Client.GetWithTriesAsync(ApiUrl.MakeRelativeUri($"/v1/blog/{bookId}/post/") - .AppendQueryParameter("limit", 10).AppendQueryParameter("offset", content.Extra.Offset)); + response = await Config.Client.GetWithTriesAsync( + ApiUrl.MakeRelativeUri($"/v1/blog/{bookId}/post/").AppendQueryParameter("limit", 10).AppendQueryParameter("offset", content.Extra.Offset)); + content = await response.Content.ReadFromJsonAsync>(); result.AddRange(content.Data); } @@ -118,8 +125,6 @@ private async Task> GetToc(string bookId) private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("link[rel=image_src]")?.Attributes["href"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/Elib2Ebook.ExternalServices.Boosty.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/Elib2Ebook.ExternalServices.Boosty.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/Elib2Ebook.ExternalServices.Boosty.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/Types/BoostyApiResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/Types/BoostyApiResponse.cs new file mode 100644 index 00000000..4825f7c0 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/Types/BoostyApiResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Boosty.Types; + +internal class BoostyApiResponse +{ + [JsonPropertyName("data")] + public T Data { get; set; } + + [JsonPropertyName("extra")] + public BoostyExtra Extra { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/Types/BoostyExtra.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/Types/BoostyExtra.cs new file mode 100644 index 00000000..ac2b641a --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/Types/BoostyExtra.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Boosty.Types; + +internal class BoostyExtra +{ + [JsonPropertyName("isLast")] + public bool IsLast { get; set; } + + [JsonPropertyName("offset")] + public string Offset { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/Types/BoostyPost.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/Types/BoostyPost.cs new file mode 100644 index 00000000..d411cf34 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Boosty/Types/BoostyPost.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Boosty.Types; + +internal class BoostyPost +{ + [JsonPropertyName("id")] + public Guid Id { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("hasAccess")] + public bool HasAccess { get; set; } +} diff --git a/Core/Logic/Getters/BoovelGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Boovel/BoovelGetter.cs similarity index 64% rename from Core/Logic/Getters/BoovelGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Boovel/BoovelGetter.cs index 35fe67b3..582b0d8c 100644 --- a/Core/Logic/Getters/BoovelGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Boovel/BoovelGetter.cs @@ -1,19 +1,15 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Boovel; public class BoovelGetter(BookGetterConfig config) : GetterBase(config) { @@ -44,7 +40,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(HtmlDocument doc) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in GetToc(doc)) { @@ -56,7 +55,7 @@ private async Task> FillChapters(HtmlDocument doc) var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -72,8 +71,7 @@ private IEnumerable GetToc(HtmlDocument doc) { var result = doc .QuerySelectorAll("a.chapter-group__list-item-link[href]") - .Select(a => - new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) + .Select(a => new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) .ToList(); return SliceToc(result, c => c.Title); @@ -83,12 +81,12 @@ private async Task GetChapter(UrlChapter urlChapter) { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); var encoded = Convert.FromBase64String(doc.QuerySelector("#chapter-content").InnerText); - var secret = doc.QuerySelector("body").Attributes["data-post-id"].Value + AppSecrets.BoovelSalt; + var secret = doc.QuerySelector("body").Attributes["data-post-id"].Value + "skajldayzonePVPkruto0"; var crypto = SHA256.HashData(Encoding.UTF8.GetBytes(secret).AsSpan(0, Encoding.UTF8.GetByteCount(secret))); using var aes = Aes.Create(); - const int IV_SHIFT = AppSecrets.AesIvShift; + const int IV_SHIFT = 16; aes.Key = crypto; aes.IV = encoded[..IV_SHIFT]; @@ -100,38 +98,12 @@ private async Task GetChapter(UrlChapter urlChapter) var output = new MemoryStream(); await cs.CopyToAsync(output); - return FilterChapterContent(Encoding.UTF8.GetString(output.ToArray())); - } - - private static HtmlDocument FilterChapterContent(string data) - { - var sb = new StringBuilder(); - - var doc = data.AsHtmlDoc(); - - foreach (var node in doc.QuerySelectorAll("> *")) - { - var tag = node.Name; - var value = node.QuerySelector("boovell")?.InnerHtml; - if (tag != "img" && tag != "br") - { - if (node.GetType() != typeof(HtmlTextNode) && node.GetType() != typeof(HtmlCommentNode)) - sb.Append($"<{tag}>{value}"); - } - else - { - sb.Append(node?.OuterHtml); - } - } - - return sb.AsHtmlDoc(); + return Encoding.UTF8.GetString(output.ToArray()).AsHtmlDoc(); } private Task GetCover(HtmlDocument doc, Uri bookUri) { - var imagePath = doc.QuerySelector("a.story__thumbnail-href")?.Attributes["href"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + var imagePath = doc.QuerySelector("img.wp-post-image")?.Attributes["src"]?.Value; + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Boovel/Elib2Ebook.ExternalServices.Boovel.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Boovel/Elib2Ebook.ExternalServices.Boovel.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Boovel/Elib2Ebook.ExternalServices.Boovel.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/DarkNovelsGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/DarkNovelsGetter.cs similarity index 71% rename from Core/Logic/Getters/DarkNovelsGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/DarkNovelsGetter.cs index 665e5bb7..697c052d 100644 --- a/Core/Logic/Getters/DarkNovelsGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/DarkNovelsGetter.cs @@ -1,30 +1,24 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.IO.Compression; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Http.Json; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.DarkNovels; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.DarkNovels.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.DarkNovels; public class DarkNovelsGetter : GetterBase { - private const string ALPHABET = "аАбБвВгГдДеЕёЁжЖзЗиИйЙкКлЛмМнНоОпПрРсСтТуУфФхХцЦчЧшШщЩъЪыЫьЬэЭюЮяЯ"; private static readonly Dictionary _map = new(); + private const string ALPHABET = "аАбБвВгГдДеЕёЁжЖзЗиИйЙкКлЛмМнНоОпПрРсСтТуУфФхХцЦчЧшШщЩъЪыЫьЬэЭюЮяЯ"; public DarkNovelsGetter(BookGetterConfig config) : base(config) { @@ -44,12 +38,14 @@ public override Task Init() public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var payload = new { - email = Config.Options.Login, - password = Config.Options.Password + email = Config.Options.Login, password = Config.Options.Password }; var response = await Config.Client.PostAsJsonAsync(_apiUrl.MakeRelativeUri("/v1/users/login"), payload); @@ -61,7 +57,7 @@ public override async Task Authorize() } else { - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {data.Message}"); + throw new Exception($"Не удалось авторизоваться. {data.Message}"); } } @@ -71,7 +67,10 @@ private static void InitMap() const int shift = 38; foreach (var c in ALPHABET) { - for (var i = start; i < start + shift; i++) _map[i] = c; + for (var i = start; i < start + shift; i++) + { + _map[i] = c; + } start += shift; } @@ -120,7 +119,10 @@ private async Task GetMainUrl(Uri url) private async Task> FillChapters(string bookId) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var darkNovelsChapter in await GetToc(bookId)) { @@ -145,27 +147,28 @@ private Task GetCover(HtmlDocument doc, Uri bookUri) if (string.IsNullOrWhiteSpace(imagePath)) { var match = new Regex("\"image\": \"(?.*?)\"").Match(doc.Text); - if (match.Success) imagePath = match.Groups["url"].Value; + if (match.Success) + { + imagePath = match.Groups["url"].Value; + } } - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private async Task> GetToc(string bookId) { - return await Config.Client - .GetFromJsonAsync>(_apiUrl.MakeRelativeUri($"/v2/toc/{bookId}")) - .ContinueWith(t => - SliceToc(t.Result?.Data.Where(c => !c.Title.StartsWith("Volume:")).ToList(), c => c.Title)); + return await Config.Client.GetFromJsonAsync>(_apiUrl.MakeRelativeUri($"/v2/toc/{bookId}")) + .ContinueWith(t => SliceToc(t.Result?.Data.Where(c => !c.Title.StartsWith("Volume:")).ToList(), c => c.Title)); } private async Task FillChapter(string bookId, DarkNovelsChapter darkNovelsChapter, Chapter chapter) { - var data = await Config.Client.PostWithTriesAsync(_apiUrl.MakeRelativeUri("/v2/chapter/"), - GetData(bookId, darkNovelsChapter.Id, "html"), TimeSpan.FromMilliseconds(200)); - if (data.StatusCode == HttpStatusCode.BadRequest) return; + var data = await Config.Client.PostWithTriesAsync(_apiUrl.MakeRelativeUri("/v2/chapter/"), GetData(bookId, darkNovelsChapter.Id, "html"), TimeSpan.FromMilliseconds(200)); + if (data.StatusCode == HttpStatusCode.BadRequest) + { + return; + } using var zip = new ZipArchive(await data.Content.ReadAsStreamAsync()); var sb = new StringBuilder(); @@ -173,7 +176,10 @@ private async Task FillChapter(string bookId, DarkNovelsChapter darkNovelsChapte foreach (var entry in zip.Entries) { using var sr = new StreamReader(entry.Open()); - foreach (var c in await sr.ReadToEndAsync()) sb.Append(_map.GetValueOrDefault(c, c)); + foreach (var c in await sr.ReadToEndAsync()) + { + sb.Append(_map.GetValueOrDefault(c, c)); + } } var chapterDoc = sb.AsHtmlDoc().RemoveNodes("h1"); @@ -185,10 +191,18 @@ private static MultipartFormDataContent GetData(string bookId, int chapterId, st { return new MultipartFormDataContent { - { new StringContent(bookId), "b" }, - { new StringContent(format), "f" }, - { new StringContent("l"), "t" }, - { new StringContent(chapterId.ToString()), "c" } + { + new StringContent(bookId), "b" + }, + { + new StringContent(format), "f" + }, + { + new StringContent("l"), "t" + }, + { + new StringContent(chapterId.ToString()), "c" + } }; } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Elib2Ebook.ExternalServices.DarkNovels.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Elib2Ebook.ExternalServices.DarkNovels.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Elib2Ebook.ExternalServices.DarkNovels.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Types/DarkNovelsAuthData.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Types/DarkNovelsAuthData.cs new file mode 100644 index 00000000..b94ab2cf --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Types/DarkNovelsAuthData.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.DarkNovels.Types; + +internal class DarkNovelsAuthResponse +{ + [JsonPropertyName("token")] + public DarkNovelsAuthToken Token { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Types/DarkNovelsAuthToken.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Types/DarkNovelsAuthToken.cs new file mode 100644 index 00000000..ef4db6a7 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Types/DarkNovelsAuthToken.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.DarkNovels.Types; + +internal class DarkNovelsAuthToken +{ + [JsonPropertyName("accessToken")] + public string AccessToken { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Types/DarkNovelsChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Types/DarkNovelsChapter.cs new file mode 100644 index 00000000..48892852 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Types/DarkNovelsChapter.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.DarkNovels.Types; + +internal class DarkNovelsChapter +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("payed")] + public int Payed { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Types/DarkNovelsData.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Types/DarkNovelsData.cs new file mode 100644 index 00000000..30259262 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.DarkNovels/Types/DarkNovelsData.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.DarkNovels.Types; + +internal class DarkNovelsData +{ + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } + + [JsonPropertyName("data")] + public T Data { get; set; } +} diff --git a/Core/Logic/Getters/DesuGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Desu/DesuGetter.cs similarity index 77% rename from Core/Logic/Getters/DesuGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Desu/DesuGetter.cs index 4958bc03..1d27fc2e 100644 --- a/Core/Logic/Getters/DesuGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Desu/DesuGetter.cs @@ -1,19 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.DomainServices.Extensions; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Desu; public class DesuGetter(BookGetterConfig config) : GetterBase(config) { @@ -50,9 +47,7 @@ public override async Task Get(Uri url) private Author GetAuthor(HtmlDocument doc) { var a = doc.QuerySelector("ul.translators a"); - return a is null - ? new Author("Desu") - : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); + return a == null ? new Author("Desu") : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); } private static string GetTitle(HtmlDocument doc) @@ -63,7 +58,10 @@ private static string GetTitle(HtmlDocument doc) private async Task> FillChapters(HtmlDocument doc) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in GetToc(doc)) { @@ -75,7 +73,7 @@ private async Task> FillChapters(HtmlDocument doc) var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -91,8 +89,7 @@ private IEnumerable GetToc(HtmlDocument doc) { var result = doc .QuerySelectorAll("ul.chlist a[href]") - .Select(a => - new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) + .Select(a => new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) .Reverse() .ToList(); @@ -102,8 +99,9 @@ private IEnumerable GetToc(HtmlDocument doc) private async Task GetChapter(UrlChapter urlChapter) { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url.AppendQueryParameter("mtr", "1")); - var images = Regex.Match(doc.ParsedText, @"Reader\.init(.*?)(?\[\[(.*?)\]\]),", RegexOptions.Singleline) - .Groups["data"].Value.Replace("'", "\"").Deserialize>(); + var images = Regex.Match(doc.ParsedText, @"Reader\.init(.*?)(?\[\[(.*?)\]\]),", RegexOptions.Singleline).Groups["data"].Value.Replace("'", "\"") + .Deserialize>(); + var dir = Regex.Match(doc.ParsedText, "dir: \"(?(.*?))\"").Groups["data"].Value; var sb = new StringBuilder(); @@ -120,8 +118,6 @@ private async Task GetChapter(UrlChapter urlChapter) private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("div.c-poster img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Desu/Elib2Ebook.ExternalServices.Desu.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Desu/Elib2Ebook.ExternalServices.Desu.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Desu/Elib2Ebook.ExternalServices.Desu.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/DreameGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/DreameGetter.cs similarity index 80% rename from Core/Logic/Getters/DreameGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/DreameGetter.cs index 6e81a3a3..9d3dff45 100644 --- a/Core/Logic/Getters/DreameGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/DreameGetter.cs @@ -1,18 +1,16 @@ -using System; -using System.Collections.Generic; using System.Text; using System.Text.Json; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Dreame; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Dreame.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Dreame; public class DreameGetter(BookGetterConfig config) : GetterBase(config) { @@ -21,9 +19,7 @@ public class DreameGetter(BookGetterConfig config) : GetterBase(config) private Uri _apiUrl => new($"https://wap-api.{SystemUrl.Host}/"); protected override string GetId(Uri url) - { - return url.GetSegment(2).Split(".")[0].Split("-")[0]; - } + => url.GetSegment(2).Split(".")[0].Split("-")[0]; public override async Task Get(Uri url) { @@ -60,7 +56,10 @@ private static T GetNextData(HtmlDocument doc, string node) private async Task> FillChapters(Uri url, string bookId) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in await GetToc(bookId)) { @@ -71,7 +70,7 @@ private async Task> FillChapters(Uri url, string bookId) }; var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -86,8 +85,8 @@ private async Task> FillChapters(Uri url, string bookId) private async Task GetChapter(DreameChapter urlChapter) { var response = await Config.Client.GetFromJsonWithTriesAsync>( - _apiUrl.MakeRelativeUri( - $"/novel/getChapter?channel=dreamepmian-173&product=1&osType=2&cid={urlChapter.Id}&systemFlag=android&port=web")); + _apiUrl.MakeRelativeUri($"/novel/getChapter?channel=dreamepmian-173&product=1&osType=2&cid={urlChapter.Id}&systemFlag=android&port=web")); + return Encoding.UTF8.GetString(Convert.FromBase64String(response.Data.Content)).AsHtmlDoc(); } @@ -95,15 +94,16 @@ private async Task> GetToc(string bookId) { var response = await Config.Client.GetFromJsonWithTriesAsync>( _apiUrl.MakeRelativeUri($"novel/getcatelog?channel=dreamepmian-173&product=1&osType=2&nid={bookId}")); + return SliceToc(response.Data.Pager.ChapterList, c => c.Title); } private Author GetAuthor(HtmlDocument doc, DreameNovel data) { var a = doc.QuerySelector("a a[class*=story_author-name]"); - return a is not null - ? new Author(data.AuthorName, SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)) - : new Author(data.AuthorName); + return a != null ? + new Author(data.AuthorName, SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)) : + new Author(data.AuthorName); } private Task GetCover(string url) diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Elib2Ebook.ExternalServices.Dreame.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Elib2Ebook.ExternalServices.Dreame.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Elib2Ebook.ExternalServices.Dreame.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreameApiResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreameApiResponse.cs new file mode 100644 index 00000000..25584465 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreameApiResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Dreame.Types; + +internal class DreameApiResponse +{ + [JsonPropertyName("data")] + public T Data { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreameCatalog.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreameCatalog.cs new file mode 100644 index 00000000..43640df5 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreameCatalog.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Dreame.Types; + +internal class DreameCatalog +{ + [JsonPropertyName("pager")] + public DreamePager Pager { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreameChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreameChapter.cs new file mode 100644 index 00000000..a76be5a4 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreameChapter.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Dreame.Types; + +internal class DreameChapter +{ + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("content")] + public string Content { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreameNovel.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreameNovel.cs new file mode 100644 index 00000000..a978e94c --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreameNovel.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Dreame.Types; + +internal class DreameNovel +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("author_id")] + public string AuthorId { get; set; } + + [JsonPropertyName("author_name")] + public string AuthorName { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("descr")] + public string Description { get; set; } + + [JsonPropertyName("language")] + public string Language { get; set; } + + [JsonPropertyName("cover_url")] + public string Cover { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreamePager.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreamePager.cs new file mode 100644 index 00000000..b511d2f0 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Dreame/Types/DreamePager.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Dreame.Types; + +internal class DreamePager +{ + [JsonPropertyName("chap_list")] + public List ChapterList { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Elib2Ebook.ExternalServices.FanFicus.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Elib2Ebook.ExternalServices.FanFicus.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Elib2Ebook.ExternalServices.FanFicus.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/FanFicusGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/FanFicusGetter.cs similarity index 59% rename from Core/Logic/Getters/FanFicusGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/FanFicusGetter.cs index dbca0291..fea1182d 100644 --- a/Core/Logic/Getters/FanFicusGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/FanFicusGetter.cs @@ -1,26 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.FanFicus; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.FanFicus.Types; using HtmlAgilityPack; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.FanFicus; public class FanFicusGetter(BookGetterConfig config) : GetterBase(config) { - private readonly Uri _apiHost = new("https://fanficus-server-mirror-879c30cd977f.herokuapp.com/"); protected override Uri SystemUrl => new("https://fanficus.com/"); + private readonly Uri _apiHost = new("https://fanficus-server-mirror-879c30cd977f.herokuapp.com/"); + protected override string GetId(Uri url) { return url.GetSegment(2); @@ -28,20 +25,21 @@ protected override string GetId(Uri url) public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } - var response = - await Config.Client.PostAsJsonAsync(_apiHost.MakeRelativeUri("api/v1/user/login"), GenerateAuthData()); + var response = await Config.Client.PostAsJsonAsync(_apiHost.MakeRelativeUri("api/v1/user/login"), GenerateAuthData()); if (response.StatusCode == HttpStatusCode.OK) { var user = await response.Content.ReadFromJsonAsync>(); - Config.Client.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", user.Value.Token); + Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", user.Value.Token); Config.Logger.LogInformation("Успешно авторизовались"); } else { - throw new Elib2EbookAuthException("Не удалось авторизоваться. Неверный пароль или имейл"); + throw new Exception("Не удалось авторизоваться. Неверный пароль или имейл"); } } @@ -49,8 +47,7 @@ private object GenerateAuthData() { return new { - username = Config.Options.Login, - password = Config.Options.Password + username = Config.Options.Login, password = Config.Options.Password, }; } @@ -59,9 +56,7 @@ public override async Task Get(Uri url) var bookId = GetId(url); url = SystemUrl.MakeRelativeUri($"/post/{bookId}"); - var response = - await Config.Client.GetFromJsonWithTriesAsync>( - _apiHost.MakeRelativeUri($"api/v1/post/{bookId}")); + var response = await Config.Client.GetFromJsonWithTriesAsync>(_apiHost.MakeRelativeUri($"api/v1/post/{bookId}")); var book = new Book(url) { @@ -78,23 +73,22 @@ await Config.Client.GetFromJsonWithTriesAsync> private Author GetAuthor(FanFicusBook book) { var creator = book.Creators.FirstOrDefault(); - return creator is null - ? new Author("FanFicus") - : new Author(creator.NickName, SystemUrl.MakeRelativeUri($"/user/{creator.Id}")); + return creator == null ? new Author("FanFicus") : new Author(creator.NickName, SystemUrl.MakeRelativeUri($"/user/{creator.Id}")); } private async Task> GetToc(string bookId) { - var response = - await Config.Client.GetFromJsonWithTriesAsync>( - _apiHost.MakeRelativeUri($"/api/v1/post/{bookId}/post-part")); + var response = await Config.Client.GetFromJsonWithTriesAsync>(_apiHost.MakeRelativeUri($"/api/v1/post/{bookId}/post-part")); return SliceToc(response.Value, c => c.Title); } private async Task> FillChapters(string bookId) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var fanFicusPart in await GetToc(bookId)) { @@ -106,7 +100,7 @@ private async Task> FillChapters(string bookId) Config.Logger.LogInformation($"Загружаю главу {fanFicusPart.Title.CoverQuotes()}"); var chapterDoc = await GetChapter(bookId, fanFicusPart); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -120,17 +114,13 @@ private async Task> FillChapters(string bookId) private async Task GetChapter(string bookId, FanFicusPart fanFicusPart) { - var response = - await Config.Client.GetFromJsonWithTriesAsync>( - _apiHost.MakeRelativeUri($"api/v1/post/{bookId}/post-part/{fanFicusPart.Id}")); + var response = await Config.Client.GetFromJsonWithTriesAsync>(_apiHost.MakeRelativeUri($"api/v1/post/{bookId}/post-part/{fanFicusPart.Id}")); return response.Value.Text.AsHtmlDoc(); } private Task GetCover(FanFicusBook doc, Uri uri) { var imagePath = doc.Images.FirstOrDefault()?.Url; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusApiResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusApiResponse.cs new file mode 100644 index 00000000..57295940 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusApiResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.FanFicus.Types; + +internal class FanFicusApiResponse +{ + [JsonPropertyName("value")] + public T Value { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusBook.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusBook.cs new file mode 100644 index 00000000..38ba0fd9 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusBook.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.FanFicus.Types; + +internal class FanFicusBook +{ + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("creatorId")] + public FanFicusCreator[] Creators { get; set; } + + [JsonPropertyName("images")] + public FanFicusImage[] Images { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusChapter.cs new file mode 100644 index 00000000..0d6a978a --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusChapter.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.FanFicus.Types; + +internal class FanFicusChapter +{ + [JsonPropertyName("text")] + public string Text { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusCreator.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusCreator.cs new file mode 100644 index 00000000..e6675774 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusCreator.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.FanFicus.Types; + +internal class FanFicusCreator +{ + [JsonPropertyName("_id")] + public string Id { get; set; } + + [JsonPropertyName("nickName")] + public string NickName { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusImage.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusImage.cs new file mode 100644 index 00000000..ba788b1b --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusImage.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.FanFicus.Types; + +internal class FanFicusImage +{ + [JsonPropertyName("url")] + public string Url { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusPart.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusPart.cs new file mode 100644 index 00000000..dba47c99 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusPart.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.FanFicus.Types; + +internal class FanFicusPart +{ + [JsonPropertyName("_id")] + public string Id { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusUser.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusUser.cs new file mode 100644 index 00000000..c94eb60d --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.FanFicus/Types/FanFicusUser.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.FanFicus.Types; + +internal class FanFicusUser +{ + [JsonPropertyName("token")] + public string Token { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Fb2Top/Elib2Ebook.ExternalServices.Fb2Top.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Fb2Top/Elib2Ebook.ExternalServices.Fb2Top.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Fb2Top/Elib2Ebook.ExternalServices.Fb2Top.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/Fb2Top/Fb2TopGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Fb2Top/Getters/Fb2TopGetter.cs similarity index 61% rename from Core/Logic/Getters/Fb2Top/Fb2TopGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Fb2Top/Getters/Fb2TopGetter.cs index ae6c572f..8eda498e 100644 --- a/Core/Logic/Getters/Fb2Top/Fb2TopGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Fb2Top/Getters/Fb2TopGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.Fb2Top; +namespace Elib2Ebook.ExternalServices.Fb2Top.Getters; public class Fb2TopGetter(BookGetterConfig config) : Fb2TopGetterBase(config) { diff --git a/Core/Logic/Getters/Fb2Top/Fb2TopGetterBase.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Fb2Top/Getters/Fb2TopGetterBase.cs similarity index 72% rename from Core/Logic/Getters/Fb2Top/Fb2TopGetterBase.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Fb2Top/Getters/Fb2TopGetterBase.cs index f56f8c45..751e82ba 100644 --- a/Core/Logic/Getters/Fb2Top/Fb2TopGetterBase.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Fb2Top/Getters/Fb2TopGetterBase.cs @@ -1,23 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.Fb2Top; +namespace Elib2Ebook.ExternalServices.Fb2Top.Getters; public abstract class Fb2TopGetterBase(BookGetterConfig config) : GetterBase(config) { protected override string GetId(Uri url) - { - return url.GetSegment(1); - } + => url.GetSegment(1); public override async Task Get(Uri url) { @@ -40,7 +35,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in GetToc(doc, url)) { @@ -51,7 +49,7 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) }; var chapterDoc = await GetChapter(urlChapter.Url); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, urlChapter.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -73,37 +71,35 @@ private async Task GetChapter(Uri url) private IEnumerable GetToc(HtmlDocument doc, Uri url) { - var urlChapters = doc.QuerySelectorAll("div.card-body li a") - .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())).ToList(); + var urlChapters = doc.QuerySelectorAll("div.card-body li a").Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())).ToList(); return SliceToc(urlChapters, c => c.Title); } private static Seria GetSeria(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("div.book-info-body a[href*=/series/]"); - if (a is not null) + if (a != null) + { return new Seria { - Name = a.GetText(), - Url = url.MakeRelativeUri(a.Attributes["href"].Value) + Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; + } - return default; + return null; } private static Author GetAuthor(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("div.book-info-body a[href*=/authors/]"); - return a is not null - ? new Author(a.GetText().ReplaceNewLine(), url.MakeRelativeUri(a.Attributes["href"].Value)) - : new Author("Fb2Top"); + return a != null ? + new Author(a.GetText().ReplaceNewLine(), url.MakeRelativeUri(a.Attributes["href"].Value)) : + new Author("Fb2Top"); } private Task GetCover(HtmlDocument doc, Uri url) { var imagePath = doc.QuerySelector("img.book-info-poster-img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(url.MakeRelativeUri(imagePath.HtmlDecode())) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(url.MakeRelativeUri(imagePath.HtmlDecode())) : Task.FromResult(default(TempFile)); } } diff --git a/Core/Logic/Getters/Fb2Top/LadyLibGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Fb2Top/Getters/LadyLibGetter.cs similarity index 61% rename from Core/Logic/Getters/Fb2Top/LadyLibGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Fb2Top/Getters/LadyLibGetter.cs index 3e47580d..5adc9cf6 100644 --- a/Core/Logic/Getters/Fb2Top/LadyLibGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Fb2Top/Getters/LadyLibGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.Fb2Top; +namespace Elib2Ebook.ExternalServices.Fb2Top.Getters; public class LadyLibGetter(BookGetterConfig config) : Fb2TopGetterBase(config) { diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Ficbook/Elib2Ebook.ExternalServices.Ficbook.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Ficbook/Elib2Ebook.ExternalServices.Ficbook.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Ficbook/Elib2Ebook.ExternalServices.Ficbook.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/FicbookGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Ficbook/FicbookGetter.cs similarity index 81% rename from Core/Logic/Getters/FicbookGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Ficbook/FicbookGetter.cs index ea1068cf..fa337423 100644 --- a/Core/Logic/Getters/FicbookGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Ficbook/FicbookGetter.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Ficbook; public class FicbookGetter(BookGetterConfig config) : GetterBase(config) { @@ -20,15 +17,14 @@ public class FicbookGetter(BookGetterConfig config) : GetterBase(config) protected Uri _appUrl => new("https://fanficlets.xyz/"); protected override string GetId(Uri url) - { - return url.GetSegment(2); - } + => url.GetSegment(2); public override async Task Init() { await base.Init(); Config.Client.DefaultRequestHeaders.Remove("User-Agent"); - Config.Client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", + Config.Client.DefaultRequestHeaders.TryAddWithoutValidation( + "User-Agent", "Mozilla/5.0 (iPhone); iOS CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1 ||| ficbook (1.3.2) | iOS (26.1) | iPhone 13 | ao2gvvnhkv0t22lo"); } @@ -59,15 +55,16 @@ private static Author GetAuthor(HtmlDocument doc, Uri url) private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("fanfic-cover")?.Attributes["src-original"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private async Task> FillChapters(HtmlDocument doc, Uri url, string title) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var ficbookChapter in GetToc(doc, url, title)) { @@ -96,9 +93,15 @@ private async Task GetChapter(UrlChapter urlChapter) while (true) { var line = await sr.ReadLineAsync(); - if (line == null) break; + if (line == null) + { + break; + } - if (string.IsNullOrWhiteSpace(line)) continue; + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } text.Append(line.HtmlEncode().CoverTag("p")); } @@ -112,15 +115,20 @@ private IEnumerable GetToc(HtmlDocument doc, Uri url, string title) var links = doc.QuerySelectorAll("li.part"); if (links.Count == 0) + { result.Add(new UrlChapter(url, title)); + } else + { foreach (var li in links) { var a = li.QuerySelector("a.part-link"); if (a != null) - result.Add(new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), - li.GetTextBySelector("h3"))); + { + result.Add(new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), li.GetTextBySelector("h3"))); + } } + } return SliceToc(result, c => c.Title); } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.FictionBook/Elib2Ebook.ExternalServices.FictionBook.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.FictionBook/Elib2Ebook.ExternalServices.FictionBook.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.FictionBook/Elib2Ebook.ExternalServices.FictionBook.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/FictionBookGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.FictionBook/FictionBookGetter.cs similarity index 82% rename from Core/Logic/Getters/FictionBookGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.FictionBook/FictionBookGetter.cs index fd9edd13..d87e805d 100644 --- a/Core/Logic/Getters/FictionBookGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.FictionBook/FictionBookGetter.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.FictionBook; public class FictionBookGetter(BookGetterConfig config) : GetterBase(config) { @@ -20,7 +17,9 @@ public class FictionBookGetter(BookGetterConfig config) : GetterBase(config) private Uri GetMainUrl(Uri url) { if (url.Segments.Last().StartsWith("read_online.html")) + { url = SystemUrl.MakeRelativeUri(string.Join(string.Empty, url.Segments[..^1])); + } return url; } @@ -39,7 +38,7 @@ public override async Task Get(Uri url) Chapters = await FillChapters(url, name), Title = name, Author = GetAuthor(doc), - Annotation = doc.QuerySelector("div.biblio_book__annotation")?.InnerHtml + Annotation = doc.QuerySelector("div.biblio_book__annotation")?.InnerHtml, }; return book; @@ -48,22 +47,23 @@ public override async Task Get(Uri url) private Author GetAuthor(HtmlDocument doc) { var a = doc.QuerySelector("div.info a[data-widget-litres-author][href]"); - return a is null - ? new Author("FictionBook") - : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); + return a == null ? + new Author("FictionBook") : + new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); } private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("div.cover_float img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) { - if (chapter == null) return; + if (chapter == null) + { + return; + } var chapterDoc = text.AsHtmlDoc(); chapter.Images = await GetImages(chapterDoc, SystemUrl.MakeRelativeUri("/br/")); @@ -92,8 +92,7 @@ private static Chapter CreateChapter(string title) private Task GetChapter(Uri url, int page) { - return Config.Client.GetHtmlDocWithTriesAsync(url.AppendSegment("read_online.html") - .AppendQueryParameter("page", page)); + return Config.Client.GetHtmlDocWithTriesAsync(url.AppendSegment("read_online.html").AppendQueryParameter("page", page)); } private async Task GetPages(Uri url) @@ -110,7 +109,10 @@ private async Task GetPages(Uri url) private async Task> FillChapters(Uri url, string name) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } Chapter chapter = null; var singleChapter = true; @@ -126,6 +128,7 @@ private async Task> FillChapters(Uri url, string name) singleChapter = i == 1 ? IsSingleChapter(nodes) : singleChapter; foreach (var node in nodes) + { if (singleChapter || !IsChapterHeader(node)) { if (node.Name == "img" && node.Attributes["src"] != null) @@ -135,7 +138,9 @@ private async Task> FillChapters(Uri url, string name) else { if (!string.IsNullOrWhiteSpace(node.InnerHtml)) + { text.Append(node.InnerHtml.HtmlDecode().CoverTag("p")); + } } } else @@ -144,6 +149,7 @@ private async Task> FillChapters(Uri url, string name) text.Clear(); chapter = CreateChapter(node.InnerText.HtmlDecode()); } + } } await AddChapter(result, chapter ?? CreateChapter(name), text); diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Elib2Ebook.ExternalServices.Freedlit.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Elib2Ebook.ExternalServices.Freedlit.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Elib2Ebook.ExternalServices.Freedlit.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/FreedlitGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/FreedlitGetter.cs similarity index 64% rename from Core/Logic/Getters/FreedlitGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/FreedlitGetter.cs index 7d5b5dbb..af63a4db 100644 --- a/Core/Logic/Getters/FreedlitGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/FreedlitGetter.cs @@ -1,21 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http.Json; using System.Text.Json.Nodes; -using System.Threading.Tasks; using System.Web; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Freedlit; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Freedlit.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Freedlit; public class FreedlitGetter(BookGetterConfig config) : GetterBase(config) { @@ -23,37 +19,39 @@ public class FreedlitGetter(BookGetterConfig config) : GetterBase(config) public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } await Config.Client.GetAsync(SystemUrl.MakeRelativeUri("/login")); SetToken(); using var post = await Config.Client.PostAsJsonAsync(SystemUrl.MakeRelativeUri("/login"), GenerateAuthData()); var doc = await post.Content.ReadAsStreamAsync().ContinueWith(c => c.Result.AsHtmlDoc()); - var errors = doc.QuerySelector("#app").Attributes["data-page"].Value.HtmlDecode() - .Deserialize>().Props.Errors; + var errors = doc.QuerySelector("#app").Attributes["data-page"].Value.HtmlDecode().Deserialize>().Props.Errors; if (string.IsNullOrWhiteSpace(errors?.Email)) + { Config.Logger.LogInformation("Успешно авторизовались"); + } else - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {errors.Email}"); + { + throw new Exception($"Не удалось авторизоваться. {errors.Email}"); + } } private object GenerateAuthData() { return new { - email = Config.Options.Login, - password = Config.Options.Password, - remember = false + email = Config.Options.Login, password = Config.Options.Password, remember = false, }; } private void SetToken() { Config.Client.DefaultRequestHeaders.Remove("X-XSRF-TOKEN"); - Config.Client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", - HttpUtility.UrlDecode(Config.CookieContainer.GetAllCookies().FirstOrDefault(c => c.Name == "XSRF-TOKEN") - .Value)); + Config.Client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", HttpUtility.UrlDecode(Config.CookieContainer.GetAllCookies().FirstOrDefault(c => c.Name == "XSRF-TOKEN").Value)); } public override async Task Get(Uri url) @@ -61,8 +59,7 @@ public override async Task Get(Uri url) url = SystemUrl.MakeRelativeUri($"/book/{GetId(url)}"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); - var freedlitBook = doc.QuerySelector("#app").Attributes["data-page"].Value.HtmlDecode() - .Deserialize>().Props.Book; + var freedlitBook = doc.QuerySelector("#app").Attributes["data-page"].Value.HtmlDecode().Deserialize>().Props.Book; SetToken(); var book = new Book(url) @@ -85,10 +82,14 @@ private Author GetAuthor(FreedlitBook book) private async Task> GetToc(FreedlitBook book) { - var response = await Config.Client.PostAsJsonAsync(SystemUrl.MakeRelativeUri("/api/bookpage/get-chapters"), - new { book_id = book.Id }); - var data = - await response.Content.ReadFromJsonAsync>>(); + var response = await Config.Client.PostAsJsonAsync( + SystemUrl.MakeRelativeUri("/api/bookpage/get-chapters"), + new + { + book_id = book.Id + }); + + var data = await response.Content.ReadFromJsonAsync>>(); return SliceToc(data.Success.Items, c => c.Header); } @@ -96,7 +97,10 @@ private async Task> GetToc(FreedlitBook book) private async Task> FillChapters(FreedlitBook book) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var freedlitChapter in await GetToc(book)) { @@ -108,7 +112,7 @@ private async Task> FillChapters(FreedlitBook book) Config.Logger.LogInformation($"Загружаю главу {freedlitChapter.Header.CoverQuotes()}"); var chapterDoc = await GetChapter(book, freedlitChapter); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -122,10 +126,18 @@ private async Task> FillChapters(FreedlitBook book) private async Task GetChapter(FreedlitBook book, FreedlitChapter chapter) { - var response = await Config.Client.PostAsJsonAsync(SystemUrl.MakeRelativeUri("/reader/get-content"), - new { book_id = book.Id, chapter_id = chapter.Id }); + var response = await Config.Client.PostAsJsonAsync( + SystemUrl.MakeRelativeUri("/reader/get-content"), + new + { + book_id = book.Id, chapter_id = chapter.Id + }); + var fullChapter = await response.Content.ReadFromJsonAsync>(); - if (string.IsNullOrWhiteSpace(fullChapter.Success?.Content)) return default; + if (string.IsNullOrWhiteSpace(fullChapter.Success?.Content)) + { + return null; + } var doc = fullChapter.Success.Content.AsHtmlDoc(); return doc.QuerySelector("body").InnerHtml.AsHtmlDoc(); @@ -133,8 +145,6 @@ private async Task GetChapter(FreedlitBook book, FreedlitChapter c private Task GetCover(FreedlitBook book) { - return !string.IsNullOrWhiteSpace(book.Cover) - ? SaveImage(SystemUrl.MakeRelativeUri($"/storage/{book.Cover}")) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(book.Cover) ? SaveImage(SystemUrl.MakeRelativeUri($"/storage/{book.Cover}")) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitApiResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitApiResponse.cs new file mode 100644 index 00000000..9fecd96e --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitApiResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Freedlit.Types; + +internal class FreedlitApiResponse +{ + [JsonPropertyName("success")] + public T Success { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitApp.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitApp.cs new file mode 100644 index 00000000..6e209e24 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitApp.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Freedlit.Types; + +internal class FreedlitApp +{ + [JsonPropertyName("props")] + public FreedlitProps Props { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitAuthError.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitAuthError.cs new file mode 100644 index 00000000..2a05797c --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitAuthError.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Freedlit.Types; + +internal class FreedlitAuthError +{ + [JsonPropertyName("email")] + public string Email { get; set; } + + [JsonPropertyName("password")] + public string[] Password { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitAuthor.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitAuthor.cs new file mode 100644 index 00000000..ae2f69da --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitAuthor.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Freedlit.Types; + +internal class FreedlitAuthor +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("user_link")] + public string UserLink { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitBook.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitBook.cs new file mode 100644 index 00000000..eb86085d --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitBook.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Freedlit.Types; + +internal class FreedlitBook +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("cover")] + public string Cover { get; set; } + + [JsonPropertyName("annotation")] + public string Annotation { get; set; } + + [JsonPropertyName("language")] + public string Language { get; set; } + + [JsonPropertyName("main_author")] + public FreedlitAuthor MainAuthor { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitChapter.cs new file mode 100644 index 00000000..f2219c06 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitChapter.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Freedlit.Types; + +internal class FreedlitChapter +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("header")] + public string Header { get; set; } + + [JsonPropertyName("content")] + public string Content { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitItemsContent.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitItemsContent.cs new file mode 100644 index 00000000..f8ff928e --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitItemsContent.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Freedlit.Types; + +internal class FreedlitItemsContent +{ + [JsonPropertyName("items")] + public List Items { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitProps.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitProps.cs new file mode 100644 index 00000000..2002c555 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitProps.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Freedlit.Types; + +internal class FreedlitProps +{ + [JsonPropertyName("book")] + public FreedlitBook Book { get; set; } + + [JsonPropertyName("errors")] + public TErr Errors { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitSuccessResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitSuccessResponse.cs new file mode 100644 index 00000000..2710f1a6 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedlit/Types/FreedlitSuccessResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Freedlit.Types; + +internal class FreedlitSuccessResponse +{ + [JsonPropertyName("items")] + public List Items { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Freedom/Elib2Ebook.ExternalServices.Freedom.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedom/Elib2Ebook.ExternalServices.Freedom.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedom/Elib2Ebook.ExternalServices.Freedom.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/Freedom/BookHamsterGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedom/Getters/BookHamsterGetter.cs similarity index 62% rename from Core/Logic/Getters/Freedom/BookHamsterGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Freedom/Getters/BookHamsterGetter.cs index c208ec3d..6cacb9a3 100644 --- a/Core/Logic/Getters/Freedom/BookHamsterGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedom/Getters/BookHamsterGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.Freedom; +namespace Elib2Ebook.ExternalServices.Freedom.Getters; public class BookHamsterGetter(BookGetterConfig config) : FreedomGetterBase(config) { diff --git a/Core/Logic/Getters/Freedom/FreedomGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedom/Getters/FreedomGetter.cs similarity index 61% rename from Core/Logic/Getters/Freedom/FreedomGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Freedom/Getters/FreedomGetter.cs index 69ade5e7..18d6060c 100644 --- a/Core/Logic/Getters/Freedom/FreedomGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedom/Getters/FreedomGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.Freedom; +namespace Elib2Ebook.ExternalServices.Freedom.Getters; public class FreedomGetter(BookGetterConfig config) : FreedomGetterBase(config) { diff --git a/Core/Logic/Getters/Freedom/FreedomGetterBase.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedom/Getters/FreedomGetterBase.cs similarity index 73% rename from Core/Logic/Getters/Freedom/FreedomGetterBase.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Freedom/Getters/FreedomGetterBase.cs index f6b64868..217dc448 100644 --- a/Core/Logic/Getters/Freedom/FreedomGetterBase.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Freedom/Getters/FreedomGetterBase.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.Freedom; +namespace Elib2Ebook.ExternalServices.Freedom.Getters; public abstract class FreedomGetterBase(BookGetterConfig config) : GetterBase(config) { @@ -22,10 +19,7 @@ public override async Task Get(Uri url) var book = new Book(url) { - Cover = await GetCover(doc, url), - Chapters = await FillChapters(doc, url), - Title = doc.GetTextBySelector("h1.entry-title"), - Author = GetAuthor() + Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1.entry-title"), Author = GetAuthor() }; return book; @@ -33,7 +27,10 @@ public override async Task Get(Uri url) private async Task GetMainUrl(Uri url) { - if (url.GetSegment(1) == "ranobe") return url; + if (url.GetSegment(1) == "ranobe") + { + return url; + } var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); return url.MakeRelativeUri(doc.QuerySelector("div.bun2 a").Attributes["href"].Value); @@ -53,7 +50,10 @@ private IEnumerable GetToc(HtmlDocument doc, Uri url) private async Task> FillChapters(HtmlDocument doc, Uri uri) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in GetToc(doc, uri)) { @@ -65,7 +65,7 @@ private async Task> FillChapters(HtmlDocument doc, Uri uri) Config.Logger.LogInformation($"Загружаю главу {urlChapter.Title.CoverQuotes()}"); var chapterDoc = await GetChapter(urlChapter.Url); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, urlChapter.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -82,9 +82,9 @@ private async Task GetChapter(Uri url) var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var content = doc.QuerySelector("div.entry-content"); var notice = content.QuerySelector("div.single-notice"); - return notice?.GetText() == "Для чтения купите главу." - ? default - : content.InnerHtml.AsHtmlDoc().RemoveNodes("div[class*=adv]"); + return notice?.GetText() == "Для чтения купите главу." ? + null : + content.InnerHtml.AsHtmlDoc().RemoveNodes("div[class*=adv]"); } private static Author GetAuthor() @@ -95,8 +95,6 @@ private static Author GetAuthor() private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("div.img-ranobe img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.HogwartsNet/Elib2Ebook.ExternalServices.HogwartsNet.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.HogwartsNet/Elib2Ebook.ExternalServices.HogwartsNet.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HogwartsNet/Elib2Ebook.ExternalServices.HogwartsNet.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/HogwartsNetGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.HogwartsNet/HogwartsNetGetter.cs similarity index 69% rename from Core/Logic/Getters/HogwartsNetGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.HogwartsNet/HogwartsNetGetter.cs index c1b4cf97..ae2fdbe7 100644 --- a/Core/Logic/Getters/HogwartsNetGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HogwartsNet/HogwartsNetGetter.cs @@ -1,46 +1,43 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.HogwartsNet; public class HogwartsNetGetter(BookGetterConfig config) : GetterBase(config) { - private static readonly Encoding _encoding = Encoding.GetEncoding(1251); protected override Uri SystemUrl => new("https://hogwartsnet.ru/"); + private static Encoding _encoding = Encoding.GetEncoding(1251); + protected override string GetId(Uri url) - { - return url.GetQueryParameter("fid"); - } + => url.GetQueryParameter("fid"); public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var payload = new Dictionary { - ["id"] = Config.Options.Login, - ["pwd"] = Config.Options.Password + ["id"] = Config.Options.Login, ["pwd"] = Config.Options.Password }; - var doc = await Config.Client.PostHtmlDocWithTriesAsync("https://hogwartsnet.ru/mfanf/member.php".AsUri(), - new FormUrlEncodedContent(payload), _encoding); + var doc = await Config.Client.PostHtmlDocWithTriesAsync("https://hogwartsnet.ru/mfanf/member.php".AsUri(), new FormUrlEncodedContent(payload), _encoding); var anketa = doc.QuerySelector("tr.top_fanf a[href*=anketa.php]"); - if (anketa is null) - throw new Elib2EbookAuthException("Не удалось авторизоваться. Проверьте правильность ID/пароля"); + if (anketa == null) + { + throw new Exception("Не удалось авторизоваться. Проверьте правильность ID/пароля"); + } Config.Logger.LogInformation("Успешно авторизовались"); } @@ -65,7 +62,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in GetToc(doc, url)) { @@ -76,7 +76,7 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) }; var chapterDoc = await GetChapter(urlChapter.Url); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, urlChapter.Url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -94,17 +94,24 @@ private async Task GetChapter(Uri url) const string mature = "Текст фанфика доступен только зарегистрированным пользователям старше 18 лет"; if (doc.ParsedText.Contains(mature)) - throw new Elib2EbookParseException($"{mature}. Добавьте авторизацию по ID и паролю."); + { + throw new Exception($"{mature}. Добавьте авторизацию по ID и паролю."); + } var text = new StringBuilder(); - using var sr = new StringReader(doc.QuerySelector("#chap_text").RemoveNodes("center, div[id*=yandex]").InnerText - .HtmlDecode()); + using var sr = new StringReader(doc.QuerySelector("#chap_text").RemoveNodes("center, div[id*=yandex]").InnerText.HtmlDecode()); while (true) { var line = await sr.ReadLineAsync(); - if (line == null) break; + if (line == null) + { + break; + } - if (string.IsNullOrWhiteSpace(line)) continue; + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } text.Append(line.HtmlEncode().CoverTag("p")); } @@ -131,21 +138,22 @@ private IEnumerable GetToc(HtmlDocument doc, Uri url) private static Seria GetSeria(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("td.fichead a[href*=findex.php]"); - if (a is not null) + if (a != null) + { return new Seria { - Name = a.GetText(), - Url = url.MakeRelativeUri(a.Attributes["href"].Value) + Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; + } - return default; + return null; } private static Author GetAuthor(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("td.fichead a[href*=member.php]"); - return a is not null - ? new Author(a.GetText().ReplaceNewLine(), url.MakeRelativeUri(a.Attributes["href"].Value)) - : new Author("HogwartsNet"); + return a != null ? + new Author(a.GetText().ReplaceNewLine(), url.MakeRelativeUri(a.Attributes["href"].Value)) : + new Author("HogwartsNet"); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Elib2Ebook.ExternalServices.HotNovelPub.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Elib2Ebook.ExternalServices.HotNovelPub.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Elib2Ebook.ExternalServices.HotNovelPub.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/HotNovelPub/EzNovelsGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/EzNovelsGetter.cs similarity index 67% rename from Core/Logic/Getters/HotNovelPub/EzNovelsGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/EzNovelsGetter.cs index 09ec379b..39bca9ac 100644 --- a/Core/Logic/Getters/HotNovelPub/EzNovelsGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/EzNovelsGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.HotNovelPub; +namespace Elib2Ebook.ExternalServices.HotNovelPub.Getters; public class EzNovelsGetter(BookGetterConfig config) : HotNovelPubGetterBase(config) { diff --git a/Core/Logic/Getters/HotNovelPub/HotNovelPubGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/HotNovelPubGetter.cs similarity index 67% rename from Core/Logic/Getters/HotNovelPub/HotNovelPubGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/HotNovelPubGetter.cs index 7d37af9b..4a4ec45e 100644 --- a/Core/Logic/Getters/HotNovelPub/HotNovelPubGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/HotNovelPubGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.HotNovelPub; +namespace Elib2Ebook.ExternalServices.HotNovelPub.Getters; public class HotNovelPubGetter(BookGetterConfig config) : HotNovelPubGetterBase(config) { diff --git a/Core/Logic/Getters/HotNovelPub/HotNovelPubGetterBase.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/HotNovelPubGetterBase.cs similarity index 71% rename from Core/Logic/Getters/HotNovelPub/HotNovelPubGetterBase.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/HotNovelPubGetterBase.cs index acc0fa2a..156263d8 100644 --- a/Core/Logic/Getters/HotNovelPub/HotNovelPubGetterBase.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/HotNovelPubGetterBase.cs @@ -1,18 +1,16 @@ -using System; -using System.Collections.Generic; using System.Net.Http.Json; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.HotNovelPub; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.HotNovelPub.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.HotNovelPub; +namespace Elib2Ebook.ExternalServices.HotNovelPub.Getters; public abstract class HotNovelPubGetterBase(BookGetterConfig config) : GetterBase(config) { @@ -61,7 +59,10 @@ private async Task GetMainUrl(Uri url) private async Task> FillChapters(ICollection toc) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var ezChapter in SliceToc(toc, c => c.Title)) { @@ -86,17 +87,18 @@ private async Task> FillChapters(ICollection GetChapter(HotNovelPubChapter ezChapter) { var doc = Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri(ezChapter.Slug)); - var additional = - Config.Client.GetFromJsonAsync>( - SystemUrl.MakeRelativeUri($"/server/getContent?slug={ezChapter.Slug}")); + var additional = Config.Client.GetFromJsonAsync>(SystemUrl.MakeRelativeUri($"/server/getContent?slug={ezChapter.Slug}")); const string watermark = ".copy right hot novel pub"; - var sb = new StringBuilder((await doc).QuerySelector("#content-item").InnerHtml.HtmlDecode() - .CleanInvalidXmlChars().Replace(watermark, string.Empty)); + var sb = new StringBuilder((await doc).QuerySelector("#content-item").InnerHtml.HtmlDecode().CleanInvalidXmlChars().Replace(watermark, string.Empty)); foreach (var row in (await additional)!.Data) - foreach (var p in row.Split("\n", StringSplitOptions.RemoveEmptyEntries)) - sb.Append(p.HtmlDecode().CleanInvalidXmlChars().Replace(watermark, string.Empty).Trim().CoverTag("p")); + { + foreach (var p in row.Split("\n", StringSplitOptions.RemoveEmptyEntries)) + { + sb.Append(p.HtmlDecode().CleanInvalidXmlChars().Replace(watermark, string.Empty).Trim().CoverTag("p")); + } + } return sb.AsHtmlDoc(); } @@ -108,16 +110,12 @@ private Author GetAuthor(HotNovelPubBook book) private Task GetCover(HotNovelPubBook book) { - return !string.IsNullOrWhiteSpace(book.Image) - ? SaveImage(SystemUrl.MakeRelativeUri(book.Image)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(book.Image) ? SaveImage(SystemUrl.MakeRelativeUri(book.Image)) : Task.FromResult(default(TempFile)); } private async Task GetBook(string id) { - var response = - await Config.Client.GetFromJsonAsync>( - _apiUrl.MakeRelativeUri($"/book/{id}")); + var response = await Config.Client.GetFromJsonAsync>(_apiUrl.MakeRelativeUri($"/book/{id}")); return response!.Data; } } diff --git a/Core/Logic/Getters/HotNovelPub/LaNovelsGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/LaNovelsGetter.cs similarity index 67% rename from Core/Logic/Getters/HotNovelPub/LaNovelsGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/LaNovelsGetter.cs index 43fa2586..9128b65e 100644 --- a/Core/Logic/Getters/HotNovelPub/LaNovelsGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/LaNovelsGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.HotNovelPub; +namespace Elib2Ebook.ExternalServices.HotNovelPub.Getters; public class LaNovelsGetter(BookGetterConfig config) : HotNovelPubGetterBase(config) { diff --git a/Core/Logic/Getters/HotNovelPub/LightNovelDailyGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/LightNovelDailyGetter.cs similarity index 68% rename from Core/Logic/Getters/HotNovelPub/LightNovelDailyGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/LightNovelDailyGetter.cs index ff8b8ea3..d84d2d01 100644 --- a/Core/Logic/Getters/HotNovelPub/LightNovelDailyGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Getters/LightNovelDailyGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.HotNovelPub; +namespace Elib2Ebook.ExternalServices.HotNovelPub.Getters; public class LightNovelDailyGetter(BookGetterConfig config) : HotNovelPubGetterBase(config) { diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubApiResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubApiResponse.cs new file mode 100644 index 00000000..c19607a3 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubApiResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.HotNovelPub.Types; + +internal class HotNovelPubApiResponse +{ + [JsonPropertyName("data")] + public T Data { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubAuthorize.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubAuthorize.cs new file mode 100644 index 00000000..1e573d35 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubAuthorize.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.HotNovelPub.Types; + +internal class HotNovelPubAuthorize +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("slug")] + public string Slug { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubBook.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubBook.cs new file mode 100644 index 00000000..8c904629 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubBook.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.HotNovelPub.Types; + +internal class HotNovelPubBook +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("authorize")] + public HotNovelPubAuthorize Authorize { get; set; } + + [JsonPropertyName("image")] + public string Image { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubBookResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubBookResponse.cs new file mode 100644 index 00000000..73e06964 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubBookResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.HotNovelPub.Types; + +internal class HotNovelPubBookResponse +{ + [JsonPropertyName("book")] + public HotNovelPubBook Book { get; set; } + + [JsonPropertyName("chapters")] + public List Chapters { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubChapter.cs new file mode 100644 index 00000000..984115c7 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HotNovelPub/Types/HotNovelPubChapter.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.HotNovelPub.Types; + +internal class HotNovelPubChapter +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("slug")] + public string Slug { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.HubBook/Elib2Ebook.ExternalServices.HubBook.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.HubBook/Elib2Ebook.ExternalServices.HubBook.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HubBook/Elib2Ebook.ExternalServices.HubBook.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/HubBookGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.HubBook/HubBookGetter.cs similarity index 79% rename from Core/Logic/Getters/HubBookGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.HubBook/HubBookGetter.cs index 602732da..e8b4d1c2 100644 --- a/Core/Logic/Getters/HubBookGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.HubBook/HubBookGetter.cs @@ -1,25 +1,21 @@ -using System; -using System.Collections.Generic; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.HubBook; public class HubBookGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://hub-book.com/"); protected override string GetId(Uri url) - { - return url.GetSegment(2); - } + => url.GetSegment(2); public override async Task Get(Uri url) { @@ -35,7 +31,7 @@ public override async Task Get(Uri url) Chapters = await FillChapters(doc, url, title), Title = title, Author = author, - Annotation = doc.QuerySelector("div.b-book-desc div.more_text")?.InnerHtml + Annotation = doc.QuerySelector("div.b-book-desc div.more_text")?.InnerHtml, }; return book; @@ -43,7 +39,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url, string title) { - if (Config.Options.NoChapters) return []; + if (Config.Options.NoChapters) + { + return []; + } var pages = int.Parse(doc.GetTextBySelector("span[itemprop=numberOfPages]")); var text = new StringBuilder(); @@ -62,15 +61,16 @@ private async Task> FillChapters(HtmlDocument doc, Uri url, chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; - return new[] { chapter }; + return + [ + chapter, + ]; } private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("div.b-book-image img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private static Author GetAuthor(HtmlDocument doc, Uri uri) diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.IGram/Elib2Ebook.ExternalServices.IGram.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.IGram/Elib2Ebook.ExternalServices.IGram.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.IGram/Elib2Ebook.ExternalServices.IGram.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/IGramGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.IGram/IGramGetter.cs similarity index 70% rename from Core/Logic/Getters/IGramGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.IGram/IGramGetter.cs index b687e30c..d11a18ee 100644 --- a/Core/Logic/Getters/IGramGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.IGram/IGramGetter.cs @@ -1,57 +1,52 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; using System.Net.Http.Json; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Litexit; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.IGram.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.IGram; public class IGramGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://i-gram.ru/"); protected override string GetId(Uri url) - { - return url.GetSegment(2); - } + => url.GetSegment(2); public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/account/login/")); var token = doc.QuerySelector("input[name=csrfmiddlewaretoken]")?.Attributes["value"]?.Value; Config.Client.DefaultRequestHeaders.Add("Referer", SystemUrl.MakeRelativeUri("/account/login/").ToString()); - using var post = - await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("/account/login/"), GenerateAuthData(token)); - var checkLogin = - await Config.Client.GetFromJsonAsync(SystemUrl.MakeRelativeUri("/api/v2/users/current/")); + using var post = await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("/account/login/"), GenerateAuthData(token)); + var checkLogin = await Config.Client.GetFromJsonAsync(SystemUrl.MakeRelativeUri("/api/v2/users/current/")); if (checkLogin?.Id > 0) + { Config.Logger.LogInformation("Успешно авторизовались"); + } else - throw new Elib2EbookAuthException("Не удалось авторизоваться"); + { + throw new Exception("Не удалось авторизоваться"); + } } private FormUrlEncodedContent GenerateAuthData(string token) { var data = new Dictionary { - ["csrfmiddlewaretoken"] = token, - ["remember"] = "checked", - ["login"] = Config.Options.Login, - ["password"] = Config.Options.Password + ["csrfmiddlewaretoken"] = token, ["remember"] = "checked", ["login"] = Config.Options.Login, ["password"] = Config.Options.Password }; return new FormUrlEncodedContent(data); @@ -78,20 +73,24 @@ public override async Task Get(Uri url) private static Seria GetSeria(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("div.bk-genre a[href^=/b/cycle/]"); - if (a is not null) + if (a != null) + { return new Seria { - Name = a.GetText(), - Url = url.MakeRelativeUri(a.Attributes["href"].Value) + Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; + } - return default; + return null; } private async Task> FillChapters(Uri uri, HtmlDocument doc) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var bookChapter in await GetToc(doc)) { @@ -116,8 +115,7 @@ private async Task> FillChapters(Uri uri, HtmlDocument doc) private async Task> GetToc(HtmlDocument doc) { - var response = - await Config.Client.GetAsync(SystemUrl.MakeRelativeUri($"/api/v1/book/{GetInternalId(doc)}/chapters")); + var response = await Config.Client.GetAsync(SystemUrl.MakeRelativeUri($"/api/v1/book/{GetInternalId(doc)}/chapters")); var content = await response.Content.ReadAsStringAsync(); return SliceToc(content.Deserialize>().OrderBy(c => c.Id).ToList(), c => c.Title); @@ -137,8 +135,6 @@ private static string GetInternalId(HtmlDocument doc) private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("div.bk-img img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.IGram/Types/LitexitChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.IGram/Types/LitexitChapter.cs new file mode 100644 index 00000000..6119a3ea --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.IGram/Types/LitexitChapter.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.IGram.Types; + +internal class LitexitChapter +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("text")] + public string Text { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.IGram/Types/LitexitUser.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.IGram/Types/LitexitUser.cs new file mode 100644 index 00000000..4a231070 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.IGram/Types/LitexitUser.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.IGram.Types; + +internal class LitexitUser +{ + [JsonPropertyName("id")] + public int Id { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Jaomix/Elib2Ebook.ExternalServices.Jaomix.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Jaomix/Elib2Ebook.ExternalServices.Jaomix.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Jaomix/Elib2Ebook.ExternalServices.Jaomix.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/JaomixGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Jaomix/JaomixGetter.cs similarity index 69% rename from Core/Logic/Getters/JaomixGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Jaomix/JaomixGetter.cs index 1f4d304d..eccb9cc6 100644 --- a/Core/Logic/Getters/JaomixGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Jaomix/JaomixGetter.cs @@ -1,26 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Jaomix; public class JaomixGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://jaomix.ru/"); protected override string GetId(Uri url) - { - return url.GetSegment(1); - } + => url.GetSegment(1); public override async Task Get(Uri url) { @@ -29,10 +24,7 @@ public override async Task Get(Uri url) var book = new Book(url) { - Cover = await GetCover(doc, url), - Chapters = await FillChapters(doc, url), - Title = doc.GetTextBySelector("h1"), - Author = new Author("Jaomix") + Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1"), Author = new Author("Jaomix") }; return book; @@ -41,7 +33,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var jaomixChapter in GetToc(doc, url)) { @@ -64,8 +59,7 @@ private async Task GetChapter(Uri url) var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); while (doc.QuerySelector("div.themeform div.h-captcha, div.themeform div.but-captcha") != null) { - Config.Logger.LogInformation( - $"Обнаружена капча. Перейдите по ссылке {url}, введите капчу и нажмите Enter..."); + Config.Logger.LogInformation($"Обнаружена капча. Перейдите по ссылке {url}, введите капчу и нажмите Enter..."); Console.Read(); doc = await Config.Client.GetHtmlDocWithTriesAsync(url); } @@ -73,12 +67,13 @@ private async Task GetChapter(Uri url) var sb = new StringBuilder(); foreach (var node in doc.QuerySelector("div.themeform").ChildNodes) - if (node.Name != "br" && node.Name != "script" && !string.IsNullOrWhiteSpace(node.InnerHtml) && - node.Attributes["class"]?.Value?.Contains("adblock-service") == null) + { + if (node.Name != "br" && node.Name != "script" && !string.IsNullOrWhiteSpace(node.InnerHtml) && node.Attributes["class"]?.Value?.Contains("adblock-service") == null) { var tag = node.Name == "#text" ? "p" : node.Name; sb.Append(node.InnerHtml.HtmlDecode().CoverTag(tag)); } + } return sb.AsHtmlDoc(); } @@ -94,14 +89,12 @@ private IEnumerable GetToc(HtmlDocument doc, Uri url) private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("div.img-book img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private static IEnumerable ParseChapters(HtmlDocument doc, Uri url) { - return doc.QuerySelectorAll("form.download-chapter .hiddenstab .flex-dow-txt a").Select(a => - new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.InnerText.Trim())).Reverse(); + return doc.QuerySelectorAll("form.download-chapter .hiddenstab .flex-dow-txt a").Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.InnerText.Trim())) + .Reverse(); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Elib2Ebook.ExternalServices.LibSocial.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Elib2Ebook.ExternalServices.LibSocial.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Elib2Ebook.ExternalServices.LibSocial.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/LibSocial/HentaiLibGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/HentaiLibGetter.cs similarity index 73% rename from Core/Logic/Getters/LibSocial/HentaiLibGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/HentaiLibGetter.cs index f9780e4e..47ec8164 100644 --- a/Core/Logic/Getters/LibSocial/HentaiLibGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/HentaiLibGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.LibSocial; +namespace Elib2Ebook.ExternalServices.LibSocial.Getters; public class HentaiLibGetter(BookGetterConfig config) : MangaLibGetterBase(config) { diff --git a/Core/Logic/Getters/LibSocial/MangaLibGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/MangaLibGetter.cs similarity index 53% rename from Core/Logic/Getters/LibSocial/MangaLibGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/MangaLibGetter.cs index d2a09295..127a2dbd 100644 --- a/Core/Logic/Getters/LibSocial/MangaLibGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/MangaLibGetter.cs @@ -1,11 +1,10 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.LibSocial; +namespace Elib2Ebook.ExternalServices.LibSocial.Getters; public class MangaLibGetter(BookGetterConfig config) : MangaLibGetterBase(config) { - protected override Uri ImagesHost => new("https://img33.imgslib.link/"); + protected override Uri ImagesHost => new("https://img3.cdnlibs.org/"); protected override Uri SystemUrl => new("https://mangalib.me/"); diff --git a/Core/Logic/Getters/LibSocial/MangaLibGetterBase.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/MangaLibGetterBase.cs similarity index 61% rename from Core/Logic/Getters/LibSocial/MangaLibGetterBase.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/MangaLibGetterBase.cs index 514375a0..4ad6041c 100644 --- a/Core/Logic/Getters/LibSocial/MangaLibGetterBase.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/MangaLibGetterBase.cs @@ -1,10 +1,10 @@ using System.Text; -using Core.Configs; -using Core.Extensions; -using Core.Types.SocialLib; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.ExternalServices.LibSocial.Types.SocialLib; using HtmlAgilityPack; -namespace Core.Logic.Getters.LibSocial; +namespace Elib2Ebook.ExternalServices.LibSocial.Getters; public abstract class MangaLibGetterBase(BookGetterConfig config) : NewLibSocialGetterBase(config) { @@ -14,7 +14,7 @@ protected override HtmlDocument ResponseToHtmlDoc(SocialLibBookChapter chapterRe foreach (var page in chapterResponse.Pages) { - var url = SystemUrl.MakeRelativeUri(page.Url.TrimStart('/')); + var url = ImagesHost.MakeRelativeUri(page.Url.TrimStart('/')); sb.Append($""); } diff --git a/Core/Logic/Getters/LibSocial/MangaLibOrgGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/MangaLibOrgGetter.cs similarity index 74% rename from Core/Logic/Getters/LibSocial/MangaLibOrgGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/MangaLibOrgGetter.cs index c4ebd36c..98630922 100644 --- a/Core/Logic/Getters/LibSocial/MangaLibOrgGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/MangaLibOrgGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.LibSocial; +namespace Elib2Ebook.ExternalServices.LibSocial.Getters; public class MangaLibOrgGetter(BookGetterConfig config) : MangaLibGetterBase(config) { diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/NewLibSocialGetterBase.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/NewLibSocialGetterBase.cs new file mode 100644 index 00000000..2f27c86d --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/NewLibSocialGetterBase.cs @@ -0,0 +1,512 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.LibSocial.Types.SocialLib; +using HtmlAgilityPack; +using HtmlAgilityPack.CssSelectors.NetCore; +using Microsoft.Extensions.Logging; + +namespace Elib2Ebook.ExternalServices.LibSocial.Getters; + +public abstract class NewLibSocialGetterBase(BookGetterConfig config) : GetterBase(config) +{ + private static Uri AuthHost => new("https://auth.lib.social/"); + + private static Uri ApiHost => new("https://api.cdnlibs.org/"); + + private static Uri BaseImageHost => new("https://cover.imglib.info/"); + + protected virtual Uri ImagesHost => BaseImageHost; + + private const string ALPHABET_BASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + private const string ALPHABET_CHALLENGE = ALPHABET_BASE + "-_"; + + protected abstract int SiteId { get; } + + private static readonly Dictionary RecursiveTag = new() + { + { + "paragraph", "p" + }, + { + "orderedList", "ol" + }, + { + "bulletList", "ul" + }, + { + "listItem", "li" + }, + { + "blockquote", "blockquote" + }, + }; + + private static readonly Dictionary InlineTag = new() + { + { + "horizontalRule", "
" + }, + { + "hardBreak", "
" + }, + }; + + private static readonly Dictionary MarkTag = new() + { + { + "italic", "i" + }, + { + "bold", "b" + }, + { + "underline", "u" + }, + }; + + public override async Task Init() + { + await base.Init(); + + Config.Client.DefaultRequestHeaders.Remove("Site-Id"); + Config.Client.DefaultRequestHeaders.Add("Site-Id", SiteId.ToString()); + + try + { + var timeZoneId = TimeZoneInfo.Local.Id; + Config.Client.DefaultRequestHeaders.Remove("Client-Time-Zone"); + Config.Client.DefaultRequestHeaders.Add("Client-Time-Zone", timeZoneId); + } + catch (TimeZoneNotFoundException) + { + // Игнорируем: таймзона влияет только на аналитические метрики сайта + } + catch (InvalidTimeZoneException) + { + // Игнорируем: таймзона влияет только на аналитические метрики сайта + } + } + + private static string TrimForLog(string value, int maxLength = 300) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + var trimmed = value.Trim(); + return trimmed.Length <= maxLength ? trimmed : trimmed[..maxLength] + "…"; + } + + private static string Challenge(string str) + { + var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(str).AsSpan(0, Encoding.UTF8.GetByteCount(str))); + var result = string.Empty; + + for (var t = 0; t < bytes.Length; t += 3) + { + result += ALPHABET_CHALLENGE[bytes[t] >> 2]; + result += ALPHABET_CHALLENGE[(bytes[t] & 3) << 4 | bytes[t + 1] >> 4]; + result += ALPHABET_CHALLENGE[(bytes[t + 1] & 15) << 2 | (t + 2 < bytes.Length ? bytes[t + 2] : 0) >> 6]; + result += ALPHABET_CHALLENGE[(t + 2 < bytes.Length ? bytes[t + 2] : 0) & 63]; + } + + return (bytes.Length % 3) switch + { + 2 => result[..^1], + 1 => result[..^2], + _ => result + }; + } + + private static string GetRandom(int length) + { + return Enumerable + .Repeat(0, length) + .Aggregate(string.Empty, (c, _) => c + ALPHABET_BASE[new Random().Next(ALPHABET_BASE.Length)]); + } + + public override async Task Authorize() + { + if (!Config.HasCredentials) + { + return; + } + + var secret = GetRandom(128); + var state = GetRandom(40); + var redirectUri = SystemUrl.MakeRelativeUri("/ru/front/auth/oauth/callback"); + + var challenge = Challenge(secret); + var challengeUrl = AuthHost.MakeRelativeUri( + $"/auth/oauth/authorize?scope=&client_id=1&response_type=code&redirect_uri={redirectUri}&state={state}&code_challenge={challenge}&code_challenge_method=S256&prompt=consent"); + + var loginForm = await Config.Client.GetHtmlDocWithTriesAsync(challengeUrl); + var tokenInput = loginForm.QuerySelector("input[name=_token]"); + if (tokenInput == null) + { + throw new Exception("Не удалось получить csrf токен формы авторизации сайта (возможно, заблокировал DDoS-Guard)"); + } + + var payload = new Dictionary + { + { + "_token", tokenInput.Attributes["value"].Value + }, + { + "login", Config.Options.Login + }, + { + "password", Config.Options.Password + }, + }; + + await Config.Client.PostHtmlDocWithTriesAsync(AuthHost.MakeRelativeUri("/auth/login"), new FormUrlEncodedContent(payload)); + var login = await Config.Client.GetHtmlDocWithTriesAsync(challengeUrl); + if (login.QuerySelector(".g-recaptcha") != null) + { + throw new Exception("Авторизация заблокирована капчей. Укажите --flare с адресом FlareSolverr"); + } + + var error = login.QuerySelector(".form-field__error"); + if (error != null && !string.IsNullOrWhiteSpace(error.InnerText)) + { + throw new Exception($"Не удалось авторизоваться. {error.InnerText.Trim()}"); + } + + var postForm = login.QuerySelector("form[method=post]"); + if (postForm == null) + { + throw new Exception("Не удалось получить форму подтверждения OAuth на сайте (проверьте логин/пароль)"); + } + + payload = postForm + .QuerySelectorAll("input[type=hidden]") + .ToDictionary(input => input.Attributes["name"].Value, input => input.Attributes["value"].Value); + + using var authorize = await Config.Client.PostAsync(AuthHost.MakeRelativeUri(postForm.Attributes["action"].Value), new FormUrlEncodedContent(payload)); + if ((int)authorize.StatusCode == 419) + { + throw new Exception("Авторизация отклонена (код 419). Обычно это означает истекший CSRF токен или необходимость пройти капчу. Попробуйте указать --flare"); + } + + if (authorize == null || authorize.RequestMessage?.RequestUri == null) + { + throw new Exception("Сайт не вернул код авторизации (проверьте логин/пароль)"); + } + + var authCode = authorize.RequestMessage.RequestUri.GetQueryParameter("code"); + if (string.IsNullOrWhiteSpace(authCode)) + { + throw new Exception("Сайт не вернул code после авторизации (проверьте логин/пароль)"); + } + + var tokenResponse = await Config.Client.PostAsync( + ApiHost.MakeRelativeUri("/api/auth/oauth/token"), + JsonContent.Create( + new + { + grant_type = "authorization_code", + client_id = 1, + redirect_uri = redirectUri, + code_verifier = secret, + code = authCode + })); + + if (tokenResponse == null) + { + throw new Exception("Не удалось получить ответ от сайта при запросе access token"); + } + + if (tokenResponse.StatusCode != HttpStatusCode.OK) + { + var errorBody = await tokenResponse.Content.ReadAsStringAsync(); + var snippet = string.IsNullOrWhiteSpace(errorBody) + ? string.Empty + : $" Ответ сервера: {TrimForLog(errorBody)}"; + + throw new Exception($"Сайт не выдал access token. Код {(int)tokenResponse.StatusCode} ({tokenResponse.StatusCode}).{snippet}"); + } + + var token = await tokenResponse.Content.ReadFromJsonAsync(); + if (token == null || string.IsNullOrWhiteSpace(token.AccessToken)) + { + throw new Exception("Сайт не вернул access token. Проверьте корректность логина и пароля"); + } + + Config.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); + Config.Logger.LogInformation("Успешно авторизовались"); + } + + protected override string GetId(Uri url) + { + var id = url.GetSegment(2); + return id is "book" or "read" or "manga" ? url.GetSegment(3) : id; + } + + public override async Task Get(Uri url) + { + var bid = url.GetQueryParameter("bid"); + var details = await GetBookDetails(url); + + var book = new Book(url) + { + Cover = await GetCover(details), + Chapters = await FillChapters(details, bid), + Title = string.IsNullOrWhiteSpace(details.Data.RusName) ? details.Data.Name : details.Data.RusName, + Author = GetAuthor(details), + CoAuthors = GetCoAuthors(details), + Annotation = GetDescription(details).DocumentNode.InnerHtml, + }; + + return book; + } + + private async Task GetBookDetails(Uri url) + { + url = ApiHost + .AppendSegment("api/manga/" + GetId(url)) + .AppendQueryParameter("fields[]", "background") + .AppendQueryParameter("fields[]", "teams") + .AppendQueryParameter("fields[]", "authors") + .AppendQueryParameter("fields[]", "summary") + .AppendQueryParameter("fields[]", "chap_count"); + + var response = await Config.Client.GetWithTriesAsync(url); + if (response == null) + { + throw new Exception("Не удалось получить ответ от сайта при загрузке информации о книге"); + } + + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception($"Ошибка загрузки информации о книге. Код {(int)response.StatusCode} ({response.StatusCode})."); + } + + return await response.Content.ReadFromJsonAsync(); + } + + private async Task> GetToc(RanobeLibBookDetails book, string bid) + { + var url = ApiHost.MakeRelativeUri($"/api/manga/{book.Data.SlugUrl}/chapters"); + + Config.Logger.LogInformation("Загружаю оглавление"); + + var response = await Config.Client.GetWithTriesAsync(url); + if (response == null) + { + throw new Exception("Не удалось получить ответ от сайта при загрузке оглавления"); + } + + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception($"Ошибка загрузки оглавления. Код {(int)response.StatusCode} ({response.StatusCode})."); + } + + var result = await response.Content.ReadFromJsonAsync().ContinueWith(t => t.Result.Chapters); + return SliceToc(WhereBranchIdEqualTo(result, bid), c => c.Name); + } + + private List WhereBranchIdEqualTo(List chapters, string bid) + { + if (!string.IsNullOrWhiteSpace(bid)) + { + var result = new List(); + + foreach (var chapter in chapters) + { + var branches = chapter.Branches switch + { + JsonArray a => a.Deserialize>(), + JsonObject o => o.Deserialize>().Select(d => d.Value).ToList(), + _ => throw new ArgumentException($"Неизвестный тип {chapter.Branches}"), + }; + + if (branches.Any(b => b.BranchId.ToString() == bid)) + { + result.Add(chapter); + } + } + + return result; + } + + return chapters; + } + + private Author GetAuthor(RanobeLibBookDetails details) + { + var author = details.Data.Authors.FirstOrDefault(); + return author == null ? new Author("Ranobelib") : new Author(author.Name, SystemUrl.MakeRelativeUri($"/ru/people/{author.SlugUrl}")); + } + + private IEnumerable GetCoAuthors(RanobeLibBookDetails details) + { + return details.Data.Authors + .Skip(1) + .Select(author => new Author(author.Name, SystemUrl.MakeRelativeUri($"/ru/people/{author.SlugUrl}"))).ToList(); + } + + private Task GetCover(RanobeLibBookDetails details) + { + return !string.IsNullOrWhiteSpace(details.Data.Cover.Default) ? SaveImage(details.Data.Cover.Default.AsUri()) : Task.FromResult(default(TempFile)); + } + + protected override HttpRequestMessage GetImageRequestMessage(Uri uri) + { + var message = base.GetImageRequestMessage(uri); + message.Headers.Add("Referer", SystemUrl.ToString()); + return message; + } + + private async Task> FillChapters(RanobeLibBookDetails book, string bid) + { + var result = new List(); + if (Config.Options.NoChapters) + { + return result; + } + + foreach (var socialChapter in await GetToc(book, bid)) + { + var title = socialChapter.Name.ReplaceNewLine(); + Config.Logger.LogInformation($"Загружаю главу {title.CoverQuotes()}"); + + var chapter = new Chapter + { + Title = title, + }; + + var chapterDoc = await GetChapter(book, socialChapter, bid); + chapter.Images = await GetImages(chapterDoc, ImagesHost); + chapter.Content = chapterDoc.DocumentNode.InnerHtml; + + result.Add(chapter); + } + + return result; + } + + private async Task GetChapter(RanobeLibBookDetails book, SocialLibBookChapter chapter, string bid) + { + var uri = ApiHost.MakeRelativeUri($"/api/manga/{book.Data.SlugUrl}/chapter?number={chapter.Number}&volume={chapter.Volume}"); + if (!string.IsNullOrWhiteSpace(bid)) + { + uri = uri.AppendQueryParameter("branch_id", bid); + } + + var chapterResponse = await Config.Client.GetFromJsonWithTriesAsync(uri, TimeSpan.FromSeconds(30)); + return ResponseToHtmlDoc(chapterResponse.Data); + } + + protected StringBuilder AsHtml(SocialLibChapterAttachment[] attachments, IEnumerable contents) + { + var sb = new StringBuilder(); + + foreach (var content in contents) + { + if (RecursiveTag.TryGetValue(content.Type, out var tag)) + { + sb.Append(AsHtml(attachments, content.Content).ToString().CoverTag(tag)); + continue; + } + + if (InlineTag.TryGetValue(content.Type, out tag)) + { + sb.Append(tag); + continue; + } + + switch (content.Type) + { + case "heading": + var level = content.Attrs.GetValueOrDefault("level"); + var headingContent = AsHtml(attachments, content.Content).ToString(); + + if (content.Attrs.TryGetValue("textAlign", out var alignAttr)) + { + if (alignAttr.ToString() == "center") + { + headingContent = headingContent.CoverTag("center"); + } + else + { + Config.Logger.LogInformation($"Неизвестное значения атрибута textAlign - {alignAttr}"); + } + } + + sb.Append(headingContent.CoverTag("h" + (level?.ToString() ?? "4"))); + continue; + case "text": + { + var text = content.Text.HtmlEncode(); + + foreach (var mark in content.Marks) + { + if (MarkTag.TryGetValue(mark.Type, out tag)) + { + text = text.CoverTag(tag); + } + else + { + Config.Logger.LogInformation($"Неизвестный тип форматирования - {mark.Type}"); + } + } + + sb.Append(text); + continue; + } + case "image": + { + if (content.Attrs.TryGetValue("images", out var images)) + { + foreach (var image in images.Deserialize[]>()) + { + if (!image.TryGetValue("image", out var imageId)) + { + continue; + } + + var attachment = attachments.FirstOrDefault(a => a.Name == imageId); + if (attachment == null) + { + continue; + } + + sb.Append($""); + } + } + + continue; + } + default: + Config.Logger.LogInformation($"Неизвестный тип - {content.Type}"); + break; + } + } + + return sb; + } + + private HtmlDocument GetDescription(RanobeLibBookDetails details) + { + return details.Data.Summary switch + { + JsonValue e => e.GetValue().AsHtmlDoc(), + JsonObject o => AsHtml([], o.Deserialize().Content).AsHtmlDoc(), + _ => throw new Exception("Неизвестный тип"), + }; + } + + protected abstract HtmlDocument ResponseToHtmlDoc(SocialLibBookChapter chapterResponse); +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/RanobeLibGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/RanobeLibGetter.cs new file mode 100644 index 00000000..8d255caf --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/RanobeLibGetter.cs @@ -0,0 +1,25 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.ExternalServices.LibSocial.Types.SocialLib; +using HtmlAgilityPack; + +namespace Elib2Ebook.ExternalServices.LibSocial.Getters; + +public class RanobeLibGetter(BookGetterConfig config) : NewLibSocialGetterBase(config) +{ + protected override Uri SystemUrl => new("https://ranobelib.me/"); + + protected override int SiteId => 3; + + protected override HtmlDocument ResponseToHtmlDoc(SocialLibBookChapter chapterResponse) + { + return chapterResponse.Content switch + { + JsonValue e => e.GetValue().AsHtmlDoc(), + JsonObject o => AsHtml(chapterResponse.Attachments, o.Deserialize().Content).AsHtmlDoc(), + _ => throw new Exception("Неизвестный тип"), + }; + } +} diff --git a/Core/Logic/Getters/LibSocial/SlashLibGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/SlashLibGetter.cs similarity index 68% rename from Core/Logic/Getters/LibSocial/SlashLibGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/SlashLibGetter.cs index ef91938d..0022e435 100644 --- a/Core/Logic/Getters/LibSocial/SlashLibGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Getters/SlashLibGetter.cs @@ -1,13 +1,10 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.LibSocial; +namespace Elib2Ebook.ExternalServices.LibSocial.Getters; public class SlashLibGetter : MangaLibGetter { - public SlashLibGetter(BookGetterConfig config) : base(config) - { - } + public SlashLibGetter(BookGetterConfig config) : base(config) { } protected override Uri SystemUrl => new("https://v2.slashlib.me/"); diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/MangaLib/MangaLibPg.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/MangaLib/MangaLibPg.cs new file mode 100644 index 00000000..909654f9 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/MangaLib/MangaLibPg.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.LibSocial.Types.MangaLib; + +internal class MangaLibPg +{ + [JsonPropertyName("p")] + public int P { get; set; } + + [JsonPropertyName("u")] + public string U { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/LibSocialToken.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/LibSocialToken.cs new file mode 100644 index 00000000..b8a84dc7 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/LibSocialToken.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.LibSocial.Types.SocialLib; + +internal class LibSocialToken +{ + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/RanobeLibBookDetail.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/RanobeLibBookDetail.cs new file mode 100644 index 00000000..0f0bf424 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/RanobeLibBookDetail.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.LibSocial.Types.SocialLib; + +internal class RanobeLibBookDetails +{ + [JsonPropertyName("data")] + public RLBDData Data { get; set; } +} + +internal class RLBDData +{ + public RLBDData(int? id) + { + Id = id; + } + + [JsonPropertyName("id")] + public int? Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("rus_name")] + public string RusName { get; set; } + + [JsonPropertyName("slug_url")] + public string SlugUrl { get; set; } + + [JsonPropertyName("cover")] + public RLBDCover Cover { get; set; } + + [JsonPropertyName("authors")] + public List Authors { get; set; } + + [JsonPropertyName("summary")] + public JsonNode Summary { get; set; } +} + +internal class RLBDCover +{ + [JsonPropertyName("default")] + public string Default { get; set; } +} + +internal class RLBDAuthor +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("slug_url")] + public string SlugUrl { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/SocialLibBookChapters.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/SocialLibBookChapters.cs new file mode 100644 index 00000000..8695a62d --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/SocialLibBookChapters.cs @@ -0,0 +1,100 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.LibSocial.Types.SocialLib; + +internal class SocialLibBookChapters +{ + [JsonPropertyName("data")] + public List Chapters { get; set; } +} + +internal class SocialLibBookChapterResponse +{ + [JsonPropertyName("data")] + public SocialLibBookChapter Data { get; set; } +} + +public class SocialLibChapterBranch +{ + [JsonPropertyName("branch_id")] + public long? BranchId { get; set; } +} + +public class SocialLibChapterAttachment +{ + [JsonPropertyName("url")] + public string Url { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } +} + +public class SocialLibBookChapter +{ + [JsonPropertyName("volume")] + public string Volume { get; set; } + + [JsonPropertyName("number")] + public string Number { get; set; } + + [JsonPropertyName("branches")] + public JsonNode Branches { get; set; } + + [JsonPropertyName("attachments")] + public SocialLibChapterAttachment[] Attachments { get; set; } = []; + + [JsonPropertyName("pages")] + public SocialLibPage[] Pages { get; set; } = []; + + [JsonPropertyName("content")] + public JsonNode Content { get; set; } + + private string RawName { get; set; } + + [JsonPropertyName("name")] + public string Name + { + get + { + var name = $"Том {Volume} Глава {Number}"; + if (!string.IsNullOrWhiteSpace(RawName)) + { + name += $" - {RawName}"; + } + + return name; + } + set { RawName = value; } + } +} + +public class SocialLibChapterContent +{ + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("text")] + public string Text { get; set; } + + [JsonPropertyName("marks")] + public SocialLibChapterMark[] Marks { get; set; } = []; + + [JsonPropertyName("content")] + public SocialLibChapterContent[] Content { get; set; } = []; + + [JsonPropertyName("attrs")] + public Dictionary Attrs { get; set; } = new(); +} + +public class SocialLibChapterMark +{ + [JsonPropertyName("type")] + public string Type { get; set; } +} + +public class SocialLibPage +{ + [JsonPropertyName("url")] + public string Url { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/SocialLibChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/SocialLibChapter.cs new file mode 100644 index 00000000..7741a404 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/SocialLibChapter.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.LibSocial.Types.SocialLib; + +internal class SocialLibChapter +{ + [JsonPropertyName("chapter_number")] + public string ChapterNumber { get; set; } + + [JsonPropertyName("chapter_volume")] + public int ChapterVolume { get; set; } + + [JsonPropertyName("chapter_name")] + public string ChapterName { get; set; } + + [JsonPropertyName("chapter_slug")] + public string ChapterSlug { get; set; } + + [JsonPropertyName("branch_id")] + public int? BranchId { get; set; } + + public string Name => $"Том {ChapterVolume} Глава {ChapterNumber} {ChapterName}".Trim(); +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/SocialLibChapterOld.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/SocialLibChapterOld.cs new file mode 100644 index 00000000..4b76f226 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/SocialLibChapterOld.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.LibSocial.Types.SocialLib; + +internal class SocialLibChapterOld +{ + [JsonPropertyName("number")] + public string ChapterNumber { get; set; } + + [JsonPropertyName("volume")] + public string ChapterVolume { get; set; } + + [JsonPropertyName("name")] + public string ChapterName { get; set; } + + [JsonPropertyName("slug")] + public string ChapterSlug { get; set; } + + public string GetName() + { + return $"Том {ChapterVolume} Глава {ChapterNumber} {ChapterName}".Trim(); + } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/SocialLibChapters.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/SocialLibChapters.cs new file mode 100644 index 00000000..6a4e5a5a --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/SocialLibChapters.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.LibSocial.Types.SocialLib; + +internal class SocialLibChapters +{ + [JsonPropertyName("list")] + public List List { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/User.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/User.cs new file mode 100644 index 00000000..bc50ef56 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/User.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.LibSocial.Types.SocialLib; + +internal class User +{ + [JsonPropertyName("id")] + public long Id { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/WindowData.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/WindowData.cs new file mode 100644 index 00000000..9514db2b --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LibSocial/Types/SocialLib/WindowData.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.LibSocial.Types.SocialLib; + +internal class WindowData +{ + [JsonPropertyName("chapters")] + public SocialLibChapters Chapters { get; set; } + + [JsonPropertyName("user")] + public User User { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Libbox/Elib2Ebook.ExternalServices.Libbox.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Libbox/Elib2Ebook.ExternalServices.Libbox.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Libbox/Elib2Ebook.ExternalServices.Libbox.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/LibboxGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Libbox/LibboxGetter.cs similarity index 81% rename from Core/Logic/Getters/LibboxGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Libbox/LibboxGetter.cs index 457e811d..396340e3 100644 --- a/Core/Logic/Getters/LibboxGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Libbox/LibboxGetter.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Libbox; public class LibboxGetter(BookGetterConfig config) : GetterBase(config) { @@ -41,18 +38,23 @@ public override async Task Get(Uri url) private static Seria GetSeria(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("div.product__meta a[href*=/book-series/]"); - if (string.IsNullOrWhiteSpace(a.GetText())) return default; + if (string.IsNullOrWhiteSpace(a.GetText())) + { + return null; + } return new Seria { - Name = a.GetText(), - Url = url.MakeRelativeUri(a.Attributes["href"].Value) + Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; } private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) { - if (chapter == null) return; + if (chapter == null) + { + return; + } var chapterDoc = text.AsHtmlDoc(); chapter.Images = await GetImages(chapterDoc, SystemUrl); @@ -87,7 +89,10 @@ private async Task GetPages(string bookId) private async Task> FillChapters(string bookId, string title) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } Chapter chapter = null; var singleChapter = true; @@ -97,8 +102,7 @@ private async Task> FillChapters(string bookId, string titl for (var i = 1; i <= pages; i++) { Config.Logger.LogInformation($"Получаю страницу {i}/{pages}"); - var page = await Config.Client.GetHtmlDocWithTriesAsync( - SystemUrl.MakeRelativeUri($"/books/{bookId}?page_book={i}")); + var page = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/books/{bookId}?page_book={i}")); var content = page.QuerySelector("div.entry-content"); var nodes = content.QuerySelectorAll("> h2, > p, > img"); @@ -106,6 +110,7 @@ private async Task> FillChapters(string bookId, string titl singleChapter = i == 1 ? IsSingleChapter(nodes) : singleChapter; foreach (var node in nodes) + { if (singleChapter || node.Name != "h2") { if (node.Name == "img" && node.Attributes["src"] != null) @@ -115,7 +120,9 @@ private async Task> FillChapters(string bookId, string titl else { if (!string.IsNullOrWhiteSpace(node.InnerHtml)) + { text.Append(node.InnerHtml.HtmlDecode().CoverTag("p")); + } } } else @@ -124,6 +131,7 @@ private async Task> FillChapters(string bookId, string titl text.Clear(); chapter = CreateChapter(node.InnerHtml.HtmlDecode()); } + } } await AddChapter(result, chapter ?? CreateChapter(title), text); @@ -133,15 +141,12 @@ private async Task> FillChapters(string bookId, string titl private static Author GetAuthor(HtmlDocument doc, Uri url) { var author = doc.QuerySelector("div.author a"); - return new Author(author.InnerText.HtmlDecode(), - url.MakeRelativeUri(author.Attributes["href"]?.Value ?? string.Empty)); + return new Author(author.InnerText.HtmlDecode(), url.MakeRelativeUri(author.Attributes["href"]?.Value ?? string.Empty)); } private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("div.book-images img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Libking/Elib2Ebook.ExternalServices.Libking.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Libking/Elib2Ebook.ExternalServices.Libking.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Libking/Elib2Ebook.ExternalServices.Libking.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/LibkingGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Libking/LibkingGetter.cs similarity index 70% rename from Core/Logic/Getters/LibkingGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Libking/LibkingGetter.cs index fc42b48f..3520247a 100644 --- a/Core/Logic/Getters/LibkingGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Libking/LibkingGetter.cs @@ -1,16 +1,14 @@ -using System; -using System.Collections.Generic; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Libking; public class LibkingGetter(BookGetterConfig config) : GetterBase(config) { @@ -28,7 +26,7 @@ public override async Task Get(Uri url) Chapters = await FillChapters(doc, url, name), Title = name, Author = GetAuthor(doc), - Annotation = doc.QuerySelector("div.mov-desc-text")?.InnerHtml + Annotation = doc.QuerySelector("div.mov-desc-text")?.InnerHtml, }; return book; @@ -37,17 +35,13 @@ public override async Task Get(Uri url) private Author GetAuthor(HtmlDocument doc) { var a = doc.QuerySelector("a[itemprop=author]"); - return a is null - ? new Author("LibKing") - : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); + return a == null ? new Author("LibKing") : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); } private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("div.mov-img img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) @@ -69,7 +63,10 @@ private static Chapter CreateChapter(string title) private Uri GetFirstPage(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("div.pages a.pagenav"); - if (a.GetText() != "1") return url; + if (a.GetText() != "1") + { + return url; + } return url.MakeRelativeUri(a.Attributes["href"].Value); } @@ -77,7 +74,10 @@ private Uri GetFirstPage(HtmlDocument doc, Uri url) private Uri GetNextPage(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("a.load_text"); - if (a is null) return default; + if (a == null) + { + return null; + } return url.MakeRelativeUri(a.Attributes["href"].Value); } @@ -91,39 +91,47 @@ private HtmlNode GetTitleNode(HtmlDocument doc) { foreach (var node in doc.DocumentNode.ChildNodes) { - if (node.Name == "#text" && string.IsNullOrWhiteSpace(node.GetText())) continue; + if (node.Name == "#text" && string.IsNullOrWhiteSpace(node.GetText())) + { + continue; + } - if (IsTitle(node)) return node; + if (IsTitle(node)) + { + return node; + } - return default; + return null; } - return default; + return null; } private async Task> FillChapters(HtmlDocument doc, Uri baseUrl, string name) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } var fullText = new StringBuilder(); var i = 1; - var url = GetFirstPage(doc, baseUrl); - while (url is not null) + for (var url = GetFirstPage(doc, baseUrl); url != null;) { Config.Logger.LogInformation($"Получаю страницу {i}"); doc = await Config.Client.GetHtmlDocWithTriesAsync(url); url = GetNextPage(doc, baseUrl); - fullText.Append(doc.QuerySelector("#book") - .RemoveNodes(".navigation, .cpab, .cpab1, .load_text, #twentypersent").InnerHtml); + fullText.Append(doc.QuerySelector("#book").RemoveNodes(".navigation, .cpab, .cpab1, .load_text, #twentypersent").InnerHtml); i++; } var book = fullText.AsHtmlDoc(); var node = GetTitleNode(book); - if (node is not null) - while (node is not null) + if (node != null) + { + while (node != null) { var title = node.GetText(); var chapterText = new StringBuilder(); @@ -132,13 +140,16 @@ private async Task> FillChapters(HtmlDocument doc, Uri baseUrl, st { node = node.NextSibling; chapterText.Append(node.OuterHtml); - } while (node.NextSibling is not null && !IsTitle(node.NextSibling)); + } while (node.NextSibling != null && !IsTitle(node.NextSibling)); node = node.NextSibling; await AddChapter(result, CreateChapter(title), chapterText); } + } else + { await AddChapter(result, CreateChapter(name), fullText); + } return result; } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Librebook/Elib2Ebook.ExternalServices.Librebook.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Librebook/Elib2Ebook.ExternalServices.Librebook.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Librebook/Elib2Ebook.ExternalServices.Librebook.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/LibrebookGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Librebook/LibrebookGetter.cs similarity index 63% rename from Core/Logic/Getters/LibrebookGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Librebook/LibrebookGetter.cs index 768dce65..85aafff1 100644 --- a/Core/Logic/Getters/LibrebookGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Librebook/LibrebookGetter.cs @@ -1,21 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; using System.Text.Json; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Librebook; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Librebook.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Librebook; public class LibrebookGetter(BookGetterConfig config) : GetterBase(config) { @@ -39,11 +34,12 @@ public override async Task Init() public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } - var loginPage = - "https://1.grouple.co/internal/auth/login?siteId=6&targetUri=%2Flogin%2FcontinueSso%3FsiteId%3D6%26targetUri%3D%252F" - .AsUri(); + var loginPage = "https://1.grouple.co/internal/auth/login?siteId=6&targetUri=%2Flogin%2FcontinueSso%3FsiteId%3D6%26targetUri%3D%252F".AsUri(); var doc = await Config.Client.GetHtmlDocWithTriesAsync(loginPage); var formUrl = loginPage.MakeRelativeUri(doc.QuerySelector("form")?.Attributes["action"]?.Value); @@ -52,21 +48,37 @@ public override async Task Authorize() var noty = Regex.Match(doc.ParsedText, @"showNoty\((.*?)\)"); var result = JsonSerializer.Deserialize(noty.Groups[1].Value); if (result.Type == "success") + { Config.Logger.LogInformation("Успешно авторизовались"); + } else - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {result.Text}"); + { + throw new Exception($"Не удалось авторизоваться. {result.Text}"); + } } private FormUrlEncodedContent GenerateAuthData() { var payload = new Dictionary { - { "targetUri", "/login/continueSso?siteId=6&targetUri=%2F" }, - { "username", Config.Options.Login }, - { "password", Config.Options.Password }, - { "remember_me", "true" }, - { "_remember_me_yes", string.Empty }, - { "remember_me_yes", "on" } + { + "targetUri", "/login/continueSso?siteId=6&targetUri=%2F" + }, + { + "username", Config.Options.Login + }, + { + "password", Config.Options.Password + }, + { + "remember_me", "true" + }, + { + "_remember_me_yes", string.Empty + }, + { + "remember_me_yes", "on" + }, }; return new FormUrlEncodedContent(payload); @@ -78,15 +90,17 @@ public override async Task Get(Uri url) var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var chapterLink = doc.QuerySelector("a.chapter-link"); - if (chapterLink is null) - throw new Elib2EbookParseException("Книга недоступна для чтения. Возможно, нужна авторизация."); + if (chapterLink == null) + { + throw new Exception("Книга недоступна для чтения. Возможно, нужна авторизация."); + } var startUrl = url.MakeRelativeUri(chapterLink.Attributes["href"].Value); var book = new Book(url) { Cover = await GetCover(doc, url), Chapters = await FillChapters(startUrl), - Title = doc.GetTextBySelector("h1.names > span.name"), + Title = doc.GetTextBySelector("h1.cr-hero-names__main"), Author = GetAuthor(doc, url), Annotation = doc.QuerySelector("div.manga-description > div")?.InnerHtml }; @@ -97,15 +111,16 @@ public override async Task Get(Uri url) private Author GetAuthor(HtmlDocument doc, Uri bookUrl) { var a = doc.QuerySelector("span.elem_author a"); - return a is null - ? new Author("Librebook") - : new Author(a.GetText(), bookUrl.MakeRelativeUri(a.Attributes["href"].Value)); + return a == null ? new Author("Librebook") : new Author(a.GetText(), bookUrl.MakeRelativeUri(a.Attributes["href"].Value)); } private async Task> FillChapters(Uri startUrl) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in await GetToc(startUrl)) { @@ -117,7 +132,7 @@ private async Task> FillChapters(Uri startUrl) var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -134,8 +149,7 @@ private async Task> GetToc(Uri startUrl) var doc = await Config.Client.GetHtmlDocWithTriesAsync(startUrl); var result = doc .QuerySelectorAll("#chapters-list a.chapter-link") - .Select(a => - new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) + .Select(a => new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) .ToList(); return SliceToc(result, c => c.Title); @@ -144,14 +158,16 @@ private async Task> GetToc(Uri startUrl) private async Task GetChapter(UrlChapter urlChapter) { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); - return doc.QuerySelector("div.b-chapter").InnerHtml.AsHtmlDoc(); + var chapter = doc.QuerySelector("div.b-chapter"); + if (chapter == null) { + return null; + } + return chapter.InnerHtml.AsHtmlDoc(); } private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("img.fotorama__img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Librebook/Types/LibrebookAuthResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Librebook/Types/LibrebookAuthResponse.cs new file mode 100644 index 00000000..beae6257 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Librebook/Types/LibrebookAuthResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Librebook.Types; + +internal class LibrebookAuthResponse +{ + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("text")] + public string Text { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Libst/Elib2Ebook.ExternalServices.Libst.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Libst/Elib2Ebook.ExternalServices.Libst.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Libst/Elib2Ebook.ExternalServices.Libst.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/LibstGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Libst/LibstGetter.cs similarity index 68% rename from Core/Logic/Getters/LibstGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Libst/LibstGetter.cs index aefcf339..5bcb0c5b 100644 --- a/Core/Logic/Getters/LibstGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Libst/LibstGetter.cs @@ -1,30 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Libst; public class LibstGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://libst.ru/"); protected override string GetId(Uri url) - { - return url.GetQueryParameter("BookID") ?? base.GetId(url); - } + => url.GetQueryParameter("BookID") ?? base.GetId(url); public override async Task Get(Uri url) { @@ -34,10 +27,7 @@ public override async Task Get(Uri url) var book = new Book(url) { - Cover = await GetCover(doc, url), - Chapters = await FillChapters(doc), - Title = doc.GetTextBySelector("h1"), - Author = GetAuthor(doc, url) + Cover = await GetCover(doc, url), Chapters = await FillChapters(doc), Title = doc.GetTextBySelector("h1"), Author = GetAuthor(doc, url), }; return book; @@ -45,27 +35,31 @@ public override async Task Get(Uri url) public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl); var token = doc.QuerySelector("input[name=__RequestVerificationToken]").Attributes["value"].Value; - doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/Account/Login?ReturnUrl=/"), - GenerateAuthData(token)); + doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/Account/Login?ReturnUrl=/"), GenerateAuthData(token)); var error = doc.GetTextBySelector("div.validation-summary-errors"); if (string.IsNullOrEmpty(error)) + { Config.Logger.LogInformation("Авторизация прошла успешно"); + } else - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {error}"); + { + throw new Exception($"Не удалось авторизоваться. {error}"); + } } private FormUrlEncodedContent GenerateAuthData(string token) { var data = new Dictionary { - ["__RequestVerificationToken"] = token, - ["username"] = Config.Options.Login, - ["password"] = Config.Options.Password + ["__RequestVerificationToken"] = token, ["username"] = Config.Options.Login, ["password"] = Config.Options.Password }; return new FormUrlEncodedContent(data); @@ -74,7 +68,10 @@ private FormUrlEncodedContent GenerateAuthData(string token) private async Task> FillChapters(HtmlDocument doc) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var bookChapter in GetToc(doc)) { @@ -86,7 +83,10 @@ private async Task> FillChapters(HtmlDocument doc) }; var chapterDoc = await GetChapter(bookChapter); - if (chapterDoc is not null) chapter.Content = chapterDoc.DocumentNode.InnerHtml; + if (chapterDoc != null) + { + chapter.Content = chapterDoc.DocumentNode.InnerHtml; + } result.Add(chapter); } @@ -96,15 +96,20 @@ private async Task> FillChapters(HtmlDocument doc) private async Task GetChapter(IdChapter bookChapter) { - if (string.IsNullOrWhiteSpace(bookChapter.Id)) return default; + if (string.IsNullOrWhiteSpace(bookChapter.Id)) + { + return null; + } var sb = new StringBuilder(); for (var i = 1;; i++) { - var response = await Config.Client.GetAsync( - SystemUrl.MakeRelativeUri($"/Detail/GetChapterTextForCanvas?ChapID={bookChapter.Id}&k=0&page={i}")); - if (response.StatusCode != HttpStatusCode.OK) break; + var response = await Config.Client.GetAsync(SystemUrl.MakeRelativeUri($"/Detail/GetChapterTextForCanvas?ChapID={bookChapter.Id}&k=0&page={i}")); + if (response.StatusCode != HttpStatusCode.OK) + { + break; + } var data = await response.Content.ReadAsStringAsync().ContinueWith(t => t.Result.Deserialize()); sb.AppendJoin(string.Empty, data.Select(r => r.Trim().CoverTag("p"))); @@ -137,9 +142,7 @@ private IEnumerable GetToc(HtmlDocument doc) private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("img.coverBook")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private static Author GetAuthor(HtmlDocument doc, Uri uri) diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.LitLife/Elib2Ebook.ExternalServices.LitLife.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.LitLife/Elib2Ebook.ExternalServices.LitLife.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LitLife/Elib2Ebook.ExternalServices.LitLife.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/LitLifeGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.LitLife/LitLifeGetter.cs similarity index 71% rename from Core/Logic/Getters/LitLifeGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.LitLife/LitLifeGetter.cs index 18271b25..03689f59 100644 --- a/Core/Logic/Getters/LitLifeGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.LitLife/LitLifeGetter.cs @@ -1,19 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.LitLife; public class LitLifeGetter(BookGetterConfig config) : GetterBase(config) { @@ -26,28 +21,42 @@ protected override string GetId(Uri url) public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl); var token = doc.QuerySelector("#login_form input[name=_token]").Attributes["value"].Value; - doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/login"), - GenerateAuthData(token)); + doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/login"), GenerateAuthData(token)); var alert = doc.QuerySelector("div.alert-danger"); - if (alert is null) + if (alert == null) + { Config.Logger.LogInformation("Успешно авторизовались"); + } else - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {alert.GetText()}"); + { + throw new Exception($"Не удалось авторизоваться. {alert.GetText()}"); + } } private FormUrlEncodedContent GenerateAuthData(string token) { var payload = new Dictionary { - { "_token", token }, - { "login", Config.Options.Login }, - { "login_password", Config.Options.Password }, - { "remember", "on" } + { + "_token", token + }, + { + "login", Config.Options.Login + }, + { + "login_password", Config.Options.Password + }, + { + "remember", "on" + }, }; return new FormUrlEncodedContent(payload); @@ -60,7 +69,10 @@ public override async Task Get(Uri url) var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var noAccess = doc.QuerySelector("p.no_access_text"); - if (noAccess is not null) throw new Elib2EbookParseException("Книна не доступна для чтения."); + if (noAccess != null) + { + throw new Exception("Книна не доступна для чтения."); + } var book = new Book(url) { @@ -78,14 +90,15 @@ public override async Task Get(Uri url) private Seria GetSeria(HtmlDocument doc) { var a = doc.QuerySelector("div.card-body a.sequence"); - if (a is not null) + if (a != null) + { return new Seria { - Name = a.GetText(), - Url = SystemUrl.MakeRelativeUri(a.Attributes["href"].Value) + Name = a.GetText(), Url = SystemUrl.MakeRelativeUri(a.Attributes["href"].Value) }; + } - return default; + return null; } private Author GetAuthor(HtmlDocument doc) @@ -96,12 +109,10 @@ private Author GetAuthor(HtmlDocument doc) private async Task> GetToc(string id) { - var doc = await Config.Client.GetHtmlDocWithTriesAsync( - SystemUrl.MakeRelativeUri($"/books/{id}/sections/list_go_to")); + var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/books/{id}/sections/list_go_to")); var result = doc.QuerySelectorAll("ul a") - .Select(a => new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), - a.RemoveNodes("span").GetText().ReplaceNewLine())) + .Select(a => new UrlChapter(SystemUrl.MakeRelativeUri(a.Attributes["href"].Value), a.RemoveNodes("span").GetText().ReplaceNewLine())) .ToList(); return SliceToc(result, c => c.Title); @@ -110,7 +121,10 @@ private async Task> GetToc(string id) private async Task> FillChapters(string id) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in await GetToc(id)) { @@ -121,7 +135,7 @@ private async Task> FillChapters(string id) Title = urlChapter.Title }; - if (urlChapter.Url is not null) + if (urlChapter.Url != null) { var chapterDoc = await GetChapter(urlChapter); chapter.Images = await GetImages(chapterDoc, urlChapter.Url); @@ -147,11 +161,13 @@ private async Task GetChapter(UrlChapter urlChapter) .ToList(); if (pages.Count > 0) + { for (var i = 2; i <= pages.Max(); i++) { doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url.AppendQueryParameter("page", i)); result.Append(doc.QuerySelector("div.book_text").RemoveNodes("#read_online_ad, h4").InnerHtml); } + } return result.AsHtmlDoc(); } @@ -159,8 +175,6 @@ private async Task GetChapter(UrlChapter urlChapter) private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("div.cover img")?.Attributes["data-src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/Elib2Ebook.ExternalServices.Litgorod.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/Elib2Ebook.ExternalServices.Litgorod.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/Elib2Ebook.ExternalServices.Litgorod.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/LitgorodGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/LitgorodGetter.cs similarity index 74% rename from Core/Logic/Getters/LitgorodGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/LitgorodGetter.cs index edc18ae2..9b1249d0 100644 --- a/Core/Logic/Getters/LitgorodGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/LitgorodGetter.cs @@ -1,21 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http.Json; using System.Text; -using System.Threading.Tasks; using System.Web; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Litgorod; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Litgorod.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Litgorod; public class LitgorodGetter(BookGetterConfig config) : GetterBase(config) { @@ -48,11 +44,16 @@ public override async Task Init() var csrf = doc.QuerySelector("[name=csrf-token]")?.Attributes["content"]?.Value; if (string.IsNullOrWhiteSpace(csrf)) + { throw new ArgumentException("Не удалось получить csrf-token", nameof(csrf)); + } var cookies = response.Headers.SingleOrDefault(header => header.Key == "Set-Cookie").Value; var xsrfCookie = cookies.FirstOrDefault(c => c.StartsWith("XSRF-TOKEN=")); - if (xsrfCookie == null) throw new ArgumentException("Не удалось получить XSRF-TOKEN", nameof(xsrfCookie)); + if (xsrfCookie == null) + { + throw new ArgumentException("Не удалось получить XSRF-TOKEN", nameof(xsrfCookie)); + } var xsrf = HttpUtility.UrlDecode(xsrfCookie.Split(";")[0].Replace("XSRF-TOKEN=", "")); @@ -64,20 +65,26 @@ public override async Task Init() public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var payload = new { - email = Config.Options.Login, - password = Config.Options.Password + email = Config.Options.Login, password = Config.Options.Password }; var response = await Config.Client.PostAsJsonAsync(SystemUrl.MakeRelativeUri("login"), payload); var data = await response.Content.ReadFromJsonAsync(); if (string.IsNullOrWhiteSpace(data?.Message)) + { Config.Logger.LogInformation("Успешно авторизовались"); + } else - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {data.Message}"); + { + throw new Exception($"Не удалось авторизоваться. {data.Message}"); + } } private static Author GetAuthor(HtmlDocument doc, Uri url) @@ -88,27 +95,30 @@ private static Author GetAuthor(HtmlDocument doc, Uri url) private static IEnumerable GetCoAuthors(HtmlDocument doc, Uri url) { - return doc.QuerySelectorAll("div.b-book_item__author a").Skip(1) - .Select(a => new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value))).ToList(); + return doc.QuerySelectorAll("div.b-book_item__author a").Skip(1).Select(a => new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value))).ToList(); } private static Seria GetSeria(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("div.b-book_item__cycle a"); - if (a is not null) + if (a != null) + { return new Seria { - Name = a.GetText(), - Url = url.MakeRelativeUri(a.Attributes["href"].Value) + Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; + } - return default; + return null; } private async Task> FillChapters(HtmlDocument doc, Uri uri) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var bookChapter in GetToc(doc, uri)) { @@ -120,7 +130,7 @@ private async Task> FillChapters(HtmlDocument doc, Uri uri) Config.Logger.LogInformation($"Загружаю главу {bookChapter.Title.CoverQuotes()}"); var chapterDoc = await GetChapter(bookChapter.Url); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, uri); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -135,18 +145,26 @@ private async Task> FillChapters(HtmlDocument doc, Uri uri) private async Task GetChapter(Uri url) { var response = await Config.Client.GetWithTriesAsync(url); - if (response is null) return default; + if (response == null) + { + return null; + } var doc = await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc()); var content = doc.QuerySelector("book-reader"); - if (content is null) return default; + if (content == null) + { + return null; + } var sb = new StringBuilder(); var chapterContent = doc.QuerySelector("book-reader").Attributes[":current_chapter"].Value; foreach (var paragraph in HttpUtility.HtmlDecode(chapterContent).Deserialize().Paragraphs) + { sb.Append(paragraph.HtmlDecode()); + } return sb.AsHtmlDoc(); } @@ -155,8 +173,7 @@ private IEnumerable GetToc(HtmlDocument doc, Uri url) { var result = doc .QuerySelectorAll("div.b-tab__content ul.list-unstyled a") - .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), - string.IsNullOrWhiteSpace(a.GetText()) ? "Без названия" : a.GetText())) + .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), string.IsNullOrWhiteSpace(a.GetText()) ? "Без названия" : a.GetText())) .ToList(); return SliceToc(result, c => c.Title); @@ -164,10 +181,7 @@ private IEnumerable GetToc(HtmlDocument doc, Uri url) private Task GetCover(HtmlDocument doc, Uri uri) { - var imagePath = doc.QuerySelector("div.b-book_cover a[href]")?.Attributes["href"]?.Value ?? - doc.QuerySelector("div.b-book_cover img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + var imagePath = doc.QuerySelector("div.b-book_cover a[href]")?.Attributes["href"]?.Value ?? doc.QuerySelector("div.b-book_cover img")?.Attributes["src"]?.Value; + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/Types/LitgorodAuthResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/Types/LitgorodAuthResponse.cs new file mode 100644 index 00000000..30cfe3fe --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/Types/LitgorodAuthResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litgorod.Types; + +internal class LitgorodAuthResponse +{ + [JsonPropertyName("message")] + public string Message { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/Types/LitgorodBookPart.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/Types/LitgorodBookPart.cs new file mode 100644 index 00000000..e62b48e6 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/Types/LitgorodBookPart.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litgorod.Types; + +internal class LitgorodBookPart +{ + [JsonPropertyName("text")] + public string Text { get; set; } +} diff --git a/Core/Types/Litgorod/LitgorodChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/Types/LitgorodChapter.cs similarity index 61% rename from Core/Types/Litgorod/LitgorodChapter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/Types/LitgorodChapter.cs index 18cedd46..089eda7f 100644 --- a/Core/Types/Litgorod/LitgorodChapter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litgorod/Types/LitgorodChapter.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litgorod; +namespace Elib2Ebook.ExternalServices.Litgorod.Types; -public class LitgorodChapter +internal class LitgorodChapter { [JsonPropertyName("explodedParagraphs")] public string[] Paragraphs { get; set; } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Elib2Ebook.ExternalServices.Litmarket.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Elib2Ebook.ExternalServices.Litmarket.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Elib2Ebook.ExternalServices.Litmarket.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/LitmarketGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/LitmarketGetter.cs similarity index 71% rename from Core/Logic/Getters/LitmarketGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/LitmarketGetter.cs index a18dad01..2bc4b961 100644 --- a/Core/Logic/Getters/LitmarketGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/LitmarketGetter.cs @@ -1,21 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http.Json; using System.Text; -using System.Threading.Tasks; using System.Web; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Litmarket; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Litmarket.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Litmarket; public class LitmarketGetter(BookGetterConfig config) : GetterBase(config) { @@ -32,11 +28,16 @@ public override async Task Init() var csrf = doc.QuerySelector("[name=csrf-token]")?.Attributes["content"]?.Value; if (string.IsNullOrWhiteSpace(csrf)) + { throw new ArgumentException("Не удалось получить csrf-token", nameof(csrf)); + } var cookies = response.Headers.SingleOrDefault(header => header.Key == "Set-Cookie").Value; var xsrfCookie = cookies.FirstOrDefault(c => c.StartsWith("XSRF-TOKEN=")); - if (xsrfCookie == null) throw new ArgumentException("Не удалось получить XSRF-TOKEN", nameof(xsrfCookie)); + if (xsrfCookie == null) + { + throw new ArgumentException("Не удалось получить XSRF-TOKEN", nameof(xsrfCookie)); + } var xsrf = HttpUtility.UrlDecode(xsrfCookie.Split(";")[0].Replace("XSRF-TOKEN=", "")); @@ -71,68 +72,72 @@ public override async Task Get(Uri url) private static Seria GetSeria(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("div.card-cycle a"); - if (a is null || !a.GetText().Contains('#')) return default; + if (a == null || !a.GetText().Contains('#')) + { + return null; + } var parts = a.GetText().Split('#', StringSplitOptions.RemoveEmptyEntries); return new Seria { - Name = parts[0].HtmlDecode(), - Number = parts[1].HtmlDecode(), - Url = url.MakeRelativeUri(a.Attributes["href"].Value) + Name = parts[0].HtmlDecode(), Number = parts[1].HtmlDecode(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; } private Author GetAuthor(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("div.card-author a"); - return a is null - ? new Author(doc.GetTextBySelector("div.card-author").Replace("Автор:", "").ReplaceNewLine()) - : new Author(a.GetText().ReplaceNewLine(), - url.MakeRelativeUri(a.Attributes["href"].Value).ReplaceHost(SystemUrl.Host)); + return a == null ? + new Author(doc.GetTextBySelector("div.card-author").Replace("Автор:", "").ReplaceNewLine()) : + new Author(a.GetText().ReplaceNewLine(), url.MakeRelativeUri(a.Attributes["href"].Value).ReplaceHost(SystemUrl.Host)); } /// - /// Авторизация в системе + /// Авторизация в системе /// /// public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var payload = new { - email = Config.Options.Login, - password = Config.Options.Password + email = Config.Options.Login, password = Config.Options.Password }; using var post = await Config.Client.PostAsJsonAsync(SystemUrl.MakeRelativeUri("/auth/login"), payload); try { var data = await post.Content.ReadFromJsonAsync(); - if (data is { Success: false }) throw new Elib2EbookAuthException("Не удалось авторизоваться"); + if (data is { Success: false }) + { + throw new Exception("Не удалось авторизоваться"); + } } catch { - throw new Elib2EbookAuthException("Не удалось авторизоваться"); + throw new Exception("Не удалось авторизоваться"); } } private Task GetCover(HtmlDocument doc, Uri uri) { - var imagePath = doc.QuerySelector("div.front img")?.Attributes["data-src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath.AsUri().AbsolutePath)) - : Task.FromResult(default(TempFile)); + var imagePath = doc.QuerySelector("img.img.bookCardSkeleton")?.Attributes["src"]?.Value; + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private List GetToc(Response response, string title) { var toc = response.Toc.Deserialize>(); if (toc?.Count == 0) + { toc = [ - new Block + new() { Index = 0, Chunk = new Chunk @@ -147,6 +152,7 @@ private List GetToc(Response response, string title) } } ]; + } return SliceToc(toc, c => c.Chunk.Mods[0].Text).ToList(); } @@ -154,37 +160,43 @@ private List GetToc(Response response, string title) private async Task> FillChapters(List toc, Block[] blocks, Uri bookUri, long eBookId) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } for (var i = 0; i < toc.Count; i++) { - var chapterTitle = string.IsNullOrWhiteSpace(toc[i].Chunk.Mods[0].Text.Trim()) - ? "Без названия" - : toc[i].Chunk.Mods[0].Text.Trim(); + var chapterTitle = string.IsNullOrWhiteSpace(toc[i].Chunk.Mods[0].Text.Trim()) ? "Без названия" : toc[i].Chunk.Mods[0].Text.Trim(); Config.Logger.LogInformation($"Загружаю главу {chapterTitle.CoverQuotes()}"); var sb = new StringBuilder(); var chapter = new Chapter(); - foreach (var block in blocks.Where(b => - b.Index >= toc[i].Index && (i == toc.Count - 1 || b.Index < toc[i + 1].Index))) + foreach (var block in blocks.Where(b => b.Index >= toc[i].Index && (i == toc.Count - 1 || b.Index < toc[i + 1].Index))) { var p = new StringBuilder(); foreach (var mod in block.Chunk.Mods) + { switch (mod.Type) { case "IMAGE": - p.Append( - $""); + p.Append($""); break; case "LINK": - p.Append( - $"{mod.Mods?.FirstOrDefault()?.Text ?? string.Empty}"); + p.Append($"{mod.Mods?.FirstOrDefault()?.Text ?? string.Empty}"); break; default: var text = $"{mod.Text ?? string.Empty}"; + if (sb.Length == 0 && text == chapterTitle) + { + continue; + } + if (mod.Styles?.Length > 0) + { foreach (var style in mod.Styles) + { switch (style) { case "ITALIC": @@ -200,10 +212,13 @@ private async Task> FillChapters(List toc, Block[] blocks, Config.Logger.LogInformation(style); break; } + } + } p.Append(text); break; } + } sb.Append(p.ToString().CoverTag("p")); } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/AuthResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/AuthResponse.cs new file mode 100644 index 00000000..44a93df2 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/AuthResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litmarket.Types; + +internal class AuthResponse +{ + [JsonPropertyName("success")] + public bool Success { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/Block.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/Block.cs new file mode 100644 index 00000000..ea7d5810 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/Block.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litmarket.Types; + +internal class Block +{ + [JsonPropertyName("chunk")] + public Chunk Chunk { get; set; } + + [JsonPropertyName("index")] + public int Index { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/Chunk.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/Chunk.cs new file mode 100644 index 00000000..3c551fc4 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/Chunk.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litmarket.Types; + +internal class Chunk +{ + [JsonPropertyName("mods")] + public Mod[] Mods { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/LBook.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/LBook.cs new file mode 100644 index 00000000..c6d4cd6a --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/LBook.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litmarket.Types; + +internal class LBook +{ + [JsonPropertyName("ebookId")] + public int EbookId { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/Mod.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/Mod.cs new file mode 100644 index 00000000..7ea8726f --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/Mod.cs @@ -0,0 +1,22 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litmarket.Types; + +internal class Mod +{ + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("styles")] + public string[] Styles { get; set; } + + [JsonPropertyName("text")] + public string Text { get; set; } + + [JsonPropertyName("data")] + public JsonElement Data { get; set; } + + [JsonPropertyName("mods")] + public Mod[] Mods { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/Response.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/Response.cs new file mode 100644 index 00000000..246b386e --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmarket/Types/Response.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litmarket.Types; + +internal class Response +{ + [JsonPropertyName("book")] + public LBook Book { get; set; } + + [JsonPropertyName("tableOfContent")] + public string Toc { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litmir/Elib2Ebook.ExternalServices.Litmir.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmir/Elib2Ebook.ExternalServices.Litmir.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmir/Elib2Ebook.ExternalServices.Litmir.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/LitmirGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmir/LitmirGetter.cs similarity index 82% rename from Core/Logic/Getters/LitmirGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Litmir/LitmirGetter.cs index 88bbb3b4..ac9ab911 100644 --- a/Core/Logic/Getters/LitmirGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litmir/LitmirGetter.cs @@ -1,26 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Litmir; public class LitmirGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://litmir.me/"); protected override string GetId(Uri url) - { - return url.GetQueryParameter("b"); - } + => url.GetQueryParameter("b"); public override async Task Get(Uri url) { @@ -37,8 +32,7 @@ public override async Task Get(Uri url) Cover = await GetCover(doc, url), Chapters = await FillChapters(bookId, pages, name), Title = name, - Author = new Author(doc.QuerySelector("span[itemprop=author] meta")?.Attributes["content"]?.Value ?? - "Litmir"), + Author = new Author(doc.QuerySelector("span[itemprop=author] meta")?.Attributes["content"]?.Value ?? "Litmir"), Annotation = doc.QuerySelector("div[itemprop=description]")?.InnerHtml, Seria = GetSeria(doc, url) }; @@ -49,7 +43,7 @@ public override async Task Get(Uri url) private static Seria GetSeria(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("td.bd_desc2 a[href^=/books_in_series/]"); - if (a is not null) + if (a != null) { var text = a.GetText(); if (!string.IsNullOrWhiteSpace(text)) @@ -81,20 +75,21 @@ private static Seria GetSeria(HtmlDocument doc, Uri url) } } - return default; + return null; } private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("img[jq=BookCover]")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) { - if (chapter == null) return; + if (chapter == null) + { + return; + } var chapterDoc = text.AsHtmlDoc(); chapter.Images = await GetImages(chapterDoc, SystemUrl.MakeRelativeUri("/br/")); @@ -123,12 +118,17 @@ private static Chapter CreateChapter(string title) private async Task GetChapter(string bookId, int page) { - using var response = - await Config.Client.GetWithTriesAsync(SystemUrl.MakeRelativeUri($"/br/?b={bookId}&p={page}")); - if (response is null) return default; + using var response = await Config.Client.GetWithTriesAsync(SystemUrl.MakeRelativeUri($"/br/?b={bookId}&p={page}")); + if (response == null) + { + return null; + } var doc = await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc()); - foreach (var node in doc.QuerySelectorAll("p")) node.Attributes.RemoveAll(); + foreach (var node in doc.QuerySelectorAll("p")) + { + node.Attributes.RemoveAll(); + } return doc; } @@ -136,7 +136,10 @@ private async Task GetChapter(string bookId, int page) private async Task> FillChapters(string bookId, long pages, string name) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } Chapter chapter = null; var singleChapter = true; @@ -151,6 +154,7 @@ private async Task> FillChapters(string bookId, long pages, string singleChapter = i == 1 ? IsSingleChapter(nodes) : singleChapter; foreach (var node in nodes) + { if (singleChapter || !IsChapterHeader(node)) { if (node.Name == "img" && node.Attributes["src"] != null) @@ -160,7 +164,9 @@ private async Task> FillChapters(string bookId, long pages, string else { if (!string.IsNullOrWhiteSpace(node.InnerHtml)) + { text.Append(node.InnerHtml.HtmlDecode().CoverTag("p")); + } } } else @@ -169,6 +175,7 @@ private async Task> FillChapters(string bookId, long pages, string text.Clear(); chapter = CreateChapter(node.InnerText.HtmlDecode()); } + } } await AddChapter(result, chapter ?? CreateChapter(name), text); diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Elib2Ebook.ExternalServices.Litnet.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Elib2Ebook.ExternalServices.Litnet.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Elib2Ebook.ExternalServices.Litnet.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/Litnet/BooknetComGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Getters/BooknetComGetter.cs similarity index 62% rename from Core/Logic/Getters/Litnet/BooknetComGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Getters/BooknetComGetter.cs index bc6b402f..4813fb3f 100644 --- a/Core/Logic/Getters/Litnet/BooknetComGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Getters/BooknetComGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.Litnet; +namespace Elib2Ebook.ExternalServices.Litnet.Getters; public class BooknetComGetter(BookGetterConfig config) : LitnetGetterBase(config) { diff --git a/Core/Logic/Getters/Litnet/BooknetUaGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Getters/BooknetUaGetter.cs similarity index 62% rename from Core/Logic/Getters/Litnet/BooknetUaGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Getters/BooknetUaGetter.cs index a1663c69..b0885c7b 100644 --- a/Core/Logic/Getters/Litnet/BooknetUaGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Getters/BooknetUaGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.Litnet; +namespace Elib2Ebook.ExternalServices.Litnet.Getters; public class BooknetUaGetter(BookGetterConfig config) : LitnetGetterBase(config) { diff --git a/Core/Logic/Getters/Litnet/LitnetGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Getters/LitnetGetter.cs similarity index 61% rename from Core/Logic/Getters/Litnet/LitnetGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Getters/LitnetGetter.cs index e6ff292b..ef25ba94 100644 --- a/Core/Logic/Getters/Litnet/LitnetGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Getters/LitnetGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.Litnet; +namespace Elib2Ebook.ExternalServices.Litnet.Getters; public class LitnetGetter(BookGetterConfig config) : LitnetGetterBase(config) { diff --git a/Core/Logic/Getters/Litnet/LitnetGetterBase.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Getters/LitnetGetterBase.cs similarity index 60% rename from Core/Logic/Getters/Litnet/LitnetGetterBase.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Getters/LitnetGetterBase.cs index b6f161f9..ec14a93f 100644 --- a/Core/Logic/Getters/Litnet/LitnetGetterBase.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Getters/LitnetGetterBase.cs @@ -1,32 +1,22 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Http.Json; using System.Security.Cryptography; using System.Text; -using System.Text.Json; -using System.Threading.Tasks; using System.Web; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Litnet; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Litnet.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.Litnet; +namespace Elib2Ebook.ExternalServices.Litnet.Getters; public abstract class LitnetGetterBase(BookGetterConfig config) : GetterBase(config) { - private const string SECRET = AppSecrets.LitnetSecret; - - private string DeviceId = Guid.NewGuid().ToString().ToUpperInvariant(); private Uri ApiUrl => new($"https://api.{SystemUrl.Host}/"); //cloudflare :( @@ -34,17 +24,18 @@ public abstract class LitnetGetterBase(BookGetterConfig config) : GetterBase(con protected virtual Uri SiteIp => SystemUrl; + private static readonly string DeviceId = Guid.NewGuid().ToString().ToUpper(); + private const string SECRET = "14a6579a984b3c6abecda6c2dfa83a64"; + private string _token { get; set; } protected override string GetId(Uri url) - { - return base.GetId(url).Split('-').Last().Replace("b", string.Empty); - } + => base.GetId(url).Split('-').Last().Replace("b", string.Empty); private static byte[] Decrypt(string text) { using var aes = Aes.Create(); - const int IV_SHIFT = AppSecrets.AesIvShift; + const int IV_SHIFT = 16; aes.Key = Encoding.UTF8.GetBytes(SECRET); aes.IV = Encoding.UTF8.GetBytes(text)[..IV_SHIFT]; @@ -59,58 +50,25 @@ private static byte[] Decrypt(string text) return output.ToArray()[IV_SHIFT..]; } - private string GetSign(string token) + private static string GetSign(string token) { var inputBytes = Encoding.ASCII.GetBytes(DeviceId + SECRET + (token ?? string.Empty)); var hashBytes = MD5.HashData(inputBytes); - return Convert.ToHexString(hashBytes).ToLowerInvariant(); + return Convert.ToHexString(hashBytes).ToLower(); } /// - /// Авторизация в системе + /// Авторизация в системе /// /// public override async Task Authorize() { var path = Config.HasCredentials ? "user/find-by-login" : "registration/registration-by-device"; - var saveCreds = string.Empty; - var useAuthCache = Config.HasCredentials; - if (useAuthCache) - { - const string directory = "LNCache"; - if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); - - var cacheKey = Config.Options.Login?.RemoveInvalidChars(); - saveCreds = !string.IsNullOrWhiteSpace(cacheKey) ? $"{directory}/{cacheKey}" : string.Empty; - useAuthCache = !string.IsNullOrWhiteSpace(saveCreds); - - if (useAuthCache && File.Exists(saveCreds)) - { - var timeDiff = DateTime.Now - File.GetLastWriteTime(saveCreds); - if (timeDiff.TotalHours < 2) - { - var savedAuth = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => - t.Result.Deserialize()); - _token = savedAuth.Token; - DeviceId = savedAuth.DeviceID; - - if (!string.IsNullOrWhiteSpace(_token)) - { - Config.Logger.LogInformation("Litnet: auth cache hit"); - return; - } - } - else - { - File.Delete(saveCreds); - } - } - } - var url = ApiIp.MakeRelativeUri( $"v1/{path}?login={HttpUtility.UrlEncode(Config.Options.Login?.TrimStart('+') ?? string.Empty)}&password={HttpUtility.UrlEncode(Config.Options.Password)}&app=android&device_id={DeviceId}&sign={GetSign(string.Empty)}"); + var data = await GetApiData(url); if (!string.IsNullOrWhiteSpace(data?.Token)) @@ -120,41 +78,32 @@ public override async Task Authorize() } else { - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {data?.Error}"); - } - - if (useAuthCache) - { - if (data is not null) data.DeviceID = DeviceId; - - await File.WriteAllTextAsync(saveCreds, JsonSerializer.Serialize(data)); + throw new Exception($"Не удалось авторизоваться. {data?.Error}"); } } private async Task GetBook(string token, string bookId) { - var url = ApiIp.MakeRelativeUri( - $"/v1/book/get/{bookId}?app=android&device_id={DeviceId}&user_token={token}&sign={GetSign(token)}"); + var url = ApiIp.MakeRelativeUri($"/v1/book/get/{bookId}?app=android&device_id={DeviceId}&user_token={token}&sign={GetSign(token)}"); var data = await GetApiData(url); if (!Config.HasCredentials && data!.AdultOnly) - throw new Elib2EbookAuthException("Произведение 18+. Необходимо добавить логин и пароль."); + { + throw new Exception("Произведение 18+. Необходимо добавить логин и пароль."); + } return data; } private Task GetBookContents(string token, string bookId) { - var url = ApiIp.MakeRelativeUri( - $"/v1/book/contents?bookId={bookId}&app=android&device_id={DeviceId}&user_token={token}&sign={GetSign(token)}"); + var url = ApiIp.MakeRelativeUri($"/v1/book/contents?bookId={bookId}&app=android&device_id={DeviceId}&user_token={token}&sign={GetSign(token)}"); return GetApiData(url); } - private async Task> GetToc(string token, - IEnumerable contents) + private async Task> GetToc(string token, IEnumerable contents) { var chapters = string.Join("&", contents.Select(t => $"chapter_ids[]={t.Id}")); - var url = ApiIp.MakeRelativeUri( - $"/v1/book/get-chapters-texts/?{chapters}&app=android&device_id={DeviceId}&sign={GetSign(token)}&user_token={token}"); + var url = ApiIp.MakeRelativeUri($"/v1/book/get-chapters-texts/?{chapters}&app=android&device_id={DeviceId}&sign={GetSign(token)}&user_token={token}"); var data = await GetApiData(url); return SliceToc(data, c => c.Text); } @@ -163,9 +112,14 @@ private async Task GetApiData(Uri uri) { var response = await Config.Client.SendAsync(GetDefaultMessage(uri, ApiUrl)); if (response.StatusCode == HttpStatusCode.Forbidden) - throw new Elib2EbookParseException("Получен бан. Попробуйте позже."); + { + throw new Exception("Получен бан. Попробуйте позже."); + } - if (response.StatusCode == HttpStatusCode.NotFound) return default; + if (response.StatusCode == HttpStatusCode.NotFound) + { + return default; + } var data = await response.Content.ReadFromJsonAsync(); return data; @@ -197,71 +151,72 @@ private async Task GetSeria(Uri url, LitnetBookResponse book) { try { - var doc = await Config.Client.SendAsync(GetDefaultMessage(url.ReplaceHost(SiteIp.Host), SystemUrl)) - .ContinueWith(t => t.Result.Content.ReadAsStream().AsHtmlDoc()); + var doc = await Config.Client.SendAsync(GetDefaultMessage(url.ReplaceHost(SiteIp.Host), SystemUrl)).ContinueWith(t => t.Result.Content.ReadAsStream().AsHtmlDoc()); var a = doc.QuerySelector("div.book-view-info-coll a[href*='sort=cycles']"); - if (a is not null) + if (a != null) + { return new Seria { - Name = a.GetText(), - Url = url.MakeRelativeUri(a.Attributes["href"].Value), - Number = book.CyclePriority is > 0 ? book.CyclePriority.Value.ToString() : string.Empty + Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value), Number = book.CyclePriority is > 0 ? book.CyclePriority.Value.ToString() : string.Empty }; + } } catch (Exception ex) { Config.Logger.LogInformation(ex.ToString()); } - return default; + return null; } private Author GetAuthor(LitnetBookResponse book) { - return new Author((book.AuthorName ?? SystemUrl.Host).Trim(), - SystemUrl.MakeRelativeUri($"/ru/{book.AuthorId}")); + return new Author((book.AuthorName ?? SystemUrl.Host).Trim(), SystemUrl.MakeRelativeUri($"/ru/{book.AuthorId}")); } private IEnumerable GetCoAuthors(LitnetBookResponse book) { var result = new List(); if (!string.IsNullOrWhiteSpace(book.CoAuthorName) && book.CoAuthorId.HasValue) - result.Add(new Author((book.CoAuthorName ?? SystemUrl.Host).Trim(), - SystemUrl.MakeRelativeUri($"/ru/{book.CoAuthorId.Value}"))); + { + result.Add(new Author((book.CoAuthorName ?? SystemUrl.Host).Trim(), SystemUrl.MakeRelativeUri($"/ru/{book.CoAuthorId.Value}"))); + } return result; } private static string GetAnnotation(LitnetBookResponse book) { - return string.IsNullOrWhiteSpace(book.Annotation) - ? string.Empty - : string.Join("", - book.Annotation.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim().CoverTag("p"))); + return string.IsNullOrWhiteSpace(book.Annotation) ? + string.Empty : + string.Join("", book.Annotation.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim().CoverTag("p"))); } private Task GetCover(LitnetBookResponse book) { - return !string.IsNullOrWhiteSpace(book.Cover) - ? SaveImage(book.Cover.AsUri()) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(book.Cover) ? SaveImage(book.Cover.AsUri()) : Task.FromResult(default(TempFile)); } protected override HttpRequestMessage GetImageRequestMessage(Uri uri) { if (uri.IsSameHost(SystemUrl) || uri.IsSameSubDomain(SystemUrl)) + { return GetDefaultMessage(SystemUrl.MakeRelativeUri(uri.AbsolutePath), uri); + } return base.GetImageRequestMessage(uri); } private HttpRequestMessage GetDefaultMessage(Uri uri, Uri host, HttpContent content = null) { - var message = new HttpRequestMessage(content is null ? HttpMethod.Get : HttpMethod.Post, uri); + var message = new HttpRequestMessage(content == null ? HttpMethod.Get : HttpMethod.Post, uri); message.Content = content; message.Version = Config.Client.DefaultRequestVersion; - foreach (var header in Config.Client.DefaultRequestHeaders) message.Headers.Add(header.Key, header.Value); + foreach (var header in Config.Client.DefaultRequestHeaders) + { + message.Headers.Add(header.Key, header.Value); + } message.Headers.Host = host.Host; @@ -271,17 +226,26 @@ private HttpRequestMessage GetDefaultMessage(Uri uri, Uri host, HttpContent cont private async Task> FillChapters(string token, LitnetBookResponse book, string bookId) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } var contents = await GetBookContents(token, bookId); - if (contents is null || contents.Length == 0) return result; + if (contents == null || contents.Length == 0) + { + return result; + } var chapters = await GetToc(token, contents); var map = chapters.ToDictionary(t => t.Id); foreach (var content in contents) { - if (!map.TryGetValue(content.Id, out var litnetChapter)) litnetChapter = new LitnetChapterResponse(); + if (!map.TryGetValue(content.Id, out var litnetChapter)) + { + litnetChapter = new LitnetChapterResponse(); + } Config.Logger.LogInformation($"Загружаю главу {content.Title.Trim().CoverQuotes()}"); var chapter = new Chapter diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Types/LitnetAuthResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Types/LitnetAuthResponse.cs new file mode 100644 index 00000000..857f9039 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Types/LitnetAuthResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litnet.Types; + +internal class LitnetAuthResponse +{ + [JsonPropertyName("error")] + public string Error { get; set; } + + [JsonPropertyName("token")] + public string Token { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Types/LitnetBookResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Types/LitnetBookResponse.cs new file mode 100644 index 00000000..77eaf5eb --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Types/LitnetBookResponse.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litnet.Types; + +internal class LitnetBookResponse +{ + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("author_name")] + public string AuthorName { get; set; } + + [JsonPropertyName("cover")] + public string Cover { get; set; } + + [JsonPropertyName("annotation")] + public string Annotation { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + + [JsonPropertyName("author_id")] + public long AuthorId { get; set; } + + [JsonPropertyName("lang")] + public string Lang { get; set; } + + [JsonPropertyName("cycle_priority")] + public int? CyclePriority { get; set; } + + [JsonPropertyName("adult_only")] + public bool AdultOnly { get; set; } + + [JsonPropertyName("co_author_name")] + public string CoAuthorName { get; set; } + + [JsonPropertyName("co_author")] + public long? CoAuthorId { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Types/LitnetChapterResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Types/LitnetChapterResponse.cs new file mode 100644 index 00000000..851706f7 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Types/LitnetChapterResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litnet.Types; + +internal class LitnetChapterResponse +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("text")] + public string Text { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Types/LitnetContentsResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Types/LitnetContentsResponse.cs new file mode 100644 index 00000000..c18328bf --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litnet/Types/LitnetContentsResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litnet.Types; + +internal class LitnetContentsResponse +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Elib2Ebook.ExternalServices.Litres.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Elib2Ebook.ExternalServices.Litres.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Elib2Ebook.ExternalServices.Litres.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/LitresGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/LitresGetter.cs similarity index 53% rename from Core/Logic/Getters/LitresGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Litres/LitresGetter.cs index 448122bc..ef845b5c 100644 --- a/Core/Logic/Getters/LitresGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/LitresGetter.cs @@ -1,49 +1,32 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.IO.Compression; -using System.Linq; using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; using System.Net.Http.Json; using System.Security.Cryptography; using System.Text; using System.Text.Json; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Misc; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Litres; -using Core.Types.Litres.Response; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Litres.Types; +using Elib2Ebook.ExternalServices.Litres.Types.Requests; +using Elib2Ebook.ExternalServices.Litres.Types.Response; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Litres; public class LitresGetter(BookGetterConfig config) : GetterBase(config) { - private const string SECRET_KEY = AppSecrets.LitresSecretKey; + private const string SECRET_KEY = "AsAAfdV000-1kksn6591x:[}A{}<> new("https://www.litres.ru"); - protected static Uri ApiUrl => new("https://api.litres.ru"); private static Uri GetShortUri(LitresArt art) - { - return new Uri($"https://catalit.litres.ru/pub/t/{art.Id}.fb3"); - } + => new($"https://catalit.litres.ru/pub/t/{art.Id}.fb3"); private static long GetCurrentMilli() { @@ -52,6 +35,12 @@ private static long GetCurrentMilli() return (long)javaSpan.TotalMilliseconds / 1000; } + public override async Task Init() + { + await base.Init(); + Config.Client.DefaultRequestHeaders.Add("app-id", "1"); + } + private Uri GetFullUri(LitresArt art, string path, LitresFile file, string host = null) { var ts = GetCurrentMilli(); @@ -62,152 +51,109 @@ private Uri GetFullUri(LitresArt art, string path, LitresFile file, string host // Текстовая книга if (art.ArtType != LitresArtTypeEnum.Audio) { - var result = - new Uri( - $"https://{host ?? "catalit.litres.ru"}/pages/{path}?art={art.Id}&sid={_authData.Sid}&uilang=ru&libapp={APP}×tamp={ts}&md5={Convert.ToHexString(hashBytes).ToLowerInvariant()}"); - if (file is null) return result.AppendQueryParameter("type", "fb3"); + var result = new Uri( + $"https://{host ?? "catalit.litres.ru"}/pages/{path}?art={art.Id}&sid={_authData.Sid}&uilang=ru&libapp={APP}×tamp={ts}&md5={Convert.ToHexString(hashBytes).ToLower()}"); + + if (file == null) + { + return result.AppendQueryParameter("type", "fb3"); + } result = result.AppendQueryParameter("file", file.Id); if (!string.IsNullOrWhiteSpace(file.Extension)) + { result = result.AppendQueryParameter("type", file.Extension); + } return result; } // Аудиокнига return new Uri( - $"https://{host ?? "ios.litres.ru"}/pages/{path}/{art.Id}/{file.Id}.mp3?sid={_authData.Sid}&uilang=ru&libapp={APP}×tamp={ts}&md5={Convert.ToHexString(hashBytes).ToLowerInvariant()}"); + $"https://{host ?? "ios.litres.ru"}/pages/{path}/{art.Id}/{file.Id}.mp3?sid={_authData.Sid}&uilang=ru&libapp={APP}×tamp={ts}&md5={Convert.ToHexString(hashBytes).ToLower()}"); } + private LitresAuthResponseData _authData = new() + { + Sid = "6ufp4b2wbx1acc4k1f0wdo571762c0fn" + }; + + private LitresMe _me; + public override async Task Authorize() { - if (string.IsNullOrEmpty(Config.Options.Token) && !Config.HasCredentials) + if (!Config.HasCredentials) { Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); return; } - if (!string.IsNullOrEmpty(Config.Options.Token)) - { - Config.Logger.LogInformation("Подставлен токен"); - _authData.Sid = Config.Options.Token; - Config.Client.DefaultRequestHeaders.Add("Session-Id", Config.Options.Token); - return; - } - // Литрес очень не любит много авторизовывать // поэтому пришлось добавить кеширование токенов const string directory = "LitresCache"; - if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } var saveCreds = $"{directory}/{Config.Options.Login.RemoveInvalidChars()}"; if (File.Exists(saveCreds)) { - var timeDiff = DateTime.Now - File.GetLastWriteTime(saveCreds); - if (timeDiff.TotalHours < 4) - { - _authData = await File.ReadAllTextAsync(saveCreds) - .ContinueWith(t => t.Result.Deserialize()); - Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); - - var checkResponse = - await Config.Client.GetFromJsonAsync>( - ApiUrl.MakeRelativeUri("/foundation/api/users/me/detailed")); - if (checkResponse?.Payload?.Data is not null) - { - _me = checkResponse.Payload.Data; - return; - } - } - else + _authData = await File.ReadAllTextAsync(saveCreds).ContinueWith(t => t.Result.Deserialize()); + Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); + + var checkResponse = await Config.Client.GetFromJsonAsync>("https://api.litres.ru/foundation/api/users/me/detailed"); + if (checkResponse?.Payload?.Data != null) { - File.Delete(saveCreds); + _me = checkResponse.Payload.Data; + return; } } - var auth = new Dictionary + var payload = new LitresAuthData { - { "login", Config.Options.Login }, - { "password", Config.Options.Password } + Login = Config.Options.Login, + Password = Config.Options.Password, }; - var authPayload = new StringContent(JsonSerializer.Serialize(auth)); - authPayload.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - Config.Client.DefaultRequestHeaders.Remove("app-id"); - Config.Client.DefaultRequestHeaders.Add("app-id", "115"); - var authResponse = - await Config.Client.PostWithTriesAsync(ApiUrl.MakeRelativeUri("/foundation/api/auth/login"), authPayload); - Config.Client.DefaultRequestHeaders.Remove("app-id"); + var response = await Config.Client.PostWithTriesAsync("https://api.litres.ru/foundation/api/auth/login".AsUri(), JsonContent.Create(payload)); + var responseData = await response.Content.ReadFromJsonAsync>(); - _authData = await authResponse.Content.ReadAsStringAsync().ContinueWith(t => + if (!string.IsNullOrWhiteSpace(responseData.Error)) { - var authData = t.Result.Deserialize(); - var newAuthData = new LitresAuthResponseData(); - newAuthData.Success = authData.Status == 200; - newAuthData.Sid = authData.Payload.Data.Sid; - newAuthData.ErrorMessage = authData.Error; - - return newAuthData; - }); + throw new Exception($"Не удалось авторизоваться. {responseData.Error}"); + } - if (!_authData.Success) - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {_authData.ErrorMessage}"); + _authData = responseData.Payload.Data; Config.Client.DefaultRequestHeaders.Add("Session-Id", _authData.Sid); - var meResponse = - await Config.Client.GetFromJsonAsync>( - "https://api.litres.ru/foundation/api/users/me/detailed"); - if (meResponse?.Payload?.Data is null) return; + var meResponse = await Config.Client.GetFromJsonAsync>("https://api.litres.ru/foundation/api/users/me/detailed"); + if (meResponse?.Payload?.Data == null) + { + return; + } _me = meResponse.Payload.Data; await File.WriteAllTextAsync(saveCreds, JsonSerializer.Serialize(_authData)); } - private async Task GetResponse(LitresPayload payload) - { - var resp = await Config.Client.PostWithTriesAsync("https://catalit.litres.ru/catalitv2".AsUri(), - CreatePayload(payload)); - return await resp.Content.ReadAsStringAsync().ContinueWith(t => - { - return t.Result.Deserialize>().Data; - }); - } - private async Task GetResponse(Uri url) { var resp = await Config.Client.GetWithTriesAsync(url); - if (resp is null) return default; - - return await resp.Content.ReadAsStringAsync().ContinueWith(t => - { - return t.Result.Deserialize>().Payload.Data; - }); - } - - private static FormUrlEncodedContent CreatePayload(LitresPayload payload) - { - var d = new Dictionary + if (resp == null) { - ["jdata"] = JsonSerializer.Serialize(payload) - }; + return default; + } - return new FormUrlEncodedContent(d); + return await resp.Content.ReadAsStringAsync().ContinueWith(t => t.Result.Deserialize>().Payload.Data); } public override async Task Get(Uri url) { var bookId = GetBookId(url); - var art = new LitresArt(); - try - { - art = await GetResponse(ApiUrl.MakeRelativeUri($"/foundation/api/arts/{bookId}")); - } - catch (Exception) - { - Config.Logger.LogWarning("Litres: Не удалось получить метаданные книги из API."); - } + var art = await GetResponse($"https://api.litres.ru/foundation/api/arts/{bookId}".AsUri()); var book = new Book(SystemUrl.MakeRelativeUri(bookId)) { @@ -220,13 +166,19 @@ public override async Task Get(Uri url) }; await FillAdditional(book, art); - foreach (var linked in art.LinkedArts ?? []) await FillAdditional(book, linked); + foreach (var linked in art.LinkedArts ?? []) + { + await FillAdditional(book, linked); + } var fb3File = book.AdditionalFiles.Get(AdditionalTypeEnum.Books).FirstOrDefault(f => f.Extension == ".fb3"); - if (fb3File is null) + if (fb3File == null) + { Config.Logger.LogInformation("Нет файла fb3. Сформировать файл книги невозможно"); + } else + { try { await using var stream = fb3File.GetStream(); @@ -237,38 +189,35 @@ public override async Task Get(Uri url) { Config.Logger.LogInformation($"Не удалось обработать оригинальный файл {fb3File.FullName}"); } + } return book; } private async Task FillAdditional(Book book, LitresArt art) { - if (_authData is not null) + if (_authData != null) { var type = art.ArtType == LitresArtTypeEnum.Audio ? AdditionalTypeEnum.Audio : AdditionalTypeEnum.Books; if (Config.Options.HasAdditionalType(type)) { - var files = await GetResponse( - ApiUrl.MakeRelativeUri($"/foundation/api/arts/{art.Id}/files/grouped")); + var files = await GetResponse($"https://api.litres.ru/foundation/api/arts/{art.Id}/files/grouped".AsUri()); foreach (var file in files.SelectMany(f => f.Files)) { using var fileResponse = await GetFileResponse(art, file); - if (fileResponse is not null) + if (fileResponse != null) { var tempFile = await CreateTempFile(fileResponse); - book.AdditionalFiles.Add( - art.ArtType != LitresArtTypeEnum.Audio - ? AdditionalTypeEnum.Books - : AdditionalTypeEnum.Audio, tempFile); + book.AdditionalFiles.Add(art.ArtType != LitresArtTypeEnum.Audio ? AdditionalTypeEnum.Books : AdditionalTypeEnum.Audio, tempFile); } } } else if (art.ArtType != LitresArtTypeEnum.Audio) { using var fileResponse = await GetFileResponse(art, null); - if (fileResponse is not null) + if (fileResponse != null) { var tempFile = await CreateTempFile(fileResponse); book.AdditionalFiles.Add(AdditionalTypeEnum.Books, tempFile); @@ -276,31 +225,40 @@ private async Task FillAdditional(Book book, LitresArt art) } } - if (_authData is null || (book.AdditionalFiles.Get(AdditionalTypeEnum.Books).Count == 0 && - art.ArtType != LitresArtTypeEnum.Audio)) + if (_authData == null || (book.AdditionalFiles.Get(AdditionalTypeEnum.Books).Count == 0 && art.ArtType != LitresArtTypeEnum.Audio)) { var fileResponse = await GetShortBook(art); - if (fileResponse is not null) + if (fileResponse != null) + { book.AdditionalFiles.Add(AdditionalTypeEnum.Books, await CreateTempFile(fileResponse)); + } } } private async Task CreateTempFile(HttpResponseMessage response) { - return await TempFile.Create(response.RequestMessage.RequestUri, Config.TempFolder.Path, - response.Content.Headers.ContentDisposition?.FileName?.Trim('\"') ?? - response.RequestMessage.RequestUri.GetFileName(), await response.Content.ReadAsStreamAsync()); + return await TempFile.Create( + response.RequestMessage.RequestUri, + Config.TempFolder.Path, + response.Content.Headers.ContentDisposition?.FileName?.Trim('\"') ?? response.RequestMessage.RequestUri.GetFileName(), + await response.Content.ReadAsStreamAsync()); } private static string GetBookId(Uri url) { var art = url.GetQueryParameter("art"); - if (!string.IsNullOrWhiteSpace(art)) return art; + if (!string.IsNullOrWhiteSpace(art)) + { + return art; + } art = url.GetSegment(url.Segments.Length - 1).Split("-").Last(); - if (long.TryParse(art, out _)) return art; + if (long.TryParse(art, out _)) + { + return art; + } - throw new Elib2EbookParseException("Не удалось определить artId"); + throw new Exception("Не удалось определить artId"); } private Seria GetSeria(LitresArt art) @@ -309,21 +267,17 @@ private Seria GetSeria(LitresArt art) .Sequences .Select(s => new Seria { - Name = s.Name, - Url = SystemUrl.MakeRelativeUri("serii-knig/").AppendQueryParameter("id", s.Id), - Number = s.SequenceNumber + Name = s.Name, Url = SystemUrl.MakeRelativeUri("serii-knig/").AppendQueryParameter("id", s.Id), Number = s.SequenceNumber }).FirstOrDefault(); } private async Task GetAuthor(LitresArt art) { var person = art.Persons.FirstOrDefault(a => a.Role == "author"); - var author = person == null - ? default - : await GetResponse>(ApiUrl.MakeRelativeUri($"/foundation/api/persons/{person.Id}")); - return author is null - ? new Author("Litres") - : new Author(author.FullName, SystemUrl.MakeRelativeUri(author.Url)); + var author = person == null ? null : await GetResponse>($"https://api.litres.ru/foundation/api/persons/{person.Id}".AsUri()); + return author == null ? + new Author("Litres") : + new Author(author.FullName, SystemUrl.MakeRelativeUri(author.Url)); } private async Task> GetCoAuthors(LitresArt art) @@ -331,10 +285,11 @@ private async Task> GetCoAuthors(LitresArt art) var result = new List(); foreach (var person in art.Persons.Where(a => a.Role == "author").Skip(1)) { - var author = person == null - ? default - : await GetResponse>(ApiUrl.MakeRelativeUri($"/foundation/api/persons/{person.Id}")); - if (author is not null) result.Add(new Author(author.FullName, SystemUrl.MakeRelativeUri(author.Url))); + var author = person == null ? null : await GetResponse>($"https://api.litres.ru/foundation/api/persons/{person.Id}".AsUri()); + if (author != null) + { + result.Add(new Author(author.FullName, SystemUrl.MakeRelativeUri(author.Url))); + } } return result; @@ -342,19 +297,23 @@ private async Task> GetCoAuthors(LitresArt art) private Task GetCover(string imagePath) { - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(SystemUrl.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(SystemUrl.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private async Task> FillChapters(LitresBook book, string title) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var section in book.Content.QuerySelectorAll("section")) { - if (section.QuerySelector("> section") != null) continue; + if (section.QuerySelector("> section") != null) + { + continue; + } var chapter = new Chapter { @@ -405,27 +364,40 @@ private async Task> GetImages(HtmlNode doc, LitresBook boo private async Task GetFileResponse(LitresArt art, LitresFile file) { - var paths = new[] { "download_book_j", "download_book_subscr", "download_my_book_j" }; - var hosts = new List { null, SystemUrl.Host }; - if (_me?.PartnerSubscriptions?.Subscriptions is not null) - hosts.AddRange(_me.PartnerSubscriptions.Subscriptions - .Where(s => s.IsActive && s.Type == (art.ArtType == LitresArtTypeEnum.Audio ? "audio" : "text")) - .Select(s => s.Host).Distinct()); + var paths = new[] + { + "download_book_j", "download_book_subscr", "download_my_book_j" + }; + + var hosts = new List + { + null, SystemUrl.Host + }; + + if (_me?.PartnerSubscriptions?.Subscriptions != null) + { + hosts.AddRange( + _me.PartnerSubscriptions.Subscriptions.Where(s => s.IsActive && s.Type == (art.ArtType == LitresArtTypeEnum.Audio ? "audio" : "text")).Select(s => s.Host).Distinct()); + } if (_authData != null) + { foreach (var host in hosts) - foreach (var path in paths) { - var uri = GetFullUri(art, path, file, host); - var response = await Config.Client.GetAsync(uri); - if (response.StatusCode == HttpStatusCode.OK && response.Headers.AcceptRanges.Count != 0) + foreach (var path in paths) { - Config.Logger.LogInformation($"Дополнительный файл доступен по ссылке {uri}"); - return response; + var uri = GetFullUri(art, path, file, host); + var response = await Config.Client.GetAsync(uri); + if (response.StatusCode == HttpStatusCode.OK && response.Headers.AcceptRanges.Count != 0) + { + Config.Logger.LogInformation($"Дополнительный файл доступен по ссылке {uri}"); + return response; + } } } + } - return default; + return null; } private async Task GetShortBook(LitresArt art) @@ -450,24 +422,34 @@ private async Task GetBook(Stream stream) } foreach (var (key, value) in map) + { if (key.EndsWith("body.xml")) + { result.Content = Encoding.UTF8.GetString(value).AsXHtmlDoc(); + } else if (key.EndsWith("body.xml.rels")) + { foreach (var r in Encoding.UTF8.GetString(value).AsXHtmlDoc().QuerySelectorAll("Relationship")) { var id = r.Attributes["Id"]?.Value; var target = r.Attributes["Target"]?.Value; - if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(target)) continue; + if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(target)) + { + continue; + } - if (!map.TryGetValue(target, out var t)) continue; + if (!map.TryGetValue(target, out var t)) + { + continue; + } result.Targets[id] = new LitresTarget { - Id = id, - Target = target, - Content = t + Id = id, Target = target, Content = t }; } + } + } return result; } diff --git a/Core/Types/Litres/LitresBook.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/LitresBook.cs similarity index 58% rename from Core/Types/Litres/LitresBook.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/LitresBook.cs index b04d420c..8773b762 100644 --- a/Core/Types/Litres/LitresBook.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/LitresBook.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; using HtmlAgilityPack; -namespace Core.Types.Litres; +namespace Elib2Ebook.ExternalServices.Litres.Types; -public class LitresBook +internal class LitresBook { public HtmlDocument Content; diff --git a/Core/Types/Litres/LitresTarget.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/LitresTarget.cs similarity index 59% rename from Core/Types/Litres/LitresTarget.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/LitresTarget.cs index 1bae77ad..0590ad01 100644 --- a/Core/Types/Litres/LitresTarget.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/LitresTarget.cs @@ -1,6 +1,6 @@ -namespace Core.Types.Litres; +namespace Elib2Ebook.ExternalServices.Litres.Types; -public class LitresTarget +internal class LitresTarget { public string Id { get; set; } public string Target { get; set; } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Requests/LitresAuthData.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Requests/LitresAuthData.cs new file mode 100644 index 00000000..a3be4744 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Requests/LitresAuthData.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litres.Types.Requests; + +internal class LitresAuthData +{ + [JsonPropertyName("login")] + public string Login { get; set; } + + [JsonPropertyName("password")] + public string Password { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresArt.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresArt.cs new file mode 100644 index 00000000..d33c8415 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresArt.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litres.Types.Response; + +internal class LitresArt +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("html_annotation")] + public string Annotation { get; set; } + + [JsonPropertyName("persons")] + public LitresPerson[] Persons { get; set; } = []; + + [JsonPropertyName("cover_url")] + public string Cover { get; set; } + + [JsonPropertyName("series")] + public LitresSeria[] Sequences { get; set; } = []; + + [JsonPropertyName("linked_arts")] + public LitresArt[] LinkedArts { get; set; } + + [JsonPropertyName("art_type")] + public LitresArtTypeEnum ArtType { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresArtTypeEnum.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresArtTypeEnum.cs new file mode 100644 index 00000000..f55227e2 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresArtTypeEnum.cs @@ -0,0 +1,8 @@ +namespace Elib2Ebook.ExternalServices.Litres.Types.Response; + +internal enum LitresArtTypeEnum +{ + Text = 0, + Audio = 1, + Pdf = 4 +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresAuthResponseData.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresAuthResponseData.cs new file mode 100644 index 00000000..3e9071ed --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresAuthResponseData.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litres.Types.Response; + +internal class LitresAuthResponseData +{ + [JsonPropertyName("sid")] + public string Sid { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresFile.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresFile.cs new file mode 100644 index 00000000..dc50607e --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresFile.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litres.Types.Response; + +internal class LitresFile +{ + [JsonPropertyName("extension")] + public string Extension { get; set; } + + [JsonPropertyName("id")] + public long Id { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresFiles.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresFiles.cs new file mode 100644 index 00000000..705f2ce1 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresFiles.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litres.Types.Response; + +internal class LitresFiles +{ + [JsonPropertyName("files")] + public LitresFile[] Files { get; set; } +} diff --git a/Core/Types/Litres/Response/LitresMe.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresMe.cs similarity index 66% rename from Core/Types/Litres/Response/LitresMe.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresMe.cs index 427f07ef..7ed2a5bd 100644 --- a/Core/Types/Litres/Response/LitresMe.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresMe.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace Core.Types.Litres.Response; +namespace Elib2Ebook.ExternalServices.Litres.Types.Response; -public class LitresMe +internal class LitresMe { [JsonPropertyName("partner_subscriptions")] public LitresPartnerSubscriptions PartnerSubscriptions { get; set; } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresPartnerSubscriptions.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresPartnerSubscriptions.cs new file mode 100644 index 00000000..9464f290 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresPartnerSubscriptions.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litres.Types.Response; + +internal class LitresPartnerSubscriptions +{ + [JsonPropertyName("subscriptions")] + public LitresSubscription[] Subscriptions { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresPerson.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresPerson.cs new file mode 100644 index 00000000..bee94f35 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresPerson.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litres.Types.Response; + +internal class LitresPerson +{ + [JsonPropertyName("id")] + public TId Id { get; set; } + + [JsonPropertyName("full_name")] + public string FullName { get; set; } + + [JsonPropertyName("role")] + public string Role { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresResponse.cs new file mode 100644 index 00000000..4af2506f --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litres.Types.Response; + +internal class LitresResponse +{ + [JsonPropertyName("data")] + public T Data { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresSeria.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresSeria.cs new file mode 100644 index 00000000..def26e3c --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresSeria.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litres.Types.Response; + +internal class LitresSeria +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("sequence_number")] + public string SequenceNumber { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresStaticResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresStaticResponse.cs new file mode 100644 index 00000000..0897df02 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresStaticResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litres.Types.Response; + +internal class LitresStaticResponse +{ + [JsonPropertyName("errors")] + public string Error { get; set; } + + [JsonPropertyName("payload")] + public LitresResponse Payload { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresSubscription.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresSubscription.cs new file mode 100644 index 00000000..ce057bdf --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litres/Types/Response/LitresSubscription.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litres.Types.Response; + +internal class LitresSubscription +{ + [JsonPropertyName("is_active")] + public bool IsActive { get; set; } + + [JsonPropertyName("host")] + public string Host { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litsovet/Elib2Ebook.ExternalServices.Litsovet.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Litsovet/Elib2Ebook.ExternalServices.Litsovet.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litsovet/Elib2Ebook.ExternalServices.Litsovet.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/LitsovetGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litsovet/LitsovetGetter.cs similarity index 71% rename from Core/Logic/Getters/LitsovetGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Litsovet/LitsovetGetter.cs index aa4433a8..8fe5a210 100644 --- a/Core/Logic/Getters/LitsovetGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litsovet/LitsovetGetter.cs @@ -1,50 +1,55 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; using System.Net.Http.Json; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Litsovet; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Litsovet.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Litsovet; public class LitsovetGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://litsovet.ru/"); protected override string GetId(Uri url) - { - return url.GetSegment(2); - } + => url.GetSegment(2); public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } - using var post = await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("/ajax/login_signup_remind"), - GenerateAuthData()); + using var post = await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("/ajax/login_signup_remind"), GenerateAuthData()); var response = await post.Content.ReadFromJsonAsync(); if (response.Ok == 1) + { Config.Logger.LogInformation("Успешно авторизовались"); + } else - throw new Elib2EbookAuthException("Не удалось авторизоваться."); + { + throw new Exception("Не удалось авторизоваться."); + } } private FormUrlEncodedContent GenerateAuthData() { var payload = new Dictionary { - { "action", "login" }, - { "email", Config.Options.Login }, - { "password", Config.Options.Password } + { + "action", "login" + }, + { + "email", Config.Options.Login + }, + { + "password", Config.Options.Password + }, }; return new FormUrlEncodedContent(payload); @@ -81,9 +86,10 @@ private async Task> GetToc(Uri url) foreach (var a in doc.QuerySelectorAll("div.book-navi-list li a")) { var href = a.Attributes["onclick"]?.Value?.Split(" ").FirstOrDefault(s => s.Contains("books"))?.Trim('\''); - result.Add(string.IsNullOrEmpty(href) - ? new UrlChapter(null, a.GetText().ReplaceNewLine()) - : new UrlChapter(url.MakeRelativeUri(href), a.GetText().ReplaceNewLine())); + result.Add( + string.IsNullOrEmpty(href) + ? new UrlChapter(null, a.GetText().ReplaceNewLine()) + : new UrlChapter(url.MakeRelativeUri(href), a.GetText().ReplaceNewLine())); } return SliceToc(result, c => c.Title); @@ -92,7 +98,10 @@ private async Task> GetToc(Uri url) private async Task> FillChapters(Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in await GetToc(url)) { @@ -102,7 +111,7 @@ private async Task> FillChapters(Uri url) Title = urlChapter.Title }; - if (urlChapter.Url is not null) + if (urlChapter.Url != null) { var chapterDoc = await GetChapter(urlChapter); chapter.Images = await GetImages(chapterDoc, urlChapter.Url); @@ -124,8 +133,6 @@ private async Task GetChapter(UrlChapter urlChapter) private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("div.p-book-cover img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Litsovet/Types/LitsovetLoginResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Litsovet/Types/LitsovetLoginResponse.cs new file mode 100644 index 00000000..b2892185 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Litsovet/Types/LitsovetLoginResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Litsovet.Types; + +internal class LitsovetLoginResponse +{ + [JsonPropertyName("ok")] + public int Ok { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.MangaMammy/Elib2Ebook.ExternalServices.MangaMammy.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.MangaMammy/Elib2Ebook.ExternalServices.MangaMammy.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.MangaMammy/Elib2Ebook.ExternalServices.MangaMammy.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/MangaMammyGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.MangaMammy/MangaMammyGetter.cs similarity index 77% rename from Core/Logic/Getters/MangaMammyGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.MangaMammy/MangaMammyGetter.cs index 468444d8..8588880b 100644 --- a/Core/Logic/Getters/MangaMammyGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.MangaMammy/MangaMammyGetter.cs @@ -1,26 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.MangaMammy; public class MangaMammyGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://mangamammy.ru/"); protected override string GetId(Uri url) - { - return url.GetSegment(2); - } + => url.GetSegment(2); public override async Task Get(Uri url) { @@ -42,15 +37,16 @@ public override async Task Get(Uri url) private static Author GetAuthor(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("div.author-content a"); - return a is null - ? new Author("MangaMammy") - : new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); + return a == null ? new Author("MangaMammy") : new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); } private async Task> FillChapters(HtmlDocument doc, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in GetToc(doc, url)) { @@ -62,7 +58,7 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -96,7 +92,10 @@ private async Task GetChapter(UrlChapter urlChapter) var sb = new StringBuilder(); - foreach (var image in images) sb.Append($""); + foreach (var image in images) + { + sb.Append($""); + } return sb.AsHtmlDoc(); } @@ -104,8 +103,6 @@ private async Task GetChapter(UrlChapter urlChapter) private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("div.summary_image img")?.Attributes["data-lazy-src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.MirKnig/Elib2Ebook.ExternalServices.MirKnig.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.MirKnig/Elib2Ebook.ExternalServices.MirKnig.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.MirKnig/Elib2Ebook.ExternalServices.MirKnig.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/MirKnigGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.MirKnig/MirKnigGetter.cs similarity index 73% rename from Core/Logic/Getters/MirKnigGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.MirKnig/MirKnigGetter.cs index 59ed7693..fe4e5583 100644 --- a/Core/Logic/Getters/MirKnigGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.MirKnig/MirKnigGetter.cs @@ -1,27 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.MirKnig; public class MirKnigGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://mir-knig.com/"); protected override string GetId(Uri url) - { - return base.GetId(url).Split('_').Last().Split('-').First(); - } + => base.GetId(url).Split('_').Last().Split('-').First(); public override async Task Get(Uri url) { @@ -44,7 +39,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(string bookId, string title) { - if (Config.Options.NoChapters) return []; + if (Config.Options.NoChapters) + { + return []; + } var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/read_{bookId}-1")); var pages = int.Parse(doc.QuerySelectorAll("select.allp option").Last().Attributes["value"].Value); @@ -60,12 +58,13 @@ private async Task> FillChapters(string bookId, string titl doc = sb.AsHtmlDoc(); var chapter = new Chapter { - Title = title, - Images = await GetImages(doc, SystemUrl), - Content = doc.DocumentNode.InnerHtml + Title = title, Images = await GetImages(doc, SystemUrl), Content = doc.DocumentNode.InnerHtml }; - return new[] { chapter }; + return + [ + chapter, + ]; } private static Author GetAuthor(HtmlDocument doc, Uri url) @@ -78,18 +77,19 @@ private Task GetCover(HtmlDocument doc, Uri bookUri) { var thumb = doc.QuerySelector("div.cover div.thumb"); var imagePath = string.Empty; - if (thumb is not null) + if (thumb != null) { var style = thumb.Attributes["style"]?.Value; if (!string.IsNullOrWhiteSpace(style)) { var match = Regex.Match(style, @"url\((?.*?)\)"); - if (match.Success) imagePath = match.Groups["url"].Value; + if (match.Success) + { + imagePath = match.Groups["url"].Value; + } } } - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Elib2Ebook.ExternalServices.MyBook.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Elib2Ebook.ExternalServices.MyBook.csproj new file mode 100644 index 00000000..555db2cf --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Elib2Ebook.ExternalServices.MyBook.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Core/Logic/Getters/MyBookGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/MyBookGetter.cs similarity index 71% rename from Core/Logic/Getters/MyBookGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/MyBookGetter.cs index 571c7309..b4e5dc3f 100644 --- a/Core/Logic/Getters/MyBookGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/MyBookGetter.cs @@ -1,44 +1,36 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Misc; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.MyBook; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.MyBook.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; using OAuth; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.MyBook; public class MyBookGetter(BookGetterConfig config) : GetterBase(config) { - private const string CONSUMER_KEY = AppSecrets.MyBookConsumerKey; - private const string CONSUMER_SECRET = AppSecrets.MyBookConsumerSecret; - private const string IDENTIFIER = AppSecrets.MyBookIdentifier; + protected override Uri SystemUrl => new("https://mybook.ru/"); + + private const string CONSUMER_KEY = "830968793b2a44c688400319f4a77231"; + private const string CONSUMER_SECRET = "HhaxXsMryrLz49R4"; + private const string IDENTIFIER = "47b17a4c8231a28f06c6c836d0b5f6f2134cb74d"; private HttpClient _apiClient; private MyBookAuth _token = new() { - Secret = AppSecrets.MyBookDefaultAuthSecret, - Token = AppSecrets.MyBookDefaultAuthToken + Secret = "fYFPndYhOTW6YxIJ", Token = "f8d32ef906664ed3a1525b7298aac461" }; - protected override Uri SystemUrl => new("https://mybook.ru/"); - private Uri GetMainUrl(Uri uri) { return SystemUrl.MakeRelativeUri($"{uri.GetSegment(1)}/{uri.GetSegment(2)}/{uri.GetSegment(3)}"); @@ -67,22 +59,22 @@ private void SetAuthHeader(string method, Uri uri) TokenSecret = _token.Secret }; - _apiClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", - client.GetAuthorizationHeader().Replace("OAuth", string.Empty).Trim()); + _apiClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", client.GetAuthorizationHeader().Replace("OAuth", string.Empty).Trim()); } public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var authUrl = SystemUrl.MakeRelativeUri("/api/auth/"); SetAuthHeader("POST", authUrl); var payload = new { - identifier = IDENTIFIER, - email = Config.Options.Login, - password = Config.Options.Password + identifier = IDENTIFIER, email = Config.Options.Login, password = Config.Options.Password }; using var response = await _apiClient.PostAsJsonAsync(authUrl, payload); @@ -93,7 +85,7 @@ public override async Task Authorize() else { var message = await response.Content.ReadAsStringAsync(); - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {message}"); + throw new Exception($"Не удалось авторизоваться. {message}"); } _token = await response.Content.ReadFromJsonAsync(); @@ -119,7 +111,10 @@ private async Task GetDetails(Uri url) if (details.Type == "audio") { var bookId = details.Connected?.Id ?? details.MapFiles.FirstOrDefault(b => b.Book != 0)?.Book; - if (bookId.HasValue) return await GetDetailsApi(bookId.Value); + if (bookId.HasValue) + { + return await GetDetailsApi(bookId.Value); + } } return details; @@ -139,10 +134,7 @@ public override async Task Get(Uri url) var book = new Book(url) { - Cover = await GetCover(details), - Title = details.Name, - Author = GetAuthor(details), - Annotation = details.Annotation + Cover = await GetCover(details), Title = details.Name, Author = GetAuthor(details), Annotation = details.Annotation, }; var bookUrl = SystemUrl.MakeRelativeUri(details.BookFile); @@ -152,23 +144,31 @@ public override async Task Get(Uri url) { using var response = await _apiClient.GetAsync(bookUrl); if (response.StatusCode != HttpStatusCode.OK) - throw new Elib2EbookParseException("Не удалось получить книгу"); + { + throw new Exception("Не удалось получить книгу"); + } - var origBook = await TempFile.Create(bookUrl, Config.TempFolder.Path, bookUrl.GetFileName(), - await response.Content.ReadAsStreamAsync()); + var origBook = await TempFile.Create(bookUrl, Config.TempFolder.Path, bookUrl.GetFileName(), await response.Content.ReadAsStreamAsync()); if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) + { book.AdditionalFiles.Add(AdditionalTypeEnum.Books, origBook); + } book.Chapters = await FillChaptersFromEpub(origBook); } if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Audio)) { - if (details.Type == "audio") book.AdditionalFiles.Add(AdditionalTypeEnum.Audio, await GetAudio(details.Id)); + if (details.Type == "audio") + { + book.AdditionalFiles.Add(AdditionalTypeEnum.Audio, await GetAudio(details.Id)); + } if (details.Connected is { Type: "audio" }) + { book.AdditionalFiles.Add(AdditionalTypeEnum.Audio, await GetAudio(details.Connected.Id)); + } } return book; @@ -178,8 +178,7 @@ private async Task> GetAudio(long bookId) { var result = new List(); var details = await GetDetailsApi(bookId); - var files = details.Files.Where(node => node is JsonObject).Select(node => node.Deserialize()) - .ToList(); + var files = details.Files.Where(node => node is JsonObject).Select(node => node.Deserialize()).ToList(); for (var i = 0; i < files.Count; i++) { @@ -193,8 +192,7 @@ private async Task> GetAudio(long bookId) var ext = Path.GetExtension(url.GetFileName()); using var response = await _apiClient.GetAsync(url); - result.Add(await TempFile.Create(url, Config.TempFolder.Path, $"{file.Order}_{name}{ext}", - await response.Content.ReadAsStreamAsync())); + result.Add(await TempFile.Create(url, Config.TempFolder.Path, $"{file.Order}_{name}{ext}", await response.Content.ReadAsStreamAsync())); Config.Logger.LogInformation($"Аудиофайл файл {i + 1}/{files.Count} {url} загружен"); } @@ -203,19 +201,18 @@ private async Task> GetAudio(long bookId) private Task GetCover(MyBookBook book) { - return !string.IsNullOrWhiteSpace(book.Cover) - ? SaveImage($"https://i3.mybook.io/p/x378/{book.Cover.TrimStart('/')}".AsUri()) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(book.Cover) ? SaveImage($"https://i3.mybook.io/p/x378/{book.Cover.TrimStart('/')}".AsUri()) : Task.FromResult(default(TempFile)); } private Author GetAuthor(MyBookBook book) { - if (book.Authors.Length == 0) return new Author("MyBook"); + if (book.Authors.Length == 0) + { + return new Author("MyBook"); + } var author = book.Authors[0]; - return string.IsNullOrWhiteSpace(author.Url) - ? new Author(author.Name) - : new Author(author.Name, SystemUrl.MakeRelativeUri(author.Url)); + return string.IsNullOrWhiteSpace(author.Url) ? new Author(author.Name) : new Author(author.Name, SystemUrl.MakeRelativeUri(author.Url)); } public override void Dispose() diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookAuth.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookAuth.cs new file mode 100644 index 00000000..2c15539e --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookAuth.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.MyBook.Types; + +internal class MyBookAuth +{ + [JsonPropertyName("token")] + public string Token { get; set; } + + [JsonPropertyName("secret")] + public string Secret { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookAuthor.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookAuthor.cs new file mode 100644 index 00000000..09f6e1fc --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookAuthor.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.MyBook.Types; + +internal class MyBookAuthor +{ + [JsonPropertyName("cover_name")] + public string Name { get; set; } + + [JsonPropertyName("absolute_url")] + public string Url { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookBook.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookBook.cs new file mode 100644 index 00000000..ca317454 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookBook.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.MyBook.Types; + +internal class MyBookBook +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("annotation")] + public string Annotation { get; set; } + + [JsonPropertyName("bookfile")] + public string BookFile { get; set; } + + [JsonPropertyName("default_cover")] + public string Cover { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("authors")] + public MyBookAuthor[] Authors { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("mapfiles")] + public MyBookMapFile[] MapFiles { get; set; } + + [JsonPropertyName("connected_book")] + public MyBookBook Connected { get; set; } + + [JsonPropertyName("bookfiles")] + public JsonNode[] Files { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookFile.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookFile.cs new file mode 100644 index 00000000..c1ce8e53 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookFile.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.MyBook.Types; + +internal class MyBookFile +{ + [JsonPropertyName("url")] + public string Url { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("order")] + public int Order { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookMapFile.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookMapFile.cs new file mode 100644 index 00000000..a50c561b --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.MyBook/Types/MyBookMapFile.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.MyBook.Types; + +internal class MyBookMapFile +{ + [JsonPropertyName("book")] + public long Book { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Elib2Ebook.ExternalServices.Neobook.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Elib2Ebook.ExternalServices.Neobook.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Elib2Ebook.ExternalServices.Neobook.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/NeobookGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/NeobookGetter.cs similarity index 62% rename from Core/Logic/Getters/NeobookGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/NeobookGetter.cs index 07630e4b..af1a17f2 100644 --- a/Core/Logic/Getters/NeobookGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/NeobookGetter.cs @@ -1,26 +1,23 @@ -using System; -using System.Collections.Generic; using System.Net; -using System.Net.Http; using System.Net.Http.Json; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Neobook; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Neobook.Types; using HtmlAgilityPack; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Neobook; public class NeobookGetter(BookGetterConfig config) : GetterBase(config) { - private readonly Uri _apiUrl = new("https://api.neobook.org/"); protected override Uri SystemUrl => new("http://neobook.org/"); + private Uri _apiUrl = new("https://api.neobook.org/"); + protected override string GetId(Uri url) { return url.GetSegment(1) == "book" ? url.GetSegment(2) : url.GetQueryParameter("book"); @@ -28,12 +25,17 @@ protected override string GetId(Uri url) public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var response = await Config.Client.PostWithTriesAsync(_apiUrl, GenerateAuthData()); var data = await response.Content.ReadFromJsonAsync(); if (!string.IsNullOrWhiteSpace(data?.Error)) - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {data.Error}"); + { + throw new Exception($"Не удалось авторизоваться. {data.Error}"); + } Config.Logger.LogInformation("Успешно авторизовались"); Config.CookieContainer.Add(SystemUrl, new Cookie("utoken", data?.Login.Utoken)); @@ -42,16 +44,32 @@ public override async Task Authorize() private MultipartFormDataContent GenerateAuthData() { - return new MultipartFormDataContent + return new() { - { new StringContent("3.1"), "version" }, - { new StringContent("0"), "uid" }, - { new StringContent(string.Empty), "utoken" }, - { new StringContent("authorization"), "resource" }, - { new StringContent("login_by_email"), "action" }, - { new StringContent(Config.Options.Login), "email" }, - { new StringContent(Config.Options.Password), "password" }, - { new StringContent("3"), "platform_id" } + { + new StringContent("3.1"), "version" + }, + { + new StringContent("0"), "uid" + }, + { + new StringContent(string.Empty), "utoken" + }, + { + new StringContent("authorization"), "resource" + }, + { + new StringContent("login_by_email"), "action" + }, + { + new StringContent(Config.Options.Login), "email" + }, + { + new StringContent(Config.Options.Password), "password" + }, + { + new StringContent("3"), "platform_id" + }, }; } @@ -82,14 +100,16 @@ private static NeobookPostData GetPostData(HtmlDocument doc) private Author GetAuthor(NeobookPostData data) { - return new Author(data.User.LastName + " " + data.User.FirstName, - SystemUrl.MakeRelativeUri(data.User.UserName)); + return new Author(data.User.LastName + " " + data.User.FirstName, SystemUrl.MakeRelativeUri(data.User.UserName)); } private async Task> FillChapters(NeobookPostData data, string bookId) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var neobookChapter in SliceToc(data.Chapters, c => c.Title)) { @@ -100,7 +120,7 @@ private async Task> FillChapters(NeobookPostData data, stri }; var chapterDoc = await GetChapter(neobookChapter, bookId); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -114,16 +134,26 @@ private async Task> FillChapters(NeobookPostData data, stri private async Task GetChapter(NeobookTocChapter neobookTocChapter, string bookId) { - var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("reader/") - .AppendQueryParameter("book", bookId).AppendQueryParameter("chapter", neobookTocChapter.Token)); + var doc = await Config.Client.GetHtmlDocWithTriesAsync( + SystemUrl.MakeRelativeUri("reader/").AppendQueryParameter("book", bookId).AppendQueryParameter("chapter", neobookTocChapter.Token)); + var data = Regex.Match(doc.ParsedText, "var data = (?.*?);\n").Groups["data"].Value; - if (string.IsNullOrWhiteSpace(data)) return default; + if (string.IsNullOrWhiteSpace(data)) + { + return null; + } var neobookBook = data.Deserialize(); - if (neobookBook.ActiveChapterIndex > neobookBook.Chapters.Length) return default; + if (neobookBook.ActiveChapterIndex > neobookBook.Chapters.Length) + { + return null; + } var chapter = neobookBook.Chapters[neobookBook.ActiveChapterIndex]; - if (string.IsNullOrWhiteSpace(chapter.Data?.Html)) return default; + if (string.IsNullOrWhiteSpace(chapter.Data?.Html)) + { + return null; + } return chapter.Data.Html.AsHtmlDoc(); } @@ -131,8 +161,6 @@ private async Task GetChapter(NeobookTocChapter neobookTocChapter, private Task GetCover(NeobookPostData data) { var imagePath = data.Attachment?.Cover?.GetValueOrDefault("l"); - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(imagePath.AsUri()) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(imagePath.AsUri()) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookAttachment.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookAttachment.cs new file mode 100644 index 00000000..70f86ad5 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookAttachment.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Neobook.Types; + +internal class NeobookAttachment +{ + [JsonPropertyName("image")] + public Dictionary Cover { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookAuth.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookAuth.cs new file mode 100644 index 00000000..4e5efb17 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookAuth.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Neobook.Types; + +internal class NeobookAuth +{ + [JsonPropertyName("error")] + public string Error { get; set; } + + [JsonPropertyName("login")] + public NeobookLogin Login { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookBook.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookBook.cs new file mode 100644 index 00000000..1761315a --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookBook.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Neobook.Types; + +internal class NeobookBook +{ + [JsonPropertyName("chapters")] + public NeobookChapter[] Chapters { get; set; } + + [JsonPropertyName("active_chapter_index")] + public int ActiveChapterIndex { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookChapter.cs new file mode 100644 index 00000000..10da2071 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookChapter.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Neobook.Types; + +internal class NeobookChapter +{ + [JsonPropertyName("data")] + public NeobookChapterData Data { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookChapterData.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookChapterData.cs new file mode 100644 index 00000000..a9389221 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookChapterData.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Neobook.Types; + +internal class NeobookChapterData +{ + [JsonPropertyName("html")] + public string Html { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookLogin.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookLogin.cs new file mode 100644 index 00000000..d1ee0601 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookLogin.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Neobook.Types; + +internal class NeobookLogin +{ + [JsonPropertyName("utoken")] + public string Utoken { get; set; } + + [JsonPropertyName("uid")] + public string Uid { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookPostData.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookPostData.cs new file mode 100644 index 00000000..62851635 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookPostData.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Neobook.Types; + +internal class NeobookPostData +{ + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("text")] + public string Text { get; set; } + + [JsonPropertyName("user")] + public NeobookUser User { get; set; } + + [JsonPropertyName("chapters")] + public NeobookTocChapter[] Chapters { get; set; } + + [JsonPropertyName("attachment")] + public NeobookAttachment Attachment { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookTocChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookTocChapter.cs new file mode 100644 index 00000000..a4f15413 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookTocChapter.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Neobook.Types; + +internal class NeobookTocChapter +{ + [JsonPropertyName("token")] + public string Token { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookUser.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookUser.cs new file mode 100644 index 00000000..caa40742 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Neobook/Types/NeobookUser.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Neobook.Types; + +internal class NeobookUser +{ + [JsonPropertyName("username")] + public string UserName { get; set; } + + [JsonPropertyName("firstname")] + public string FirstName { get; set; } + + [JsonPropertyName("lastname")] + public string LastName { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.NovelTranslate/Elib2Ebook.ExternalServices.NovelTranslate.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.NovelTranslate/Elib2Ebook.ExternalServices.NovelTranslate.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.NovelTranslate/Elib2Ebook.ExternalServices.NovelTranslate.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/NovelTranslateGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.NovelTranslate/NovelTranslateGetter.cs similarity index 75% rename from Core/Logic/Getters/NovelTranslateGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.NovelTranslate/NovelTranslateGetter.cs index eecb811a..1e244fbf 100644 --- a/Core/Logic/Getters/NovelTranslateGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.NovelTranslate/NovelTranslateGetter.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.NovelTranslate; public class NovelTranslateGetter(BookGetterConfig config) : GetterBase(config) { @@ -19,10 +16,14 @@ public class NovelTranslateGetter(BookGetterConfig config) : GetterBase(config) protected override string GetId(Uri url) { for (var i = 0; i < url.Segments.Length; i++) + { if (url.Segments[i].StartsWith("novel")) + { return url.Segments[i + 1].Trim('/'); + } + } - throw new Elib2EbookParseException($"Не могу определить ID книги {url}"); + throw new Exception($"Не могу определить ID книги {url}"); } private static string GetLang(Uri url) @@ -34,9 +35,7 @@ private static string GetLang(Uri url) public override async Task Get(Uri url) { var lang = GetLang(url); - url = string.IsNullOrWhiteSpace(lang) - ? SystemUrl.MakeRelativeUri($"/novel/{GetId(url)}/") - : SystemUrl.MakeRelativeUri($"/{lang}/novel/{GetId(url)}/"); + url = string.IsNullOrWhiteSpace(lang) ? SystemUrl.MakeRelativeUri($"/novel/{GetId(url)}/") : SystemUrl.MakeRelativeUri($"/{lang}/novel/{GetId(url)}/"); var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); @@ -56,7 +55,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(HtmlDocument doc, string lang, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in await GetToc(doc, lang, url)) { @@ -80,14 +82,12 @@ private async Task> FillChapters(HtmlDocument doc, string l private async Task GetChapter(UrlChapter urlChapter) { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); - return doc.QuerySelector("div.reading-content div.text-left") - .RemoveNodes("div.novel-before-content, div.tptn_counter, div.novel-after-content").InnerHtml.AsHtmlDoc(); + return doc.QuerySelector("div.reading-content div.text-left").RemoveNodes("div.novel-before-content, div.tptn_counter, div.novel-after-content").InnerHtml.AsHtmlDoc(); } private async Task> GetToc(HtmlDocument doc, string lang, Uri url) { - var lastChapter = url.MakeRelativeUri(doc.QuerySelector("div.listing-chapters_wrap li.wp-manga-chapter a") - .Attributes["href"].Value); + var lastChapter = url.MakeRelativeUri(doc.QuerySelector("div.listing-chapters_wrap li.wp-manga-chapter a").Attributes["href"].Value); var chapterDoc = await Config.Client.GetHtmlDocWithTriesAsync(lastChapter); var chapters = new List(); @@ -96,7 +96,9 @@ private async Task> GetToc(HtmlDocument doc, string lang var name = option.GetText(); var chapterUri = option.Attributes["data-redirect"].Value.AsUri(); if (!string.IsNullOrWhiteSpace(lang) && !chapterUri.LocalPath.Trim('/').StartsWith(lang)) + { chapterUri = url.MakeRelativeUri($"/{lang}/{chapterUri.LocalPath.Trim('/')}"); + } chapters.Add(new UrlChapter(chapterUri, name)); } @@ -113,16 +115,12 @@ private static string GetAnnotation(HtmlDocument doc) private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("div.summary_image img")?.Attributes["data-lazy-src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private static Author GetAuthor(HtmlDocument doc, Uri uri) { var a = doc.QuerySelector("div.summary-content div.author-content a"); - return a is not null - ? new Author(a.GetText(), uri.MakeRelativeUri(a.Attributes["href"].Value)) - : new Author("NovelTranslate"); + return a != null ? new Author(a.GetText(), uri.MakeRelativeUri(a.Attributes["href"].Value)) : new Author("NovelTranslate"); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Novelhall/Elib2Ebook.ExternalServices.Novelhall.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Novelhall/Elib2Ebook.ExternalServices.Novelhall.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Novelhall/Elib2Ebook.ExternalServices.Novelhall.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/NovelhallGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Novelhall/NovelhallGetter.cs similarity index 78% rename from Core/Logic/Getters/NovelhallGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Novelhall/NovelhallGetter.cs index 7891edf4..fd9fb374 100644 --- a/Core/Logic/Getters/NovelhallGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Novelhall/NovelhallGetter.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Novelhall; public class NovelhallGetter(BookGetterConfig config) : GetterBase(config) { @@ -42,7 +39,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in GetToc(doc, url)) { @@ -71,8 +71,7 @@ private async Task GetChapter(UrlChapter urlChapter) private IEnumerable GetToc(HtmlDocument doc, Uri url) { - var chapters = doc.QuerySelectorAll("#morelist ul li a[href]") - .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())).ToList(); + var chapters = doc.QuerySelectorAll("#morelist ul li a[href]").Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())).ToList(); return SliceToc(chapters, c => c.Title); } @@ -84,14 +83,12 @@ private static string GetAnnotation(HtmlDocument doc) private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("div.book-img img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private static Author GetAuthor(HtmlDocument doc) { var a = doc.QuerySelector("meta[property='books:author']"); - return a is not null ? new Author(a.Attributes["content"].Value) : new Author("NovelHall"); + return a != null ? new Author(a.Attributes["content"].Value) : new Author("NovelHall"); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Novelxo/Elib2Ebook.ExternalServices.Novelxo.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Novelxo/Elib2Ebook.ExternalServices.Novelxo.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Novelxo/Elib2Ebook.ExternalServices.Novelxo.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/Novelxo/NovelxoGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Novelxo/Getters/NovelxoGetter.cs similarity index 61% rename from Core/Logic/Getters/Novelxo/NovelxoGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Novelxo/Getters/NovelxoGetter.cs index 0dc04a3a..3f614464 100644 --- a/Core/Logic/Getters/Novelxo/NovelxoGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Novelxo/Getters/NovelxoGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.Novelxo; +namespace Elib2Ebook.ExternalServices.Novelxo.Getters; public class NovelxoGetter(BookGetterConfig config) : NovelxoGetterBase(config) { diff --git a/Core/Logic/Getters/Novelxo/NovelxoGetterBase.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Novelxo/Getters/NovelxoGetterBase.cs similarity index 74% rename from Core/Logic/Getters/Novelxo/NovelxoGetterBase.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Novelxo/Getters/NovelxoGetterBase.cs index d0df631c..c49ee9d3 100644 --- a/Core/Logic/Getters/Novelxo/NovelxoGetterBase.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Novelxo/Getters/NovelxoGetterBase.cs @@ -1,31 +1,24 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.Novelxo; +namespace Elib2Ebook.ExternalServices.Novelxo.Getters; public abstract class NovelxoGetterBase(BookGetterConfig config) : GetterBase(config) { - private static readonly byte[] Key = StringToByteArray(AppSecrets.NovelxoKeyHex); - private static readonly byte[] IV = StringToByteArray(AppSecrets.NovelxoIvHex); + private static readonly byte[] Key = StringToByteArray("61626326312a7e235e325e2373305e3d295e5e3725623334"); + private static readonly byte[] IV = StringToByteArray("31323334353637383930383533373237"); protected override string GetId(Uri url) - { - return url.GetSegment(1); - } + => url.GetSegment(1); public override async Task Get(Uri url) { @@ -46,7 +39,10 @@ public override async Task Get(Uri url) private async Task GetMainUrl(Uri url) { - if (url.Segments.Length == 2) return SystemUrl.MakeRelativeUri(GetId(url)); + if (url.Segments.Length == 2) + { + return SystemUrl.MakeRelativeUri(GetId(url)); + } var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); return url.MakeRelativeUri(doc.QuerySelector("div.readerheader-wg a").Attributes["href"].Value); @@ -55,7 +51,10 @@ private async Task GetMainUrl(Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } var start = url.MakeRelativeUri(doc.QuerySelector("div.actions a.read").Attributes["href"].Value); @@ -66,6 +65,7 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) var container = doc.QuerySelector("div.readerbody-wg"); foreach (var node in container.QuerySelectorAll("p, div.ctp")) + { if (node.Name == "p") { sb.Append(node.InnerHtml.HtmlDecode().CoverTag(node.Name)); @@ -73,18 +73,20 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) else if (node.Name == "div" && node.Attributes["class"]?.Value?.Contains("ctp") == true) { if (node.Attributes["class"].Value.Contains("protected")) + { sb.Append(await GetProtected(doc, node)); + } else - sb.Append(Encoding.UTF8.GetString(Transform(Convert.FromBase64String(node.InnerText), Key, IV)) - .HtmlDecode()); + { + sb.Append(Encoding.UTF8.GetString(Transform(Convert.FromBase64String(node.InnerText), Key, IV)).HtmlDecode()); + } } + } var chapterDoc = sb.AsHtmlDoc(); var chapter = new Chapter { - Title = doc.GetTextBySelector("h2.chapter-title"), - Images = await GetImages(chapterDoc, start), - Content = chapterDoc.DocumentNode.InnerHtml + Title = doc.GetTextBySelector("h2.chapter-title"), Images = await GetImages(chapterDoc, start), Content = chapterDoc.DocumentNode.InnerHtml }; result.Add(chapter); @@ -92,7 +94,10 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) Config.Logger.LogInformation($"Загружена глава {chapter.Title.CoverQuotes()}"); var next = doc.QuerySelector("div.readernav-wg a.next"); - if (next is null) return result; + if (next == null) + { + return result; + } start = start.MakeRelativeUri(next.Attributes["href"].Value); } @@ -103,23 +108,20 @@ private async Task GetProtected(HtmlDocument doc, HtmlNode node) var id = node.Attributes["data-id"].Value; var header = Regex.Match(doc.ParsedText, @"http.setRequestHeader\((?.*?), '(?.*?)'"); - var headerName = Regex.Match(doc.ParsedText, $"{header.Groups["name"].Value} = \'(?.*?)\'").Groups["id"] - .Value; + var headerName = Regex.Match(doc.ParsedText, $"{header.Groups["name"].Value} = \'(?.*?)\'").Groups["id"].Value; var headerValue = header.Groups["value"].Value; var key = StringToByteArray(Regex.Match(doc.ParsedText, "signKey = \'(?.*?)\'").Groups["id"].Value); var encode = Transform(Encoding.UTF8.GetBytes(id), key, IV); - var url = new Uri( - $"https://a.novelxo.com/v1/chapters/{id}/ctp?width=1140&sign=932870{Atob(Convert.ToBase64String(encode))}182906"); + var url = new Uri($"https://a.novelxo.com/v1/chapters/{id}/ctp?width=1140&sign=932870{Atob(Convert.ToBase64String(encode))}182906"); var message = new HttpRequestMessage(HttpMethod.Get, url); message.Headers.Add(headerName, headerValue); message.Headers.Add("referer", SystemUrl.ToString()); var response = await Config.Client.SendAsync(message); var readAsStringAsync = await response.Content.ReadAsStringAsync(); - var result = Encoding.UTF8.GetString(Transform(Convert.FromBase64String(readAsStringAsync.Trim('\"')), Key, IV)) - .HtmlDecode(); + var result = Encoding.UTF8.GetString(Transform(Convert.FromBase64String(readAsStringAsync.Trim('\"')), Key, IV)).HtmlDecode(); return result; } @@ -132,25 +134,23 @@ private static string Atob(string toEncode) private static string GetAnnotation(HtmlDocument doc) { var html = doc.QuerySelector("#bookinfo")?.InnerHtml; - return string.IsNullOrWhiteSpace(html) - ? string.Empty - : html.AsHtmlDoc().RemoveNodes("script, ins").DocumentNode.InnerHtml; + return string.IsNullOrWhiteSpace(html) ? + string.Empty : + html.AsHtmlDoc().RemoveNodes("script, ins").DocumentNode.InnerHtml; } private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("div.cover img")?.Attributes["data-src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private static Author GetAuthor(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("tr.authors a"); - return a is null - ? new Author("Novelxo") - : new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); + return a == null ? + new Author("Novelxo") : + new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); } private static byte[] StringToByteArray(string hex) @@ -184,10 +184,17 @@ private static void AesCtrTransform(byte[] key, byte[] salt, Stream inputStream, counterEncryptor.TransformBlock(counter, 0, counter.Length, counterModeBlock, 0); for (var i = counter.Length - 1; i >= 0; i--) + { if (++counter[i] != 0) + { break; + } + } - foreach (var b2 in counterModeBlock) xorMask.Enqueue(b2); + foreach (var b2 in counterModeBlock) + { + xorMask.Enqueue(b2); + } } var mask = xorMask.Dequeue(); diff --git a/Core/Logic/Getters/Novelxo/NovelxoRuGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Novelxo/Getters/NovelxoRuGetter.cs similarity index 62% rename from Core/Logic/Getters/Novelxo/NovelxoRuGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Novelxo/Getters/NovelxoRuGetter.cs index 968a40c6..f87f3e4b 100644 --- a/Core/Logic/Getters/Novelxo/NovelxoRuGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Novelxo/Getters/NovelxoRuGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.Novelxo; +namespace Elib2Ebook.ExternalServices.Novelxo.Getters; public class NovelxoRuGetter(BookGetterConfig config) : NovelxoGetterBase(config) { diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.OnlineKnigi/Elib2Ebook.ExternalServices.OnlineKnigi.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.OnlineKnigi/Elib2Ebook.ExternalServices.OnlineKnigi.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.OnlineKnigi/Elib2Ebook.ExternalServices.OnlineKnigi.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/OnlineKnigiGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.OnlineKnigi/OnlineKnigiGetter.cs similarity index 80% rename from Core/Logic/Getters/OnlineKnigiGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.OnlineKnigi/OnlineKnigiGetter.cs index 57591609..6e9ede91 100644 --- a/Core/Logic/Getters/OnlineKnigiGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.OnlineKnigi/OnlineKnigiGetter.cs @@ -1,25 +1,21 @@ -using System; -using System.Collections.Generic; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.OnlineKnigi; public class OnlineKnigiGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://online-knigi.com.ua/"); protected override string GetId(Uri url) - { - return url.GetSegment(2); - } + => url.GetSegment(2); public override async Task Get(Uri url) { @@ -34,7 +30,7 @@ public override async Task Get(Uri url) Chapters = await FillChapters(bookId, title), Title = title, Author = GetAuthor(doc, url), - Annotation = doc.QuerySelector("div.book_description")?.InnerHtml + Annotation = doc.QuerySelector("div.book_description")?.InnerHtml, }; return book; @@ -42,7 +38,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(string bookId, string title) { - if (Config.Options.NoChapters) return []; + if (Config.Options.NoChapters) + { + return []; + } var chapter = new Chapter(); var sb = new StringBuilder(); @@ -62,7 +61,10 @@ private async Task> FillChapters(string bookId, string titl chapter.Title = title; chapter.Content = chapterDoc.DocumentNode.InnerHtml; - return new[] { chapter }; + return + [ + chapter, + ]; } private async Task GetPages(string bookId) @@ -74,9 +76,7 @@ private async Task GetPages(string bookId) private Task GetCover(HtmlDocument doc) { var imagePath = doc.QuerySelector("meta[property=og:image]")?.Attributes["content"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(SystemUrl.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(SystemUrl.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private static Author GetAuthor(HtmlDocument doc, Uri uri) diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Prodaman/Elib2Ebook.ExternalServices.Prodaman.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Prodaman/Elib2Ebook.ExternalServices.Prodaman.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Prodaman/Elib2Ebook.ExternalServices.Prodaman.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/ProdamanGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Prodaman/ProdamanGetter.cs similarity index 73% rename from Core/Logic/Getters/ProdamanGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Prodaman/ProdamanGetter.cs index 202194e6..c21ef6ae 100644 --- a/Core/Logic/Getters/ProdamanGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Prodaman/ProdamanGetter.cs @@ -1,24 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Prodaman; public class ProdamanGetter : GetterBase { - private static readonly Dictionary Map = new(); - public ProdamanGetter(BookGetterConfig config) : base(config) { InitMap(); @@ -26,32 +19,40 @@ public ProdamanGetter(BookGetterConfig config) : base(config) protected override Uri SystemUrl => new("https://prodaman.ru/"); + private static readonly Dictionary Map = new(); + private static void Append(string str, int start) { - for (var i = 0; i < str.Length; i++) Map[start + i] = str[i]; + for (var i = 0; i < str.Length; i++) + { + Map[start + i] = str[i]; + } } /// - /// Авторизация в системе + /// Авторизация в системе /// /// public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/login"), GetAuthData()); if (!string.IsNullOrWhiteSpace(doc.GetTextBySelector("p.error"))) - throw new Elib2EbookAuthException("Не удалось авторизоваться"); + { + throw new Exception("Не удалось авторизоваться"); + } } private FormUrlEncodedContent GetAuthData() { var data = new Dictionary { - ["email"] = Config.Options.Login, - ["pass"] = Config.Options.Password, - ["remember"] = "on" + ["email"] = Config.Options.Login, ["pass"] = Config.Options.Password, ["remember"] = "on", }; return new FormUrlEncodedContent(data); @@ -142,7 +143,10 @@ public override async Task Get(Uri url) private static Author GetAuthor(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("a[data-widget-feisovet-author]"); - if (a is null) return new Author("Prodaman"); + if (a == null) + { + return new Author("Prodaman"); + } return new Author(a.GetText(), url.MakeRelativeUri(a.Attributes["href"].Value)); } @@ -150,14 +154,15 @@ private static Author GetAuthor(HtmlDocument doc, Uri url) private static Seria GetSeria(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("p.blog-info a[href*=/series/]"); - if (a is not null) + if (a != null) + { return new Seria { - Name = a.GetText(), - Url = url.MakeRelativeUri(a.Attributes["href"].Value) + Name = a.GetText(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; + } - return default; + return null; } private async Task GetPages(Uri url) @@ -187,7 +192,10 @@ private static Chapter CreateChapter(string title) private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text, Uri url) { - if (chapter == null) return; + if (chapter == null) + { + return; + } var chapterDoc = text.ToString().AsHtmlDoc(); chapter.Images = await GetImages(chapterDoc, url); @@ -203,7 +211,10 @@ private static string Decode(string encode) private async Task> FillChapter(Uri url, string title) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } Chapter chapter = null; var text = new StringBuilder(); @@ -218,13 +229,50 @@ private async Task> FillChapter(Uri url, string title) var page = await Config.Client.GetHtmlDocWithTriesAsync(appendSegment); var content = page.QuerySelector("div.blog-text").RemoveNodes("a[href*=snoska], div[id*=snoska]"); var nodes = content.ChildNodes; - if (i == 1 && !IsHeaderStart(nodes)) chapter = CreateChapter(title); + if (i == 1 && !IsHeaderStart(nodes)) + { + chapter = CreateChapter(title); + } foreach (var node in nodes) { if (node.Name != "h3" || (node.Name == "h3" && node.GetText() == "***")) { - ProcessContentNode(text, node, ref firstBr); + if (node.Name == "br") + { + if (firstBr) + { + firstBr = false; + text.Append("

"); + } + else + { + text.Append("

"); + } + + continue; + } + + if (node.Name == "img" && node.Attributes["src"] != null) + { + text.Append($""); + } + else if (!string.IsNullOrWhiteSpace(node.InnerHtml)) + { + var pText = Decode(node.InnerHtml.HtmlDecode()); + + if (node.InnerHtml.StartsWith(" ")) + { + pText = " " + pText; + } + + if (node.InnerHtml.EndsWith(" ")) + { + pText += " "; + } + + text.Append(pText.CoverTag(node.Name == "#text" ? string.Empty : node.Name)); + } } else { @@ -241,45 +289,9 @@ private async Task> FillChapter(Uri url, string title) return result; } - private void ProcessContentNode(StringBuilder text, HtmlNode node, ref bool firstBr) - { - if (node.Name == "br") - { - if (firstBr) - { - firstBr = false; - text.Append("

"); - } - else - { - text.Append("

"); - } - - return; - } - - if (node.Name == "img" && node.Attributes["src"] != null) - { - text.Append($""); - } - else if (!string.IsNullOrWhiteSpace(node.InnerHtml)) - { - var pText = Decode(node.InnerHtml.HtmlDecode()); - - if (node.InnerHtml.StartsWith(" ")) pText = " " + pText; - - if (node.InnerHtml.EndsWith(" ")) pText += " "; - - text.Append(pText.CoverTag(node.Name == "#text" ? string.Empty : node.Name)); - } - } - private Task GetCover(HtmlDocument doc, Uri url) { - var imagePath = doc.QuerySelector("div[itemprop=aggregateRating] img[itemprop=image]")?.Attributes["src"] - ?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(url.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + var imagePath = doc.QuerySelector("div[itemprop=aggregateRating] img[itemprop=image]")?.Attributes["src"]?.Value; + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(url.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Elib2Ebook.ExternalServices.Ranobe.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Elib2Ebook.ExternalServices.Ranobe.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Elib2Ebook.ExternalServices.Ranobe.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/RanobeGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/RanobeGetter.cs similarity index 80% rename from Core/Logic/Getters/RanobeGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/RanobeGetter.cs index 3f8fe142..ec62a11a 100644 --- a/Core/Logic/Getters/RanobeGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/RanobeGetter.cs @@ -1,27 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.Json; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Ranobe; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Ranobe.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Ranobe; public class RanobeGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://ранобэ.рф/"); protected override string GetId(Uri url) - { - return url.Segments.Length == 2 ? base.GetId(url) : url.GetSegment(1); - } + => url.Segments.Length == 2 ? base.GetId(url) : url.GetSegment(1); public override async Task Get(Uri url) { @@ -44,7 +39,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(RanobeBook ranobeBook, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } ranobeBook.Chapters.Reverse(); foreach (var ranobeChapter in SliceToc(ranobeBook.Chapters, c => c.Title)) @@ -82,8 +80,6 @@ private static T GetNextData(HtmlDocument doc, string node) private Task GetCover(RanobeBook book, Uri bookUri) { var imagePath = book.Image?.Url ?? book.Images.MaxBy(t => t.Height)?.Url; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeBook.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeBook.cs new file mode 100644 index 00000000..f5f711ba --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeBook.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Ranobe.Types; + +internal class RanobeBook +{ + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("author")] + public string Author { get; set; } + + [JsonPropertyName("verticalImages")] + public RanobeImage[] Images { get; set; } + + [JsonPropertyName("verticalImage")] + public RanobeImage Image { get; set; } + + [JsonPropertyName("chapters")] + public List Chapters { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeChapter.cs new file mode 100644 index 00000000..507863fd --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeChapter.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Ranobe.Types; + +internal class RanobeChapter +{ + [JsonPropertyName("content")] + public RanobeChapterContent Content { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeChapterContent.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeChapterContent.cs new file mode 100644 index 00000000..3a8f6ac6 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeChapterContent.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Ranobe.Types; + +internal class RanobeChapterContent +{ + [JsonPropertyName("text")] + public string Text { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeChapterShort.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeChapterShort.cs new file mode 100644 index 00000000..2a81f023 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeChapterShort.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Ranobe.Types; + +internal class RanobeChapterShort +{ + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeImage.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeImage.cs new file mode 100644 index 00000000..fba84cbd --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Ranobe/Types/RanobeImage.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Ranobe.Types; + +internal class RanobeImage +{ + [JsonPropertyName("height")] + public int Height { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/Elib2Ebook.ExternalServices.RanobeHub.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/Elib2Ebook.ExternalServices.RanobeHub.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/Elib2Ebook.ExternalServices.RanobeHub.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/RanobeHubGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/RanobeHubGetter.cs similarity index 65% rename from Core/Logic/Getters/RanobeHubGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/RanobeHubGetter.cs index d92b4fb5..28e166ae 100644 --- a/Core/Logic/Getters/RanobeHubGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/RanobeHubGetter.cs @@ -1,18 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http.Json; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.RanobeHub; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.RanobeHub.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.RanobeHub; public class RanobeHubGetter(BookGetterConfig config) : GetterBase(config) { @@ -25,10 +22,7 @@ public override async Task Get(Uri url) var book = new Book(url) { - Cover = await GetCover(doc, url), - Chapters = await FillChapters(doc, url), - Title = doc.GetTextBySelector("h1.header"), - Author = new Author("RanobeHub") + Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1.header"), Author = new Author("RanobeHub") }; return book; @@ -37,7 +31,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var ranobeChapter in await GetToc(doc)) { @@ -60,24 +57,29 @@ private async Task GetChapter(string url) var doc = await Config.Client.GetHtmlDocWithTriesAsync(url.AsUri()); while (doc.QuerySelector("div[data-callback=correctCaptcha]") != null) { - Config.Logger.LogInformation( - $"Обнаружена капча. Перейдите по ссылке {url}, введите капчу и нажмите Enter..."); + Config.Logger.LogInformation($"Обнаружена капча. Перейдите по ссылке {url}, введите капчу и нажмите Enter..."); Console.Read(); doc = await Config.Client.GetHtmlDocWithTriesAsync(url.AsUri()); } - var result = doc.QuerySelector("div.container[data-container]") - .RemoveNodes("div.title-wrapper, div.ads-desktop, div.tablet, div.chapter-hoticons").InnerHtml.AsHtmlDoc(); + var result = doc.QuerySelector("div.container[data-container]").RemoveNodes("div.title-wrapper, div.ads-desktop, div.tablet, div.chapter-hoticons").InnerHtml.AsHtmlDoc(); foreach (var img in result.QuerySelectorAll("img")) { var id = img.Attributes["data-media-id"]?.Value; - if (string.IsNullOrWhiteSpace(id)) continue; + if (string.IsNullOrWhiteSpace(id)) + { + continue; + } if (img.Attributes.Contains("src")) + { img.Attributes["src"].Value = $"/api/media/{id}"; + } else + { img.Attributes.Add("src", $"/api/media/{id}"); + } } return result; @@ -86,17 +88,13 @@ private async Task GetChapter(string url) private async Task> GetToc(HtmlDocument doc) { var internalId = doc.QuerySelector("html[data-id]").Attributes["data-id"].Value; - var response = - await Config.Client.GetFromJsonAsync( - SystemUrl.MakeRelativeUri($"/api/ranobe/{internalId}/contents")); + var response = await Config.Client.GetFromJsonAsync(SystemUrl.MakeRelativeUri($"/api/ranobe/{internalId}/contents")); return SliceToc(response?.Volumes.SelectMany(t => t.Chapters).ToList(), c => c.Name); } private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("div.poster-slider img")?.Attributes["data-src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/Types/RanobeHubApiResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/Types/RanobeHubApiResponse.cs new file mode 100644 index 00000000..5dd82d8f --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/Types/RanobeHubApiResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.RanobeHub.Types; + +internal class RanobeHubApiResponse +{ + [JsonPropertyName("volumes")] + public RanobeHubVolume[] Volumes { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/Types/RanobeHubChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/Types/RanobeHubChapter.cs new file mode 100644 index 00000000..b38d7026 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/Types/RanobeHubChapter.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.RanobeHub.Types; + +internal class RanobeHubChapter +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/Types/RanobeHubVolume.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/Types/RanobeHubVolume.cs new file mode 100644 index 00000000..2d6de1e8 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeHub/Types/RanobeHubVolume.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.RanobeHub.Types; + +internal class RanobeHubVolume +{ + [JsonPropertyName("chapters")] + public RanobeHubChapter[] Chapters { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Elib2Ebook.ExternalServices.RanobeOvh.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Elib2Ebook.ExternalServices.RanobeOvh.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Elib2Ebook.ExternalServices.RanobeOvh.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/RanobeOvh/MangaOvhGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Getters/MangaOvhGetter.cs similarity index 55% rename from Core/Logic/Getters/RanobeOvh/MangaOvhGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Getters/MangaOvhGetter.cs index 4e489730..f0ca935b 100644 --- a/Core/Logic/Getters/RanobeOvh/MangaOvhGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Getters/MangaOvhGetter.cs @@ -1,13 +1,11 @@ -using System; using System.Net.Http.Json; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.RanobeOvh; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.ExternalServices.RanobeOvh.Types; using HtmlAgilityPack; -namespace Core.Logic.Getters.RanobeOvh; +namespace Elib2Ebook.ExternalServices.RanobeOvh.Getters; public class MangaOvhGetter(BookGetterConfig config) : RanobeOvhGetterBase(config) { @@ -15,11 +13,13 @@ public class MangaOvhGetter(BookGetterConfig config) : RanobeOvhGetterBase(confi protected override async Task GetChapter(RanobeOvhChapterShort ranobeOvhChapterFull) { - var data = await Config.Client.GetFromJsonAsync( - $"https://api.{SystemUrl.Host}/chapter/{ranobeOvhChapterFull.Id}"); + var data = await Config.Client.GetFromJsonAsync($"https://api.{SystemUrl.Host}/chapter/{ranobeOvhChapterFull.Id}"); var sb = new StringBuilder(); - foreach (var page in data.Pages) sb.Append($""); + foreach (var page in data.Pages) + { + sb.Append($""); + } return sb.AsHtmlDoc(); } diff --git a/Core/Logic/Getters/RanobeOvh/NovelOvhGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Getters/NovelOvhGetter.cs similarity index 69% rename from Core/Logic/Getters/RanobeOvh/NovelOvhGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Getters/NovelOvhGetter.cs index 4bb392fc..efd9907d 100644 --- a/Core/Logic/Getters/RanobeOvh/NovelOvhGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Getters/NovelOvhGetter.cs @@ -1,47 +1,59 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http.Json; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.RanobeOvh; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.ExternalServices.RanobeOvh.Types; using HtmlAgilityPack; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.RanobeOvh; +namespace Elib2Ebook.ExternalServices.RanobeOvh.Getters; public class NovelOvhGetter(BookGetterConfig config) : RanobeOvhGetterBase(config) { + protected override Uri SystemUrl => new("https://novel.ovh/"); + private static readonly Dictionary RecursiveTag = new() { - { "paragraph", "p" }, - { "orderedList", "ol" }, - { "listItem", "li" }, - { "blockquote", "blockquote" } + { + "paragraph", "p" + }, + { + "orderedList", "ol" + }, + { + "listItem", "li" + }, + { + "blockquote", "blockquote" + }, }; private static readonly Dictionary InlineTag = new() { - { "horizontalRule", "


" }, - { "hardBreak", "
" } + { + "horizontalRule", "
" + }, + { + "hardBreak", "
" + }, }; private static readonly Dictionary MarkTag = new() { - { "italic", "i" }, - { "bold", "b" }, - { "underline", "u" } + { + "italic", "i" + }, + { + "bold", "b" + }, + { + "underline", "u" + }, }; - protected override Uri SystemUrl => new("https://novel.ovh/"); - - private StringBuilder AsHtml(RanobeOvhChapterFull chapterFullResponse, - IEnumerable contents) + private StringBuilder AsHtml(RanobeOvhChapterFull chapterFullResponse, IEnumerable contents) { var sb = new StringBuilder(); @@ -66,10 +78,16 @@ private StringBuilder AsHtml(RanobeOvhChapterFull chapterFullResponse, var text = content.Text.HtmlEncode(); foreach (var mark in content.Marks) + { if (MarkTag.TryGetValue(mark.Type, out tag)) + { text = text.CoverTag(tag); + } else + { Config.Logger.LogInformation($"Неизвестый тип форматирования {mark.Type}"); + } + } sb.Append(text); continue; @@ -77,13 +95,18 @@ private StringBuilder AsHtml(RanobeOvhChapterFull chapterFullResponse, case "image": { if (content.Attrs.TryGetValue("pages", out var pages)) + { foreach (var imageId in pages.Deserialize()) { var attachment = chapterFullResponse.Pages.FirstOrDefault(a => a.Id == imageId); - if (attachment is null) continue; + if (attachment == null) + { + continue; + } sb.Append($""); } + } continue; } @@ -98,13 +121,12 @@ private StringBuilder AsHtml(RanobeOvhChapterFull chapterFullResponse, protected override async Task GetChapter(RanobeOvhChapterShort ranobeOvhChapterFull) { - var data = await Config.Client.GetFromJsonAsync( - $"https://api.{SystemUrl.Host}/v2/chapters/{ranobeOvhChapterFull.Id}"); + var data = await Config.Client.GetFromJsonAsync($"https://api.{SystemUrl.Host}/v2/chapters/{ranobeOvhChapterFull.Id}"); return data.Content switch { JsonValue e => e.GetValue().AsHtmlDoc(), JsonObject o => AsHtml(data, o.Deserialize().Content).AsHtmlDoc(), - _ => throw new Elib2EbookParseException("Неизвестный тип") + _ => throw new Exception("Неизвестный тип") }; } } diff --git a/Core/Logic/Getters/RanobeOvh/RanobeOvhGetterBase.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Getters/RanobeOvhGetterBase.cs similarity index 80% rename from Core/Logic/Getters/RanobeOvh/RanobeOvhGetterBase.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Getters/RanobeOvhGetterBase.cs index 14f9e32e..75da435b 100644 --- a/Core/Logic/Getters/RanobeOvh/RanobeOvhGetterBase.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Getters/RanobeOvhGetterBase.cs @@ -1,19 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; using System.Text.Json; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.RanobeOvh; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.RanobeOvh.Types; using HtmlAgilityPack; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.RanobeOvh; +namespace Elib2Ebook.ExternalServices.RanobeOvh.Getters; public abstract class RanobeOvhGetterBase(BookGetterConfig config) : GetterBase(config) { @@ -52,11 +48,13 @@ protected override HttpRequestMessage GetImageRequestMessage(Uri uri) return message; } - private async Task> FillChapters(RanobeOvhBranch branch, RanobeOvhChapterShort[] chapters, - Uri url) + private async Task> FillChapters(RanobeOvhBranch branch, RanobeOvhChapterShort[] chapters, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var ranobeOvhChapter in GetToc(branch, chapters)) { @@ -76,8 +74,7 @@ private async Task> FillChapters(RanobeOvhBranch branch, Ra private T GetNextData(HtmlDocument doc, string node) { - var json = Regex.Match(doc.ParsedText, "__remixContext = (?.*?);", RegexOptions.Singleline) - .Groups["data"].Value; + var json = Regex.Match(doc.ParsedText, "__remixContext = (?.*?);", RegexOptions.Singleline).Groups["data"].Value; return JsonDocument.Parse(json) .RootElement.GetProperty("state") .GetProperty("loaderData") @@ -100,7 +97,10 @@ private RanobeOvhBranch GetBranch(HtmlDocument doc) private Author GetAuthor(RanobeOvhBranch branch) { - if (branch.Translators == null) return new Author("RanobeOvh"); + if (branch.Translators == null) + { + return new Author("RanobeOvh"); + } var translator = branch.Translators[0]; return new Author(translator.Name, SystemUrl.MakeRelativeUri($"/translator/{translator.Slug}")); @@ -108,8 +108,6 @@ private Author GetAuthor(RanobeOvhBranch branch) private Task GetCover(RanobeOvhManga manga, Uri uri) { - return !string.IsNullOrWhiteSpace(manga.Poster) - ? SaveImage(uri.MakeRelativeUri(manga.Poster)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(manga.Poster) ? SaveImage(uri.MakeRelativeUri(manga.Poster)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhBranch.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhBranch.cs new file mode 100644 index 00000000..834ffe78 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhBranch.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.RanobeOvh.Types; + +internal class RanobeOvhBranch +{ + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("translators")] + public RanoveOvhTranslator[] Translators { get; set; } + + [JsonPropertyName("chaptersCount")] + public long ChaptersCount { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhChapterFull.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhChapterFull.cs new file mode 100644 index 00000000..e68a8baa --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhChapterFull.cs @@ -0,0 +1,69 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.RanobeOvh.Types; + +internal class RanobeOvhChapterFull +{ + [JsonPropertyName("pages")] + public RanobeOvhPage[] Pages { get; set; } + + [JsonPropertyName("content")] + public JsonNode Content { get; set; } +} + +public class RanobeOvhChapterShort +{ + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("number")] + public decimal Number { get; set; } + + [JsonPropertyName("volume")] + public int? Volume { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("branchId")] + public string BranchId { get; set; } + + public string FullName + { + get + { + if (!Volume.HasValue) + { + return Title; + } + + var shortName = $"Том {Volume}. Глава {(int)Number}"; + return string.IsNullOrWhiteSpace(Title) ? shortName : Title; + } + } +} + +internal class RanobeOvhChapterContent +{ + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("text")] + public string Text { get; set; } + + [JsonPropertyName("marks")] + public RanobeOvhChapterMark[] Marks { get; set; } = []; + + [JsonPropertyName("content")] + public RanobeOvhChapterContent[] Content { get; set; } = []; + + [JsonPropertyName("attrs")] + public Dictionary Attrs { get; set; } = new(); +} + +internal class RanobeOvhChapterMark +{ + [JsonPropertyName("type")] + public string Type { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhManga.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhManga.cs new file mode 100644 index 00000000..03582f23 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhManga.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.RanobeOvh.Types; + +internal class RanobeOvhManga +{ + [JsonPropertyName("name")] + public RanobeOvhMangaName Name { get; set; } + + [JsonPropertyName("poster")] + public string Poster { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhMangaName.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhMangaName.cs new file mode 100644 index 00000000..28d1c139 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhMangaName.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.RanobeOvh.Types; + +internal class RanobeOvhMangaName +{ + [JsonPropertyName("ru")] + public string Ru { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhPage.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhPage.cs new file mode 100644 index 00000000..e1b3ddd7 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanobeOvhPage.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.RanobeOvh.Types; + +internal class RanobeOvhPage +{ + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("image")] + public string Image { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanoveOvhTranslator.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanoveOvhTranslator.cs new file mode 100644 index 00000000..2807c011 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobeOvh/Types/RanoveOvhTranslator.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.RanobeOvh.Types; + +internal class RanoveOvhTranslator +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("slug")] + public string Slug { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RanobesCom/Elib2Ebook.ExternalServices.RanobesCom.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobesCom/Elib2Ebook.ExternalServices.RanobesCom.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobesCom/Elib2Ebook.ExternalServices.RanobesCom.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/RanobesComGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobesCom/RanobesComGetter.cs similarity index 73% rename from Core/Logic/Getters/RanobesComGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.RanobesCom/RanobesComGetter.cs index 2596a876..a41bf02b 100644 --- a/Core/Logic/Getters/RanobesComGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobesCom/RanobesComGetter.cs @@ -1,31 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Http.Json; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Ranobes; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.RanobesCom.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.RanobesCom; public class RanobesComGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://ranobes.com/"); protected override string GetId(Uri url) - { - return base.GetId(url).Split(".")[0]; - } + => base.GetId(url).Split(".")[0]; public override async Task Get(Uri url) { @@ -60,30 +54,67 @@ private async Task Antibot(HtmlDocument doc, Uri referrer) var data = new Dictionary { - { "hdc", "0" }, - { "scheme", "https" }, - { "a", "0" }, - { "date", date }, - { "country", "RU" }, - { "h1", h1 }, - { "h2", h2 }, - { "ip", ip }, - { "v", v }, - { "cid", cid }, - { "ptr", ptr }, - { "w", "2560" }, - { "h", "1440" }, - { "cw", "2560" }, - { "ch", "770" }, - { "co", "24" }, - { "pi", "24" }, - { "ref", "ranobes.com" }, - { "xxx", string.Empty } + { + "hdc", "0" + }, + { + "scheme", "https" + }, + { + "a", "0" + }, + { + "date", date + }, + { + "country", "RU" + }, + { + "h1", h1 + }, + { + "h2", h2 + }, + { + "ip", ip + }, + { + "v", v + }, + { + "cid", cid + }, + { + "ptr", ptr + }, + { + "w", "2560" + }, + { + "h", "1440" + }, + { + "cw", "2560" + }, + { + "ch", "770" + }, + { + "co", "24" + }, + { + "pi", "24" + }, + { + "ref", "ranobes.com" + }, + { + "xxx", string.Empty + }, }; Config.Client.DefaultRequestHeaders.Add("Referer", referrer.ToString()); - var post = await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("antibot8/ab.php"), - new FormUrlEncodedContent(data)); + var post = await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("antibot8/ab.php"), new FormUrlEncodedContent(data)); var cookie = await post.Content.ReadFromJsonAsync(); Config.CookieContainer.Add(SystemUrl, new Cookie($"antibot_{antibot}", cookie.Cookie + "-" + date)); Config.Client.DefaultRequestHeaders.Remove("Referer"); @@ -103,7 +134,10 @@ private async Task GetMainUrl(Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var ranobeChapter in await GetToc(GetTocLink(doc, url))) { @@ -133,7 +167,10 @@ private async Task GetChapter(UrlChapter chapter) } else { - if (node.InnerHtml?.Contains("window.yaContextCb") == false) sb.Append(node.InnerHtml.CoverTag(tag)); + if (node.InnerHtml?.Contains("window.yaContextCb") == false) + { + sb.Append(node.InnerHtml.CoverTag(tag)); + } } } @@ -143,17 +180,16 @@ private async Task GetChapter(UrlChapter chapter) private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("div.poster img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private Uri GetTocLink(HtmlDocument doc, Uri uri) { - var relativeUri = doc.QuerySelector("div.r-fullstory-btns a[title~=оглавление]").Attributes["href"].Value - .HtmlDecode(); + var relativeUri = doc.QuerySelector("div.r-fullstory-btns a[title~=оглавление]").Attributes["href"].Value.HtmlDecode(); if (!relativeUri.Contains("chapters")) + { relativeUri = $"/chapters/{string.Join("-", GetId(uri).Split(".")[0].Split("-").Skip(1))}"; + } return uri.MakeRelativeUri(relativeUri.AsUri().AbsolutePath.Trim('/')); } @@ -171,10 +207,7 @@ private async Task> GetToc(Uri tocUri) doc = await Config.Client.GetHtmlDocWithTriesAsync(tocUri.AppendSegment($"/page/{i}")); var chapters = doc .QuerySelectorAll("#dle-content > .cat_block.cat_line a[title]") - .Select(a => new UrlChapter(a.Attributes["href"].Value.AsUri(), - string.IsNullOrWhiteSpace(a.Attributes["title"].Value) - ? "Без названия" - : a.Attributes["title"].Value)) + .Select(a => new UrlChapter(a.Attributes["href"].Value.AsUri(), string.IsNullOrWhiteSpace(a.Attributes["title"].Value) ? "Без названия" : a.Attributes["title"].Value)) .ToList(); result.AddRange(chapters); diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RanobesCom/Types/RanobesCookie.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobesCom/Types/RanobesCookie.cs new file mode 100644 index 00000000..51c5f31a --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RanobesCom/Types/RanobesCookie.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.RanobesCom.Types; + +internal class RanobesCookie +{ + [JsonPropertyName("cookie")] + public string Cookie { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.ReadManga/Elib2Ebook.ExternalServices.ReadManga.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.ReadManga/Elib2Ebook.ExternalServices.ReadManga.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.ReadManga/Elib2Ebook.ExternalServices.ReadManga.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/ReadMangaGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.ReadManga/ReadMangaGetter.cs similarity index 76% rename from Core/Logic/Getters/ReadMangaGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.ReadManga/ReadMangaGetter.cs index 9d8caa8f..20f9ac6a 100644 --- a/Core/Logic/Getters/ReadMangaGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.ReadManga/ReadMangaGetter.cs @@ -1,28 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.ReadManga; public class ReadMangaGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://readmanga.live/"); protected override string GetId(Uri url) - { - return url.GetSegment(1); - } + => url.GetSegment(1); public override async Task Get(Uri url) { @@ -44,7 +39,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in GetToc(doc, url)) { @@ -56,7 +54,7 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, url); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -75,16 +73,19 @@ private IEnumerable GetToc(HtmlDocument doc, Uri url) .Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText().ReplaceNewLine())) .Reverse() .ToList(); + return SliceToc(result, c => c.Title); } private async Task GetChapter(UrlChapter urlChapter) { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url.AppendQueryParameter("mtr", "1")); - if (doc.QuerySelector("div.buy-button") is not null) return default; + if (doc.QuerySelector("div.buy-button") != null) + { + return null; + } - var json = Regex.Match(doc.ParsedText, @"readerInit(.*?)(?\[\[(.*?)]]),").Groups["data"].Value - .Replace("'", "\"").Deserialize>(); + var json = Regex.Match(doc.ParsedText, @"readerInit(.*?)(?\[\[(.*?)]]),").Groups["data"].Value.Replace("'", "\"").Deserialize>(); var sb = new StringBuilder(); @@ -92,7 +93,10 @@ private async Task GetChapter(UrlChapter urlChapter) { var imageUri = elem[0].GetString().AsUri().MakeRelativeUri(elem[2].GetString()); // Это не костыль. На сайте также - if (imageUri.Host == "one-way.work") imageUri = imageUri.MakeRelativeUri(imageUri.AbsolutePath); + if (imageUri.Host == "one-way.work") + { + imageUri = imageUri.MakeRelativeUri(imageUri.AbsolutePath); + } sb.Append($""); } @@ -103,8 +107,6 @@ private async Task GetChapter(UrlChapter urlChapter) private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("div.picture-fotorama img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Readli/Elib2Ebook.ExternalServices.Readli.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Readli/Elib2Ebook.ExternalServices.Readli.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Readli/Elib2Ebook.ExternalServices.Readli.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/ReadliGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Readli/ReadliGetter.cs similarity index 77% rename from Core/Logic/Getters/ReadliGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Readli/ReadliGetter.cs index fe4d8d10..d9ae05d8 100644 --- a/Core/Logic/Getters/ReadliGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Readli/ReadliGetter.cs @@ -1,23 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Readli; public class ReadliGetter(BookGetterConfig config) : GetterBase(config) { + protected override Uri SystemUrl => new("https://readli.net"); + // cloudflare :( private readonly Uri _host = new("https://213.227.140.81/"); - protected override Uri SystemUrl => new("https://readli.net"); public override async Task Init() { @@ -52,46 +50,45 @@ public override async Task Get(Uri url) private static Author GetAuthor(HtmlDocument doc, Uri url) { var author = doc.QuerySelector("a.main-info__link"); - return new Author(author.InnerText.HtmlDecode(), - url.MakeRelativeUri(author.Attributes["href"]?.Value ?? string.Empty)); + return new Author(author.InnerText.HtmlDecode(), url.MakeRelativeUri(author.Attributes["href"]?.Value ?? string.Empty)); } private static Seria GetSeria(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("div.book-info a.book-info__link[href^=/serie/]"); - if (string.IsNullOrWhiteSpace(a.GetText())) return default; + if (string.IsNullOrWhiteSpace(a.GetText())) + { + return null; + } var text = a.GetText(); var uri = url.MakeRelativeUri(a.Attributes["href"].Value); if (!text.Contains('#')) + { return new Seria { - Name = text, - Url = uri + Name = text, Url = uri }; + } var parts = text.Split('#', StringSplitOptions.RemoveEmptyEntries); return new Seria { - Name = parts[0].HtmlDecode(), - Number = parts[1].HtmlDecode(), - Url = uri + Name = parts[0].HtmlDecode(), Number = parts[1].HtmlDecode(), Url = uri }; } private static string GetBookId(Uri uri) { - return uri.GetQueryParameter("b") ?? - throw new InvalidOperationException("Не удалось определить идентификатор книги"); + return uri.GetQueryParameter("b") ?? throw new InvalidOperationException("Не удалось определить идентификатор книги"); } private async Task GetMainUrl(Uri url) { if (GetId(url).StartsWith("chitat-online", StringComparison.InvariantCultureIgnoreCase)) { - var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl - .MakeRelativeUri($"/chitat-online/?b={GetBookId(url)}").ReplaceHost(_host.Host)); + var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/chitat-online/?b={GetBookId(url)}").ReplaceHost(_host.Host)); var href = doc.QuerySelector("h1 a").Attributes["href"].Value; return url.MakeRelativeUri(href).ReplaceHost(_host.Host); } @@ -102,18 +99,18 @@ private async Task GetMainUrl(Uri url) private Task GetCover(HtmlNode doc, Uri bookUri) { var imagePath = doc.QuerySelector("img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath).ReplaceHost(_host.Host)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath).ReplaceHost(_host.Host)) : Task.FromResult(default(TempFile)); } private async Task AddChapter(ICollection chapters, Chapter chapter, StringBuilder text) { - if (chapter == null) return; + if (chapter == null) + { + return; + } var chapterDoc = text.AsHtmlDoc(); - chapter.Images = - await GetImages(chapterDoc, SystemUrl.MakeRelativeUri("/chitat-online/").ReplaceHost(_host.Host)); + chapter.Images = await GetImages(chapterDoc, SystemUrl.MakeRelativeUri("/chitat-online/").ReplaceHost(_host.Host)); chapter.Content = chapterDoc.DocumentNode.InnerHtml; chapters.Add(chapter); } @@ -134,9 +131,11 @@ private static Chapter CreateChapter(string title) private async Task GetChapter(string bookId, int page) { - var response = await Config.Client.GetWithTriesAsync(SystemUrl - .MakeRelativeUri($"/chitat-online/?b={bookId}&pg={page}").ReplaceHost(_host.Host)); - if (response is null) return default; + var response = await Config.Client.GetWithTriesAsync(SystemUrl.MakeRelativeUri($"/chitat-online/?b={bookId}&pg={page}").ReplaceHost(_host.Host)); + if (response == null) + { + return null; + } return await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc()); } @@ -144,7 +143,10 @@ private async Task GetChapter(string bookId, int page) private async Task> FillChapters(string bookId, long pages, string name) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } Chapter chapter = null; var singleChapter = true; @@ -154,7 +156,7 @@ private async Task> FillChapters(string bookId, long pages, string { Config.Logger.LogInformation($"Получаю страницу {i}/{pages}"); var page = await GetChapter(bookId, i); - if (page is null) + if (page == null) { Config.Logger.LogInformation("Неожиданный конец"); break; @@ -166,6 +168,7 @@ private async Task> FillChapters(string bookId, long pages, string singleChapter = i == 1 ? IsSingleChapter(nodes) : singleChapter; foreach (var node in nodes) + { if (singleChapter || node.Name != "h3") { if (node.Name == "img" && node.Attributes["src"] != null) @@ -175,7 +178,9 @@ private async Task> FillChapters(string bookId, long pages, string else { if (!string.IsNullOrWhiteSpace(node.InnerHtml)) + { text.Append(node.InnerHtml.HtmlDecode().CoverTag("p")); + } } } else @@ -184,6 +189,7 @@ private async Task> FillChapters(string bookId, long pages, string text.Clear(); chapter = CreateChapter(node.InnerHtml.HtmlDecode()); } + } } await AddChapter(result, chapter ?? CreateChapter(name), text); diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Elib2Ebook.ExternalServices.Renovels.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Elib2Ebook.ExternalServices.Renovels.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Elib2Ebook.ExternalServices.Renovels.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/Renovels/RemangaGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Getters/RemangaGetter.cs similarity index 68% rename from Core/Logic/Getters/Renovels/RemangaGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Getters/RemangaGetter.cs index 48ba6bb2..36b5cb8a 100644 --- a/Core/Logic/Getters/Renovels/RemangaGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Getters/RemangaGetter.cs @@ -1,13 +1,12 @@ -using System; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; -using Core.Configs; -using Core.Extensions; -using Core.Types.Renovels; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.ExternalServices.Renovels.Types; using HtmlAgilityPack; -namespace Core.Logic.Getters.Renovels; +namespace Elib2Ebook.ExternalServices.Renovels.Getters; public class RemangaGetter(BookGetterConfig config) : RenovelsGetterBase(config) { @@ -28,12 +27,13 @@ protected override HtmlDocument GetChapterAsHtml(RenovelsChapter response) break; case JsonArray pages: { - foreach (var page in pages) sb.Append($""); + foreach (var page in pages) + { + sb.Append($""); + } break; } - default: - break; } } diff --git a/Core/Logic/Getters/Renovels/RenovelsGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Getters/RenovelsGetter.cs similarity index 65% rename from Core/Logic/Getters/Renovels/RenovelsGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Getters/RenovelsGetter.cs index cc7e97aa..2a1bd0d4 100644 --- a/Core/Logic/Getters/Renovels/RenovelsGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Getters/RenovelsGetter.cs @@ -1,10 +1,9 @@ -using System; -using Core.Configs; -using Core.Extensions; -using Core.Types.Renovels; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.ExternalServices.Renovels.Types; using HtmlAgilityPack; -namespace Core.Logic.Getters.Renovels; +namespace Elib2Ebook.ExternalServices.Renovels.Getters; public class RenovelsGetter(BookGetterConfig config) : RenovelsGetterBase(config) { diff --git a/Core/Logic/Getters/Renovels/RenovelsGetterBase.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Getters/RenovelsGetterBase.cs similarity index 73% rename from Core/Logic/Getters/Renovels/RenovelsGetterBase.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Getters/RenovelsGetterBase.cs index 32b24ee4..a85f2227 100644 --- a/Core/Logic/Getters/Renovels/RenovelsGetterBase.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Getters/RenovelsGetterBase.cs @@ -1,21 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Renovels; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Renovels.Types; using HtmlAgilityPack; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.Renovels; +namespace Elib2Ebook.ExternalServices.Renovels.Getters; public abstract class RenovelsGetterBase(BookGetterConfig config) : GetterBase(config) { @@ -26,9 +22,7 @@ public abstract class RenovelsGetterBase(BookGetterConfig config) : GetterBase(c protected abstract HtmlDocument GetChapterAsHtml(RenovelsChapter response); protected override string GetId(Uri url) - { - return url.GetSegment(2); - } + => url.GetSegment(2); public override Task Init() { @@ -39,16 +33,17 @@ public override Task Init() public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var payload = new { - user = Config.Options.Login, - password = Config.Options.Password + user = Config.Options.Login, password = Config.Options.Password }; - var response = - await Config.Client.PostAsJsonAsync("https://api.renovels.org/api/users/login/".AsUri(), payload); + var response = await Config.Client.PostAsJsonAsync("https://api.renovels.org/api/users/login/".AsUri(), payload); var data = await response.Content.ReadFromJsonAsync>(); if (string.IsNullOrWhiteSpace(data.Message)) { @@ -58,7 +53,7 @@ public override async Task Authorize() } else { - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {data.Message}"); + throw new Exception($"Не удалось авторизоваться. {data.Message}"); } } @@ -81,9 +76,15 @@ public override async Task Get(Uri url) private Author GetAuthor(RenovelsContent content) { - if (content.Branches.Length == 0) return new Author("Renovels"); + if (content.Branches.Length == 0) + { + return new Author("Renovels"); + } - if (content.Branches[0].Publishers.Length == 0) return new Author("Renovels"); + if (content.Branches[0].Publishers.Length == 0) + { + return new Author("Renovels"); + } var author = content.Branches[0].Publishers[0]; return new Author(author.Name, SystemUrl.MakeRelativeUri($"/team/{author.Dir}")); @@ -91,15 +92,17 @@ private Author GetAuthor(RenovelsContent content) private async Task GetContent(string bookId) { - var response = - await Config.Client.GetFromJsonAsync(_apiUrl.MakeRelativeUri($"/api/v2/titles/{bookId}/")); + var response = await Config.Client.GetFromJsonAsync(_apiUrl.MakeRelativeUri($"/api/v2/titles/{bookId}/")); return response; } private async Task> FillChapters(RenovelsContent content, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var ranobeChapter in await GetToc(content)) { @@ -128,9 +131,7 @@ private async Task GetChapter(RenovelsChapter ranobeChapter) private Task GetCover(RenovelsContent book, Uri bookUri) { var imagePath = book.Cover.GetValueOrDefault("high", null) ?? book.Cover.FirstOrDefault().Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private async Task> GetToc(RenovelsContent content) @@ -139,8 +140,7 @@ private async Task> GetToc(RenovelsContent content) for (var i = 1;; i++) { - var uri = _apiUrl.MakeRelativeUri( - $"/api/v2/titles/chapters/?branch_id={content.Branches[0].Id}&ordering=-index&user_data=1&count=40&page={i}"); + var uri = _apiUrl.MakeRelativeUri($"/api/v2/titles/chapters/?branch_id={content.Branches[0].Id}&ordering=-index&user_data=1&count=40&page={i}"); var response = await Config.Client.GetFromJsonAsync(uri); result.AddRange(response.Results); diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsApiResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsApiResponse.cs new file mode 100644 index 00000000..5992997d --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsApiResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Renovels.Types; + +internal class RenovelsApiResponse +{ + [JsonPropertyName("msg")] + public string Message { get; set; } + + [JsonPropertyName("content")] + public T Content { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsAuthResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsAuthResponse.cs new file mode 100644 index 00000000..d79a7efe --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsAuthResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Renovels.Types; + +internal class RenovelsAuthResponse +{ + [JsonPropertyName("access_token")] + public string Token { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsBranch.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsBranch.cs new file mode 100644 index 00000000..57716326 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsBranch.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Renovels.Types; + +internal class RenovelsBranch +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("publishers")] + public RenovelsPublisher[] Publishers { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsChapter.cs new file mode 100644 index 00000000..be41412d --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsChapter.cs @@ -0,0 +1,45 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Renovels.Types; + +public class RenovelsChapter +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("chapter")] + public string Chapter { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("tome")] + public int Tome { get; set; } + + [JsonPropertyName("content")] + public string Content { get; set; } + + [JsonPropertyName("is_paid")] + public bool IsPaid { get; set; } + + [JsonPropertyName("is_bought")] + public bool? IsBought { get; set; } + + [JsonPropertyName("pages")] + public JsonArray Pages { get; set; } + + public string Title + { + get + { + var name = $"Том {Tome}. Глава {Chapter}"; + if (string.IsNullOrWhiteSpace(Name)) + { + return name; + } + + return name + $" - {Name}"; + } + } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsContent.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsContent.cs new file mode 100644 index 00000000..6605cc94 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsContent.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Renovels.Types; + +internal class RenovelsContent +{ + [JsonPropertyName("cover")] + public Dictionary Cover { get; set; } + + [JsonPropertyName("main_name")] + public string MainName { get; set; } + + [JsonPropertyName("secondary_name")] + public string SecondaryName { get; set; } + + [JsonPropertyName("another_name")] + public string AnotherName { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("branches")] + public RenovelsBranch[] Branches { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsPage.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsPage.cs new file mode 100644 index 00000000..5fd9e903 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsPage.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Renovels.Types; + +internal class RenovelsPage +{ + [JsonPropertyName("link")] + public string Link { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsPublisher.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsPublisher.cs new file mode 100644 index 00000000..043265c7 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsPublisher.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Renovels.Types; + +internal class RenovelsPublisher +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("dir")] + public string Dir { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsTocResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsTocResponse.cs new file mode 100644 index 00000000..b383aecd --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Renovels/Types/RenovelsTocResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Renovels.Types; + +internal class RenovelsTocResponse +{ + [JsonPropertyName("results")] + public RenovelsChapter[] Results { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Romfant/Elib2Ebook.ExternalServices.Romfant.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Romfant/Elib2Ebook.ExternalServices.Romfant.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Romfant/Elib2Ebook.ExternalServices.Romfant.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/RomfantGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Romfant/RomfantGetter.cs similarity index 70% rename from Core/Logic/Getters/RomfantGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Romfant/RomfantGetter.cs index c0651a5f..bafd9dbd 100644 --- a/Core/Logic/Getters/RomfantGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Romfant/RomfantGetter.cs @@ -1,48 +1,55 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Romfant; public class RomfantGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://romfant.ru"); /// - /// Авторизация в системе + /// Авторизация в системе /// /// public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } using var post = await Config.Client.PostAsync(SystemUrl.MakeRelativeUri("ajax/"), GenerateAuthData()); var response = await post.Content.ReadAsStringAsync(); if (response == "ok") + { Config.Logger.LogInformation("Успешно авторизовались"); + } else - throw new Elib2EbookAuthException( - $"Не удалось авторизоваться. {response.AsHtmlDoc().DocumentNode.GetText()}"); + { + throw new Exception($"Не удалось авторизоваться. {response.AsHtmlDoc().DocumentNode.GetText()}"); + } } private MultipartFormDataContent GenerateAuthData() { - return new MultipartFormDataContent + return new() { - { new StringContent("auth"), "action" }, - { new StringContent(Config.Options.Login), "login" }, - { new StringContent(Config.Options.Password), "pass" } + { + new StringContent("auth"), "action" + }, + { + new StringContent(Config.Options.Login), "login" + }, + { + new StringContent(Config.Options.Password), "pass" + }, }; } @@ -59,7 +66,7 @@ public override async Task Get(Uri url) Chapters = await FillChapters(bookId, title), Title = title, Author = new Author("Витамина Мятная"), - Annotation = doc.QuerySelector("div.subscribe div.text-center p")?.InnerHtml.HtmlDecode() + Annotation = doc.QuerySelector("div.subscribe div.text-center p")?.InnerHtml.HtmlDecode(), }; return book; @@ -67,7 +74,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(string bookId, string title) { - if (Config.Options.NoChapters) return []; + if (Config.Options.NoChapters) + { + return []; + } var chapter = new Chapter(); var sb = new StringBuilder(); @@ -89,14 +99,20 @@ private async Task> FillChapters(string bookId, string titl foreach (var p in doc.QuerySelectorAll("div.book p")) { var content = p.GetText(); - if (!string.IsNullOrWhiteSpace(content)) sb.AppendLine(content.CoverTag("p")); + if (!string.IsNullOrWhiteSpace(content)) + { + sb.AppendLine(content.CoverTag("p")); + } } } chapter.Title = title; chapter.Content = sb.AsHtmlDoc().DocumentNode.InnerHtml; - return new[] { chapter }; + return + [ + chapter, + ]; } private async Task GetPages(string bookId) @@ -113,8 +129,6 @@ private async Task GetPages(string bookId) private Task GetCover(HtmlDocument doc) { var imagePath = doc.QuerySelector("div.subscribe img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(SystemUrl.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(SystemUrl.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.RoyalRoad/Elib2Ebook.ExternalServices.RoyalRoad.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.RoyalRoad/Elib2Ebook.ExternalServices.RoyalRoad.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RoyalRoad/Elib2Ebook.ExternalServices.RoyalRoad.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/RoyalRoadGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.RoyalRoad/RoyalRoadGetter.cs similarity index 77% rename from Core/Logic/Getters/RoyalRoadGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.RoyalRoad/RoyalRoadGetter.cs index 59df50c3..62cb6580 100644 --- a/Core/Logic/Getters/RoyalRoadGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.RoyalRoad/RoyalRoadGetter.cs @@ -1,25 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.RoyalRoad; public class RoyalRoadGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://royalroad.com/"); protected override string GetId(Uri url) - { - return url.GetSegment(2); - } + => url.GetSegment(2); public override async Task Get(Uri url) { @@ -32,7 +27,7 @@ public override async Task Get(Uri url) Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1"), Author = GetAuthor(doc, url), - Annotation = doc.QuerySelector("div.description div.hidden-content")?.InnerHtml + Annotation = doc.QuerySelector("div.description div.hidden-content")?.InnerHtml, }; return book; @@ -41,7 +36,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var bookChapter in GetToc(doc, url)) { @@ -65,7 +63,10 @@ private async Task GetChapter(Uri url) var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); var content = doc.QuerySelector("div.chapter-content"); - foreach (var node in content.QuerySelectorAll("p, span")) node.Attributes.RemoveAll(); + foreach (var node in content.QuerySelectorAll("p, span")) + { + node.Attributes.RemoveAll(); + } return content.InnerHtml.AsHtmlDoc(); } @@ -84,16 +85,14 @@ private IEnumerable GetToc(HtmlDocument doc, Uri url) private Author GetAuthor(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("div.fic-header h4 a[href*=profile]"); - return a is null - ? new Author("Royalroad") - : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); + return a == null ? + new Author("Royalroad") : + new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); } private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("div.cover-art-container img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Elib2Ebook.ExternalServices.Rulate.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Elib2Ebook.ExternalServices.Rulate.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Elib2Ebook.ExternalServices.Rulate.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/Rulate/BllateGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/BllateGetter.cs similarity index 67% rename from Core/Logic/Getters/Rulate/BllateGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/BllateGetter.cs index 105a6cbe..ccdc23c2 100644 --- a/Core/Logic/Getters/Rulate/BllateGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/BllateGetter.cs @@ -1,7 +1,6 @@ -using System; -using Core.Configs; +using Elib2Ebook.DomainServices.Configs; -namespace Core.Logic.Getters.Rulate; +namespace Elib2Ebook.ExternalServices.Rulate.Getters; public class BllateGetter(BookGetterConfig config) : RulateGetterBase(config) { diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/ErolateGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/ErolateGetter.cs new file mode 100644 index 00000000..120e7667 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/ErolateGetter.cs @@ -0,0 +1,9 @@ +using Elib2Ebook.DomainServices.Configs; + +namespace Elib2Ebook.ExternalServices.Rulate.Getters; + +public class ErolateGetter(BookGetterConfig config) : RulateGetterBase(config) +{ + protected override Uri SystemUrl => new("https://erolate.com/"); + protected override string Mature => "7da3ee594b38fc5355692d978fe8f5adbeb3d17di%3A1%3B"; +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/MlateGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/MlateGetter.cs new file mode 100644 index 00000000..29587e31 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/MlateGetter.cs @@ -0,0 +1,9 @@ +using Elib2Ebook.DomainServices.Configs; + +namespace Elib2Ebook.ExternalServices.Rulate.Getters; + +public class MlateGetter(BookGetterConfig config) : RulateGetterBase(config) +{ + protected override Uri SystemUrl => new("https://mlate.ru/"); + protected override string Mature => "c3a2ed4b199a1a15f5a5483504c7a75a7030dc4bi%3A1%3B"; +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/RulateGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/RulateGetter.cs new file mode 100644 index 00000000..58546f8e --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/RulateGetter.cs @@ -0,0 +1,9 @@ +using Elib2Ebook.DomainServices.Configs; + +namespace Elib2Ebook.ExternalServices.Rulate.Getters; + +public class RulateGetter(BookGetterConfig config) : RulateGetterBase(config) +{ + protected override Uri SystemUrl => new("https://tl.rulate.ru"); + protected override string Mature => "c3a2ed4b199a1a15f5a5483504c7a75a7030dc4bi%3A1%3B"; +} diff --git a/Core/Logic/Getters/Rulate/RulateGetterBase.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/RulateGetterBase.cs similarity index 68% rename from Core/Logic/Getters/Rulate/RulateGetterBase.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/RulateGetterBase.cs index 64afc49d..f3d42933 100644 --- a/Core/Logic/Getters/Rulate/RulateGetterBase.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Getters/RulateGetterBase.cs @@ -1,30 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Rulate; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Rulate.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.Rulate; +namespace Elib2Ebook.ExternalServices.Rulate.Getters; public abstract class RulateGetterBase(BookGetterConfig config) : GetterBase(config) { - protected abstract string Mature { get; } - protected override string GetId(Uri url) - { - return url.Segments.Length == 3 ? base.GetId(url) : url.GetSegment(2); - } + => url.Segments.Length == 3 ? base.GetId(url) : url.GetSegment(2); + + protected abstract string Mature { get; } public override async Task Init() { @@ -35,40 +28,44 @@ public override async Task Init() public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } - var doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl, GetAuthData()); + var main = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl); + var doc = await Config.Client.PostHtmlDocWithTriesAsync(SystemUrl, GetAuthData(main)); var alertBlock = doc.QuerySelector("div.alert.in.alert-block"); - if (alertBlock is null) + if (alertBlock == null) { RulateAuthResponse error = null; try { error = doc.ParsedText.Deserialize(); } - catch - { - // Ignore deserialization errors if the response is not valid JSON - } + catch { } if (!string.IsNullOrWhiteSpace(error?.Error)) - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {error.Error}"); + { + throw new Exception($"Не удалось авторизоваться. {error.Error}"); + } Config.Logger.LogInformation("Успешно авторизовались"); } else { - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {alertBlock.GetText()}"); + throw new Exception($"Не удалось авторизоваться. {alertBlock.GetText()}"); } } - private FormUrlEncodedContent GetAuthData() + private FormUrlEncodedContent GetAuthData(HtmlDocument doc) { var data = new Dictionary { ["login[login]"] = Config.Options.Login, - ["login[pass]"] = Config.Options.Password + ["login[pass]"] = Config.Options.Password, + ["csrf_token"] = doc.QuerySelector("meta[name=csrf-token]").Attributes["content"].Value }; return new FormUrlEncodedContent(data); @@ -107,12 +104,15 @@ private Author GetAuthor(HtmlDocument doc) if (strong != null && strong.InnerText.Contains("Автор")) { var em = p.QuerySelector("em"); - if (em is null) return def; + if (em == null) + { + return def; + } var a = em.QuerySelector("a[href]"); - return a is null - ? new Author(em.GetText()) - : new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); + return a == null ? + new Author(em.GetText()) : + new Author(a.GetText(), SystemUrl.MakeRelativeUri(a.Attributes["href"].Value)); } } @@ -122,15 +122,16 @@ private Author GetAuthor(HtmlDocument doc) private Task GetCover(HtmlDocument doc, Uri bookUri) { var imagePath = doc.QuerySelector("div.slick img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(bookUri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(bookUri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private async Task> FillChapters(HtmlDocument doc, Uri bookUri, string bookId) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var (id, name) in GetToc(doc)) { @@ -150,11 +151,8 @@ private async Task> FillChapters(HtmlDocument doc, Uri bookUri, st private async Task GetChapter(string bookId, string chapterId) { - var s = await Config.Client.GetFromJsonWithTriesAsync( - SystemUrl.MakeRelativeUri($"/book/{bookId}/{chapterId}/readyajax")); - return (s.CanRead - ? s.Content.AsHtmlDoc().QuerySelector("div.content-text")?.InnerHtml ?? string.Empty - : string.Empty).AsHtmlDoc(); + var s = await Config.Client.GetFromJsonWithTriesAsync(SystemUrl.MakeRelativeUri($"/book/{bookId}/{chapterId}/readyajax")); + return (s.CanRead ? s.Content.AsHtmlDoc().QuerySelector("div.content-text")?.InnerHtml ?? string.Empty : string.Empty).AsHtmlDoc(); } private IEnumerable GetToc(HtmlDocument doc) diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Types/RulateAuthResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Types/RulateAuthResponse.cs new file mode 100644 index 00000000..8843479f --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Types/RulateAuthResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Rulate.Types; + +internal class RulateAuthResponse +{ + [JsonPropertyName("error")] + public string Error { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Types/RulateChapter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Types/RulateChapter.cs new file mode 100644 index 00000000..c6cd36c6 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Rulate/Types/RulateChapter.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Rulate.Types; + +internal class RulateChapter +{ + [JsonPropertyName("can_read")] + public bool CanRead { get; set; } + + [JsonPropertyName("content")] + public string Content { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Samlib/Elib2Ebook.ExternalServices.Samlib.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Samlib/Elib2Ebook.ExternalServices.Samlib.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Samlib/Elib2Ebook.ExternalServices.Samlib.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/SamlibGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Samlib/SamlibGetter.cs similarity index 78% rename from Core/Logic/Getters/SamlibGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Samlib/SamlibGetter.cs index 8b6f3dbe..8a5b3110 100644 --- a/Core/Logic/Getters/SamlibGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Samlib/SamlibGetter.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Samlib; public class SamlibGetter(BookGetterConfig config) : GetterBase(config) { @@ -22,7 +19,7 @@ public class SamlibGetter(BookGetterConfig config) : GetterBase(config) private const string START_LINK_BLOCK_PATTERN = "Блок ссылок на произведения"; private const string END_LINK_BLOCK_PATTERN = "Подножие"; - private static readonly Encoding _encoding = Encoding.GetEncoding(1251); + private static Encoding _encoding = Encoding.GetEncoding(1251); protected override Uri SystemUrl => new("http://samlib.ru/"); @@ -33,10 +30,7 @@ public override async Task Get(Uri url) var title = doc.GetTextBySelector("h2, h3 font"); var book = new Book(url) { - Cover = null, - Chapters = await FillChapters(doc, url, title), - Title = title, - Author = GetAuthor(doc, url) + Cover = null, Chapters = await FillChapters(doc, url, title), Title = title, Author = GetAuthor(doc, url) }; return book; @@ -47,10 +41,16 @@ private static Author GetAuthor(HtmlDocument doc, Uri url) var def = new Author("Samlib"); var h3 = doc.QuerySelector("h3"); - if (h3 is null) return def; + if (h3 == null) + { + return def; + } var a = h3.QuerySelector("a[href]"); - if (a is null) return def; + if (a == null) + { + return def; + } return new Author(h3.RemoveNodes("small").GetText().Trim(':'), url.MakeRelativeUri(a.Attributes["href"].Value)); } @@ -58,7 +58,10 @@ private static Author GetAuthor(HtmlDocument doc, Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url, string title) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in GetToc(doc, url, title)) { @@ -73,26 +76,37 @@ private IEnumerable GetToc(HtmlDocument doc, Uri url, string title) { var content = GetStringBetween(doc.Text, START_LINK_BLOCK_PATTERN, END_LINK_BLOCK_PATTERN); if (string.IsNullOrWhiteSpace(content)) + { yield return new UrlChapter(url, title); + } else + { foreach (var a in content.AsHtmlDoc().QuerySelectorAll("li > a")) + { yield return new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText()); + } + } } private static string GetStringBetween(string origin, string startPattern, string endPattern) { var start = origin.IndexOf(startPattern, StringComparison.InvariantCultureIgnoreCase); - if (start == -1) return string.Empty; + if (start == -1) + { + return string.Empty; + } start = origin.IndexOf(">", start, StringComparison.InvariantCultureIgnoreCase) + 1; var stop = origin.IndexOf(endPattern, start, StringComparison.InvariantCultureIgnoreCase); for (var i = stop;; i--) + { if (origin[i] == '<') { stop = i - 1; break; } + } return origin[start..stop]; } @@ -100,20 +114,28 @@ private static string GetStringBetween(string origin, string startPattern, strin private static string GetBookContent(string origin) { var start = origin.IndexOf(START_BOOK_PATTERN, StringComparison.InvariantCultureIgnoreCase); - if (start == -1) return string.Empty; + if (start == -1) + { + return string.Empty; + } var about = origin.LastIndexOf(ABOUT_BLOCK_PATTERN, StringComparison.OrdinalIgnoreCase); - if (about == -1) return string.Empty; + if (about == -1) + { + return string.Empty; + } start = origin.IndexOf(">", start, StringComparison.InvariantCultureIgnoreCase) + 1; var stop = origin.LastIndexOf(END_BOOK_PATTERN, about, StringComparison.InvariantCultureIgnoreCase); for (var i = stop;; i--) + { if (origin[i] == '<') { stop = i - 1; break; } + } return origin[start..stop].HtmlDecode(); } @@ -130,14 +152,24 @@ private async Task GetChapter(UrlChapter urlChapter) while (true) { var line = await sr.ReadLineAsync(); - if (line == null) break; + if (line == null) + { + break; + } - if (string.IsNullOrWhiteSpace(line)) continue; + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } var htmlDoc = line.AsHtmlDoc(); foreach (var node in htmlDoc.DocumentNode.ChildNodes) + { if (!string.IsNullOrWhiteSpace(node.InnerText) || node.QuerySelector("img") != null) + { text.Append(node.InnerHtml.HtmlDecode().CoverTag("p")); + } + } } var chapterDoc = text.AsHtmlDoc(); diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Elib2Ebook.ExternalServices.StrokiMts.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Elib2Ebook.ExternalServices.StrokiMts.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Elib2Ebook.ExternalServices.StrokiMts.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/StrokiMtsGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/StrokiMtsGetter.cs similarity index 61% rename from Core/Logic/Getters/StrokiMtsGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/StrokiMtsGetter.cs index 7ba7257e..815bb816 100644 --- a/Core/Logic/Getters/StrokiMtsGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/StrokiMtsGetter.cs @@ -1,23 +1,17 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Http.Json; using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Misc; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.StrokiMts; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.StrokiMts.Types; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.StrokiMts; public class StrokiMtsGetter(BookGetterConfig config) : GetterBase(config) { @@ -31,7 +25,10 @@ protected override string GetId(Uri url) public override Task Authorize() { var token = Config.Options.Login ?? Config.Options.Password; - if (string.IsNullOrWhiteSpace(token)) throw new Elib2EbookAuthException("Не указан авторизационный token."); + if (string.IsNullOrWhiteSpace(token)) + { + throw new Exception("Не указан авторизационный token."); + } Config.Client.DefaultRequestHeaders.Add("access-token", token); return Task.CompletedTask; @@ -42,9 +39,16 @@ public override async Task Get(Uri url) var id = GetId(url); var details = await GetBook(id); - var detailBook = await FindDetailBook(details, id); + var detailBook = details?.Items.FirstOrDefault(d => d.TextBook != null)?.TextBook ?? + details?.Items.FirstOrDefault(d => d.AudioBook != null)?.AudioBook ?? + details?.Items.FirstOrDefault(d => d.ComicBook != null)?.ComicBook ?? + details?.Items.FirstOrDefault(d => d.PressBook != null)?.PressBook ?? + await GetSerial(id); - if (detailBook is null) throw new Elib2EbookParseException("Не удалось получить книгу"); + if (detailBook == null) + { + throw new Exception("Не удалось получить книгу"); + } var book = new Book(url) { @@ -52,63 +56,67 @@ public override async Task Get(Uri url) Title = detailBook.Title, Author = GetAuthor(detailBook), CoAuthors = GetCoAuthors(detailBook), - Annotation = detailBook.Annotation + Annotation = detailBook.Annotation, }; - if (Config.Options.NoChapters) return book; + if (Config.Options.NoChapters) + { + return book; + } var origBooks = await GetOriginalFiles(id); if (origBooks.Count == 1) { if (detailBook.ContentInfo.FileType == "EPUB") + { book.Chapters = await FillChaptersFromEpub(origBooks[0]); + } else + { Config.Logger.LogInformation("Эта книга не в формате epub. Получить главы невозможно"); + } } else { - if (detailBook.ContentInfo.FileType == "JPG") book.Chapters = FillChaptersFromImages(detailBook, origBooks); + if (detailBook.ContentInfo.FileType == "JPG") + { + book.Chapters = FillChaptersFromImages(detailBook, origBooks); + } } if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Books)) + { foreach (var origBook in origBooks) + { book.AdditionalFiles.Add(AdditionalTypeEnum.Books, origBook); + } + } if (Config.Options.HasAdditionalType(AdditionalTypeEnum.Audio)) - book.AdditionalFiles.Add(AdditionalTypeEnum.Audio, await GetAudio(details)); - - return book; - } - - private async Task FindDetailBook(StrokiMtsApiMultiResponse details, string id) - { - if (details?.Items is not null) { - foreach (var item in details.Items) - { - if (item.TextBook is not null) return item.TextBook; - if (item.AudioBook is not null) return item.AudioBook; - if (item.ComicBook is not null) return item.ComicBook; - if (item.PressBook is not null) return item.PressBook; - } + book.AdditionalFiles.Add(AdditionalTypeEnum.Audio, await GetAudio(details)); } - return await GetSerial(id); + return book; } private static IEnumerable FillChaptersFromImages(StrokiMtsBookItem book, IEnumerable images) { var sb = new StringBuilder(); - foreach (var image in images) sb.Append($""); + foreach (var image in images) + { + sb.Append($""); + } var chapter = new Chapter { - Images = images, - Content = sb.ToString(), - Title = book.Title + Images = images, Content = sb.ToString(), Title = book.Title, }; - return new List { chapter }; + return new List + { + chapter + }; } private async Task> GetOriginalFiles(string bookId) @@ -117,16 +125,17 @@ private async Task> GetOriginalFiles(string bookId) var result = new ConcurrentBag>(); var tuples = fileMetas.Select((file, i) => Tuple.Create(file, i)); - await Parallel.ForEachAsync(tuples, async (t, _) => - { - var fileUrl = await GetFileUrl(t.Item1); - var response = await Config.Client.GetWithTriesAsync(fileUrl.Url.AsUri()); - Config.Logger.LogInformation($"Оригинальный файл доступен по ссылке {fileUrl.Url.AsUri()}"); - var tempFile = await TempFile.Create(fileUrl.Url.AsUri(), Config.TempFolder.Path, - fileUrl.Url.AsUri().GetFileName(), await response.Content.ReadAsStreamAsync(_)); + await Parallel.ForEachAsync( + tuples, + async (t, _) => + { + var fileUrl = await GetFileUrl(t.Item1); + var response = await Config.Client.GetWithTriesAsync(fileUrl.Url.AsUri()); + Config.Logger.LogInformation($"Оригинальный файл доступен по ссылке {fileUrl.Url.AsUri()}"); + var tempFile = await TempFile.Create(fileUrl.Url.AsUri(), Config.TempFolder.Path, fileUrl.Url.AsUri().GetFileName(), await response.Content.ReadAsStreamAsync(_)); - result.Add(Tuple.Create(tempFile, t.Item2)); - }); + result.Add(Tuple.Create(tempFile, t.Item2)); + }); return result.OrderBy(t => t.Item2).Select(t => t.Item1).ToList(); } @@ -135,8 +144,11 @@ private async Task> GetAudio(StrokiMtsApiMultiResponse details) { var result = new List(); - var detail = details?.Items.FirstOrDefault(d => d.AudioBook is not null); - if (detail is null) return result; + var detail = details?.Items.FirstOrDefault(d => d.AudioBook != null); + if (detail == null) + { + return result; + } var audio = detail.AudioBook; @@ -149,8 +161,7 @@ private async Task> GetAudio(StrokiMtsApiMultiResponse details) Config.Logger.LogInformation($"Загружаю аудиоверсию {i + 1}/{fileMetas.Length} {fileUrl.Url}"); var response = await Config.Client.GetWithTriesAsync(fileUrl.Url.AsUri()); - result.Add(await TempFile.Create(fileUrl.Url.AsUri(), Config.TempFolder.Path, - $"{i}_{fileUrl.Url.AsUri().GetFileName()}", await response.Content.ReadAsStreamAsync())); + result.Add(await TempFile.Create(fileUrl.Url.AsUri(), Config.TempFolder.Path, $"{i}_{fileUrl.Url.AsUri().GetFileName()}", await response.Content.ReadAsStreamAsync())); Config.Logger.LogInformation($"Аудиоверсия {i + 1}/{fileMetas.Length} {fileUrl.Url} загружена"); } @@ -160,50 +171,41 @@ private async Task> GetAudio(StrokiMtsApiMultiResponse details) private Task GetCover(StrokiMtsBookItem book) { var url = book.ImageUrl.TryGetValue("extraLarge", out var item) ? item : book.ImageUrl.FirstOrDefault().Value; - return !string.IsNullOrWhiteSpace(url) - ? SaveImage(SystemUrl.MakeRelativeUri(url)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(url) ? SaveImage(SystemUrl.MakeRelativeUri(url)) : Task.FromResult(default(TempFile)); } private Author GetAuthor(StrokiMtsBookItem book) { var author = book.Authors.FirstOrDefault(); - return author is null - ? new Author("Stroki") - : new Author(author.Name, SystemUrl.MakeRelativeUri(author.FriendlyUrl)); + return author == null ? new Author("Stroki") : new Author(author.Name, SystemUrl.MakeRelativeUri(author.FriendlyUrl)); } private IEnumerable GetCoAuthors(StrokiMtsBookItem book) { - return book.Authors.Skip(1) - .Select(author => new Author(author.Name, SystemUrl.MakeRelativeUri(author.FriendlyUrl))).ToList(); + return book.Authors.Skip(1).Select(author => new Author(author.Name, SystemUrl.MakeRelativeUri(author.FriendlyUrl))).ToList(); } private async Task GetFileMeta(string id) { - var json = await SendAsync>(() => - GetMessage(SystemUrl.MakeRelativeUri("/api/books/files").AppendQueryParameter("bookId", id), "5.0", "5.0")); + var json = await SendAsync>(() => GetMessage(SystemUrl.MakeRelativeUri("/api/books/files").AppendQueryParameter("bookId", id), "5.0", "5.0")); return json.Data.Full?.Length > 0 ? json.Data.Full : [json.Data.Preview]; } private async Task GetFileUrl(StrokiMtsFile file) { - var json = await SendAsync>(() => - GetMessage(SystemUrl.MakeRelativeUri($"api/books/files/data/link/{file.FileId}"), "5.0", "5.0")); + var json = await SendAsync>(() => GetMessage(SystemUrl.MakeRelativeUri($"api/books/files/data/link/{file.FileId}"), "5.0", "5.0")); return json.Data; } private async Task GetBook(string id) { - var json = await SendAsync>(() => - GetMessage(SystemUrl.MakeRelativeUri($"/api/books/multi/{id}"), "5.53", "7.0")); + var json = await SendAsync>(() => GetMessage(SystemUrl.MakeRelativeUri($"/api/books/multi/{id}"), "5.53", "7.0")); return json.Data; } private async Task GetSerial(string id) { - var json = await SendAsync>(() => - GetMessage(SystemUrl.MakeRelativeUri($"/api/serials/episodes/{id} "), "5.53", "7.0")); + var json = await SendAsync>(() => GetMessage(SystemUrl.MakeRelativeUri($"/api/serials/episodes/{id} "), "5.53", "7.0")); return json.Data; } @@ -211,7 +213,9 @@ private async Task SendAsync(Func message) { var response = await Config.Client.SendWithTriesAsync(message); if (response.StatusCode != HttpStatusCode.OK) - throw new Elib2EbookParseException(await response.Content.ReadAsStringAsync()); + { + throw new Exception(await response.Content.ReadAsStringAsync()); + } return await response.Content.ReadFromJsonAsync(); } @@ -220,7 +224,10 @@ protected virtual HttpRequestMessage GetMessage(Uri uri, string appVersion, stri { var message = new HttpRequestMessage(HttpMethod.Get, uri); - foreach (var header in Config.Client.DefaultRequestHeaders) message.Headers.Add(header.Key, header.Value); + foreach (var header in Config.Client.DefaultRequestHeaders) + { + message.Headers.Add(header.Key, header.Value); + } message.Headers.Add("install-guid", Guid.NewGuid().ToString()); message.Headers.Add("app-version", appVersion); @@ -238,6 +245,6 @@ private static string GetSignature(Uri url) var inputBytes = Encoding.UTF8.GetBytes(url.ToString().Replace("https://", "http://") + "meg@$h!t"); var hashBytes = MD5.HashData(inputBytes); - return Convert.ToHexString(hashBytes).ToLowerInvariant(); + return Convert.ToHexString(hashBytes).ToLower(); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsApiMultiResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsApiMultiResponse.cs new file mode 100644 index 00000000..65d48fb4 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsApiMultiResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.StrokiMts.Types; + +internal class StrokiMtsApiMultiResponse +{ + [JsonPropertyName("items")] + public List Items { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsApiResponse.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsApiResponse.cs new file mode 100644 index 00000000..56cca895 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsApiResponse.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.StrokiMts.Types; + +internal class StrokiMtsApiResponse +{ + [JsonPropertyName("data")] + public T Data { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsAuthor.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsAuthor.cs new file mode 100644 index 00000000..fd28057b --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsAuthor.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.StrokiMts.Types; + +internal class StrokiMtsAuthor +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("friendlyUrl")] + public string FriendlyUrl { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsBookItem.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsBookItem.cs new file mode 100644 index 00000000..38c2d5c3 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsBookItem.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.StrokiMts.Types; + +internal class StrokiMtsBookItem +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("annotation")] + public string Annotation { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("imageUrl")] + public Dictionary ImageUrl { get; set; } + + [JsonPropertyName("authors")] + public List Authors { get; set; } + + [JsonPropertyName("contentInfo")] + public StrokiMtsContentInfo ContentInfo { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsContentInfo.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsContentInfo.cs new file mode 100644 index 00000000..e1b792f7 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsContentInfo.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.StrokiMts.Types; + +internal class StrokiMtsContentInfo +{ + [JsonPropertyName("fileType")] + public string FileType { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsFile.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsFile.cs new file mode 100644 index 00000000..20082862 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsFile.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.StrokiMts.Types; + +internal class StrokiMtsFile +{ + [JsonPropertyName("fileId")] + public long FileId { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsFileUrl.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsFileUrl.cs new file mode 100644 index 00000000..a1585bb1 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsFileUrl.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.StrokiMts.Types; + +internal class StrokiMtsFileUrl +{ + [JsonPropertyName("url")] + public string Url { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsFiles.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsFiles.cs new file mode 100644 index 00000000..6408a9c9 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsFiles.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.StrokiMts.Types; + +internal class StrokiMtsFiles +{ + [JsonPropertyName("preview")] + public StrokiMtsFile Preview { get; set; } + + [JsonPropertyName("full")] + public StrokiMtsFile[] Full { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsMultiItem.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsMultiItem.cs new file mode 100644 index 00000000..8418819f --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.StrokiMts/Types/StrokiMtsMultiItem.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.StrokiMts.Types; + +internal class StrokiMtsMultiItem +{ + [JsonPropertyName("textBook")] + public StrokiMtsBookItem TextBook { get; set; } + + [JsonPropertyName("audioBook")] + public StrokiMtsBookItem AudioBook { get; set; } + + [JsonPropertyName("comics")] + public StrokiMtsBookItem ComicBook { get; set; } + + [JsonPropertyName("pressa")] + public StrokiMtsBookItem PressBook { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.TopLiba/Elib2Ebook.ExternalServices.TopLiba.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.TopLiba/Elib2Ebook.ExternalServices.TopLiba.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.TopLiba/Elib2Ebook.ExternalServices.TopLiba.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/TopLiba/BiglibaGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.TopLiba/Getters/BiglibaGetter.cs similarity index 56% rename from Core/Logic/Getters/TopLiba/BiglibaGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.TopLiba/Getters/BiglibaGetter.cs index 060fd60d..b9887297 100644 --- a/Core/Logic/Getters/TopLiba/BiglibaGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.TopLiba/Getters/BiglibaGetter.cs @@ -1,14 +1,11 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; -namespace Core.Logic.Getters.TopLiba; +namespace Elib2Ebook.ExternalServices.TopLiba.Getters; public class BiglibaGetter(BookGetterConfig config) : TopLibaGetterBase(config) { @@ -17,7 +14,7 @@ public class BiglibaGetter(BookGetterConfig config) : TopLibaGetterBase(config) protected override Seria GetSeria(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("div.book-series a"); - if (a is not null) + if (a != null) { var text = a.GetText(); @@ -26,27 +23,22 @@ protected override Seria GetSeria(HtmlDocument doc, Uri url) var parts = text.Split(':').Last().Split("(#"); return new Seria { - Name = parts[0].Trim(), - Number = parts[1].Trim(')').Trim(), - Url = url.MakeRelativeUri(a.Attributes["href"].Value) + Name = parts[0].Trim(), Number = parts[1].Trim(')').Trim(), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; } return new Seria { - Name = text, - Url = url.MakeRelativeUri(a.Attributes["href"].Value) + Name = text, Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; } - return default; + return null; } protected override Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("img[itemprop=image]")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/Core/Logic/Getters/TopLiba/TopLibaGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.TopLiba/Getters/TopLibaGetter.cs similarity index 50% rename from Core/Logic/Getters/TopLiba/TopLibaGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.TopLiba/Getters/TopLibaGetter.cs index a2579f17..1c13d744 100644 --- a/Core/Logic/Getters/TopLiba/TopLibaGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.TopLiba/Getters/TopLibaGetter.cs @@ -1,13 +1,11 @@ -using System; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; -namespace Core.Logic.Getters.TopLiba; +namespace Elib2Ebook.ExternalServices.TopLiba.Getters; public class TopLibaGetter(BookGetterConfig config) : TopLibaGetterBase(config) { @@ -16,34 +14,31 @@ public class TopLibaGetter(BookGetterConfig config) : TopLibaGetterBase(config) protected override Seria GetSeria(HtmlDocument doc, Uri url) { var a = doc.QuerySelector("div.book-series a"); - if (a is not null) + if (a != null) { var text = a.GetText(); var number = a.NextSibling; - if (number is not null && number.GetText().Contains('#')) + if (number != null && number.GetText().Contains('#')) + { return new Seria { - Name = text, - Number = number.GetText().Replace("(", "").Replace(")", "").Replace("#", ""), - Url = url.MakeRelativeUri(a.Attributes["href"].Value) + Name = text, Number = number.GetText().Replace("(", "").Replace(")", "").Replace("#", ""), Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; + } return new Seria { - Name = text, - Url = url.MakeRelativeUri(a.Attributes["href"].Value) + Name = text, Url = url.MakeRelativeUri(a.Attributes["href"].Value) }; } - return default; + return null; } protected override Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("img[itemprop=contentUrl]")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } } diff --git a/Core/Logic/Getters/TopLiba/TopLibaGetterBase.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.TopLiba/Getters/TopLibaGetterBase.cs similarity index 80% rename from Core/Logic/Getters/TopLiba/TopLibaGetterBase.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.TopLiba/Getters/TopLibaGetterBase.cs index e2601c0b..8a44fb6b 100644 --- a/Core/Logic/Getters/TopLiba/TopLibaGetterBase.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.TopLiba/Getters/TopLibaGetterBase.cs @@ -1,26 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Exceptions; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters.TopLiba; +namespace Elib2Ebook.ExternalServices.TopLiba.Getters; public abstract class TopLibaGetterBase(BookGetterConfig config) : GetterBase(config) { protected override string GetId(Uri url) - { - return url.GetSegment(2); - } + => url.GetSegment(2); protected abstract Seria GetSeria(HtmlDocument doc, Uri url); @@ -48,7 +41,10 @@ public override async Task Get(Uri url) public override async Task Authorize() { - if (!Config.HasCredentials) return; + if (!Config.HasCredentials) + { + return; + } var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri("/login")); var token = doc.QuerySelector("meta[name=_token]").Attributes["content"].Value; @@ -56,19 +52,21 @@ public override async Task Authorize() var response = await Config.Client.PostWithTriesAsync(SystemUrl.MakeRelativeUri("/login"), GetAuthData(token)); doc = await response.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.AsHtmlDoc()); var helpBlock = doc.QuerySelector("input[type=email] + span.help-block"); - if (helpBlock is null) + if (helpBlock == null) + { Config.Logger.LogInformation("Успешно авторизовались"); + } else - throw new Elib2EbookAuthException($"Не удалось авторизоваться. {helpBlock.GetText()}"); + { + throw new Exception($"Не удалось авторизоваться. {helpBlock.GetText()}"); + } } private FormUrlEncodedContent GetAuthData(string token) { var data = new Dictionary { - ["email"] = Config.Options.Login, - ["password"] = Config.Options.Password, - ["_token"] = token + ["email"] = Config.Options.Login, ["password"] = Config.Options.Password, ["_token"] = token }; return new FormUrlEncodedContent(data); @@ -83,7 +81,10 @@ private static Author GetAuthor(HtmlDocument doc, Uri url) private async Task> FillChapters(Uri uri, string bookId, string token, string title) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var id in await GetToc(bookId)) { @@ -112,8 +113,7 @@ private async Task> FillChapters(Uri uri, string bookId, st private async Task GetChapter(string bookId, string id, string token) { - var data = await Config.Client.PostWithTriesAsync(SystemUrl.MakeRelativeUri($"/reader/{bookId}/chapter"), - GetData(id, token)); + var data = await Config.Client.PostWithTriesAsync(SystemUrl.MakeRelativeUri($"/reader/{bookId}/chapter"), GetData(id, token)); return await data.Content.ReadAsStringAsync(); } @@ -121,8 +121,7 @@ private static FormUrlEncodedContent GetData(string chapterId, string token) { var data = new Dictionary { - ["chapter"] = chapterId, - ["_token"] = token + ["chapter"] = chapterId, ["_token"] = token, }; return new FormUrlEncodedContent(data); @@ -131,8 +130,9 @@ private static FormUrlEncodedContent GetData(string chapterId, string token) private async Task> GetToc(string bookId) { var doc = await Config.Client.GetHtmlDocWithTriesAsync(SystemUrl.MakeRelativeUri($"/reader/{bookId}")); - var result = new Regex("capters: \\[(?.*?)\\]").Match(doc.Text).Groups["chapters"].Value - .Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim('\"')).ToList(); + var result = new Regex("capters: \\[(?.*?)\\]").Match(doc.Text).Groups["chapters"].Value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim('\"')) + .ToList(); + return SliceToc(result, c => c); } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.TwilightRussia/Elib2Ebook.ExternalServices.TwilightRussia.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.TwilightRussia/Elib2Ebook.ExternalServices.TwilightRussia.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.TwilightRussia/Elib2Ebook.ExternalServices.TwilightRussia.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/TwilightRussiaGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.TwilightRussia/TwilightRussiaGetter.cs similarity index 76% rename from Core/Logic/Getters/TwilightRussiaGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.TwilightRussia/TwilightRussiaGetter.cs index 49854c9c..f88026de 100644 --- a/Core/Logic/Getters/TwilightRussiaGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.TwilightRussia/TwilightRussiaGetter.cs @@ -1,15 +1,13 @@ -using System; -using System.Collections.Generic; using System.Text; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.TwilightRussia; public class TwilightRussiaGetter(BookGetterConfig config) : GetterBase(config) { @@ -24,10 +22,7 @@ public override async Task Get(Uri url) var book = new Book(url) { - Cover = null, - Chapters = await FillChapters(doc, url), - Title = doc.GetTextBySelector("a.forumBarA"), - Author = new Author(doc.GetTextBySelector("span[class^='forum_nik']")) + Cover = null, Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("a.forumBarA"), Author = new Author(doc.GetTextBySelector("span[class^='forum_nik']")) }; return book; @@ -47,7 +42,10 @@ private async Task GetMainUrl(Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url) { - if (Config.Options.NoChapters) return []; + if (Config.Options.NoChapters) + { + return []; + } var post = doc.QuerySelectorAll("td.content-block-forum"); @@ -55,7 +53,10 @@ private async Task> FillChapters(HtmlDocument doc, Uri url) foreach (var a in post.QuerySelectorAll("a[href*='/publ/']")) { var title = a.GetText(); - if (string.IsNullOrWhiteSpace(title)) continue; + if (string.IsNullOrWhiteSpace(title)) + { + continue; + } Config.Logger.LogInformation($"Загружаю главу {title}"); var href = url.MakeRelativeUri(a.Attributes["href"].Value); @@ -82,7 +83,10 @@ private async Task GetChapter(Uri url, string title) } var nodeText = node.GetText(); - if (!string.IsNullOrWhiteSpace(nodeText)) text.Append(nodeText.CoverTag("p")); + if (!string.IsNullOrWhiteSpace(nodeText)) + { + text.Append(nodeText.CoverTag("p")); + } } var chapterDoc = text.AsHtmlDoc(); diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Elib2Ebook.ExternalServices.WattpadCom.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Elib2Ebook.ExternalServices.WattpadCom.csproj new file mode 100644 index 00000000..51cdfeeb --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Elib2Ebook.ExternalServices.WattpadCom.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Types/WattpadInfo.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Types/WattpadInfo.cs new file mode 100644 index 00000000..b97e5741 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Types/WattpadInfo.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Wattpad.Types; + +internal class WattpadInfo +{ + [JsonPropertyName("user")] + public WattpadUser User { get; set; } + + [JsonPropertyName("cover")] + public string Cover { get; set; } + + [JsonPropertyName("parts")] + public WattpadPart[] Parts { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Types/WattpadMeta.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Types/WattpadMeta.cs new file mode 100644 index 00000000..648e1d32 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Types/WattpadMeta.cs @@ -0,0 +1,6 @@ +namespace Elib2Ebook.ExternalServices.Wattpad.Types; + +internal class WattpadMeta +{ + public string StoryId { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Types/WattpadPart.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Types/WattpadPart.cs new file mode 100644 index 00000000..ccb469bd --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Types/WattpadPart.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; +using Elib2Ebook.DomainServices.Extensions; + +namespace Elib2Ebook.ExternalServices.Wattpad.Types; + +internal class WattpadPart +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("title")] + public string Title { private get; set; } + + public string FullName => Title.Replace("\r", " ").CollapseWhitespace().Trim(); + + public Uri Url => new($"https://www.wattpad.com/apiv2/storytext?id={Id}"); +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Types/WattpadUser.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Types/WattpadUser.cs new file mode 100644 index 00000000..7419f4dd --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/Types/WattpadUser.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.Wattpad.Types; + +internal class WattpadUser +{ + [JsonPropertyName("name")] + public string Name { get; set; } +} diff --git a/Core/Logic/Getters/WattpadGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/WattpadGetter.cs similarity index 69% rename from Core/Logic/Getters/WattpadGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/WattpadGetter.cs index bf0cbbc4..e2f78ecb 100644 --- a/Core/Logic/Getters/WattpadGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadCom/WattpadGetter.cs @@ -1,30 +1,29 @@ -using System; -using System.Collections.Generic; using System.Net.Http.Json; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.Wattpad; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.Wattpad.Types; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Wattpad; public class WattpadGetter(BookGetterConfig config) : GetterBase(config) { protected override Uri SystemUrl => new("https://wattpad.com/"); protected override string GetId(Uri url) - { - return base.GetId(url).Split('-')[0]; - } + => base.GetId(url).Split('-')[0]; private async Task GetStoryId(Uri url) { - if (url.ToString().Contains("/story/")) return GetId(url); + if (url.ToString().Contains("/story/")) + { + return GetId(url); + } var doc = await Config.Client.GetHtmlDocWithTriesAsync(url); return doc.QuerySelector("button[data-story-id]").Attributes["data-story-id"].Value; @@ -34,8 +33,7 @@ public override async Task Get(Uri url) { var storyId = await GetStoryId(url); url = SystemUrl.MakeRelativeUri(storyId); - var wattpadInfo = - await Config.Client.GetFromJsonAsync(SystemUrl.MakeRelativeUri($"/api/v3/stories/{storyId}")); + var wattpadInfo = await Config.Client.GetFromJsonAsync(SystemUrl.MakeRelativeUri($"/api/v3/stories/{storyId}")); var book = new Book(url) { @@ -52,7 +50,10 @@ public override async Task Get(Uri url) private async Task> FillChapters(WattpadInfo wattpadInfo) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var group in SliceToc(wattpadInfo.Parts, c => c.FullName)) { @@ -73,15 +74,16 @@ private async Task> FillChapters(WattpadInfo wattpadInfo) private async Task GetChapter(WattpadPart part) { var doc = await Config.Client.GetHtmlDocWithTriesAsync(part.Url); - foreach (var node in doc.QuerySelectorAll("p")) node.Attributes.RemoveAll(); + foreach (var node in doc.QuerySelectorAll("p")) + { + node.Attributes.RemoveAll(); + } return doc; } private Task GetCover(WattpadInfo wattpadInfo) { - return !string.IsNullOrWhiteSpace(wattpadInfo.Cover) - ? SaveImage(wattpadInfo.Cover.AsUri()) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(wattpadInfo.Cover) ? SaveImage(wattpadInfo.Cover.AsUri()) : Task.FromResult(default(TempFile)); } } diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Elib2Ebook.ExternalServices.WattpadRu.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Elib2Ebook.ExternalServices.WattpadRu.csproj new file mode 100644 index 00000000..7859061c --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Elib2Ebook.ExternalServices.WattpadRu.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadAuthor.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadAuthor.cs new file mode 100644 index 00000000..924a4298 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadAuthor.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.WattpadRu.Types; + +internal class WattpadAuthor +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("slug")] + public string Slug { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadInfo.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadInfo.cs new file mode 100644 index 00000000..509e33f5 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadInfo.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.WattpadRu.Types; + +internal class WattpadInfo +{ + [JsonPropertyName("story")] + public WattpadStory Story { get; set; } + + [JsonPropertyName("parts")] + public WattpadPart[] Parts { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadMeta.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadMeta.cs new file mode 100644 index 00000000..1d02a761 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadMeta.cs @@ -0,0 +1,6 @@ +namespace Elib2Ebook.ExternalServices.WattpadRu.Types; + +internal class WattpadMeta +{ + public string StoryId { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadPart.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadPart.cs new file mode 100644 index 00000000..b0c25854 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadPart.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.WattpadRu.Types; + +internal class WattpadPart +{ + [JsonPropertyName("chapter")] + public string Chapter { get; set; } + + [JsonPropertyName("content")] + public string Content { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadStory.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadStory.cs new file mode 100644 index 00000000..740b7bd0 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/Types/WattpadStory.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.WattpadRu.Types; + +internal class WattpadStory +{ + [JsonPropertyName("authors")] + public WattpadAuthor[] Authors { get; set; } + + [JsonPropertyName("cover")] + public string Cover { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/WattpadGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/WattpadGetter.cs new file mode 100644 index 00000000..4635b88c --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WattpadRu/WattpadGetter.cs @@ -0,0 +1,91 @@ +using System.Net.Http.Json; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.WattpadRu.Types; +using HtmlAgilityPack; +using HtmlAgilityPack.CssSelectors.NetCore; +using Microsoft.Extensions.Logging; + +namespace Elib2Ebook.ExternalServices.WattpadRu; + +public class WattpadGetter(BookGetterConfig config) : GetterBase(config) +{ + protected override Uri SystemUrl => new("https://watt-pad.ru/"); + + protected override string GetId(Uri url) => url.GetSegment(2); + + public override async Task Get(Uri url) + { + var id = GetId(url); + url = SystemUrl.MakeRelativeUri($"/story/{id}"); + var wattpadInfo = await Config.Client.GetFromJsonAsync(SystemUrl.MakeRelativeUri($"/api/offline/stories/{id}")); + + var book = new Book(url) + { + Cover = await GetCover(wattpadInfo), + Chapters = await FillChapters(wattpadInfo), + Title = wattpadInfo.Story.Name, + Author = GetAuthor(wattpadInfo), + CoAuthors = GetCoAuthors(wattpadInfo), + Annotation = wattpadInfo.Story.Description + }; + + return book; + } + + private Author GetAuthor(WattpadInfo wattpadInfo) + { + var author = wattpadInfo.Story.Authors.First(); + return new Author(author.Name, SystemUrl.MakeRelativeUri($"/author/{author.Slug}")); + } + + private IEnumerable GetCoAuthors(WattpadInfo wattpadInfo) + { + return wattpadInfo.Story.Authors + .Skip(1) + .Select(author => new Author(author.Name, SystemUrl.MakeRelativeUri($"/author/{author.Slug}"))).ToList(); + } + + private async Task> FillChapters(WattpadInfo wattpadInfo) + { + var result = new List(); + if (Config.Options.NoChapters) + { + return result; + } + + foreach (var wattpadChapter in SliceToc(wattpadInfo.Parts, c => c.Chapter)) + { + Config.Logger.LogInformation($"Загружаю главу {wattpadChapter.Chapter.CoverQuotes()}"); + var chapter = new Chapter(); + + var chapterDoc = GetChapter(wattpadChapter); + chapter.Images = await GetImages(chapterDoc, SystemUrl); + chapter.Content = chapterDoc.DocumentNode.InnerHtml; + chapter.Title = wattpadChapter.Chapter; + + result.Add(chapter); + } + + return result; + } + + private static HtmlDocument GetChapter(WattpadPart part) + { + var doc = part.Content.AsHtmlDoc(); + foreach (var node in doc.QuerySelectorAll("p")) + { + node.Attributes.RemoveAll(); + } + + return doc; + } + + private Task GetCover(WattpadInfo wattpadInfo) + { + return !string.IsNullOrWhiteSpace(wattpadInfo.Story.Cover) ? SaveImage(wattpadInfo.Story.Cover.AsUri()) : Task.FromResult(default(TempFile)); + } +} diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WuxiaWorld/Elib2Ebook.ExternalServices.WuxiaWorld.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.WuxiaWorld/Elib2Ebook.ExternalServices.WuxiaWorld.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WuxiaWorld/Elib2Ebook.ExternalServices.WuxiaWorld.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.WuxiaWorld/Types/WuxiaWorld/WuxiaWorldToc.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.WuxiaWorld/Types/WuxiaWorld/WuxiaWorldToc.cs new file mode 100644 index 00000000..46a206bd --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WuxiaWorld/Types/WuxiaWorld/WuxiaWorldToc.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Elib2Ebook.ExternalServices.WuxiaWorld.Types.WuxiaWorld; + +internal class WuxiaWorldToc +{ + [JsonPropertyName("post_name")] + public string PostName { get; set; } +} diff --git a/Core/Logic/Getters/WuxiaWorldGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.WuxiaWorld/WuxiaWorldGetter.cs similarity index 71% rename from Core/Logic/Getters/WuxiaWorldGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.WuxiaWorld/WuxiaWorldGetter.cs index 1cd5725a..ddc300f5 100644 --- a/Core/Logic/Getters/WuxiaWorldGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.WuxiaWorld/WuxiaWorldGetter.cs @@ -1,21 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Http.Json; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; -using Core.Types.WuxiaWorld; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; +using Elib2Ebook.ExternalServices.WuxiaWorld.Types.WuxiaWorld; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.WuxiaWorld; public class WuxiaWorldGetter(BookGetterConfig config) : GetterBase(config) { @@ -39,10 +35,7 @@ public override async Task Get(Uri url) var book = new Book(url) { - Cover = await GetCover(doc, url), - Chapters = await FillChapters(doc, url), - Title = doc.GetTextBySelector("h1"), - Author = new Author("WuxiaWorld") + Cover = await GetCover(doc, url), Chapters = await FillChapters(doc, url), Title = doc.GetTextBySelector("h1"), Author = new Author("WuxiaWorld"), }; return book; @@ -56,17 +49,15 @@ private async Task> GetToc(HtmlDocument doc, Uri url) foreach (var span in doc.QuerySelectorAll("ul.myUL span.caret[data-id]")) { var offset = (int.Parse(span.Attributes["data-id"].Value) - 1) * 100; - var payload = new FormUrlEncodedContent(new Dictionary - { - ["cat_id"] = catId, - ["offset"] = offset.ToString() - }); - - var post = await Config.Client.PostWithTriesAsync( - SystemUrl.MakeRelativeUri("/wp-content/themes/Wuxia/template-parts/post/menu-query.php"), payload); + var payload = new FormUrlEncodedContent( + new Dictionary + { + ["cat_id"] = catId, ["offset"] = offset.ToString() + }); + + var post = await Config.Client.PostWithTriesAsync(SystemUrl.MakeRelativeUri("/wp-content/themes/Wuxia/template-parts/post/menu-query.php"), payload); var toc = await post.Content.ReadFromJsonAsync(); - result.AddRange(toc.Select((a, i) => - new UrlChapter(url.MakeRelativeUri(a.PostName), $"Глава {offset + i + 1}"))); + result.AddRange(toc.Select((a, i) => new UrlChapter(url.MakeRelativeUri(a.PostName), $"Глава {offset + i + 1}"))); } return SliceToc(result, c => c.Title); @@ -75,7 +66,10 @@ private async Task> GetToc(HtmlDocument doc, Uri url) private async Task> FillChapters(HtmlDocument doc, Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var bookChapter in await GetToc(doc, url)) { @@ -102,9 +96,7 @@ private async Task GetChapter(Uri url) private Task GetCover(HtmlDocument doc, Uri uri) { var imagePath = doc.QuerySelector("meta[property=og:image]")?.Attributes["content"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) - ? SaveImage(uri.MakeRelativeUri(imagePath)) - : Task.FromResult(default(TempFile)); + return !string.IsNullOrWhiteSpace(imagePath) ? SaveImage(uri.MakeRelativeUri(imagePath)) : Task.FromResult(default(TempFile)); } private async Task GetSafety(Uri url) @@ -112,7 +104,7 @@ private async Task GetSafety(Uri url) var response = await Config.Client.GetWithTriesAsync(url); await Task.Delay(TimeSpan.FromSeconds(1)); - while (response is null || response.StatusCode == HttpStatusCode.ServiceUnavailable) + while (response == null || response.StatusCode == HttpStatusCode.ServiceUnavailable) { Config.Logger.LogInformation("Получен бан от системы. Жду..."); var errorTimeout = TimeSpan.FromSeconds(30); diff --git a/src/ExternalServices/Elib2Ebook.ExternalServices.Younettranslate/Elib2Ebook.ExternalServices.Younettranslate.csproj b/src/ExternalServices/Elib2Ebook.ExternalServices.Younettranslate/Elib2Ebook.ExternalServices.Younettranslate.csproj new file mode 100644 index 00000000..0b984323 --- /dev/null +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Younettranslate/Elib2Ebook.ExternalServices.Younettranslate.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Logic/Getters/YounettranslateGetter.cs b/src/ExternalServices/Elib2Ebook.ExternalServices.Younettranslate/YounettranslateGetter.cs similarity index 80% rename from Core/Logic/Getters/YounettranslateGetter.cs rename to src/ExternalServices/Elib2Ebook.ExternalServices.Younettranslate/YounettranslateGetter.cs index 25891bac..d91ee769 100644 --- a/Core/Logic/Getters/YounettranslateGetter.cs +++ b/src/ExternalServices/Elib2Ebook.ExternalServices.Younettranslate/YounettranslateGetter.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Core.Configs; -using Core.Extensions; -using Core.Types.Book; -using Core.Types.Common; +using Elib2Ebook.Domain.Book; +using Elib2Ebook.Domain.Common; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Extensions; +using Elib2Ebook.DomainServices.Getters; using HtmlAgilityPack; using HtmlAgilityPack.CssSelectors.NetCore; using Microsoft.Extensions.Logging; -namespace Core.Logic.Getters; +namespace Elib2Ebook.ExternalServices.Younettranslate; public class YounettranslateGetter(BookGetterConfig config) : GetterBase(config) { @@ -43,8 +40,10 @@ private Author GetAuthor(HtmlDocument doc) foreach (var div in doc.QuerySelectorAll("div.col-md-3")) { var label = div.QuerySelector("p.small-label"); - if (label is not null && label.GetText().StartsWith("Автор")) + if (label != null && label.GetText().StartsWith("Автор")) + { return new Author(label.GetTextBySelector("+ p")); + } } return new Author("Younettranslate"); @@ -58,7 +57,10 @@ private static string GetTitle(HtmlDocument doc) private async Task> FillChapters(Uri url) { var result = new List(); - if (Config.Options.NoChapters) return result; + if (Config.Options.NoChapters) + { + return result; + } foreach (var urlChapter in await GetToc(url)) { @@ -70,7 +72,7 @@ private async Task> FillChapters(Uri url) var chapterDoc = await GetChapter(urlChapter); - if (chapterDoc is not null) + if (chapterDoc != null) { chapter.Images = await GetImages(chapterDoc, SystemUrl); chapter.Content = chapterDoc.DocumentNode.InnerHtml; @@ -89,9 +91,11 @@ private async Task> GetToc(Uri url) for (var i = 1;; i++) { var doc = await Config.Client.GetHtmlDocWithTriesAsync(url.AppendQueryParameter("page", i)); - var links = doc.QuerySelectorAll("table.catposts-table td.hidden-sm a[href]").Select(a => - new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())).ToList(); - if (links.Count == 0) break; + var links = doc.QuerySelectorAll("table.catposts-table td.hidden-sm a[href]").Select(a => new UrlChapter(url.MakeRelativeUri(a.Attributes["href"].Value), a.GetText())).ToList(); + if (links.Count == 0) + { + break; + } result.AddRange(links); } @@ -102,8 +106,7 @@ private async Task> GetToc(Uri url) private async Task GetChapter(UrlChapter urlChapter) { var doc = await Config.Client.GetHtmlDocWithTriesAsync(urlChapter.Url); - return doc.QuerySelector("div.postdata-content").RemoveNodes(n => n.InnerText.Contains("ЗАПРЕЩЕНО ПУБЛИКОВАТЬ")) - .InnerHtml.AsHtmlDoc(); + return doc.QuerySelector("div.postdata-content").RemoveNodes(n => n.InnerText.Contains("ЗАПРЕЩЕНО ПУБЛИКОВАТЬ")).InnerHtml.AsHtmlDoc(); } private async Task GetCover(Uri bookUri) @@ -111,6 +114,6 @@ private async Task GetCover(Uri bookUri) var doc = await Config.Client.GetHtmlDocWithTriesAsync("https://younettranslate.com/projects/".AsUri()); var imagePath = doc.QuerySelector($"div.category-item a[href='{bookUri}'] img")?.Attributes["src"]?.Value; - return !string.IsNullOrWhiteSpace(imagePath) ? await SaveImage(bookUri.MakeRelativeUri(imagePath)) : default; + return !string.IsNullOrWhiteSpace(imagePath) ? await SaveImage(bookUri.MakeRelativeUri(imagePath)) : null; } } diff --git a/tests/Elib2Ebook.DomainServices.Tests/Elib2Ebook.DomainServices.Tests.csproj b/tests/Elib2Ebook.DomainServices.Tests/Elib2Ebook.DomainServices.Tests.csproj new file mode 100644 index 00000000..50b87465 --- /dev/null +++ b/tests/Elib2Ebook.DomainServices.Tests/Elib2Ebook.DomainServices.Tests.csproj @@ -0,0 +1,25 @@ + + + + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/tests/Elib2Ebook.DomainServices.Tests/Extensions/HtmlDocumentExtensionsTests.cs b/tests/Elib2Ebook.DomainServices.Tests/Extensions/HtmlDocumentExtensionsTests.cs new file mode 100644 index 00000000..12f1caf9 --- /dev/null +++ b/tests/Elib2Ebook.DomainServices.Tests/Extensions/HtmlDocumentExtensionsTests.cs @@ -0,0 +1,79 @@ +using Elib2Ebook.DomainServices.Extensions; +using HtmlAgilityPack; + +namespace Elib2Ebook.DomainServices.Tests.Extensions; + +public class HtmlDocumentExtensionsTests +{ + private static HtmlDocument Doc(string html) + { + var d = new HtmlDocument(); + d.LoadHtml(html); + return d; + } + + [Theory] + [InlineData("

test

", "test")] + [InlineData("

hello

", "hello")] + public void AsString(string html, string expected) + { + Assert.Contains(expected, Doc(html).AsString()); + } + + [Theory] + [InlineData("

hello

", ".text", "hello")] + public void GetTextBySelector(string html, string selector, string expected) + { + Assert.Equal(expected, Doc(html).GetTextBySelector(selector)); + } + + [Theory] + [InlineData("

hello world

", "hello world")] + public void GetText(string html, string expected) + { + Assert.Contains(expected, Doc(html).DocumentNode.SelectSingleNode("//p").GetText()); + } + + [Fact] + public void GetText_NullNode() + { + Assert.Null(((HtmlNode)null).GetText()); + } + + [Theory] + [InlineData("

keep

remove", "span")] + public void RemoveNodes_WithPredicate(string html, string tagToRemove) + { + var d = Doc(html); + d.RemoveNodes(n => n.Name == tagToRemove); + Assert.Null(d.DocumentNode.SelectSingleNode($"//{tagToRemove}")); + } + + [Theory] + [InlineData("

a

b

", ".remove", "//p")] + [InlineData("
xy
", ".del", "//span")] + public void RemoveNodes_WithSelector(string html, string selector, string remainingXpath) + { + var d = Doc(html); + d.RemoveNodes(selector); + Assert.Single(d.DocumentNode.SelectNodes(remainingXpath)); + } + + [Theory] + [InlineData("
ab
", "a", 1)] + public void RemoveNodes_HtmlNodeWithPredicate(string html, string innerText, int expectedRemaining) + { + var d = Doc(html); + d.DocumentNode.SelectSingleNode("//div").RemoveNodes(n => n.InnerText == innerText); + Assert.Equal(expectedRemaining, d.DocumentNode.SelectNodes("//div/span")?.Count ?? 0); + } + + [Theory] + [InlineData("
xy
", ".del", "//div/span")] + public void RemoveNodes_HtmlNodeWithSelector(string html, string selector, string remainingXpath) + { + var d = Doc(html); + d.DocumentNode.SelectSingleNode("//div").RemoveNodes(selector); + Assert.Single(d.DocumentNode.SelectNodes(remainingXpath)); + } +} diff --git a/tests/Elib2Ebook.DomainServices.Tests/Extensions/StreamExtensionTests.cs b/tests/Elib2Ebook.DomainServices.Tests/Extensions/StreamExtensionTests.cs new file mode 100644 index 00000000..4c116f25 --- /dev/null +++ b/tests/Elib2Ebook.DomainServices.Tests/Extensions/StreamExtensionTests.cs @@ -0,0 +1,33 @@ +using System.Text; +using Elib2Ebook.DomainServices.Extensions; + +namespace Elib2Ebook.DomainServices.Tests.Extensions; + +public class StreamExtensionTests +{ + private static MemoryStream MakeStream(string html) + { + return new MemoryStream(Encoding.UTF8.GetBytes(html)); + } + + [Theory] + [InlineData("

test

")] + [InlineData("")] + public void AsHtmlDoc(string html) + { + Assert.NotNull(MakeStream(html).AsHtmlDoc()); + } + + [Theory] + [InlineData("

тест

", "тест")] + public void AsHtmlDoc_WithEncoding(string html, string expected) + { + Assert.Equal(expected, MakeStream(html).AsHtmlDoc(Encoding.UTF8).DocumentNode.InnerText.Trim()); + } + + [Fact] + public void AsHtmlDoc_NullEncoding() + { + Assert.NotNull(MakeStream("

hello

").AsHtmlDoc()); + } +} diff --git a/tests/Elib2Ebook.DomainServices.Tests/Extensions/StringBuilderExtensionsTests.cs b/tests/Elib2Ebook.DomainServices.Tests/Extensions/StringBuilderExtensionsTests.cs new file mode 100644 index 00000000..600e78b7 --- /dev/null +++ b/tests/Elib2Ebook.DomainServices.Tests/Extensions/StringBuilderExtensionsTests.cs @@ -0,0 +1,15 @@ +using System.Text; +using Elib2Ebook.DomainServices.Extensions; + +namespace Elib2Ebook.DomainServices.Tests.Extensions; + +public class StringBuilderExtensionsTests +{ + [Theory] + [InlineData("

test

", "test")] + [InlineData("", "")] + public void AsHtmlDoc(string html, string expectedText) + { + Assert.Equal(expectedText, new StringBuilder(html).AsHtmlDoc().DocumentNode.InnerText.Trim()); + } +} diff --git a/tests/Elib2Ebook.DomainServices.Tests/Extensions/StringExtensionsTests.cs b/tests/Elib2Ebook.DomainServices.Tests/Extensions/StringExtensionsTests.cs new file mode 100644 index 00000000..f3177af5 --- /dev/null +++ b/tests/Elib2Ebook.DomainServices.Tests/Extensions/StringExtensionsTests.cs @@ -0,0 +1,105 @@ +using Elib2Ebook.DomainServices.Extensions; + +namespace Elib2Ebook.DomainServices.Tests.Extensions; + +public class StringExtensionsTests +{ + [Theory] + [InlineData("", "")] + [InlineData("Hello World.txt", "Hello World.txt")] + public void RemoveInvalidChars(string input, string expected) + { + Assert.Equal(expected, input.RemoveInvalidChars()); + } + + [Theory] + [InlineData("test", "\"test\"")] + public void CoverQuotes(string input, string expected) + { + Assert.Equal(expected, input.CoverQuotes()); + } + + [Theory] + [InlineData("hello", 10, "hello")] + [InlineData("hello world", 5, "hello")] + public void Crop(string input, int len, string expected) + { + Assert.Equal(expected, input.Crop(len)); + } + + [Theory] + [InlineData(null, null)] + [InlineData(" ", " ")] + [InlineData("text", "text")] + public void HtmlDecode(string input, string expected) + { + Assert.Equal(expected, input.HtmlDecode()); + } + + [Theory] + [InlineData("text", "b", "text")] + [InlineData("text", "", "text")] + public void CoverTag(string input, string tag, string expected) + { + Assert.Equal(expected, input.CoverTag(tag)); + } + + [Theory] + [InlineData("line1\nline2\tline3")] + [InlineData(" ")] + [InlineData(null)] + public void ReplaceNewLine(string input) + { + var result = input.ReplaceNewLine(); + if (input is null) + { + Assert.Null(result); + } + else + { + Assert.DoesNotContain("\n", result); + Assert.DoesNotContain("\t", result); + } + } + + [Theory] + [InlineData("a b c", "a b c")] + [InlineData("", "")] + public void CollapseWhitespace(string input, string expected) + { + Assert.Equal(expected, input.CollapseWhitespace()); + } + + [Fact] + public void HtmlEncode() + { + var result = "ac".HtmlEncode(); + Assert.Contains("&", result); + } + + [Fact] + public void AsUri() + { + Assert.Equal("https://example.com/", "https://example.com".AsUri().ToString()); + } + + [Fact] + public void CleanInvalidXmlChars() + { + Assert.Equal("test", "te\x00st".CleanInvalidXmlChars()); + } + + [Theory] + [InlineData("

test

")] + public void AsHtmlDoc(string html) + { + Assert.NotNull(html.AsHtmlDoc().DocumentNode); + } + + [Theory] + [InlineData("

test

")] + public void AsXHtmlDoc(string html) + { + Assert.NotNull(html.AsXHtmlDoc()); + } +} diff --git a/tests/Elib2Ebook.DomainServices.Tests/Extensions/UriExtensionTests.cs b/tests/Elib2Ebook.DomainServices.Tests/Extensions/UriExtensionTests.cs new file mode 100644 index 00000000..6e19e111 --- /dev/null +++ b/tests/Elib2Ebook.DomainServices.Tests/Extensions/UriExtensionTests.cs @@ -0,0 +1,72 @@ +using Elib2Ebook.DomainServices.Extensions; + +namespace Elib2Ebook.DomainServices.Tests.Extensions; + +public class UriExtensionTests +{ + private static readonly Uri Base = new("https://example.com/path/to/file.txt?q=1&r=2"); + + [Fact] + public void GetFileName() + { + Assert.Equal("file.txt", Base.GetFileName()); + } + + [Theory] + [InlineData("https://example.com/path/to/file.txt?q=1&r=2/new", "new")] + [InlineData("https://example.com/path/to/file.txt?q=1&r=2/new/", "/new/")] + public void AppendSegment(string expected, string segment) + { + Assert.Equal(expected, Base.AppendSegment(segment).ToString()); + } + + [Fact] + public void GetSegment() + { + Assert.Equal("b", new Uri("https://example.com/a/b/c").GetSegment(2)); + } + + [Theory] + [InlineData("r=2")] + [InlineData("q=1")] + public void AppendQueryParameter(string expectedParam) + { + Assert.Contains(expectedParam, new Uri("https://example.com/page?q=1").AppendQueryParameter("r", 2).Query); + } + + [Fact] + public void GetQueryParameter() + { + Assert.Equal("value", new Uri("https://example.com/page?name=value").GetQueryParameter("name")); + } + + [Fact] + public void ReplaceHost() + { + Assert.Equal("https://new.com/path", new Uri("https://old.com/path").ReplaceHost("new.com").ToString()); + } + + [Theory] + [InlineData("https://example.com/p1", "https://example.com/p2", true)] + [InlineData("https://www.example.com/p", "https://example.com/p", true)] + [InlineData("https://example.com/p", "https://other.com/p", false)] + public void IsSameHost(string url1, string url2, bool expected) + { + Assert.Equal(expected, new Uri(url1).IsSameHost(new Uri(url2))); + } + + [Theory] + [InlineData("https://sub.example.com/p", "https://example.com/p", true)] + [InlineData("https://sub.other.com/p", "https://example.com/p", false)] + [InlineData("https://example.com/p", "https://example.com/p", true)] + public void IsSameSubDomain(string url1, string url2, bool expected) + { + Assert.Equal(expected, new Uri(url1).IsSameSubDomain(new Uri(url2))); + } + + [Fact] + public void MakeRelativeUri() + { + Assert.Equal("https://example.com/base/relative/path", new Uri("https://example.com/base/").MakeRelativeUri("relative/path").ToString()); + } +} diff --git a/tests/Elib2Ebook.DomainServices.Tests/Logic/Builders/BuilderTests.cs b/tests/Elib2Ebook.DomainServices.Tests/Logic/Builders/BuilderTests.cs new file mode 100644 index 00000000..00f72292 --- /dev/null +++ b/tests/Elib2Ebook.DomainServices.Tests/Logic/Builders/BuilderTests.cs @@ -0,0 +1,40 @@ +using Elib2Ebook.DomainServices.Builders; +using Elib2Ebook.DomainServices.Configs; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Elib2Ebook.DomainServices.Tests.Logic.Builders; + +public class BuilderTests +{ + private static Options Opt(string format) + => new( + [ + "https://example.com/book", + ]) + { + Format = + [ + format, + ], + BookNamePattern = "{Book.Title}", + Timeout = 120 + }; + + public static IEnumerable BuilderData() + { + yield return ["fb2", typeof(Fb2Builder)]; + yield return ["epub", typeof(EpubBuilder)]; + yield return ["cbz", typeof(CbzBuilder)]; + yield return ["txt", typeof(TxtBuilder)]; + yield return ["json", typeof(JsonBuilder)]; + yield return ["json_lite", typeof(JsonLiteBuilder)]; + } + + [Theory] + [MemberData(nameof(BuilderData))] + public void CanCreate(string format, Type type) + { + var b = (BuilderBase)Activator.CreateInstance(type, Opt(format), NullLogger.Instance)!; + Assert.IsAssignableFrom(b); + } +} diff --git a/tests/Elib2Ebook.DomainServices.Tests/Misc/MiscTests.cs b/tests/Elib2Ebook.DomainServices.Tests/Misc/MiscTests.cs new file mode 100644 index 00000000..19ff4b18 --- /dev/null +++ b/tests/Elib2Ebook.DomainServices.Tests/Misc/MiscTests.cs @@ -0,0 +1,74 @@ +using Elib2Ebook.Domain.Book; +using Elib2Ebook.DomainServices.Builders; +using Elib2Ebook.DomainServices.Configs; +using Elib2Ebook.DomainServices.Misc; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Elib2Ebook.DomainServices.Tests.Misc; + +public class BuilderProviderTests +{ + [Theory] + [InlineData("fb2", typeof(Fb2Builder))] + [InlineData("epub", typeof(EpubBuilder))] + [InlineData("json", typeof(JsonBuilder))] + [InlineData("cbz", typeof(CbzBuilder))] + [InlineData("txt", typeof(TxtBuilder))] + [InlineData("JSON_LITE", typeof(JsonLiteBuilder))] + public void Get(string format, Type type) + { + Assert.IsType( + type, + BuilderProvider.Get( + format, + new Options( + [ + "https://x.com", + ]), + NullLogger.Instance)); + } + + [Fact] + public void Get_Invalid() + { + Assert.Throws(() => BuilderProvider.Get( + "bad", + new Options( + [ + "https://x.com", + ]), + NullLogger.Instance)); + } +} + +public class BookNameBuilderLogicTests +{ + private static Book Book(string title, string author, Seria seria = null) + { + return new Book(new Uri("https://x.com")) + { + Title = title, Author = new Author(author), Seria = seria + }; + } + + [Theory] + [InlineData("{Book.Title}", "Test", "Test")] + [InlineData("{Book.Title} - {Author.Name}", "My Book", "My Book - Author")] + [InlineData("{Book.Title} - {Seria.Name} #{Seria.Number}", "Book", "Book - S #1")] + [InlineData("{Book.Title} by {Author.Name}", "Book", "Book by Author")] + public void Build(string pattern, string title, string expected) + { + var seria = pattern.Contains("Seria") ? new Seria + { + Name = "S", Number = "1" + } : null; + + Assert.Equal(expected, BookNameBuilder.Build(pattern, Book(title, "Author", seria))); + } + + [Fact] + public void Build_WithQuotes() + { + Assert.DoesNotContain("\"", BookNameBuilder.Build("\"{Book.Title}\"", Book("Test", "A"))); + } +} diff --git a/tests/Elib2Ebook.DomainServices.Tests/Misc/TempFolderTests.cs b/tests/Elib2Ebook.DomainServices.Tests/Misc/TempFolderTests.cs new file mode 100644 index 00000000..2dc0507a --- /dev/null +++ b/tests/Elib2Ebook.DomainServices.Tests/Misc/TempFolderTests.cs @@ -0,0 +1,106 @@ +using Elib2Ebook.DomainServices.Misc.TempFolder; + +namespace Elib2Ebook.DomainServices.Tests.Misc; + +public class TempFolderTests +{ + [Fact] + public void Create_WithNullPath_CreatesInTemp() + { + using var folder = TempFolderFactory.Create(null); + Assert.NotNull(folder); + Assert.NotNull(folder.Path); + Assert.True(Directory.Exists(folder.Path)); + Assert.Contains("_e2e_temp", folder.Path); + } + + [Fact] + public void Create_WithSpecifiedPath_CreatesFolder() + { + var path = Path.Combine(Path.GetTempPath(), "test_temp_folder_" + Guid.NewGuid()); + try + { + using var folder = TempFolderFactory.Create(path); + Assert.Equal(path, folder.Path); + Assert.True(Directory.Exists(path)); + } + finally + { + if (Directory.Exists(path)) + { + Directory.Delete(path); + } + } + } + + [Fact] + public void Create_WithExistingPath_DoesNotThrow() + { + var path = Path.Combine(Path.GetTempPath(), "existing_temp_" + Guid.NewGuid()); + Directory.CreateDirectory(path); + try + { + using var folder = TempFolderFactory.Create(path); + Assert.Equal(path, folder.Path); + Assert.True(Directory.Exists(path)); + } + finally + { + if (Directory.Exists(path)) + { + Directory.Delete(path); + } + } + } + + [Fact] + public void Dispose_WithEmptyFolder_DeletesIt() + { + var path = Path.Combine(Path.GetTempPath(), "dispose_test_" + Guid.NewGuid()); + var folder = new TempFolder(path); + Directory.CreateDirectory(path); + Assert.True(Directory.Exists(path)); + folder.Dispose(); + Assert.False(Directory.Exists(path)); + } + + [Fact] + public void Dispose_WithNonEmptyFolder_DoesNotDelete() + { + var path = Path.Combine(Path.GetTempPath(), "nonempty_test_" + Guid.NewGuid()); + var folder = new TempFolder(path); + Directory.CreateDirectory(path); + File.WriteAllText(Path.Combine(path, "test.txt"), "content"); + Assert.True(Directory.Exists(path)); + folder.Dispose(); + Assert.True(Directory.Exists(path)); + Directory.Delete(path, true); + } + + [Fact] + public void Dispose_WithNonExistentFolder_DoesNotThrow() + { + var path = Path.Combine(Path.GetTempPath(), "nonexistent_" + Guid.NewGuid()); + var folder = new TempFolder(path); + var exception = Record.Exception(() => folder.Dispose()); + Assert.Null(exception); + } + + [Fact] + public void Path_IsReadonly() + { + using var folder = TempFolderFactory.Create(null); + Assert.NotNull(folder.Path); + } + + [Fact] + public void Dispose_MultipleCalls_DoesNotThrow() + { + var path = Path.Combine(Path.GetTempPath(), "multiple_dispose_" + Guid.NewGuid()); + var folder = new TempFolder(path); + Directory.CreateDirectory(path); + folder.Dispose(); + var exception = Record.Exception(() => folder.Dispose()); + Assert.Null(exception); + } +}