25 Commits

Author SHA1 Message Date
FrigaT
8c5dca1491 Добавлен парсинг EnumMember
All checks were successful
Release / pack-and-publish (release) Successful in 32s
2026-04-23 17:16:18 +03:00
815283a776 readme 2026-04-21 12:51:23 +03:00
526353d679 Переделано воспроизведение аудио
All checks were successful
Release / pack-and-publish (release) Successful in 36s
2026-04-21 11:14:36 +03:00
eb1eba0162 Полный рефакторинг api. Вынесено отдельно api passport 2026-04-20 23:30:01 +03:00
FrigaT
34261d02a9 Авторизация через паспорт
All checks were successful
Release / pack-and-publish (release) Successful in 31s
2026-04-20 15:58:27 +03:00
FrigaT
5f761d4fe8 Добавлена авторизация через паспорт 2026-04-20 15:56:38 +03:00
FrigaT
b6f78da9c8 dispose
All checks were successful
Release / pack-and-publish (release) Successful in 37s
2026-04-20 14:47:43 +03:00
FrigaT
0bbaac5689 Переделан способ авторизации по qr
All checks were successful
Release / pack-and-publish (release) Successful in 1m5s
2026-04-20 14:31:47 +03:00
FrigaT
a7caf829d3 Открыл AuthToken
All checks were successful
Release / pack-and-publish (release) Successful in 34s
2026-04-19 20:19:41 +03:00
FrigaT
add7f08215 Добавлен вывод AuthStorage. Спрятаны внутренние api запросы
All checks were successful
Release / pack-and-publish (release) Successful in 32s
2026-04-19 17:41:30 +03:00
FrigaT
36e28ce3fe Полностью переписанное api
All checks were successful
Release / pack-and-publish (release) Successful in 36s
2026-04-19 17:00:05 +03:00
FrigaT
5541d0ad27 fix Artist api
All checks were successful
Release / pack-and-publish (release) Successful in 40s
2026-04-16 18:43:07 +03:00
FrigaT
ea9f392896 Добавлен конвертер string->int
All checks were successful
Release / pack-and-publish (release) Successful in 9m39s
2026-04-14 21:25:59 +03:00
FrigaT
6dcf39de56 Добавлен readme
All checks were successful
Release / pack-and-publish (release) Successful in 30s
2026-04-14 02:54:03 +03:00
FrigaT
50da85be57 Убраны лишние классы 2026-04-14 02:47:05 +03:00
FrigaT
699d38da74 Убрал синхронность 2026-04-14 02:15:15 +03:00
FrigaT
1779ecaca9 Исправлено удаление треков из плейлиста 2026-04-14 02:08:11 +03:00
FrigaT
8abc6c5074 fix
All checks were successful
Release / pack-and-publish (release) Successful in 32s
2026-04-13 15:42:42 +03:00
FrigaT
b8f78a5856 fix execute context 2026-04-13 15:41:44 +03:00
FrigaT
21a0c5abe6 обновление json parse
All checks were successful
Release / pack-and-publish (release) Successful in 45s
2026-04-13 13:55:23 +03:00
FrigaT
a40b36ef96 Доработана конвертация json
All checks were successful
Release / pack-and-publish (release) Successful in 35s
2026-04-13 13:49:37 +03:00
FrigaT
266aa2e181 fix
All checks were successful
Release / pack-and-publish (release) Successful in 31s
2026-04-13 02:30:14 +03:00
FrigaT
3e5b9beaf1 ci
All checks were successful
Release / pack-and-publish (release) Successful in 55s
2026-04-12 17:02:49 +03:00
FrigaT
6f01772bb7 ci
Some checks failed
Release / pack-and-publish (release) Has been cancelled
2026-04-12 17:00:47 +03:00
FrigaT
dd338e3f57 Добавлена сборка
Some checks failed
CI / build-test (push) Failing after 1m59s
Release / pack-and-publish (release) Failing after 1m10s
2026-04-12 16:47:50 +03:00
258 changed files with 5530 additions and 65214 deletions

View File

@@ -0,0 +1,51 @@
name: Release
on:
release:
types: [published]
jobs:
pack-and-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: Set version from tag
id: version
run: |
TAG="${GITHUB_REF_NAME#v}"
echo "PACKAGE_VERSION=$TAG" >> $GITHUB_OUTPUT
- name: Replace ProjectReference with PackageReference
run: |
sed -i "s#<ProjectReference Include=\"..\/YandexMusic.API\/YandexMusic.API.csproj\" />#<PackageReference Include=\"YandexMusic.API\" Version=\"${{ steps.version.outputs.PACKAGE_VERSION }}\" />#" YandexMusic/YandexMusic.csproj
- name: Build and Pack projects
run: |
mkdir -p artifacts
for proj in $PROJECTS; do
echo "Restoring $proj..."
dotnet restore $proj
echo "Building $proj..."
dotnet build $proj -c Release -p:Version=${{ steps.version.outputs.PACKAGE_VERSION }}
echo "Packing $proj..."
dotnet pack $proj -c Release --no-build -p:PackageVersion=${{ steps.version.outputs.PACKAGE_VERSION }} -o ./artifacts
done
env:
PROJECTS: |
YandexMusic.API
YandexMusic
- name: Publish to NuGet
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: dotnet nuget push ./artifacts/*.nupkg --source https://git.frigat.duckdns.org/api/packages/FrigaT/nuget/index.json --api-key $NUGET_API_KEY --skip-duplicate

3
.gitignore vendored
View File

@@ -360,4 +360,5 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd
YaMusicCli/

607
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,607 @@
# 🏗️ Архитектура YandexMusic
Полное описание архитектуры, компонентов и взаимодействия между слоями решения YandexMusic.
## 📋 Содержание
- [Обзор архитектуры](#обзор-архитектуры)
- [Структура слоёв](#структура-слоёв)
- [Основные компоненты](#основные-компоненты)
- [Потоки данных](#потоки-данных)
- [Паттерны проектирования](#паттерны-проектирования)
- [Модульная структура](#модульная-структура)
- [Расширяемость](#расширяемость)
## 🔍 Обзор архитектуры
### Многослойная архитектура
Проект построен на классической трёхслойной архитектуре с элементами DDD:
```
┌────────────────────────────────────────┐
│ CLI Layer (YaMusicCli) │ ← Точка входа
├────────────────────────────────────────┤
│ Client Layer (YandexMusic) │ ← Удобный интерфейс
├────────────────────────────────────────┤
│ API Layer (YandexMusic.API) │ ← Логика API
├────────────────────────────────────────┤
│ Data Layer (Models) │ ← Данные и сущности
├────────────────────────────────────────┤
│ Infrastructure Layer │ ← HTTP, Serialization
└────────────────────────────────────────┘
```
### Ключевые принципы
- **Separation of Concerns** - каждый слой отвечает за одно
- **Dependency Injection** - слабая связанность через интерфейсы
- **Single Responsibility** - каждый класс решает одну задачу
- **Open/Closed Principle** - открыто для расширения, закрыто для модификации
- **Asynchronous First** - всё асинхронное по умолчанию
## 📦 Структура слоёв
### 1. CLI Layer (YaMusicCli)
**Назначение:** Командная строка интерфейс для пользователей.
```
YaMusicCli/
├── Program.cs # Точка входа, парсинг аргументов
└── Commands/ # Команды для CLI
├── SearchCommand
├── PlaylistCommand
└── TrackCommand
```
**Ответственность:**
- Парсинг команд пользователя
- Вывод результатов в консоль
- Обработка пользовательского ввода
- Форматирование данных для вывода
**Зависимости:** YandexMusic (Client Layer)
### 2. Client Layer (YandexMusic)
**Назначение:** Удобный оборачиватель (wrapper) для работы с API.
```
YandexMusic/
└── YandexMusicClient.cs
├── Initialization (конструктор)
├── Authorization (авторизация)
├── Properties (доступ к API и хранилищу)
└── Cleanup (dispose)
```
**Основной класс:**
```csharp
public class YandexMusicClient : IDisposable
{
// Инициализация
public YandexMusicClient(
CookieContainer? cookieContainer = null,
IWebProxy? proxy = null,
TimeSpan? timeout = null,
string? userAgent = null)
// Авторизация
public async Task AuthorizeAsync(string login, string password)
public async Task AuthorizeByTokenAsync(string token)
// Свойства доступа
public YandexMusicApi Api { get; } // API Яндекс Музыки
public AuthStorage AuthStorage { get; } // Хранилище данных
public HttpClient HttpClient { get; } // HttpClient
public YAccount Account { get; } // Информация об аккаунте
public bool IsAuthorized { get; } // Статус авторизации
public YnisonPlayer? Ynison { get; } // WebSocket плеер
}
```
**Ответственность:**
- Упрощение работы с API
- Управление HttpClient и cookies
- Обработка авторизации
- Инициализация компонентов
**Зависимости:** YandexMusic.API (API Layer)
### 3. API Layer (YandexMusic.API)
**Назначение:** Низкоуровневой доступ к API Яндекс Музыки.
```
YandexMusic.API/
├── YandexMusicApi.cs # Главный класс - фасад
├── API/ # API классы по функциям
│ ├── YAlbumAPI
│ ├── YArtistAPI
│ ├── YTrackAPI
│ ├── YPlaylistAPI
│ ├── YRadioAPI
│ ├── YSearchAPI
│ └── ... (остальные API)
├── Requests/ # Построители запросов
│ ├── Common/
│ ├── Album/
│ ├── Artist/
│ └── ... (по категориям)
├── Models/ # Модели данных
│ ├── Common/
│ ├── Album/
│ ├── Track/
│ └── ... (по типам)
└── Common/ # Вспомогательные компоненты
├── AuthStorage
├── Providers/
├── Encryptor
└── Ynison/
```
**Главный класс:**
```csharp
public class YandexMusicApi
{
// API Методы
public YAlbumAPI Album { get; }
public YArtistAPI Artist { get; }
public YTrackAPI Track { get; }
public YPlaylistAPI Playlist { get; }
public YRadioAPI Radio { get; }
public YSearchAPI Search { get; }
// ... и так далее для всех веток API
}
```
**API Классы:**
Каждый API класс наследуется от `YCommonAPI`:
```csharp
public abstract class YCommonAPI
{
protected readonly YandexMusicApi api;
protected YCommonAPI(YandexMusicApi yandex) => api = yandex;
}
// Пример конкретного API
public class YTrackAPI : YCommonAPI
{
public YTrackAPI(YandexMusicApi yandex) : base(yandex) { }
public async Task<YTrack?> GetTrackAsync(string trackId) { }
public async Task<IEnumerable<YTrack>> GetTracksAsync(IEnumerable<string> ids) { }
// ... остальные методы
}
```
**Ответственность:**
- Построение HTTP запросов
- Вызов провайдера запросов
- Десериализация ответов
- Обработка ошибок API
### 4. Data Layer (Models)
**Назначение:** Модели данных, отражающие структуру API Яндекс Музыки.
```
Models/
├── Common/ # Общие модели
│ ├── YBaseModel # Базовая модель с контекстом
│ ├── YResponse<T> # Ответ от API
│ ├── YError # Ошибка
│ └── ... (остальные)
├── Album/ # Модели альбомов
│ ├── YAlbum
│ └── YAlbumMenuItem
├── Track/ # Модели треков
│ ├── YTrack
│ ├── YTrackSupplement
│ └── YTrackSimilar
├── Playlist/ # Модели плейлистов
│ ├── YPlaylist
│ ├── YPlaylistChange
│ └── YPlaylistMadeFor
└── ... (остальные модели)
```
**Базовая модель:**
```csharp
public abstract class YBaseModel
{
[JsonIgnore]
public YExecutionContext? Context { get; set; }
}
// Пример модели
public class YTrack : YBaseModel
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("duration_ms")]
public int DurationMs { get; set; }
// ... остальные свойства
}
```
**Ответственность:**
- Представление данных API
- System.Text.Json сериализация/десериализация
- Хранение контекста выполнения (опционально)
### 5. Infrastructure Layer
**Назначение:** Низкоуровневая инфраструктура для работы с HTTP и сериализацией.
```
Common/
├── Providers/ # Провайдеры запросов
│ ├── IRequestProvider # Интерфейс
│ ├── DefaultRequestProvider
│ ├── CommonRequestProvider
│ └── MockRequestProvider
├── AuthStorage # Управление авторизацией
├── Encryptor # Шифрование данных
├── DataDownloader # Загрузка файлов
└── Ynison/ # WebSocket
├── YnisonPlayer
└── YnisonWebSocket
```
**IRequestProvider:**
```csharp
public interface IRequestProvider
{
// Выполняет HTTP запрос
Task<HttpResponseMessage> GetWebResponseAsync(
HttpRequestMessage message,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead);
// Преобразует ответ в модель
Task<T> GetDataFromResponseAsync<T>(
YandexMusicApi api,
HttpResponseMessage response);
}
```
## 🔄 Потоки данных
### Типичный поток запроса
```
1. User Action
2. YandexMusicClient Method
3. YandexMusicApi.SomeAPI.SomeMethodAsync()
4. YRequestBuilder<T> builds HttpRequestMessage
5. IRequestProvider.GetWebResponseAsync()
6. HttpClient sends request
7. Server responds with HttpResponseMessage
8. IRequestProvider.GetDataFromResponseAsync<T>()
9. System.Text.Json deserializes to Model<T>
10. Model<T> returned to caller
```
### Пример: Получение трека
```csharp
// 1. Пользователь вызывает метод
var track = await client.Api.Track.GetTrackAsync("trackId123");
// 2. YTrackAPI.GetTrackAsync() создаёт запрос
var builder = new YGetTracksBuilder(api);
var message = builder.Build("trackId123");
// 3. Запрос отправляется через провайдер
var response = await authStorage.Provider.GetWebResponseAsync(message);
// 4. Ответ преобразуется в модель
var track = await provider.GetDataFromResponseAsync<YTrack>(api, response);
// 5. Модель возвращается пользователю
return track;
```
## 🎯 Паттерны проектирования
### 1. Facade Pattern (YandexMusicApi)
```csharp
// Фасад предоставляет простой интерфейс к сложной подсистеме
public class YandexMusicApi
{
public YAlbumAPI Album { get; } // Скрывает сложность
public YArtistAPI Artist { get; } // инициализации
public YTrackAPI Track { get; } // каждого компонента
}
```
### 2. Builder Pattern (Request Builders)
```csharp
// Построитель для конструирования запросов
public class YSearchBuilder : YRequestBuilder<YSearch>
{
private string _query;
private int _page = 0;
public YSearchBuilder Query(string q) { _query = q; return this; }
public YSearchBuilder Page(int p) { _page = p; return this; }
public override HttpRequestMessage Build() { /* ... */ }
}
// Использование
var search = new YSearchBuilder()
.Query("Beatles")
.Page(1)
.Build();
```
### 3. Strategy Pattern (IRequestProvider)
```csharp
// Интерфейс позволяет выбрать стратегию обработки запросов
public interface IRequestProvider
{
Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message);
}
// Разные реализации для разных сценариев
public class DefaultRequestProvider : IRequestProvider { }
public class MockRequestProvider : IRequestProvider { }
public class CustomRequestProvider : IRequestProvider { }
```
### 4. Dependency Injection
```csharp
// Зависимости передаются через конструктор
public class YTrackAPI : YCommonAPI
{
protected readonly YandexMusicApi api; // Injected
public YTrackAPI(YandexMusicApi yandex) => api = yandex;
}
```
### 5. Template Method Pattern
```csharp
// Базовый класс определяет структуру
public abstract class YCommonAPI
{
protected readonly YandexMusicApi api;
protected YCommonAPI(YandexMusicApi yandex) => api = yandex;
}
// Подклассы реализуют специфичные методы
public class YAlbumAPI : YCommonAPI
{
// Реализация методов для альбомов
}
```
## 🧩 Модульная структура
### Логическое разделение на модули
```
Core Module (YandexMusic.API)
├── Authentication Module
│ ├── AuthStorage
│ └── Authorization methods
├── Request Module
│ ├── IRequestProvider
│ ├── Request builders
│ └── HttpContext
├── Model Module
│ └── All data models
└── API Module
├── YAlbumAPI
├── YTrackAPI
├── YPlaylistAPI
└── ... (each API endpoint)
Client Module (YandexMusic)
├── YandexMusicClient
└── Client configuration
CLI Module (YaMusicCli)
├── Program.cs
└── CLI commands
```
### Зависимости между модулями
```
CLI Module
↓ depends on
Client Module
↓ depends on
Core Module
├── Authentication Module
├── Request Module
├── Model Module
└── API Module
```
## 🔌 Расширяемость
### Как добавить новый API метод
```csharp
// 1. Создайте builder в Requests/YourCategory/
public class YGetYourResourceBuilder : YRequestBuilder<YYourResource>
{
public override HttpRequestMessage Build()
{
// Реализуйте построение запроса
}
}
// 2. Добавьте метод в соответствующий API класс
public class YYourResourceAPI : YCommonAPI
{
public async Task<YYourResource?> GetYourResourceAsync(string id)
{
var builder = new YGetYourResourceBuilder(api);
var request = builder.Build();
return await new YRequest<YYourResource?>(request, api, api.AuthStorage).GetResponseAsync();
}
}
// 3. Используйте новый метод
var resource = await api.YourResource.GetYourResourceAsync("id123");
```
### Как создать custom RequestProvider
```csharp
public class MyCustomRequestProvider : IRequestProvider
{
private readonly AuthStorage _storage;
public MyCustomRequestProvider(AuthStorage storage)
{
_storage = storage;
}
public async Task<HttpResponseMessage> GetWebResponseAsync(
HttpRequestMessage message,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
// Ваша логика обработки запроса
using var handler = new HttpClientHandler();
using var client = new HttpClient(handler);
return await client.SendAsync(message, completionOption);
}
public async Task<T> GetDataFromResponseAsync<T>(
YandexMusicApi api,
HttpResponseMessage response)
{
// Ваша логика десериализации
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(content) ?? default!;
}
}
// Использование
var provider = new MyCustomRequestProvider(storage);
var storage = new AuthStorage(provider);
```
### Как создать extension method
```csharp
public static class YPlaylistExtensions
{
public static async Task<IEnumerable<YTrack>> GetAllTracksAsync(
this YPlaylist playlist,
YandexMusicApi api)
{
// Расширение функциональности существующего класса
var allTracks = new List<YTrack>();
var currentPage = 0;
while (true)
{
var page = await api.Playlist.GetPlaylistAsync(
playlist.Uid,
page: currentPage);
if (page?.Tracks?.Count == 0)
break;
allTracks.AddRange(page.Tracks ?? []);
currentPage++;
}
return allTracks;
}
}
```
## 📊 Диаграмма взаимодействия компонентов
```
┌──────────────────┐
│ CLI Layer │
│ (YaMusicCli) │
└────────┬─────────┘
┌──────────────────────────┐
│ Client Layer │
│ (YandexMusicClient) │
└────────┬─────────────────┘
┌────────────────────────────────────┐
│ API Layer │
│ ┌─────────────────────────────┐ │
│ │ YandexMusicApi (Facade) │ │
│ └──┬──────────────────────────┘ │
│ │ │
│ ┌──┴─────────┬─────────────────┐│
│ ↓ ↓ ↓│
│ YTrackAPI YPlaylistAPI YSearchAPI │
│ │ │ │ │
│ └──────┬─────┴────────┬────────┘ │
└─────────┼──────────────┼───────────┘
│ │
↓ ↓
┌──────────────────────────┐
│ IRequestProvider │
│ (Strategy Pattern) │
└──────┬───────────────────┘
┌──┴───┬──────────┐
↓ ↓ ↓
Default Common Mock
Provider Provider Provider
│ │ │
└──────┼────────┘
┌──────────────────┐
│ HttpClient │
└──────┬───────────┘
┌──────────────────┐
│ Network Layer │
└──────────────────┘
```
## 🔐 Безопасность архитектуры
1. **Инкапсуляция** - приватные поля и контролируемый доступ
2. **Валидация** - все входные параметры валидируются
3. **Null-safety** - полная поддержка nullable reference types
4. **Async-safe** - асинхронные операции без blocking
5. **Encryption** - встроенное шифрование для чувствительных данных

410
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,410 @@
# 🤝 Руководство для участников
Спасибо за интерес к проекту YandexMusic! Этот документ содержит рекомендации для тех, кто хочет внести свой вклад в развитие проекта.
## 📋 Содержание
- [Кодекс поведения](#кодекс-поведения)
- [Как начать](#как-начать)
- [Процесс разработки](#процесс-разработки)
- [Стандарты кода](#стандарты-кода)
- [Commit сообщения](#commit-сообщения)
- [Pull Requests](#pull-requests)
- [Отчёты об ошибках](#отчёты-об-ошибках)
- [Предложения по улучшениям](#предложения-по-улучшениям)
## 🤐 Кодекс поведения
- Будьте уважительны к другим участникам
- Не допускайте дискриминацию, оскорбления и враждебного поведения
- Критикуйте идеи, а не людей
- Решайте конфликты конструктивно
## 🚀 Как начать
### 1. Подготовка окружения
```bash
# Клонируем репозиторий
git clone https://git.frigat.duckdns.org/FrigaT/YandexMusic.git
cd YandexMusic
# Создаём ветку для своей работы
git checkout -b feature/my-feature
# Восстанавливаем зависимости
dotnet restore
# Собираем проект
dotnet build
```
### 2. Установка инструментов
- **Visual Studio 2026 Enterprise** (рекомендуется)
- **Visual Studio Code** + C# extension (альтернатива)
- **.NET 10 SDK**
### 3. Запуск тестов
```bash
dotnet test
```
## 🔄 Процесс разработки
### Workflow
```
1. Выберите issue или создайте новый
2. Создайте feature-ветку
3. Внесите изменения
4. Напишите/обновите тесты
5. Убедитесь что код собирается и тесты проходят
6. Создайте Pull Request
7. Ждите review
8. Исправьте замечания если нужно
9. Merge в master
```
### Версионирование веток
```
master → Production версия
├── feature/* → Новые функции
├── bugfix/* → Исправления ошибок
├── refactor/* → Рефакторинг кода
└── docs/* → Обновления документации
```
## 📐 Стандарты кода
### Общие правила
- **Язык:** C# 12
- **Платформа:** .NET 10
- **Стиль:** Microsoft C# Coding Conventions
- **Документация:** Все публичные члены должны иметь XML документацию на русском
### Пример документированного кода
```csharp
namespace YandexMusic.API.API;
/// <summary>API для работы с альбомами.</summary>
public class YAlbumAPI : YCommonAPI
{
/// <summary>Инициализирует новый экземпляр.</summary>
/// <param name="yandex">Экземпляр основного API.</param>
public YAlbumAPI(YandexMusicApi yandex) : base(yandex) { }
/// <summary>Получает информацию об альбоме по идентификатору.</summary>
/// <param name="albumId">Идентификатор альбома</param>
/// <returns>Модель альбома или null если не найден</returns>
/// <exception cref="ArgumentNullException">Если albumId null</exception>
public async Task<YAlbum?> GetAlbumAsync(string albumId)
{
ArgumentNullException.ThrowIfNull(albumId);
// Реализация
return await Task.FromResult<YAlbum?>(null);
}
}
```
### Правила именования
```csharp
// Классы: PascalCase
public class YandexMusicApi { }
// Методы: PascalCase с Async суффиксом для асинхронных
public async Task<YTrack?> GetTrackAsync(string trackId) { }
// Свойства: PascalCase
public YandexMusicApi Api { get; }
// Приватные поля: _camelCase
private readonly HttpClient _httpClient;
// Параметры: camelCase
public void DoSomething(string userName, int userId) { }
// Локальные переменные: camelCase
var trackList = new List<YTrack>();
```
### Code Style
Используйте `.editorconfig` для автоматического форматирования:
```ini
# Отступы - 4 пробела
indent_size = 4
# Новая строка для фигурных скобок
csharp_new_line_before_open_brace = all
# Используйте var где возможно
csharp_style_var_for_built_in_types = true
# Null-forgiving operator с осторожностью
csharp_style_null_forgiving_operator = false
```
### Nullable Reference Types
Всегда включен `<Nullable>enable</Nullable>`. Правила:
```csharp
// ✅ Хорошо - явно указано может быть null
public string? GetUserName() { }
// ✅ Хорошо - явно не null
public string GetTitle() => "Title";
// ❌ Плохо - неявная nullable ссылка
public object GetSomething() { }
// ✅ Хорошо - используйте null-coalescing
var result = value ?? defaultValue;
// ✅ Хорошо - используйте null-conditional
var count = list?.Count ?? 0;
```
### Асинхронное программирование
```csharp
// ✅ Хорошо - async/await
public async Task<YTrack?> GetTrackAsync(string id)
{
return await api.Track.GetTrackAsync(id);
}
// ❌ Плохо - Task.Result блокирует поток
public YTrack? GetTrack(string id)
{
return api.Track.GetTrackAsync(id).Result;
}
// ✅ Хорошо - ConfigureAwait(false) в библиотеках
public async Task<YTrack?> GetTrackAsync(string id)
{
return await api.Track.GetTrackAsync(id).ConfigureAwait(false);
}
```
### Обработка ошибок
```csharp
// ✅ Хорошо
public async Task<YTrack?> GetTrackAsync(string trackId)
{
ArgumentNullException.ThrowIfNull(trackId);
ArgumentException.ThrowIfNullOrWhiteSpace(trackId);
try
{
return await _provider.GetTrackAsync(trackId);
}
catch (HttpRequestException ex)
{
Logger.LogError(ex, "Ошибка при получении трека");
throw;
}
}
// ❌ Плохо - молча игнорируем ошибки
public async Task<YTrack?> GetTrackAsync(string trackId)
{
try
{
return await _provider.GetTrackAsync(trackId);
}
catch { }
return null;
}
```
### Структура файла
```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace YandexMusic.API.API;
/// <summary>API для работы с треками.</summary>
public class YTrackAPI : YCommonAPI
{
/// <summary>Инициализирует новый экземпляр.</summary>
public YTrackAPI(YandexMusicApi yandex) : base(yandex) { }
/// <summary>Получает трек.</summary>
public async Task<YTrack?> GetTrackAsync(string trackId)
{
// реализация
}
/// <summary>Получает несколько треков.</summary>
public async Task<IEnumerable<YTrack>> GetTracksAsync(IEnumerable<string> trackIds)
{
// реализация
}
}
```
## 📝 Commit сообщения
### Формат
```
<type>(<scope>): <subject>
<body>
<footer>
```
### Type
- `feat:` - новая функция
- `fix:` - исправление ошибки
- `docs:` - обновление документации
- `style:` - форматирование кода (не влияет на функциональность)
- `refactor:` - переписывание кода (не меняет функциональность)
- `perf:` - улучшение производительности
- `test:` - добавление или обновление тестов
- `ci:` - изменения в CI/CD
- `chore:` - обновление зависимостей, версии и т.д.
### Примеры
```bash
git commit -m "feat(track-api): add support for track recommendations"
git commit -m "fix(auth): handle refresh token expiration correctly"
git commit -m "docs(readme): update installation instructions"
git commit -m "refactor(models): simplify YTrack class structure"
git commit -m "test(playlist): add tests for playlist operations"
```
## 🔀 Pull Requests
### Перед созданием PR
- [ ] Код собирается без ошибок: `dotnet build`
- [ ] Все тесты проходят: `dotnet test`
- [ ] Код отформатирован согласно стилю
- [ ] Добавлена XML документация для всех публичных членов
- [ ] Обновлена документация если нужно
### Шаблон PR
```markdown
## Описание
Краткое описание внесённых изменений.
## Тип изменения
- [ ] Новая функция (non-breaking change)
- [ ] Исправление ошибки (non-breaking change)
- [ ] Breaking change (поясните почему)
- [ ] Обновление документации
## Как это было протестировано?
Опишите шаги для тестирования.
## Связанные Issues
Closes #issue_number
## Чеклист
- [ ] Код следует стилю проекта
- [ ] Добавлена документация
- [ ] Нет новых warnings
- [ ] Тесты проходят
- [ ] Изменения совместимы с существующим кодом
```
## 🐛 Отчёты об ошибках
### Шаблон Issue
```markdown
## Описание ошибки
Четкое и краткое описание проблемы.
## Шаги воспроизведения
1. Сделайте...
2. Затем...
3. Ошибка воспроизведится
## Ожидаемое поведение
Что должно было происходить.
## Фактическое поведение
Что происходит на самом деле.
## Окружение
- OS: Windows 11
- .NET Version: 10.0
- Visual Studio: 2026 Enterprise
- YandexMusic Version: 0.0.1
## Дополнительный контекст
Скриптстек, логи, код примера.
```
## 💡 Предложения по улучшениям
### Шаблон Feature Request
```markdown
## Описание
Четкое описание желаемой функции.
## Использование
Как бы выглядел код с использованием этой функции?
## Альтернативы
Есть ли другие способы решить эту проблему?
## Дополнительный контекст
Почему это нужно добавить?
```
## 🎯 Области для внесения вклада
### High Priority
- [ ] Документация API методов
- [ ] Unit тесты для API классов
- [ ] Обработка ошибок
- [ ] Оптимизация производительности
### Medium Priority
- [ ] Примеры использования
- [ ] Интеграционные тесты
- [ ] Улучшение логирования
- [ ] Расширение функциональности
### Low Priority
- [ ] Рефакторинг кода
- [ ] Улучшение читаемости
- [ ] Документация
## 📞 Контакты
Вопросы? Свяжитесь с разработчиком:
- **Issues:** [https://git.frigat.duckdns.org/FrigaT/YandexMusic/issues]
---
Спасибо за то, что делаете YandexMusic лучше! 🚀

446
FAQ.md Normal file
View File

@@ -0,0 +1,446 @@
# ❓ FAQ - Часто задаваемые вопросы
Ответы на самые частые вопросы по использованию YandexMusic.
## 📋 Содержание
- [Общие вопросы](#общие-вопросы)
- [Установка и настройка](#установка-и-настройка)
- [Использование](#использование)
- [Авторизация](#авторизация)
- [Ошибки и проблемы](#ошибки-и-проблемы)
- [Производительность](#производительность)
- [Разработка](#разработка)
## 📖 Общие вопросы
### Q: Что такое YandexMusic?
**A:** YandexMusic — это .NET 10 библиотека для работы с неофициальным API Яндекс Музыки. Она позволяет искать музыку, управлять плейлистами, получать информацию об альбомах и исполнителях, а также работать с WebSocket через протокол Ynison.
### Q: Это официальная библиотека?
**A:** Нет, это неофициальная библиотека. Используйте её на свой риск и соблюдайте Terms of Service Яндекс Музыки.
### Q: На каких платформах работает?
**A:** На любых платформах, поддерживающих .NET 10:
- Windows (10/11)
- Linux (Ubuntu, Debian, Red Hat, etc.)
- macOS (Intel & Apple Silicon)
### Q: Нужна ли авторизация?
**A:** Нет, многие операции работают без авторизации (поиск, получение информации о треках). Но для некоторых операций (работа с лайками, плейлистами) требуется авторизация.
### Q: Где я могу сообщить об ошибке?
**A:** Создайте issue на GitHub:
- https://git.frigat.duckdns.org/FrigaT/YandexMusic/issues
## 🚀 Установка и настройка
### Q: Какие требования?
**A:**
- .NET 10 SDK или выше
- C# 12 совместимый компилятор
- Интернет соединение
### Q: Как установить библиотеку?
**A:** Несколько способов:
1. **Через NuGet:**
```bash
dotnet add package YandexMusic
```
2. **Из GitHub:**
```bash
git clone https://git.frigat.duckdns.org/FrigaT/YandexMusic.git
cd YandexMusic
dotnet build
```
3. **Добавить ссылку на проект:**
```bash
dotnet add reference ../YandexMusic/YandexMusic.csproj
```
### Q: Как обновить библиотеку?
**A:** Через NuGet:
```bash
dotnet package update YandexMusic
```
Или если вы клонировали репозиторий:
```bash
git pull origin master
dotnet build
```
### Q: Какие зависимости нужны?
**A:** Только встроенные в .NET 10:
- System.Net.Http
- System.Text.Json
- System.Net.WebSockets
Нет дополнительных NuGet пакетов!
## 💻 Использование
### Q: Как начать использовать?
**A:** Самый простой способ:
```csharp
using YandexMusic;
var client = new YandexMusicClient();
var results = await client.Api.Search.SearchAsync("Beatles");
```
Подробнее в [QUICKSTART.md](QUICKSTART.md).
### Q: Как получить информацию о треке?
**A:**
```csharp
var track = await client.Api.Track.GetTrackAsync("trackId");
Console.WriteLine($"{track?.Title} - {track?.Artists?.FirstOrDefault()?.Title}");
```
### Q: Как найти ID трека/альбома/плейлиста?
**A:**
- В URL страницы на music.yandex.ru
- Через API при поиске
- На официальном сайте в адресной строке
Примеры:
- Трек: `https://music.yandex.ru/album/123/track/456` → ID: `456`
- Альбом: `https://music.yandex.ru/album/123` → ID: `123`
- Плейлист: `https://music.yandex.ru/playlist/123` → ID: `123`
### Q: Почему я получаю null?
**A:** Несколько причин:
1. Неправильный ID
2. Ресурс был удален
3. Нет доступа (требуется авторизация)
4. Сервер вернул ошибку
Всегда проверяйте результат:
```csharp
var track = await client.Api.Track.GetTrackAsync(id);
if (track == null)
{
Console.WriteLine("Трек не найден или нет доступа");
}
```
### Q: Как работать с пагинацией?
**A:** Используйте параметр `page`:
```csharp
var page1 = await client.Api.Search.SearchAsync("query", page: 0);
var page2 = await client.Api.Search.SearchAsync("query", page: 1);
```
### Q: Как получить большое количество данных?
**A:** Используйте цикл с пагинацией:
```csharp
var allResults = new List<YTrack>();
int page = 0;
while (true)
{
var search = await client.Api.Search.SearchAsync("query", page: page);
if (search?.Tracks?.Results?.Count == 0) break;
allResults.AddRange(search.Tracks.Results ?? []);
page++;
}
```
## 🔐 Авторизация
### Q: Как авторизироваться?
**A:** Два способа:
1. **Через логин и пароль:**
```csharp
await client.AuthorizeAsync("email@gmail.com", "password");
```
2. **Через токен:**
```csharp
await client.AuthorizeByTokenAsync("your-oauth-token");
```
### Q: Где получить OAuth токен?
**A:** Вы можете:
1. Авторизироваться через логин/пароль (библиотека получит токен автоматически)
2. Получить токен через официальное приложение Яндекса
3. Использовать токен из DevTools браузера
### Q: Как проверить авторизирован ли я?
**A:**
```csharp
if (client.IsAuthorized)
{
Console.WriteLine($"Авторизирован: {client.Account.User?.DisplayName}");
}
else
{
Console.WriteLine("Требуется авторизация");
}
```
### Q: Сохраняется ли авторизация?
**A:** По умолчанию нет. Вы должны авторизироваться при каждом запуске.
Если хотите сохранить токен:
```csharp
// После авторизации
string token = client.AuthStorage.Token;
// Сохранить в файл, БД и т.д.
// ...
// При следующем запуске
var client = new YandexMusicClient();
await client.AuthorizeByTokenAsync(savedToken);
```
### Q: Безопасно ли хранить пароль?
**A:** **Нет!** Никогда не сохраняйте пароль в открытом виде. Лучше:
1. Используйте OAuth токены
2. Сохраняйте токены в безопасном хранилище
3. Используйте переменные окружения
## ⚠️ Ошибки и проблемы
### Q: Получаю `HttpRequestException`
**A:** Проблемы с сетью или сервером:
```csharp
try
{
var track = await client.Api.Track.GetTrackAsync(id);
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Сетевая ошибка: {ex.Message}");
}
```
Проверьте:
- Интернет соединение
- Доступность сервера Яндекса
- Firewall/прокси настройки
### Q: Получаю `JsonException`
**A:** Проблема с десериализацией JSON. Обновите библиотеку или сообщите об issue.
### Q: Получаю 401 Unauthorized
**A:** Проблема с авторизацией:
```csharp
// Проверьте токен
if (!client.IsAuthorized)
{
await client.AuthorizeAsync("email", "password");
}
// Или обновите токен
await client.AuthorizeByTokenAsync(newToken);
```
### Q: Получаю 429 Too Many Requests
**A:** Вы отправляете слишком много запросов. Добавьте задержку:
```csharp
for (int i = 0; i < ids.Count; i++)
{
var track = await client.Api.Track.GetTrackAsync(ids[i]);
if (i < ids.Count - 1) await Task.Delay(100); // 100ms задержка
}
```
### Q: Результаты пусты/null
**A:** Несколько причин:
1. **Неправильный ID:**
```csharp
var track = await client.Api.Track.GetTrackAsync("invalid-id");
// Будет null если ID неверный
```
2. **Нет результатов поиска:**
```csharp
var results = await client.Api.Search.SearchAsync("абракадабра");
// results?.Tracks?.Results будет пусто
```
3. **Требуется авторизация:**
```csharp
var library = await client.Api.Library.GetLibraryAsync();
// Вернёт null если не авторизирован
```
### Q: Почему медленно работает?
**A:** Несколько причин:
1. Медленное интернет соединение
2. Сервер Яндекса перегружен
3. Получаете большое количество данных
4. Блокирующие операции вместо async
Используйте асинхронные операции:
```csharp
// ✅ Хорошо
var track = await client.Api.Track.GetTrackAsync(id);
// ❌ Плохо
var track = client.Api.Track.GetTrackAsync(id).Result;
```
## 🚀 Производительность
### Q: Как оптимизировать производительность?
**A:** Несколько советов:
1. **Используйте пакетные операции:**
```csharp
// ✅ Хорошо - один запрос
var tracks = await client.Api.Track.GetTracksAsync(["id1", "id2", "id3"]);
// ❌ Плохо - три запроса
foreach (var id in ids)
{
var track = await client.Api.Track.GetTrackAsync(id);
}
```
2. **Кешируйте результаты:**
```csharp
private Dictionary<string, YTrack?> cache = new();
public async Task<YTrack?> GetCached(string id)
{
if (cache.TryGetValue(id, out var cached)) return cached;
var track = await client.Api.Track.GetTrackAsync(id);
cache[id] = track;
return track;
}
```
3. **Запускайте запросы параллельно:**
```csharp
var (albums, artists) = await (
Task.Run(() => client.Api.Album.GetAlbumsAsync(ids)),
Task.Run(() => client.Api.Artist.GetArtistsAsync(ids))
).AsAsync();
```
4. **Добавляйте задержку между запросами:**
```csharp
for (int i = 0; i < 100; i++)
{
var track = await client.Api.Track.GetTrackAsync(id);
await Task.Delay(100); // 100ms между запросами
}
```
### Q: Есть ли какие-то лимиты?
**A:** Яндекс Музыка имеет лимиты:
- Количество запросов в секунду
- Размер результатов поиска
- Размер файлов для загрузки
Уважайте эти лимиты и не создавайте нагрузку на сервер.
## 🔧 Разработка
### Q: Как стать разработчиком?
**A:** Читайте [CONTRIBUTING.md](CONTRIBUTING.md).
### Q: Как создать pull request?
**A:**
1. Форкните репозиторий
2. Создайте ветку: `git checkout -b feature/my-feature`
3. Сделайте изменения
4. Коммитьте: `git commit -m "feat: описание"`
5. Пушьте: `git push origin feature/my-feature`
6. Создайте Pull Request
### Q: Как собрать проект?
**A:**
```bash
dotnet build
```
### Q: Как запустить тесты?
**A:**
```bash
dotnet test
```
### Q: Есть ли стиль кода?
**A:** Да, используется Microsoft C# Coding Conventions. Смотрите [CONTRIBUTING.md](CONTRIBUTING.md).
### Q: Как добавить новый API метод?
**A:** Прочитайте [ARCHITECTURE.md](ARCHITECTURE.md) раздел "Как добавить новый API метод".
### Q: Как создать тест?
**A:** Создайте файл в папке Tests:
```csharp
using Xunit;
using YandexMusic.API;
public class YTrackAPITests
{
[Fact]
public async Task GetTrackAsync_WithValidId_ReturnsTrack()
{
// Arrange
var api = new YandexMusicApi();
string trackId = "valid-id";
// Act
var result = await api.Track.GetTrackAsync(trackId);
// Assert
Assert.NotNull(result);
}
}
```
## 🆘 Не нашли ответ?
Создайте вопрос через GitHub Issues:
https://git.frigat.duckdns.org/FrigaT/YandexMusic/issues

View File

@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<ResourcePreloader />
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["PlaylistShared.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<ReconnectModal />
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>
</html>

View File

@@ -1,23 +0,0 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>

View File

@@ -1,98 +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 {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
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;
}

View File

@@ -1,51 +0,0 @@
@using Microsoft.AspNetCore.Components.Authorization
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">PlaylistShared</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="nav flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Главная
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="myplaylists">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Мои плейлисты
</NavLink>
</div>
@if (user?.Identity?.IsAuthenticated == true)
{
<div class="nav-item px-3">
<NavLink class="nav-link" href="Logout">
<span class="bi bi-box-arrow-right"></span> Выйти
</NavLink>
</div>
}
else
{
<div class="nav-item px-3">
<NavLink class="nav-link" href="Login">
<span class="bi bi-box-arrow-in-right"></span> Войти
</NavLink>
</div>
}
</nav>
</div>
@code {
private AuthenticationState? _authState;
private ClaimsPrincipal? user => _authState?.User;
[CascadingParameter] private Task<AuthenticationState>? AuthenticationStateTask { get; set; }
protected override async Task OnInitializedAsync()
{
if (AuthenticationStateTask is not null)
_authState = await AuthenticationStateTask;
}
}

View File

@@ -1,108 +0,0 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}
.bi-box-arrow-right, .bi-box-arrow-in-right {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z'/%3E%3Cpath fill-rule='evenodd' d='M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z'/%3E%3C/svg%3E");
}

View File

@@ -1,31 +0,0 @@
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
<dialog id="components-reconnect-modal" data-nosnippet>
<div class="components-reconnect-container">
<div class="components-rejoining-animation" aria-hidden="true">
<div></div>
<div></div>
</div>
<p class="components-reconnect-first-attempt-visible">
Rejoining the server...
</p>
<p class="components-reconnect-repeated-attempt-visible">
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
</p>
<p class="components-reconnect-failed-visible">
Failed to rejoin.<br />Please retry or reload the page.
</p>
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
Retry
</button>
<p class="components-pause-visible">
The session has been paused by the server.
</p>
<p class="components-resume-failed-visible">
Failed to resume the session.<br />Please retry or reload the page.
</p>
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
Resume
</button>
</div>
</dialog>

View File

@@ -1,157 +0,0 @@
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation {
display: none;
}
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
display: block;
}
#components-reconnect-modal {
background-color: white;
width: 20rem;
margin: 20vh auto;
padding: 2rem;
border: 0;
border-radius: 0.5rem;
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
&[open]
{
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
animation-fill-mode: both;
}
}
#components-reconnect-modal::backdrop {
background-color: rgba(0, 0, 0, 0.4);
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
opacity: 1;
}
@keyframes components-reconnect-modal-slideUp {
0% {
transform: translateY(30px) scale(0.95);
}
100% {
transform: translateY(0);
}
}
@keyframes components-reconnect-modal-fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes components-reconnect-modal-fadeOutOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.components-reconnect-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#components-reconnect-modal p {
margin: 0;
text-align: center;
}
#components-reconnect-modal button {
border: 0;
background-color: #6b9ed2;
color: white;
padding: 4px 24px;
border-radius: 4px;
}
#components-reconnect-modal button:hover {
background-color: #3b6ea2;
}
#components-reconnect-modal button:active {
background-color: #6b9ed2;
}
.components-rejoining-animation {
position: relative;
width: 80px;
height: 80px;
}
.components-rejoining-animation div {
position: absolute;
border: 3px solid #0087ff;
opacity: 1;
border-radius: 50%;
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.components-rejoining-animation div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes components-rejoining-animation {
0% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
4.9% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
5% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 80px;
height: 80px;
opacity: 0;
}
}

View File

@@ -1,63 +0,0 @@
// Set up event handlers
const reconnectModal = document.getElementById("components-reconnect-modal");
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
const retryButton = document.getElementById("components-reconnect-button");
retryButton.addEventListener("click", retry);
const resumeButton = document.getElementById("components-resume-button");
resumeButton.addEventListener("click", resume);
function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
location.reload();
}
}
async function retry() {
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
try {
// Reconnect will asynchronously return:
// - true to mean success
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
// - exception to mean we didn't reach the server (this can be sync or async)
const successful = await Blazor.reconnect();
if (!successful) {
// We have been able to reach the server, but the circuit is no longer available.
// We'll reload the page so the user can continue using the app as quickly as possible.
const resumeSuccessful = await Blazor.resumeCircuit();
if (!resumeSuccessful) {
location.reload();
} else {
reconnectModal.close();
}
}
} catch (err) {
// We got an exception, server is currently unavailable
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
}
}
async function resume() {
try {
const successful = await Blazor.resumeCircuit();
if (!successful) {
location.reload();
}
} catch {
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
}
}
async function retryWhenDocumentBecomesVisible() {
if (document.visibilityState === "visible") {
await retry();
}
}

View File

@@ -1,80 +0,0 @@
@page "/createplaylist"
@attribute [Authorize]
@using System.Security.Claims
@using Microsoft.AspNetCore.Components.Authorization
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject AppDbContext Db
@inject IYandexMusicService YandexService
@inject NavigationManager Navigation
<h3>Создание общего плейлиста</h3>
<EditForm Model="@model" OnValidSubmit="@HandleSubmit">
<div class="mb-3">
<label for="title">Название плейлиста</label>
<InputText id="title" class="form-control" @bind-Value="model.Title" />
<ValidationMessage For="@(() => model.Title)" />
</div>
<div class="mb-3">
<label for="description">Описание (необязательно)</label>
<InputText id="description" class="form-control" @bind-Value="model.Description" />
</div>
<button type="submit" class="btn btn-primary" disabled="@isLoading">@(isLoading ? "Создание..." : "Создать")</button>
</EditForm>
@code {
private CreateModel model = new();
private bool isLoading;
private string? userId;
public class CreateModel
{
[Required]
public string Title { get; set; } = "";
public string? Description { get; set; }
}
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
userId = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}
private async Task HandleSubmit()
{
if (string.IsNullOrEmpty(userId))
{
// пользователь не авторизован
return;
}
isLoading = true;
try
{
// 1. Создаём плейлист в Яндекс Музыке
var yandexId = await YandexService.CreatePlaylistAsync(userId, model.Title);
// 2. Сохраняем в БД
var playlist = new SharedPlaylist
{
Id = Guid.NewGuid(),
OwnerUserId = userId,
YandexPlaylistId = yandexId,
Title = model.Title,
Description = model.Description,
ShareSlug = Guid.NewGuid().ToString("N"),
CreatedAt = DateTime.UtcNow
};
Db.SharedPlaylists.Add(playlist);
await Db.SaveChangesAsync();
Navigation.NavigateTo("/myplaylists");
}
catch (Exception ex)
{
// показать ошибку (можно добавить переменную errorMessage)
}
finally
{
isLoading = false;
}
}
}

View File

@@ -1,7 +0,0 @@
@page "/Login"
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.Authentication
@inject SignInManager<ApplicationUser> SignInManager
<h3>Вход через Яндекс</h3>
<a class="btn btn-primary" href="/signin-yandex">Войти через Яндекс</a>

View File

@@ -1,12 +0,0 @@
@page "/Logout"
@using Microsoft.AspNetCore.Identity
@inject SignInManager<ApplicationUser> SignInManager
@inject NavigationManager Navigation
@code {
protected override async Task OnInitializedAsync()
{
await SignInManager.SignOutAsync();
Navigation.NavigateTo("/", true);
}
}

View File

@@ -1,57 +0,0 @@
@page "/myplaylists"
@attribute [Authorize]
@using System.Security.Claims
@using Microsoft.EntityFrameworkCore
@using Microsoft.AspNetCore.Components.Authorization
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject AppDbContext Db
@inject NavigationManager Navigation
<h3>Мои плейлисты</h3>
<a href="/createplaylist" class="btn btn-success mb-3">Создать новый плейлист</a>
@if (playlists == null)
{
<p>Загрузка...</p>
}
else if (!playlists.Any())
{
<p>У вас пока нет общих плейлистов. Создайте первый!</p>
}
else
{
<table class="table">
<thead>
<tr><th>Название</th><th>Дата создания</th><th>Ссылка</th><th>Настройки</th></tr>
</thead>
<tbody>
@foreach (var pl in playlists)
{
<tr>
<td>@pl.Title</td>
<td>@pl.CreatedAt.ToShortDateString()</td>
<td><a href="/playlist/@pl.ShareSlug">открыть</a></td>
<td><a href="/settings/@pl.Id">настройки</a></td>
</tr>
}
</tbody>
</table>
}
@code {
private List<SharedPlaylist>? playlists;
private string? userId;
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
userId = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (userId != null)
{
playlists = await Db.SharedPlaylists
.Where(p => p.OwnerUserId == userId)
.OrderByDescending(p => p.CreatedAt)
.ToListAsync();
}
}
}

View File

@@ -1,5 +0,0 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>

View File

@@ -1,92 +0,0 @@
@page "/playlist/{slug}"
@using System.Security.Claims
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.EntityFrameworkCore
@using PlaylistShared.Data.Entities
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject AppDbContext Db
@inject IYandexMusicService YandexService
@inject IJSRuntime Js
@inject HttpClient Http
<h3>@playlist?.Title</h3>
<p>Владелец: @playlist?.Owner?.UserName</p>
<p>Описание: @playlist?.Description</p>
@if (canAdd)
{
<div class="mb-3">
<input type="text" id="trackId" placeholder="ID трека Яндекс.Музыки" class="form-control" />
<input type="text" id="trackTitle" placeholder="Название трека" class="form-control mt-1" />
<input type="text" id="artistName" placeholder="Исполнитель" class="form-control mt-1" />
<button class="btn btn-primary mt-2" id="addTrackBtn">Добавить</button>
</div>
}
<ul id="trackList" class="list-group">
@foreach (var track in tracks)
{
<li class="list-group-item d-flex justify-content-between align-items-center" data-track-id="@track.YandexTrackId">
<div>
<strong>@track.Title</strong> - @track.Artist
</div>
@if (canDelete(track))
{
<button class="btn btn-sm btn-danger deleteTrackBtn" data-track-id="@track.YandexTrackId">Удалить</button>
}
</li>
}
</ul>
@code {
[Parameter] public string slug { get; set; } = "";
private SharedPlaylist? playlist;
private List<PlaylistTrack> tracks = new();
private bool canAdd;
private string? currentUserId;
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
currentUserId = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
playlist = await Db.SharedPlaylists
.Include(p => p.Owner)
.Include(p => p.Tracks)
.FirstOrDefaultAsync(p => p.ShareSlug == slug);
if (playlist == null) return;
canAdd = playlist.Permissions.Add switch
{
AccessLevel.All => true,
AccessLevel.Authorized => currentUserId != null,
_ => false
};
// Здесь можно при желании синхронизировать с Яндекс API, но пока используем локальный кеш
// var yandexTracks = await YandexService.GetPlaylistTracksAsync(playlist.OwnerUserId, playlist.YandexPlaylistId);
tracks = playlist.Tracks.OrderBy(t => t.AddedAt).ToList();
}
private bool canDelete(PlaylistTrack track)
{
if (currentUserId == playlist?.OwnerUserId) return true;
return playlist?.Permissions.Delete switch
{
DeleteAccessLevel.All => true,
DeleteAccessLevel.Authorized => currentUserId != null,
DeleteAccessLevel.AdderOnly => currentUserId != null && currentUserId == track.AddedByUserId,
DeleteAccessLevel.OwnerOnly => false,
_ => false
};
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && playlist != null)
{
await Js.InvokeVoidAsync("initPlaylistInteractions", playlist.Id);
}
}
}

View File

@@ -1,82 +0,0 @@
@page "/settings/{id:guid}"
@attribute [Authorize]
@using System.Security.Claims
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.EntityFrameworkCore
@using PlaylistShared.Data.Entities
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject AppDbContext Db
@inject NavigationManager Navigation
<h3>Настройки плейлиста</h3>
@if (playlist == null)
{
<p>Загрузка...</p>
}
else if (!isOwner)
{
<p>Только владелец может изменять настройки.</p>
}
else
{
<EditForm Model="@playlist.Permissions" OnValidSubmit="@SaveSettings">
<div class="mb-3">
<label>Кто может просматривать</label>
<InputSelect @bind-Value="playlist.Permissions.View" class="form-control">
<option value="@AccessLevel.All">Все</option>
<option value="@AccessLevel.Authorized">Авторизованные</option>
<option value="@AccessLevel.None">Никто</option>
</InputSelect>
</div>
<div class="mb-3">
<label>Кто может добавлять треки</label>
<InputSelect @bind-Value="playlist.Permissions.Add" class="form-control">
<option value="@AccessLevel.All">Все</option>
<option value="@AccessLevel.Authorized">Авторизованные</option>
<option value="@AccessLevel.None">Никто</option>
</InputSelect>
</div>
<div class="mb-3">
<label>Кто может удалять треки</label>
<InputSelect @bind-Value="playlist.Permissions.Delete" class="form-control">
<option value="@DeleteAccessLevel.All">Все</option>
<option value="@DeleteAccessLevel.Authorized">Авторизованные</option>
<option value="@DeleteAccessLevel.AdderOnly">Тот, кто добавил</option>
<option value="@DeleteAccessLevel.OwnerOnly">Только владелец</option>
</InputSelect>
</div>
<button type="submit" class="btn btn-primary">Сохранить</button>
</EditForm>
}
@code {
[Parameter] public Guid id { get; set; }
private SharedPlaylist? playlist;
private bool isOwner;
private string? currentUserId;
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
currentUserId = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
playlist = await Db.SharedPlaylists
.Include(p => p.Owner)
.FirstOrDefaultAsync(p => p.Id == id);
if (playlist != null)
{
isOwner = currentUserId == playlist.OwnerUserId;
}
}
private async Task SaveSettings()
{
if (playlist != null)
{
Db.SharedPlaylists.Update(playlist);
await Db.SaveChangesAsync();
Navigation.NavigateTo("/myplaylists");
}
}
}

View File

@@ -1,6 +0,0 @@
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -1,11 +0,0 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using PlaylistShared
@using PlaylistShared.Components
@using PlaylistShared.Components.Layout

View File

@@ -1,109 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using PlaylistShared.Models;
using System.Net.Http.Headers;
using System.Text.Json;
namespace PlaylistShared.Controllers;
[Microsoft.AspNetCore.Mvc.Route("auth")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly IConfiguration _config;
private readonly HttpClient _httpClient;
private readonly AppDbContext _db;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<AuthController> _logger;
public AuthController(
IConfiguration config,
IHttpClientFactory factory,
AppDbContext db,
SignInManager<ApplicationUser> signInManager,
ILogger<AuthController> logger)
{
_config = config;
_httpClient = factory.CreateClient();
_db = db;
_signInManager = signInManager;
_logger = logger;
}
[HttpGet("login")]
public IActionResult Login()
{
var clientId = _config["YandexOAuth:ClientId"];
var redirectUri = Url.Action(nameof(Callback), "Auth", null, Request.Scheme);
var authUrl = $"{_config["YandexOAuth:AuthorizationEndpoint"]}?response_type=code&client_id={clientId}&redirect_uri={Uri.EscapeDataString(redirectUri!)}";
return Redirect(authUrl);
}
[HttpGet("callback")]
public async Task<IActionResult> Callback(string code)
{
if (string.IsNullOrEmpty(code))
return BadRequest("Missing code");
var tokenResponse = await ExchangeCodeForTokenAsync(code);
if (tokenResponse is null)
return StatusCode(500, "Token exchange failed");
var userInfo = await GetUserInfoAsync(tokenResponse.AccessToken);
if (userInfo is null)
return StatusCode(500, "User info fetch failed");
var user = await _db.Users.FirstOrDefaultAsync(u => u.YandexId == userInfo.Id);
if (user is null)
{
user = new ApplicationUser
{
UserName = userInfo.Login,
YandexId = userInfo.Id,
Email = userInfo.DefaultEmail,
AccessToken = tokenResponse.AccessToken,
RefreshToken = tokenResponse.RefreshToken,
AccessTokenExpiresAt = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn)
};
_db.Users.Add(user);
}
else
{
user.AccessToken = tokenResponse.AccessToken;
user.RefreshToken = tokenResponse.RefreshToken;
user.AccessTokenExpiresAt = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn);
_db.Users.Update(user);
}
await _db.SaveChangesAsync();
await _signInManager.SignInAsync(user, isPersistent: false);
return Redirect("/");
}
private async Task<YandexTokenResponse?> ExchangeCodeForTokenAsync(string code)
{
var tokenUrl = _config["YandexOAuth:TokenEndpoint"];
var redirectUri = Url.Action(nameof(Callback), "Auth", null, Request.Scheme);
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("client_id", _config["YandexOAuth:ClientId"]!),
new KeyValuePair<string, string>("client_secret", _config["YandexOAuth:ClientSecret"]!),
new KeyValuePair<string, string>("redirect_uri", redirectUri!)
});
var response = await _httpClient.PostAsync(tokenUrl!, content);
if (!response.IsSuccessStatusCode) return null;
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<YandexTokenResponse>(json);
}
private async Task<YandexUserInfo?> GetUserInfoAsync(string accessToken)
{
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", accessToken);
var response = await _httpClient.GetAsync(_config["YandexOAuth:UserInfoEndpoint"]!);
if (!response.IsSuccessStatusCode) return null;
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<YandexUserInfo>(json);
}
}

View File

@@ -1,93 +0,0 @@
// Controllers/TrackController.cs
using Microsoft.AspNetCore.Mvc;
namespace PlaylistShared.Controllers;
[ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/tracks")]
[Authorize]
public class TrackController : ControllerBase
{
private readonly AppDbContext _db;
private readonly IYandexMusicService _yandex;
public TrackController(AppDbContext db, IYandexMusicService yandex)
{
_db = db;
_yandex = yandex;
}
[HttpPost("add")]
public async Task<IActionResult> AddTrack(Guid playlistId, string trackId, string? trackTitle, string? artist)
{
var playlist = await _db.SharedPlaylists
.Include(p => p.Owner)
.FirstOrDefaultAsync(p => p.Id == playlistId);
if (playlist == null) return NotFound();
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (!CanAdd(playlist, userId)) return Forbid();
// Добавляем в Яндекс
await _yandex.AddTrackToPlaylistAsync(playlist.OwnerUserId, playlist.YandexPlaylistId, trackId);
// Сохраняем информацию о треке в кеш
var track = new PlaylistTrack
{
Id = Guid.NewGuid(),
PlaylistId = playlist.Id,
YandexTrackId = trackId,
Title = trackTitle ?? "Unknown",
Artist = artist,
AddedByUserId = userId,
AddedAt = DateTime.UtcNow
};
_db.PlaylistTracks.Add(track);
await _db.SaveChangesAsync();
return Ok();
}
[HttpDelete("remove")]
public async Task<IActionResult> RemoveTrack(Guid playlistId, string trackId)
{
var playlist = await _db.SharedPlaylists
.Include(p => p.Owner)
.FirstOrDefaultAsync(p => p.Id == playlistId);
if (playlist == null) return NotFound();
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var trackEntry = await _db.PlaylistTracks
.FirstOrDefaultAsync(t => t.PlaylistId == playlistId && t.YandexTrackId == trackId);
if (!CanDelete(playlist, userId, trackEntry?.AddedByUserId))
return Forbid();
await _yandex.RemoveTrackFromPlaylistAsync(playlist.OwnerUserId, playlist.YandexPlaylistId, trackId);
if (trackEntry != null) _db.PlaylistTracks.Remove(trackEntry);
await _db.SaveChangesAsync();
return Ok();
}
private bool CanAdd(SharedPlaylist playlist, string? userId)
{
return playlist.Permissions.Add switch
{
AccessLevel.All => true,
AccessLevel.Authorized => userId != null,
AccessLevel.None => false,
_ => false
};
}
private bool CanDelete(SharedPlaylist playlist, string? userId, string? adderUserId)
{
if (userId == playlist.OwnerUserId) return true; // владелец всегда может удалить
return playlist.Permissions.Delete switch
{
DeleteAccessLevel.All => true,
DeleteAccessLevel.Authorized => userId != null,
DeleteAccessLevel.AdderOnly => userId != null && userId == adderUserId,
DeleteAccessLevel.OwnerOnly => false,
_ => false
};
}
}

View File

@@ -1,37 +0,0 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using PlaylistShared.Data.Entities;
namespace PlaylistShared.Data.Contexts;
public class AppDbContext : IdentityDbContext<ApplicationUser>
{
public DbSet<SharedPlaylist> SharedPlaylists => Set<SharedPlaylist>();
public DbSet<PlaylistTrack> PlaylistTracks => Set<PlaylistTrack>();
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<SharedPlaylist>(entity =>
{
entity.HasIndex(p => p.ShareSlug).IsUnique();
entity.OwnsOne(p => p.Permissions, per =>
{
per.Property(x => x.View).HasColumnName("PermView");
per.Property(x => x.Add).HasColumnName("PermAdd");
per.Property(x => x.Delete).HasColumnName("PermDelete");
});
});
builder.Entity<PlaylistTrack>(entity =>
{
entity.HasOne(pt => pt.Playlist)
.WithMany(p => p.Tracks)
.HasForeignKey(pt => pt.PlaylistId)
.OnDelete(DeleteBehavior.Cascade);
});
}
}

View File

@@ -1,10 +0,0 @@
namespace PlaylistShared.Data.Entities;
public class ApplicationUser : IdentityUser
{
public string? YandexId { get; set; }
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
public DateTime? AccessTokenExpiresAt { get; set; }
public string? AvatarUrl { get; set; }
}

View File

@@ -1,14 +0,0 @@
using Microsoft.EntityFrameworkCore;
namespace PlaylistShared.Data.Entities;
public enum AccessLevel { All, Authorized, None }
public enum DeleteAccessLevel { All, Authorized, AdderOnly, OwnerOnly }
[Owned]
public class PlaylistPermissions
{
public AccessLevel View { get; set; } = AccessLevel.All;
public AccessLevel Add { get; set; } = AccessLevel.Authorized;
public DeleteAccessLevel Delete { get; set; } = DeleteAccessLevel.OwnerOnly;
}

View File

@@ -1,23 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace PlaylistShared.Data.Entities;
public class PlaylistTrack
{
public Guid Id { get; set; }
public Guid PlaylistId { get; set; }
public SharedPlaylist Playlist { get; set; } = null!;
public string YandexTrackId { get; set; } = null!;
public string Title { get; set; } = null!;
public string? Artist { get; set; }
public string? AlbumTitle { get; set; }
public int DurationMs { get; set; }
public string? AddedByUserId { get; set; }
[ForeignKey(nameof(AddedByUserId))]
public ApplicationUser? AddedByUser { get; set; }
public DateTime AddedAt { get; set; }
}

View File

@@ -1,28 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PlaylistShared.Data.Entities;
public class SharedPlaylist
{
[Key]
public Guid Id { get; set; }
public string OwnerUserId { get; set; } = null!;
[ForeignKey(nameof(OwnerUserId))]
public ApplicationUser Owner { get; set; } = null!;
public string YandexPlaylistId { get; set; } = null!;
public string Title { get; set; } = null!;
public string? Description { get; set; }
[Required]
public string ShareSlug { get; set; } = null!;
public PlaylistPermissions Permissions { get; set; } = new();
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public List<PlaylistTrack> Tracks { get; set; } = new();
}

View File

@@ -1,9 +0,0 @@
global using Microsoft.AspNetCore.Authorization;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Identity;
global using Microsoft.EntityFrameworkCore;
global using PlaylistShared.Data.Contexts;
global using PlaylistShared.Data.Entities;
global using PlaylistShared.Services;
global using System.ComponentModel.DataAnnotations;
global using System.Security.Claims;

View File

@@ -1,4 +0,0 @@
namespace PlaylistShared.Models;
public record YandexTokenResponse(string AccessToken, string RefreshToken, int ExpiresIn);
public record YandexUserInfo(string Id, string Login, string? DisplayName, string? DefaultEmail, string? AvatarUrl);

View File

@@ -1,26 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\YandexMusic\YandexMusic.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Data\Migrations\" />
</ItemGroup>
</Project>

View File

@@ -1,79 +0,0 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using PlaylistShared.Data.Contexts;
using PlaylistShared.Data.Entities;
using PlaylistShared.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"));
});
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
builder.Services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/Login";
options.LogoutPath = "/Logout";
options.AccessDeniedPath = "/AccessDenied";
});
builder.Services.AddAuthentication()
.AddOAuth("Yandex", options =>
{
options.ClientId = builder.Configuration["YandexOAuth:ClientId"];
options.ClientSecret = builder.Configuration["YandexOAuth:ClientSecret"];
options.AuthorizationEndpoint = builder.Configuration["YandexOAuth:AuthorizationEndpoint"];
options.TokenEndpoint = builder.Configuration["YandexOAuth:TokenEndpoint"];
options.UserInformationEndpoint = builder.Configuration["YandexOAuth:UserInfoEndpoint"];
options.CallbackPath = "/signin-yandex";
options.ClaimActions.MapJsonKey("urn:yandex:avatar_url", "avatar_url");
options.ClaimActions.MapJsonKey("urn:yandex:display_name", "display_name");
options.Events = new Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request);
response.EnsureSuccessStatusCode();
var user = System.Text.Json.JsonDocument.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user.RootElement);
}
};
});
builder.Services.AddScoped<IYandexMusicService, YandexMusicService>();
builder.Services.AddHttpClient();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();

View File

@@ -1,23 +0,0 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5058",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7272;http://localhost:5058",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,14 +0,0 @@
namespace PlaylistShared.Services;
public interface IYandexMusicService
{
Task<string> CreatePlaylistAsync(string userId, string title);
Task AddTrackToPlaylistAsync(string userId, string yandexPlaylistId, string trackId);
Task RemoveTrackFromPlaylistAsync(string userId, string yandexPlaylistId, string trackId);
Task<List<YandexTrackInfo>> GetPlaylistTracksAsync(string userId, string yandexPlaylistId);
Task<YandexPlaylistInfo> GetPlaylistInfoAsync(string userId, string yandexPlaylistId);
Task<string?> RefreshUserTokenAsync(string userId);
}
public record YandexTrackInfo(string Id, string Title, string? Artist, string? AlbumTitle, int DurationMs);
public record YandexPlaylistInfo(string Kind, string Title, int TrackCount);

View File

@@ -1,173 +0,0 @@
using PlaylistShared.Data.Contexts;
using PlaylistShared.Data.Entities;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace PlaylistShared.Services;
public class YandexMusicService : IYandexMusicService
{
private readonly AppDbContext _db;
private readonly HttpClient _http;
private readonly IConfiguration _config;
private readonly ILogger<YandexMusicService> _logger;
public YandexMusicService(AppDbContext db, IHttpClientFactory factory, IConfiguration config, ILogger<YandexMusicService> logger)
{
_db = db;
_http = factory.CreateClient();
_config = config;
_logger = logger;
}
private async Task<ApplicationUser> GetUserWithValidTokenAsync(string userId)
{
var user = await _db.Users.FindAsync(userId);
if (user == null) throw new Exception("User not found");
if (user.AccessTokenExpiresAt <= DateTime.UtcNow.AddMinutes(5))
{
var newToken = await RefreshYandexTokenAsync(user.RefreshToken!);
user.AccessToken = newToken.access_token;
user.RefreshToken = newToken.refresh_token;
user.AccessTokenExpiresAt = DateTime.UtcNow.AddSeconds(newToken.expires_in);
await _db.SaveChangesAsync();
}
return user;
}
private async Task<(string access_token, string refresh_token, int expires_in)> RefreshYandexTokenAsync(string refreshToken)
{
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("refresh_token", refreshToken),
new KeyValuePair<string, string>("client_id", _config["YandexOAuth:ClientId"]),
new KeyValuePair<string, string>("client_secret", _config["YandexOAuth:ClientSecret"])
});
var response = await _http.PostAsync(_config["YandexOAuth:TokenEndpoint"], content);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
return (
doc.RootElement.GetProperty("access_token").GetString()!,
doc.RootElement.GetProperty("refresh_token").GetString()!,
doc.RootElement.GetProperty("expires_in").GetInt32()
);
}
public async Task<string> CreatePlaylistAsync(string userId, string title)
{
var user = await GetUserWithValidTokenAsync(userId);
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", user.AccessToken);
var payload = new { title, visibility = "public" };
var json = JsonSerializer.Serialize(payload);
var response = await _http.PostAsync("https://api.music.yandex.net/users/self/playlists/create",
new StringContent(json, Encoding.UTF8, "application/json"));
response.EnsureSuccessStatusCode();
var resultJson = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<YandexApiResponse<YandexPlaylistData>>(resultJson);
return result!.Result.Kind;
}
public async Task AddTrackToPlaylistAsync(string userId, string yandexPlaylistId, string trackId)
{
var user = await GetUserWithValidTokenAsync(userId);
var uid = user.YandexId; // или user.Id? В API Яндекса используется uid из аккаунта, но можно использовать "self"
// Получаем альбом трека (опционально, можно передать без albumId)
// Для простоты используем трек без albumId
var diff = new[]
{
new
{
op = "insert",
at = 0,
tracks = new[] { new { id = trackId } }
}
};
var json = JsonSerializer.Serialize(diff);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"https://api.music.yandex.net/users/{uid}/playlists/{yandexPlaylistId}/change";
var response = await _http.PostAsync(url, content);
response.EnsureSuccessStatusCode();
}
public async Task RemoveTrackFromPlaylistAsync(string userId, string yandexPlaylistId, string trackId)
{
var user = await GetUserWithValidTokenAsync(userId);
var uid = user.YandexId;
// Сначала нужно получить позицию трека в плейлисте упростим: удаляем по индексу (не надёжно)
// Лучше получить плейлист, найти индекс трека, потом удалить
var playlist = await GetPlaylistInfoAsync(userId, yandexPlaylistId);
var tracks = await GetPlaylistTracksAsync(userId, yandexPlaylistId);
var index = tracks.FindIndex(t => t.Id == trackId);
if (index == -1) throw new Exception("Track not found in playlist");
var diff = new[]
{
new
{
op = "delete",
from = index,
to = index + 1
}
};
var json = JsonSerializer.Serialize(diff);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"https://api.music.yandex.net/users/{uid}/playlists/{yandexPlaylistId}/change";
var response = await _http.PostAsync(url, content);
response.EnsureSuccessStatusCode();
}
public async Task<List<YandexTrackInfo>> GetPlaylistTracksAsync(string userId, string yandexPlaylistId)
{
var user = await GetUserWithValidTokenAsync(userId);
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", user.AccessToken);
var uid = user.YandexId;
var url = $"https://api.music.yandex.net/users/{uid}/playlists/{yandexPlaylistId}";
var response = await _http.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<YandexApiResponse<YandexPlaylistFull>>(json);
return data!.Result.Tracks?.Select(t => new YandexTrackInfo(
t.Id.ToString(),
t.Title,
t.Artists?.FirstOrDefault()?.Name,
t.Albums?.FirstOrDefault()?.Title,
t.DurationMs
)).ToList() ?? new();
}
public async Task<YandexPlaylistInfo> GetPlaylistInfoAsync(string userId, string yandexPlaylistId)
{
var user = await GetUserWithValidTokenAsync(userId);
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", user.AccessToken);
var uid = user.YandexId;
var url = $"https://api.music.yandex.net/users/{uid}/playlists/{yandexPlaylistId}";
var response = await _http.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<YandexApiResponse<YandexPlaylistFull>>(json);
return new YandexPlaylistInfo(data!.Result.Kind, data.Result.Title, data.Result.TrackCount);
}
public async Task<string?> RefreshUserTokenAsync(string userId)
{
var user = await _db.Users.FindAsync(userId);
if (user?.RefreshToken == null) return null;
var newToken = await RefreshYandexTokenAsync(user.RefreshToken);
user.AccessToken = newToken.access_token;
user.RefreshToken = newToken.refresh_token;
user.AccessTokenExpiresAt = DateTime.UtcNow.AddSeconds(newToken.expires_in);
await _db.SaveChangesAsync();
return newToken.access_token;
}
// Вспомогательные record
private record YandexApiResponse<T>(T Result);
private record YandexPlaylistData(string Kind);
private record YandexPlaylistFull(string Kind, string Title, int TrackCount, List<YandexTrack> Tracks);
private record YandexTrack(string Id, string Title, int DurationMs, List<YandexArtist> Artists, List<YandexAlbum> Albums);
private record YandexArtist(string Name);
private record YandexAlbum(string Title);
}

View File

@@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -1,24 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"YandexOAuth": {
"ClientId": "0916685f8a3641ca8fc382dbccf77236",
"ClientSecret": "f7398893cd814f8b84b85aeb2a0a6698",
"AuthorizationEndpoint": "https://oauth.yandex.ru/authorize",
"TokenEndpoint": "https://oauth.yandex.ru/token",
"UserInfoEndpoint": "https://login.yandex.ru/info"
},
"Jwt": {
"Secret": "very_long_secret_key_at_least_32_chars",
"Issuer": "PlaylistShared",
"Audience": "PlaylistSharedAPI"
},
"ConnectionStrings": {
"DefaultConnection": "Data Source=playlist.db"
}
}

View File

@@ -1,60 +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;
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,26 +0,0 @@
window.initPlaylistInteractions = (playlistId) => {
const addBtn = document.getElementById('addTrackBtn');
if (addBtn) {
addBtn.addEventListener('click', async () => {
const trackId = document.getElementById('trackId').value;
const trackTitle = document.getElementById('trackTitle').value;
const artist = document.getElementById('artistName').value;
const response = await fetch(`/api/tracks/add?playlistId=${playlistId}&trackId=${trackId}&trackTitle=${encodeURIComponent(trackTitle)}&artist=${encodeURIComponent(artist)}`, {
method: 'POST'
});
if (response.ok) location.reload();
else alert('Ошибка добавления трека');
});
}
document.querySelectorAll('.deleteTrackBtn').forEach(btn => {
btn.addEventListener('click', async () => {
const trackId = btn.getAttribute('data-track-id');
const response = await fetch(`/api/tracks/remove?playlistId=${playlistId}&trackId=${trackId}`, {
method: 'DELETE'
});
if (response.ok) location.reload();
else alert('Ошибка удаления трека');
});
});
};

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,597 +0,0 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--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-black: #000;
--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-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, 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-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
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: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]: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: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,594 +0,0 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--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-black: #000;
--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-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, 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-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
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: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]: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: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

536
QUICKSTART.md Normal file
View File

@@ -0,0 +1,536 @@
# ⚡ Быстрый старт - YandexMusic
5-минутное введение в использование YandexMusic для начинающих.
## 📋 Содержание
- [Установка](#установка)
- [Ваш первый запрос](#ваш-первый-запрос)
- [Авторизация](#авторизация)
- [Основные операции](#основные-операции)
- [WebSocket (Ynison)](#websocket-ynison)
- [Обработка ошибок](#обработка-ошибок)
- [Дальнейшее обучение](#дальнейшее-обучение)
## 📦 Установка
### 1. Требования
- .NET 10 SDK
- C# 12 совместимый компилятор
### 2. Создание проекта
```bash
# Создаём консольное приложение
dotnet new console -n MyMusicApp
cd MyMusicApp
# Добавляем ссылку на YandexMusic (если опубликовано в NuGet)
dotnet add package YandexMusic
# ИЛИ добавляем ссылку на проект локально
dotnet add reference ../YandexMusic/YandexMusic.csproj
```
### 3. Восстановление зависимостей
```bash
dotnet restore
```
## ✨ Ваш первый запрос
### Простой поиск (без авторизации)
```csharp
using YandexMusic;
using YandexMusic.API;
// Создаём клиент
var client = new YandexMusicClient();
// Ищем музыку
var results = await client.Api.Search.SearchAsync("Ленинград");
// Выводим первые результаты
if (results?.Tracks?.Results != null)
{
foreach (var track in results.Tracks.Results.Take(5))
{
Console.WriteLine($"🎵 {track.Title} - {string.Join(", ", track.Artists?.Select(a => a.Title) ?? [])}");
}
}
```
### Получение информации о треке
```csharp
var track = await client.Api.Track.GetTrackAsync("trackId");
if (track != null)
{
Console.WriteLine($"Трек: {track.Title}");
Console.WriteLine($"Исполнитель: {track.Artists?.FirstOrDefault()?.Title}");
Console.WriteLine($"Альбом: {track.Album?.Title}");
Console.WriteLine($"Длительность: {track.DurationMs / 1000} сек");
}
```
## 🔐 Авторизация
### Способ 1: Через логин и пароль
```csharp
var client = new YandexMusicClient();
try
{
// Авторизуемся
await client.AuthorizeAsync("your-email@gmail.com", "your-password");
Console.WriteLine($"✅ Авторизирован: {client.Account.User?.DisplayName}");
Console.WriteLine($"💰 Подписка: {client.Account.Plus?.HasPlus}");
}
catch (Exception ex)
{
Console.WriteLine($"❌ Ошибка: {ex.Message}");
}
```
### Способ 2: Через токен
```csharp
var client = new YandexMusicClient();
// Если у вас уже есть токен
await client.AuthorizeByTokenAsync("your-oauth-token");
Console.WriteLine($"✅ Авторизирован через токен");
```
## 📚 Основные операции
### 1. Поиск
```csharp
// Поиск треков
var search = await client.Api.Search.SearchAsync("Beatles");
var tracks = search?.Tracks?.Results ?? [];
foreach (var t in tracks.Take(10))
{
Console.WriteLine($" {t.Title}");
}
// Поиск альбомов
var albumSearch = await client.Api.Search.SearchAsync("Abbey Road", type: "album");
// Поиск исполнителей
var artistSearch = await client.Api.Search.SearchAsync("John Lennon", type: "artist");
```
### 2. Альбомы
```csharp
// Получение альбома
var album = await client.Api.Album.GetAlbumAsync("albumId");
Console.WriteLine($"Альбом: {album?.Title} ({album?.Year})");
Console.WriteLine($"Жанр: {album?.Genre}");
// Список треков в альбоме
foreach (var track in album?.Tracks ?? [])
{
Console.WriteLine($" {track.Position}. {track.Title}");
}
// Получение нескольких альбомов
var albums = await client.Api.Album.GetAlbumsAsync(["albumId1", "albumId2"]);
```
### 3. Исполнители
```csharp
// Получение исполнителя
var artist = await client.Api.Artist.GetArtistAsync("artistId");
Console.WriteLine($"Исполнитель: {artist?.Title}");
Console.WriteLine($"Жанр: {artist?.Genre}");
// Фото исполнителя
if (artist?.Cover?.Pic != null)
{
Console.WriteLine($"Фото: {artist.Cover.Pic}");
}
// Информация об исполнителе
Console.WriteLine($"Альбомов: {artist?.Albums?.Count}");
Console.WriteLine($"Треков: {artist?.Tracks?.Count}");
```
### 4. Плейлисты
```csharp
// Получение плейлиста
var playlist = await client.Api.Playlist.GetPlaylistAsync("playlistId");
Console.WriteLine($"Плейлист: {playlist?.Title}");
Console.WriteLine($"Описание: {playlist?.Description}");
Console.WriteLine($"Треков: {playlist?.Tracks?.Count}");
// Список треков
foreach (var track in playlist?.Tracks ?? [])
{
Console.WriteLine($" {track.Title}");
}
// Получение информации о плейлисте через UUID
var playlistByUuid = await client.Api.Playlist.GetPlaylistByUuidAsync("uuid");
```
### 5. Треки
```csharp
// Получение одного трека
var track = await client.Api.Track.GetTrackAsync("trackId");
// Получение нескольких треков
var tracks = await client.Api.Track.GetTracksAsync(["id1", "id2", "id3"]);
// Дополнительная информация о треке
var supplement = await client.Api.Track.GetTrackSupplementAsync("trackId");
// Похожие треки
var similar = await client.Api.Track.GetTrackSimilarAsync("trackId");
```
### 6. Радио
```csharp
// Получение станций
var stations = await client.Api.Radio.GetStationsAsync();
foreach (var station in stations?.Stations ?? [])
{
Console.WriteLine($"📻 {station.Title}");
}
// Треки станции
var stationTracks = await client.Api.Radio.GetStationTracksAsync("stationId");
foreach (var track in stationTracks?.Sequence ?? [])
{
Console.WriteLine($" {track.Track?.Title}");
}
```
### 7. Библиотека (Лайки)
```csharp
// Получение лайков
var library = await client.Api.Library.GetLibraryAsync();
Console.WriteLine($"Лайки: {library?.Library?.Count}");
// Добавление трека в лайки
await client.Api.Library.LibraryAddAsync("trackId");
// Удаление из лайков
await client.Api.Library.LibraryRemoveAsync("trackId");
```
### 8. Главная страница (Landing)
```csharp
// Получение рекомендаций
var landing = await client.Api.Landing.GetLandingAsync("home");
foreach (var block in landing?.Blocks ?? [])
{
Console.WriteLine($"Блок: {block.Title}");
foreach (var entity in block.Entities ?? [])
{
Console.WriteLine($" {entity.Title}");
}
}
```
## 🌐 WebSocket (Ynison)
### Подключение к Ynison
```csharp
var client = new YandexMusicClient();
// Авторизуемся
await client.AuthorizeAsync("email@gmail.com", "password");
// Получаем плеер
var player = client.Ynison;
if (player != null)
{
// Подключаемся
await player.ConnectAsync();
Console.WriteLine("✅ Подключены к Ynison");
// Используем плеер...
// Отключаемся
await player.DisconnectAsync();
}
```
## ⚠️ Обработка ошибок
### Базовая обработка
```csharp
try
{
var track = await client.Api.Track.GetTrackAsync("trackId");
if (track == null)
{
Console.WriteLine("Трек не найден");
}
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Ошибка сети: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Неожиданная ошибка: {ex.Message}");
}
```
### Проверка авторизации
```csharp
if (!client.IsAuthorized)
{
Console.WriteLine("❌ Требуется авторизация");
return;
}
// Выполняем авторизованные операции
var account = await client.Api.User.GetAccountAsync();
```
### Валидация входных параметров
```csharp
public async Task GetTrackInfo(string trackId)
{
if (string.IsNullOrWhiteSpace(trackId))
{
Console.WriteLine("❌ ID трека не может быть пустым");
return;
}
var track = await client.Api.Track.GetTrackAsync(trackId);
if (track == null)
{
Console.WriteLine($"❌ Трек с ID {trackId} не найден");
return;
}
Console.WriteLine($"✅ {track.Title}");
}
```
## 🔍 Полные примеры
### Пример 1: Поиск и загрузка информации
```csharp
using YandexMusic;
// Создание клиента
var client = new YandexMusicClient();
// Поиск
Console.Write("Введите исполнителя: ");
string query = Console.ReadLine() ?? "Beatles";
var search = await client.Api.Search.SearchAsync(query);
var artists = search?.Artists?.Results ?? [];
if (artists.Count == 0)
{
Console.WriteLine("Исполнители не найдены");
return;
}
// Выбор первого результата
var artist = artists.First();
Console.WriteLine($"Найден: {artist.Title}");
// Получение полной информации
var fullArtist = await client.Api.Artist.GetArtistAsync(artist.Id);
Console.WriteLine($"Жанр: {fullArtist?.Genre}");
Console.WriteLine($"Альбомов: {fullArtist?.Albums?.Count}");
// Список треков
Console.WriteLine("\nПопулярные треки:");
foreach (var track in fullArtist?.Tracks?.Take(5) ?? [])
{
Console.WriteLine($" 🎵 {track.Title}");
}
```
### Пример 2: Работа с плейлистом
```csharp
using YandexMusic;
var client = new YandexMusicClient();
// ID плейлиста Яндекса
string playlistId = "1130499373";
// Получение плейлиста
var playlist = await client.Api.Playlist.GetPlaylistAsync(playlistId);
Console.WriteLine($"📋 {playlist?.Title}");
Console.WriteLine($"Описание: {playlist?.Description}");
Console.WriteLine($"Треков: {playlist?.Tracks?.Count}");
// Вывод треков с номерами
Console.WriteLine("\nТреки:");
int i = 1;
foreach (var track in playlist?.Tracks ?? [])
{
var artists = string.Join(", ", track.Artists?.Select(a => a.Title) ?? []);
Console.WriteLine($"{i}. {track.Title} - {artists}");
i++;
}
// Общая длительность
var totalSeconds = (playlist?.Tracks ?? []).Sum(t => t.DurationMs ?? 0) / 1000;
var hours = totalSeconds / 3600;
var minutes = (totalSeconds % 3600) / 60;
Console.WriteLine($"\nОбщая длительность: {hours}:{minutes:D2}");
```
### Пример 3: Авторизация и библиотека
```csharp
using YandexMusic;
var client = new YandexMusicClient();
Console.Write("Email: ");
string email = Console.ReadLine() ?? "";
Console.Write("Password: ");
string password = Console.ReadLine() ?? "";
try
{
await client.AuthorizeAsync(email, password);
Console.WriteLine($"✅ Добро пожаловать, {client.Account.User?.DisplayName}!");
if (client.IsAuthorized)
{
// Получение лайков
var library = await client.Api.Library.GetLibraryAsync();
Console.WriteLine($"\n❤ Ваши лайки: {library?.Library?.Count} треков");
// Список лайков
if (library?.Library?.Count > 0)
{
Console.WriteLine("\nПервые 5 лайков:");
var trackIds = library.Library.Select(l => l.TrackId).Take(5);
var tracks = await client.Api.Track.GetTracksAsync(trackIds);
foreach (var track in tracks)
{
Console.WriteLine($" ❤️ {track?.Title}");
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ Ошибка: {ex.Message}");
}
```
## 📖 Дальнейшее обучение
### Документация
- **[README.md](README.md)** - Полное описание проекта
- **[ARCHITECTURE.md](ARCHITECTURE.md)** - Архитектура и паттерны
- **[YandexMusic.API/README.md](YandexMusic.API/README.md)** - Низкоуровневый API
- **[CONTRIBUTING.md](CONTRIBUTING.md)** - Как внести вклад
### Полезные ресурсы
- [.NET 10 Documentation](https://learn.microsoft.com/dotnet/)
- [C# 12 Features](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12)
- [System.Text.Json Guide](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json)
### Где найти ID
**Трека:** `https://music.yandex.ru/album/123456/track/789012`
- Album ID: `123456`
- Track ID: `789012`
**Плейлиста:** `https://music.yandex.ru/playlist/1130499373`
- Playlist ID: `1130499373`
**Исполнителя:** `https://music.yandex.ru/artist/123456`
- Artist ID: `123456`
## ⚡ Советы и трюки
### 1. Работа с async/await
```csharp
// Получение нескольких ресурсов параллельно
var (albums, artists, tracks) = await (
client.Api.Album.GetAlbumsAsync(ids),
client.Api.Artist.GetArtistsAsync(ids),
client.Api.Track.GetTracksAsync(ids)
).AsAsync();
```
### 2. Обработка больших списков
```csharp
// Пагинация для больших результатов
var allPlaylists = new List<YPlaylist>();
int page = 0;
while (true)
{
var playlists = await client.Api.Playlist.GetPlaylistsAsync(page: page);
if (playlists?.Playlists?.Count == 0) break;
allPlaylists.AddRange(playlists.Playlists ?? []);
page++;
}
```
### 3. Кеширование результатов
```csharp
private readonly Dictionary<string, YTrack?> _trackCache = new();
public async Task<YTrack?> GetTrackAsync(string id)
{
if (_trackCache.TryGetValue(id, out var cached))
return cached;
var track = await client.Api.Track.GetTrackAsync(id);
_trackCache[id] = track;
return track;
}
```
---
**Следующий шаг:** Прочитайте [ARCHITECTURE.md](ARCHITECTURE.md) для понимания внутреннего устройства.
**Нужна помощь?** Посмотрите [README.md](README.md) или создайте issue.
Удачи в разработке! 🚀

437
README.md Normal file
View File

@@ -0,0 +1,437 @@
# 🎵 YandexMusic - Complete Solution
Полнофункциональное решение для работы с неофициальным API Яндекс Музыки на базе .NET 10. Содержит библиотеку API, оборачиватель клиента и CLI приложение.
## 📋 Содержание
- [Описание](#описание)
- [Структура решения](#структура-решения)
- [Требования](#требования)
- [Установка](#установка)
- [Быстрый старт](#быстрый-старт)
- [Проекты](#проекты)
- [Примеры использования](#примеры-использования)
- [Архитектура](#архитектура)
- [Лицензия](#лицензия)
## 📖 Описание
**YandexMusic** — это комплексное решение для взаимодействия с API Яндекс Музыки на платформе .NET 10. Решение состоит из двух ключевых компонентов:
1. **YandexMusic.API** — низкоуровневая библиотека для прямого взаимодействия с API
2. **YandexMusic** — удобный оборачиватель (wrapper) с клиентом `YandexMusicClient`
### 🎯 Основные возможности
-**Полная асинхронная архитектура** — Async/await на всех уровнях
-**Типобезопасный код** — Nullable reference types, full type safety
-**Модульная структура** — Легко расширяемые компоненты
-**Современный стек** — .NET 10, C# 12, System.Text.Json
-**Полная документация на русском** — XML docs для всех публичных членов
-**WebSocket поддержка** — Протокол Ynison для real-time синхронизации
-**Гибкая конфигурация** — Поддержка прокси, cookies, custom headers
-**Отладка встроена** — Debug settings для логирования и анализа
## 🏗️ Структура решения
```
YandexMusic/
├── YandexMusic.API/ # Низкоуровневая библиотека API
│ ├── API/ # Классы для разных веток API
│ ├── Models/ # Модели данных
│ ├── Requests/ # Построители запросов
│ ├── Common/ # Вспомогательные компоненты
│ ├── Extensions/ # Методы расширения
│ ├── YandexMusicApi.cs # Главный класс API
│ ├── YandexMusic.API.csproj
│ └── README.md # Документация по API
├── YandexMusic/ # Оборачиватель и клиент
│ ├── YandexMusicClient.cs # Основной класс клиента
│ ├── YandexMusic.csproj
│ └── YandexMusicClient.cs # Реализация клиента
└── README.md # Этот файл
```
### Зависимости между проектами
```
YandexMusic (Client wrapper)
YandexMusic.API (Core library)
```
## 📦 Требования
- **.NET 10** или выше
- **C# 12** или выше
- **Visual Studio 2026** (рекомендуется) или Visual Studio Code
### Системные требования
- Windows 10/11, Linux, macOS (любая ОС с .NET 10)
- Минимум 512 MB RAM
- Интернет соединение для работы с API
## 🚀 Установка
### Клонирование репозитория
```bash
git clone https://git.frigat.duckdns.org/FrigaT/YandexMusic.git
cd YandexMusic
```
### Восстановление зависимостей
```bash
dotnet restore
```
### Сборка решения
```bash
dotnet build
```
### Запуск тестов (если есть)
```bash
dotnet test
```
## ⚡ Быстрый старт
### 1. Как клиент (рекомендуется для большинства случаев)
```csharp
using YandexMusic;
// Создание клиента
var client = new YandexMusicClient();
// Получение информации о треке
var track = await client.Api.Track.GetTrackAsync("trackId123");
Console.WriteLine($"Трек: {track?.Title}");
// Поиск музыки
var results = await client.Api.Search.SearchAsync("Ленинград");
Console.WriteLine($"Найдено результатов: {results?.Tracks?.Results?.Count}");
// Работа с плейлистами
var playlists = await client.Api.Playlist.GetPlaylistAsync("playlistId");
Console.WriteLine($"Плейлист: {playlists?.Title}");
```
### 2. Низкоуровневой API (расширенная работа)
```csharp
using YandexMusic.API;
// Создание API
var api = new YandexMusicApi();
// Прямая работа с API
var track = await api.Track.GetTrackAsync("trackId");
```
## 📚 Проекты
### YandexMusic.API
Низкоуровневая библиотека, предоставляющая полный доступ к API Яндекс Музыки.
**Основные компоненты:**
- `YandexMusicApi` — главный класс, содержит все API классы
- `AuthStorage` — управление авторизацией и cookies
- `IRequestProvider` — интерфейс для обработки HTTP запросов
- `YCommonAPI` — базовый класс для всех API веток
**API Классы:**
```csharp
public class YandexMusicApi
{
public YAlbumAPI Album { get; } // Альбомы
public YArtistAPI Artist { get; } // Исполнители
public YLabelAPI Label { get; } // Лейблы
public YLandingAPI Landing { get; } // Рекомендации
public YLibraryAPI Library { get; } // Библиотека
public YPlaylistAPI Playlist { get; } // Плейлисты
public YPinsAPI Pins { get; } // Закреплённые
public YRadioAPI Radio { get; } // Радио
public YSearchAPI Search { get; } // Поиск
public YTrackAPI Track { get; } // Треки
public YQueueAPI Queue { get; } // Очередь
public YUserAPI User { get; } // Пользователь
public YUgcAPI UserGeneratedContent { get; } // UGC
public YYnisonAPI Ynison { get; } // WebSocket
}
```
**Особенности:**
- 300+ моделей данных для полного покрытия API
- Асинхронные методы для всех операций
- Поддержка WebSocket (Ynison)
- Встроенная обработка ошибок
- System.Text.Json для сериализации
**Документация:** [YandexMusic.API/README.md](YandexMusic.API/README.md)
### YandexMusic
Удобный оборачиватель (wrapper) над низкоуровневой библиотекой с клиентом `YandexMusicClient`.
**Основной класс:**
```csharp
public class YandexMusicClient : IDisposable
{
// Свойства
public AuthStorage AuthStorage { get; }
public YAccount Account { get; }
public bool IsAuthorized { get; }
public YnisonPlayer? Ynison { get; }
public HttpClient HttpClient { get; }
public YandexMusicApi Api { get; }
// Методы авторизации
public Task AuthorizeAsync(string login, string password);
public Task AuthorizeByTokenAsync(string token);
}
```
**Возможности:**
- Интеграция с собственным HttpClient
- Управление cookies и прокси
- Встроенная авторизация
- WebSocket плеер Ynison
- Удобное API через свойство `Api`
**Использование:**
```csharp
// С пользовательскими настройками
var client = new YandexMusicClient(
cookieContainer: new CookieContainer(),
proxy: new WebProxy("http://proxy:8080"),
timeout: TimeSpan.FromSeconds(30),
userAgent: "Custom Agent"
);
// Авторизация
await client.AuthorizeAsync("login@gmail.com", "password");
// Использование
var playlists = await client.Api.Playlist.GetPlaylistAsync("123");
```
## 💡 Примеры использования
### Пример 1: Поиск и получение информации
```csharp
using YandexMusic;
var client = new YandexMusicClient();
// Поиск треков
var search = await client.Api.Search.SearchAsync("The Beatles");
var track = search?.Tracks?.Results?.FirstOrDefault();
if (track != null)
{
Console.WriteLine($"🎵 {track.Title}");
Console.WriteLine($"👤 {string.Join(", ", track.Artists?.Select(a => a.Title) ?? [])}");
Console.WriteLine($"⏱️ {track.DurationMs / 1000} сек");
}
```
### Пример 2: Работа с плейлистами
```csharp
// Получение плейлиста
var playlist = await client.Api.Playlist.GetPlaylistAsync("playlistId");
Console.WriteLine($"Плейлист: {playlist?.Title}");
Console.WriteLine($"Треков: {playlist?.Tracks?.Count}");
// Вывод треков
foreach (var track in playlist?.Tracks ?? [])
{
Console.WriteLine($" - {track.Title}");
}
```
### Пример 3: Работа с альбомами
```csharp
// Получение альбома
var album = await client.Api.Album.GetAlbumAsync("albumId");
Console.WriteLine($"Альбом: {album?.Title}");
Console.WriteLine($"Исполнитель: {album?.Artists?.FirstOrDefault()?.Title}");
Console.WriteLine($"Год: {album?.Year}");
Console.WriteLine($"Жанр: {album?.Genre}");
// Вывод треков в альбоме
foreach (var track in album?.Tracks ?? [])
{
Console.WriteLine($" {track.Position}. {track.Title}");
}
```
### Пример 4: Авторизация
```csharp
var client = new YandexMusicClient();
try
{
// Авторизация через логин и пароль
await client.AuthorizeAsync("your-email@gmail.com", "your-password");
Console.WriteLine($"✅ Авторизирован: {client.Account.User?.DisplayName}");
}
catch (Exception ex)
{
Console.WriteLine($"❌ Ошибка авторизации: {ex.Message}");
}
```
### Пример 5: WebSocket Ynison
```csharp
// Подключение к Ynison (если авторизирован)
var player = client.Ynison;
if (player != null)
{
await player.ConnectAsync();
Console.WriteLine("✅ Подключено к Ynison");
// Использование плеера...
await player.DisconnectAsync();
}
```
## 🏛️ Архитектура
### Слои
```
┌─────────────────────────────────────┐
│ YandexMusic (Client Wrapper) │ ← Удобный клиент
├─────────────────────────────────────┤
│ YandexMusic.API (Core Library) │ ← Низкоуровневой API
├─────────────────────────────────────┤
│ HttpClient, System.Text.Json │ ← .NET Framework
└─────────────────────────────────────┘
```
### Ключевые компоненты YandexMusic.API
```
YandexMusicApi (Main Entry Point)
├── API Classes (YAlbumAPI, YTrackAPI, etc.)
│ └── Requests (YGetAlbumBuilder, YSearchBuilder, etc.)
├── Models (YAlbum, YTrack, YPlaylist, etc.)
│ └── Common Models (YBaseModel, YResponse, etc.)
├── AuthStorage (Authorization Management)
│ └── IRequestProvider (HTTP Request Handling)
│ ├── DefaultRequestProvider
│ ├── CommonRequestProvider
│ └── MockRequestProvider
└── Extensions & Utilities
├── HttpRequestHeaderExtensions
├── StringExtensions
├── Encryptor (для шифрования)
└── DataDownloader
```
### Обработка запросов
```
Request Builder (YRequestBuilder<T>)
HttpRequestMessage
IRequestProvider.GetWebResponseAsync()
HttpResponseMessage
System.Text.Json Deserialization
Model<T>
```
## 🔧 Конфигурация
### Настройка HttpClient
```csharp
var client = new YandexMusicClient(
cookieContainer: new CookieContainer(),
proxy: new WebProxy("http://127.0.0.1:8080"),
timeout: TimeSpan.FromSeconds(30),
userAgent: "MyCustomAgent/1.0"
);
```
## 📊 Статистика проекта
| Метрика | Значение |
|---------|----------|
| Проектов | 3 |
| Целевая платформа | .NET 10 |
| Язык C# | 12 |
| Основных API методов | 50+ |
| Моделей данных | 300+ |
| Документированных членов | 100% |
| Асинхронных методов | 100% |
## 🔐 Безопасность
- ✅ Использование HTTPS для всех запросов
- ✅ Поддержка прокси для безопасности
- ✅ Встроенное шифрование для чувствительных данных
- ✅ Управление cookies и сессиями
- ✅ Валидация всех входных данных
**⚠️ Важно:** Это неофициальная библиотека. Используйте её на свой риск и соблюдайте Terms of Service Яндекс Музыки.
## 🚦 Статус проекта
-**Стабильный** — Основная функциональность работает
- 🔄 **Активная разработка** — Регулярные обновления
- 📝 **Документирован** — Полная документация на русском
## 📝 Лицензия
Это неофициальная библиотека для работы с API Яндекс Музыки.
**Дисклеймер:** Автор не несет ответственности за неправомерное использование данной библиотеки. Используйте её в соответствии с Terms of Service Яндекс Музыки.
## 👨‍💻 Автор
**FrigaT** - Разработчик
## 🤝 Поддержка
Для вопросов, багов и предложений:
- 🐛 Issues: [https://git.frigat.duckdns.org/FrigaT/YandexMusic/issues]
- 💬 Discussions: [https://git.frigat.duckdns.org/FrigaT/YandexMusic/discussions]
## 📚 Дополнительная информация
- [YandexMusic.API/README.md](YandexMusic.API/README.md) — Документация по низкоуровневому API
- [Официальный сайт Яндекс Музыки](https://music.yandex.ru)
- [.NET 10 Documentation](https://learn.microsoft.com/dotnet/)
- [C# 12 Features](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12)

View File

@@ -1,6 +1,4 @@
using YandexMusic.API.Common;
using YandexMusic.API.Models.Album;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Requests.Album;
namespace YandexMusic.API;
@@ -8,21 +6,13 @@ namespace YandexMusic.API;
/// <summary>API для работы с альбомами.</summary>
public class YAlbumAPI : YCommonAPI
{
/// <summary>Инициализирует новый экземпляр API альбомов.</summary>
/// <param name="yandex">Экземпляр основного API.</param>
public YAlbumAPI(YandexMusicApi yandex) : base(yandex) { }
public YAlbumAPI(YandexMusicApi api) : base(api) { }
/// <summary>Получает альбом по идентификатору.</summary>
/// <param name="storage">Хранилище данных авторизации.</param>
/// <param name="albumId">Идентификатор альбома.</param>
/// <returns>Ответ API с моделью альбома.</returns>
public Task<YResponse<YAlbum>> GetAsync(AuthStorage storage, string albumId)
=> new YGetAlbumBuilder(api, storage).Build(albumId).GetResponseAsync();
public Task<YAlbum?> GetAsync(string albumId)
=> new YGetAlbumBuilder(Api).ExecuteAsync(albumId);
/// <summary>Получает несколько альбомов по списку идентификаторов.</summary>
/// <param name="storage">Хранилище данных авторизации.</param>
/// <param name="albumIds">Список идентификаторов альбомов.</param>
/// <returns>Ответ API со списком альбомов.</returns>
public Task<YResponse<List<YAlbum>>> GetAsync(AuthStorage storage, IEnumerable<string> albumIds)
=> new YGetAlbumsBuilder(api, storage).Build(albumIds).GetResponseAsync();
public Task<List<YAlbum>?> GetAsync(IEnumerable<string> albumIds)
=> new YGetAlbumsBuilder(Api).ExecuteAsync(albumIds);
}

View File

@@ -1,71 +1,27 @@
using YandexMusic.API.Common;
using YandexMusic.API.Models.Artist;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Requests.Artist;
namespace YandexMusic.API;
/// <summary>
/// API для взаимодействия с исполнителями
/// </summary>
/// <summary>API для работы с исполнителями.</summary>
public class YArtistAPI : YCommonAPI
{
public YArtistAPI(YandexMusicApi yandex) : base(yandex)
public YArtistAPI(YandexMusicApi api) : base(api) { }
public Task<YArtistBriefInfo?> GetAsync(string artistId)
=> new YGetArtistBuilder(Api).ExecuteAsync(artistId);
public Task<List<YArtist>?> GetAsync(IEnumerable<string> artistIds)
=> new YGetArtistsBuilder(Api).ExecuteAsync(artistIds);
public Task<YTracksPage?> GetTracksAsync(string artistId, int page = 0, int pageSize = 20)
=> new YGetArtistTrackBuilder(Api).ExecuteAsync((artistId, page, pageSize));
public async Task<YTracksPage?> GetAllTracksAsync(string artistId)
{
var info = await GetAsync(artistId);
if (info?.Artist?.Counts?.Tracks == null)
return null;
return await GetTracksAsync(artistId, pageSize: info.Artist.Counts.Tracks);
}
/// <summary>
/// Получение исполнителя
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="artistId">Идентификатор</param>
public Task<YResponse<YArtistBriefInfo>> GetAsync(AuthStorage storage, string artistId)
{
return new YGetArtistBuilder(api, storage)
.Build(artistId)
.GetResponseAsync();
}
/// <summary>
/// Получение исполнителей
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="artistIds">Идентификаторы</param>
public Task<YResponse<List<YArtist>>> GetAsync(AuthStorage storage, IEnumerable<string> artistIds)
{
return new YGetArtistsBuilder(api, storage)
.Build(artistIds)
.GetResponseAsync();
}
/// <summary>
/// Получение треков исполнителя с пагинацией
/// <remarks>
/// Треки поставляются по <paramref name="pageSize"/> штук на страницу,
/// для получения всех треков необходимо использовать метод <see cref="GetAllTracksAsync"/>
/// </remarks>
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="artistId">Идентификатор исполнителя</param>
/// <param name="page">Страница ответов</param>
/// <param name="pageSize">Количество треков на странице ответов</param>
public Task<YResponse<YTracksPage>> GetTracksAsync(AuthStorage storage, string artistId, int page = 0, int pageSize = 20)
{
return new YGetArtistTrackBuilder(api, storage)
.Build((artistId, page, pageSize))
.GetResponseAsync();
}
/// <summary>
/// Получение всех треков исполнителя
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="artistId">Идентификатор исполнителя</param>
public async Task<YResponse<YTracksPage>> GetAllTracksAsync(AuthStorage storage, string artistId)
{
YResponse<YArtistBriefInfo> response = await GetAsync(storage, artistId);
return await GetTracksAsync(storage, artistId, pageSize: response.Result.Artist.Counts.Tracks);
}
}

View File

@@ -0,0 +1,111 @@
using System.Security.Authentication;
using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Account;
namespace YandexMusic.API;
/// <summary>API для работы с пользователем и авторизации.</summary>
public class YAuthAPI : YCommonAPI
{
public YAuthAPI(YandexMusicApi api) : base(api) { }
/// <summary>Авторизация по готовому музыкальному токену (OAuth).</summary>
public async Task AuthorizeAsync(string musicToken)
{
if (string.IsNullOrEmpty(musicToken))
throw new ArgumentException("Токен не может быть пустым", nameof(musicToken));
Api.Storage.Token = musicToken;
var authInfo = await new YGetAuthInfoBuilder(Api).ExecuteAsync(null!);
if (authInfo?.Account?.Uid == null)
throw new Exception("Пользователь не авторизован");
Api.Storage.SetAuthorized(authInfo.Account, musicToken);
}
/// <summary>Авторизация по паспортному токену (полученному, например, после QR-входа).</summary>
public async Task AuthorizeByPassportTokenAsync(string passportToken)
{
if (string.IsNullOrEmpty(passportToken))
throw new ArgumentException("Паспортный токен не может быть пустым", nameof(passportToken));
var musicToken = await Api.Passport.GetMusicTokenByPassportTokenAsync(passportToken);
if (musicToken?.AccessToken == null)
throw new Exception("Не удалось обменять паспортный токен на музыкальный");
await AuthorizeAsync(musicToken.AccessToken);
}
/// <summary>Получение информации о текущем аккаунте.</summary>
public Task<YAccountResult?> GetUserAuthAsync()
=> new YGetAuthInfoBuilder(Api).ExecuteAsync(null!);
/// <summary>Создание сессии авторизации (получение доступных методов входа).</summary>
public async Task<YAuthTypes?> CreateAuthSessionAsync(string userName)
{
if (!await Api.Passport.GetCsrfTokenAsync()) // вместо InitSessionAsync
throw new Exception("Не удалось инициализировать сессию");
var result = await new YGetAuthLoginUserBuilder(Api).ExecuteAsync((Api.Storage.AuthToken.CsfrToken, userName));
if (result?.TrackId != null)
Api.Storage.AuthToken.TrackId = result.TrackId;
return result;
}
/// <summary>Получение капчи.</summary>
public Task<YAuthCaptcha?> GetCaptchaAsync()
{
if (Api.Storage.AuthToken == null)
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
return new YGetAuthCaptchaBuilder(Api).ExecuteAsync(null!);
}
/// <summary>Авторизация по капче.</summary>
public Task<YAuthBase?> AuthorizeByCaptchaAsync(string captchaValue)
{
if (Api.Storage.AuthToken == null)
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
return new YGetAuthLoginCaptchaBuilder(Api).ExecuteAsync(captchaValue);
}
/// <summary>Отправить письмо для авторизации.</summary>
public Task<YAuthLetter?> GetAuthLetterAsync()
=> new YGetAuthLetterBuilder(Api).ExecuteAsync(null!);
/// <summary>Подтверждение входа по письму и получение музыкального токена.</summary>
public async Task<bool> AuthorizeByLetterAsync()
{
var status = await new YGetAuthLoginLetterBuilder(Api).ExecuteAsync(null!);
if (status?.Status != YAuthStatus.Ok || !status.MagicLinkConfirmed)
throw new Exception("Письмо не подтверждено");
var musicToken = await Api.Passport.GetMusicTokenByCookiesAsync();
if (musicToken?.AccessToken == null)
throw new Exception("Не удалось получить музыкальный токен после подтверждения письма");
await AuthorizeAsync(musicToken.AccessToken);
return true;
}
/// <summary>Авторизация по паролю приложения Яндекс.</summary>
public async Task<YAuthBase?> AuthorizeByAppPasswordAsync(string password)
{
if (Api.Storage.AuthToken == null)
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
var result = await new YGetAuthAppPasswordBuilder(Api).ExecuteAsync(password);
if (result?.Status != YAuthStatus.Ok)
throw new AuthenticationException("Ошибка авторизации по паролю");
var musicToken = await Api.Passport.GetMusicTokenByCookiesAsync();
if (musicToken?.AccessToken == null)
throw new Exception("Не удалось получить музыкальный токен после ввода пароля");
await AuthorizeAsync(musicToken.AccessToken);
return result;
}
/// <summary>Получение информации о пользователе через логин.</summary>
public Task<YLoginInfo?> GetLoginInfoAsync()
=> new YGetLoginInfoBuilder(Api).ExecuteAsync(null!);
}

View File

@@ -1,12 +1,10 @@
namespace YandexMusic.API;
/// <summary>Родительский класс для всех веток API.</summary>
/// <summary>Базовый класс для всех веток API.</summary>
public abstract class YCommonAPI
{
/// <summary>Основной экземпляр API.</summary>
protected readonly YandexMusicApi api;
protected YandexMusicApi Api { get; }
/// <summary>Инициализирует новый экземпляр.</summary>
/// <param name="yandex">Экземпляр основного API.</param>
protected YCommonAPI(YandexMusicApi yandex) => api = yandex;
protected YCommonAPI(YandexMusicApi api) => Api = api;
}

View File

@@ -1,39 +1,19 @@
using YandexMusic.API.Common;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Models.Label;
using YandexMusic.API.Requests.Label;
namespace YandexMusic.API;
public partial class YLabelAPI : YCommonAPI
/// <summary>API для работы с лейблами.</summary>
public class YLabelAPI : YCommonAPI
{
public YLabelAPI(YandexMusicApi yandex) : base(yandex)
{
}
public YLabelAPI(YandexMusicApi api) : base(api) { }
/// <summary>
/// Постраничное получение альбомов лейбла
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="label">Лейбл</param>
/// <param name="page">Страница</param>
public Task<YResponse<YLabelAlbums>> GetAlbumsByLabelAsync(AuthStorage storage, YLabel label, int page)
{
return new YGetLabelAlbumsBuilder(api, storage)
.Build((label, page))
.GetResponseAsync();
}
/// <summary>Получает альбомы лейбла с пагинацией.</summary>
public Task<YLabelAlbums?> GetAlbumsByLabelAsync(YLabel label, int page = 0)
=> new YGetLabelAlbumsBuilder(Api).ExecuteAsync((label, page));
/// <summary>
/// Постраничное получение артистов лейбла
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="label">Лейбл</param>
/// <param name="page">Страница</param>
public Task<YResponse<YLabelArtists>> GetArtistsByLabelAsync(AuthStorage storage, YLabel label, int page)
{
return new YGetLabelArtistsBuilder(api, storage)
.Build((label, page))
.GetResponseAsync();
}
/// <summary>Получает артистов лейбла с пагинацией.</summary>
public Task<YLabelArtists?> GetArtistsByLabelAsync(YLabel label, int page = 0)
=> new YGetLabelArtistsBuilder(Api).ExecuteAsync((label, page));
}

View File

@@ -1,43 +1,21 @@
using YandexMusic.API.Common;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Models.Feed;
using YandexMusic.API.Models.Feed;
using YandexMusic.API.Models.Landing;
using YandexMusic.API.Requests.Feed;
using YandexMusic.API.Requests.Landing;
namespace YandexMusic.API;
/// <summary>API для взаимодействия с главной страницей (лендингом).</summary>
/// <summary>API для работы с главной страницей (лендингом).</summary>
public class YLandingAPI : YCommonAPI
{
/// <summary>Инициализирует новый экземпляр API лендинга.</summary>
/// <param name="yandex">Экземпляр основного API.</param>
public YLandingAPI(YandexMusicApi yandex) : base(yandex) { }
public YLandingAPI(YandexMusicApi api) : base(api) { }
/// <summary>Получает персональные блоки лендинга.</summary>
/// <param name="storage">Хранилище авторизации.</param>
/// <param name="blocks">Типы запрашиваемых блоков.</param>
/// <returns>Ответ API с лендингом.</returns>
/// <exception cref="ArgumentNullException">Если массив blocks равен null.</exception>
public Task<YResponse<YLanding>> GetAsync(AuthStorage storage, params YLandingBlockType[] blocks)
{
if (blocks == null)
throw new ArgumentNullException(nameof(blocks), "Массив блоков не может быть null");
public Task<YLanding?> GetAsync(params YLandingBlockType[] blocks)
=> new YGetLandingBuilder(Api).ExecuteAsync(blocks);
return new YGetLandingBuilder(api, storage)
.Build(blocks)
.GetResponseAsync();
}
public Task<YFeed?> GetFeedAsync()
=> new YGetFeedBuilder(Api).ExecuteAsync(null!);
/// <summary>Получает ленту событий (фид).</summary>
/// <param name="storage">Хранилище авторизации.</param>
/// <returns>Ответ API с лентой.</returns>
public Task<YResponse<YFeed>> GetFeedAsync(AuthStorage storage)
=> new YGetFeedBuilder(api, storage).Build(null!).GetResponseAsync();
/// <summary>Получает лендинг детского раздела.</summary>
/// <param name="storage">Хранилище авторизации.</param>
/// <returns>Ответ API с детским лендингом.</returns>
public Task<YResponse<YChildrenLanding>> GetChildrenLandingAsync(AuthStorage storage)
=> new YGetChildrenLandingBuilder(api, storage).Build(null!).GetResponseAsync();
public Task<YChildrenLanding?> GetChildrenLandingAsync()
=> new YGetChildrenLandingBuilder(Api).ExecuteAsync(null!);
}

View File

@@ -1,4 +1,3 @@
using YandexMusic.API.Common;
using YandexMusic.API.Models.Album;
using YandexMusic.API.Models.Artist;
using YandexMusic.API.Models.Common;
@@ -10,244 +9,82 @@ using YandexMusic.API.Requests.Library;
namespace YandexMusic.API;
/// <summary>
/// API для взаимодействия с библиотекой
/// </summary>
public partial class YLibraryAPI : YCommonAPI
/// <summary>API для работы с библиотекой (лайки, дизлайки, недавно прослушанное).</summary>
public class YLibraryAPI : YCommonAPI
{
/// <summary>
/// Получение секции библиотеки
/// </summary>
/// <typeparam name="T">Тип объекта библиотеки</typeparam>
/// <param name="storage">Хранилище</param>
/// <param name="section">Секция</param>
/// <param name="type">Тип</param>
/// <returns>Список объектов из секции</returns>
private Task<YResponse<T>> GetLibrarySection<T>(AuthStorage storage, YLibrarySection section, YLibrarySectionType type = YLibrarySectionType.Likes)
{
return new YGetLibrarySectionBuilder<T>(api, storage)
.Build((section, type))
.GetResponseAsync();
}
public YLibraryAPI(YandexMusicApi yandex) : base(yandex)
{
}
public YLibraryAPI(YandexMusicApi api) : base(api) { }
#region Лайки
/// <summary>
/// Получение лайкнутых треков
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public Task<YResponse<YLibraryTracks>> GetLikedTracksAsync(AuthStorage storage)
{
return GetLibrarySection<YLibraryTracks>(storage, YLibrarySection.Tracks);
}
public Task<YLibraryTracks?> GetLikedTracksAsync()
=> new YGetLibrarySectionBuilder<YLibraryTracks>(Api).ExecuteAsync((YLibrarySection.Tracks, YLibrarySectionType.Likes));
/// <summary>
/// Получение лайкнутых альбомов
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public Task<YResponse<List<YLibraryAlbum>>> GetLikedAlbumsAsync(AuthStorage storage)
{
return GetLibrarySection<List<YLibraryAlbum>>(storage, YLibrarySection.Albums);
}
public Task<List<YLibraryAlbum>?> GetLikedAlbumsAsync()
=> new YGetLibrarySectionBuilder<List<YLibraryAlbum>>(Api).ExecuteAsync((YLibrarySection.Albums, YLibrarySectionType.Likes));
/// <summary>
/// Получение лайкнутых исполнителей
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public Task<YResponse<List<YArtist>>> GetLikedArtistsAsync(AuthStorage storage)
{
return GetLibrarySection<List<YArtist>>(storage, YLibrarySection.Artists);
}
public Task<List<YArtist>?> GetLikedArtistsAsync()
=> new YGetLibrarySectionBuilder<List<YArtist>>(Api).ExecuteAsync((YLibrarySection.Artists, YLibrarySectionType.Likes));
/// <summary>
/// Получение лайкнутых плейлистов
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public Task<YResponse<List<YLibraryPlaylists>>> GetLikedPlaylistsAsync(AuthStorage storage)
{
return GetLibrarySection<List<YLibraryPlaylists>>(storage, YLibrarySection.Playlists);
}
public Task<List<YLibraryPlaylists>?> GetLikedPlaylistsAsync()
=> new YGetLibrarySectionBuilder<List<YLibraryPlaylists>>(Api).ExecuteAsync((YLibrarySection.Playlists, YLibrarySectionType.Likes));
#endregion Лайки
#endregion
#region Дизлайки
/// <summary>
/// Получение дизлайкнутых треков
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public Task<YResponse<YLibraryTracks>> GetDislikedTracksAsync(AuthStorage storage)
{
return GetLibrarySection<YLibraryTracks>(storage, YLibrarySection.Tracks, YLibrarySectionType.Dislikes);
}
public Task<YLibraryTracks?> GetDislikedTracksAsync()
=> new YGetLibrarySectionBuilder<YLibraryTracks>(Api).ExecuteAsync((YLibrarySection.Tracks, YLibrarySectionType.Dislikes));
/// <summary>
/// Получение дизлайкнутых исполнителей
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public Task<YResponse<List<YArtist>>> GetDislikedArtistsAsync(AuthStorage storage)
{
return GetLibrarySection<List<YArtist>>(storage, YLibrarySection.Artists, YLibrarySectionType.Dislikes);
}
public Task<List<YArtist>?> GetDislikedArtistsAsync()
=> new YGetLibrarySectionBuilder<List<YArtist>>(Api).ExecuteAsync((YLibrarySection.Artists, YLibrarySectionType.Dislikes));
#endregion Дизлайки
#endregion
#region Добавление в списки лайков/дизлайков
#region Добавление/удаление
/// <summary>
/// Добавить трек в список лайкнутых
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="track">Трек</param>
/// <returns></returns>
public Task<YResponse<YPlaylist>> AddTrackLikeAsync(AuthStorage storage, YTrack track)
{
return new YLibraryAddBuilder<YPlaylist>(api, storage)
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Likes))
.GetResponseAsync();
}
public Task<int?> AddTrackLikeAsync(YTrack track)
=> new YLibraryAddBuilder<YRevision>(Api).ExecuteAsync((track.Id, YLibrarySection.Tracks, YLibrarySectionType.Likes))
.ContinueWith(t => t.Result?.Revision);
/// <summary>
/// Удалить трек из списка лайкнутых
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="track">Трек</param>
/// <returns></returns>
public Task<YResponse<YRevision>> RemoveTrackLikeAsync(AuthStorage storage, YTrack track)
{
return new YLibraryRemoveBuilder<YRevision>(api, storage)
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Likes))
.GetResponseAsync();
}
public Task<int?> RemoveTrackLikeAsync(YTrack track)
=> new YLibraryRemoveBuilder<YRevision>(Api).ExecuteAsync((track.Id, YLibrarySection.Tracks, YLibrarySectionType.Likes))
.ContinueWith(t => t.Result?.Revision);
/// <summary>
/// Добавить трек в список дизлайкнутых
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="track">Трек</param>
/// <returns></returns>
public Task<YResponse<YRevision>> AddTrackDislikeAsync(AuthStorage storage, YTrack track)
{
return new YLibraryAddBuilder<YRevision>(api, storage)
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
.GetResponseAsync();
}
public Task<int?> AddTrackDislikeAsync(YTrack track)
=> new YLibraryAddBuilder<YRevision>(Api).ExecuteAsync((track.Id, YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
.ContinueWith(t => t.Result?.Revision);
/// <summary>
/// Удалить трек из списка дизлайкнутых
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="track">Трек</param>
/// <returns></returns>
public Task<YResponse<YRevision>> RemoveTrackDislikeAsync(AuthStorage storage, YTrack track)
{
return new YLibraryRemoveBuilder<YRevision>(api, storage)
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
.GetResponseAsync();
}
public Task<int?> RemoveTrackDislikeAsync(YTrack track)
=> new YLibraryRemoveBuilder<YRevision>(Api).ExecuteAsync((track.Id, YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
.ContinueWith(t => t.Result?.Revision);
/// <summary>
/// Добавить альбом в список лайкнутых
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="album">Альбом</param>
/// <returns></returns>
public Task<YResponse<string>> AddAlbumLikeAsync(AuthStorage storage, YAlbum album)
{
return new YLibraryAddBuilder<string>(api, storage)
.Build((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes))
.GetResponseAsync();
}
public Task<string?> AddAlbumLikeAsync(YAlbum album)
=> new YLibraryAddBuilder<string>(Api).ExecuteAsync((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes));
/// <summary>
/// Удалить альбом из списка лайкнутых
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="album">Альбом</param>
/// <returns></returns>
public Task<YResponse<string>> RemoveAlbumLikeAsync(AuthStorage storage, YAlbum album)
{
return new YLibraryRemoveBuilder<string>(api, storage)
.Build((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes))
.GetResponseAsync();
}
public Task<string?> RemoveAlbumLikeAsync(YAlbum album)
=> new YLibraryRemoveBuilder<string>(Api).ExecuteAsync((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes));
/// <summary>
/// Добавить исполнителя в список лайкнутых
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="artist">Исполнитель</param>
/// <returns></returns>
public Task<YResponse<string>> AddArtistLikeAsync(AuthStorage storage, YArtist artist)
{
return new YLibraryAddBuilder<string>(api, storage)
.Build((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes))
.GetResponseAsync();
}
public Task<string?> AddArtistLikeAsync(YArtist artist)
=> new YLibraryAddBuilder<string>(Api).ExecuteAsync((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes));
/// <summary>
/// Удалить исполнителя из списка лайкнутых
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="artist">Исполнитель</param>
/// <returns></returns>
public Task<YResponse<string>> RemoveArtistLikeAsync(AuthStorage storage, YArtist artist)
{
return new YLibraryRemoveBuilder<string>(api, storage)
.Build((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes))
.GetResponseAsync();
}
public Task<string?> RemoveArtistLikeAsync(YArtist artist)
=> new YLibraryRemoveBuilder<string>(Api).ExecuteAsync((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes));
/// <summary>
/// Добавить плейлист в список лайкнутых
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="playlist">Плейлист</param>
/// <returns></returns>
public Task<YResponse<string>> AddPlaylistLikeAsync(AuthStorage storage, YPlaylist playlist)
{
return new YLibraryAddBuilder<string>(api, storage)
.Build((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes))
.GetResponseAsync();
}
public Task<string?> AddPlaylistLikeAsync(YPlaylist playlist)
=> new YLibraryAddBuilder<string>(Api).ExecuteAsync((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes));
/// <summary>
/// Удалить плейлист из списка лайкнутых
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="playlist">Плейлист</param>
/// <returns></returns>
public Task<YResponse<string>> RemovePlaylistLikeAsync(AuthStorage storage, YPlaylist playlist)
{
return new YLibraryRemoveBuilder<string>(api, storage)
.Build((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes))
.GetResponseAsync();
}
public Task<string?> RemovePlaylistLikeAsync(YPlaylist playlist)
=> new YLibraryRemoveBuilder<string>(Api).ExecuteAsync((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes));
#endregion Добавление/удаление в списки лайков/дизлайков
#endregion
#region Получение списка "Вы недавно слушали"
public Task<YResponse<YRecentlyListenedContext>> GetRecentlyListenedAsync(AuthStorage storage, IEnumerable<YPlayContextType> contextTypes, int trackCount, int contextCount)
{
return new YGetLibraryRecentlyListenedBuilder(api, storage)
.Build((contextTypes, trackCount, contextCount))
.GetResponseAsync();
}
#endregion Получение списка "Вы недавно слушали"
#region Недавно прослушанное
public Task<YRecentlyListenedContext?> GetRecentlyListenedAsync(
IEnumerable<YPlayContextType> contextTypes,
int trackCount = 50,
int contextCount = 10)
=> new YGetLibraryRecentlyListenedBuilder(Api).ExecuteAsync((contextTypes, trackCount, contextCount));
#endregion
}

View File

@@ -0,0 +1,230 @@
using System.Security.Authentication;
using System.Text.RegularExpressions;
using YandexMusic.API.Models.Account;
using YandexMusic.API.Models.Passport;
using YandexMusic.API.Requests.Account;
using YandexMusic.API.Requests.Passport;
namespace YandexMusic.API;
/// <summary>API для работы с яндекс паспортом</summary>
public class YPassportAPI : YCommonAPI
{
public YPassportAPI(YandexMusicApi api) : base(api) { }
public async Task<YAccessToken?> GetMusicTokenByCookiesAsync()
{
if (string.IsNullOrEmpty(Api.Storage.AuthToken.TrackId))
{
if (!await GetCsrfTokenAsync())
throw new Exception("Не удалось инициализировать сессию");
await CreateTrackAsync(); // ваш приватный метод создания track_id
}
return await new YGetAuthCookiesBuilder(Api).ExecuteAsync(null!);
}
public async Task<YAccessToken?> GetMusicTokenByPassportTokenAsync(string passportToken)
=> await GetAccessTokenAsync(passportToken);
public async Task<string?> GetAuthQRLinkAsync()
{
if (!await GetCsrfTokenAsync())
throw new Exception("Не удалось инициализировать сессию");
await CreateTrackAsync();
return $"https://passport.yandex.ru/auth/magic/code/?track_id={Api.Storage.AuthToken.TrackId}";
}
public async Task<YAuthQRStatus?> CheckQRStatusAsync()
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
var status = await new YGetQrStatus(Api).ExecuteAsync(null!);
if (!string.IsNullOrWhiteSpace(status?.TrackId))
{
Api.Storage.AuthToken.SessionTrackId = status.TrackId;
}
return status;
}
public async Task<YAuthQrSession?> AuthorizeByQRAsync()
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
if (string.IsNullOrWhiteSpace(Api.Storage.AuthToken.SessionTrackId))
throw new Exception("Токен сессии не инициализирован");
var status = await new YGetAuthLoginQRBuilder(Api).ExecuteAsync(null!);
if (status != null && status.DefaultUid != 0 && await LoginByCookiesAsync())
return status;
throw new AuthenticationException("Ошибка авторизации по QR");
}
/// <summary>Многоступенчатая авторизация: начало (передача логина).</summary>
public async Task<YMultistepStart?> MultistepStartAsync(string login)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована. Вызовите GetAuthQRLinkAsync или CreateTrackAsync");
return await new YMultistepStartBuilder(Api).ExecuteAsync(login);
}
/// <summary>Многоступенчатая авторизация: ввод пароля.</summary>
public async Task<YPassportUser?> MultistepPasswordAsync(string password)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YMultiStepPasswordBuilder(Api).ExecuteAsync(password);
}
/// <summary>Авторизация с помощью RFC OTP.</summary>
public async Task<YPassportUser?> RfcOtpPasswordAsync(string rfcOtp)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YRfcOtpBuilder(Api).ExecuteAsync(rfcOtp);
}
/// <summary>Создание сессии пользователя.</summary>
public async Task<YPassportSession?> CreateUserSessionAsync()
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YGetSessionBuilder(Api).ExecuteAsync(null!);
}
/// <summary>Проверка состояния сессии.</summary>
public async Task<YPassportSessionStatus?> GetSessionStateAsync()
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YCheckSessionBuilder(Api).ExecuteAsync(null!);
}
/// <summary>Проверка номера телефона (валидация).</summary>
public async Task<YValidatePhoneNumberResult?> ValidatePhoneNumberAsync(string phone)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YValidatePhoneNumberBuilder(Api).ExecuteAsync(phone);
}
/// <summary>Проверка доступности номера.</summary>
public async Task<YCheckAvailabilityResult?> CheckPhoneAvailabilityAsync(string phone)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YCheckPhoneAvailabilityBuilder(Api).ExecuteAsync(phone);
}
/// <summary>Запрос на отправку push-уведомления.</summary>
public async Task<YSendPushResult?> SuggestSendPushAsync(string phone)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YSendPushBuilder(Api).ExecuteAsync(phone);
}
/// <summary>Проверка push-кода.</summary>
public async Task CheckPushCodeAsync(string code)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
await new YCheckPushCodeBuilder(Api).ExecuteAsync(code);
}
/// <summary>Проверка на "сквоттера" (захват номера).</summary>
public async Task<YValidateSquatter?> ValidateSquatterAsync(string phone)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YValidateSquatterBuilder(Api).ExecuteAsync(phone);
}
/// <summary>Получение списка аккаунтов по номеру телефона.</summary>
public async Task<YSuggestByPhoneResult?> SuggestByPhoneAsync()
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YSuggestByPhoneBuilder(Api).ExecuteAsync(null!);
}
/// <summary>Вход по паролю (упрощённый, если не нужна многоступенчатость).</summary>
public async Task<YPassportUser?> LoginByPasswordAsync(string password)
{
// Сначала запускаем мультистеп (если track_id уже есть)
if (string.IsNullOrEmpty(Api.Storage.AuthToken.TrackId))
throw new Exception("TrackId не найден. Вызовите MultistepStartAsync сначала.");
return await MultistepPasswordAsync(password);
}
private async Task CreateTrackAsync()
{
if (!await GetCsrfTokenAsync())
throw new Exception("Невозможно инициализировать сессию входа.");
var track = await new YCreateTrackBuilder(Api).ExecuteAsync(null!);
if (string.IsNullOrWhiteSpace(track?.TrackId) || string.IsNullOrWhiteSpace(track?.CsrfToken))
throw new Exception("Не удалось создать трек паспорта.");
Api.Storage.AuthToken.TrackId = track.TrackId;
Api.Storage.AuthToken.CsfrToken = track.CsrfToken;
}
internal async Task<bool> GetCsrfTokenAsync()
{
using var response = await new YGetAuthMethodsBuilder(Api).ExecuteRawAsync(null!);
if (response == null || !response.IsSuccessStatusCode)
throw new HttpRequestException("Не удалось получить CSRF-токен");
var content = await response.Content.ReadAsStringAsync();
var csrfMatch = Regex.Match(content, @"window\.__CSRF__\s*=\s*""([^""]+)""");
var processMatch = Regex.Match(content, @"'process_uuid'\s*:\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'");
if (!csrfMatch.Success || !processMatch.Success)
return false;
Api.Storage.HeaderToken = new YAuthToken
{
CsfrToken = csrfMatch.Groups[1].Value,
ProcessUuid = processMatch.Groups[1].Value
};
return true;
}
internal async Task<YAccessToken?> GetAccessTokenAsync(string passportToken)
{
if (string.IsNullOrEmpty(passportToken))
throw new Exception("Сессия не инициализована");
var token = await new YGetMusicTokenBuilder(Api).ExecuteAsync(passportToken);
if (token?.AccessToken != null)
Api.Storage.Token = token.AccessToken;
return token;
}
internal async Task<bool> LoginByCookiesAsync()
{
if (Api.Storage.AuthToken == null)
throw new AuthenticationException("Сессия входа не инициализирована");
var accessToken = await new YGetAuthCookiesBuilder(Api).ExecuteAsync(null!);
if (accessToken == null || string.IsNullOrEmpty(accessToken.AccessToken))
return false;
await GetAccessTokenAsync(accessToken.AccessToken);
return true;
}
}

View File

@@ -1,20 +1,13 @@
using YandexMusic.API.Common;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Models.Pins;
using YandexMusic.API.Requests.Pins;
namespace YandexMusic.API;
/// <summary>API для взаимодействия с закреплёнными объектами (пинами).</summary>
/// <summary>API для работы с закреплёнными объектами (пинами).</summary>
public class YPinsAPI : YCommonAPI
{
/// <summary>Инициализирует новый экземпляр API пинов.</summary>
/// <param name="yandex">Экземпляр основного API.</param>
public YPinsAPI(YandexMusicApi yandex) : base(yandex) { }
public YPinsAPI(YandexMusicApi api) : base(api) { }
/// <summary>Получает список закреплённых объектов.</summary>
/// <param name="storage">Хранилище авторизации.</param>
/// <returns>Ответ API со списком пинов.</returns>
public Task<YResponse<YPins>> GetAsync(AuthStorage storage)
=> new YGetPinsBuilder(api, storage).Build(null!).GetResponseAsync();
public Task<YPins?> GetAsync()
=> new YGetPinsBuilder(Api).ExecuteAsync(null!);
}

View File

@@ -1,5 +1,3 @@
using YandexMusic.API.Common;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Models.Landing;
using YandexMusic.API.Models.Landing.Entity.Entities;
using YandexMusic.API.Models.Playlist;
@@ -8,142 +6,114 @@ using YandexMusic.API.Requests.Playlist;
namespace YandexMusic.API;
/// <summary>API для взаимодействия с плейлистами.</summary>
/// <summary>API для работы с плейлистами.</summary>
public class YPlaylistAPI : YCommonAPI
{
/// <summary>Инициализирует новый экземпляр API плейлистов.</summary>
/// <param name="yandex">Экземпляр основного API.</param>
public YPlaylistAPI(YandexMusicApi yandex) : base(yandex) { }
public YPlaylistAPI(YandexMusicApi api) : base(api) { }
/// <summary>Получает список персональных плейлистов с главной страницы.</summary>
/// <param name="storage">Хранилище авторизации.</param>
/// <returns>Список ответов с плейлистами.</returns>
public async Task<List<YResponse<YPlaylist>>> GetPersonalPlaylistsAsync(AuthStorage storage)
public async Task<List<YPlaylist>> GetPersonalPlaylistsAsync()
{
var landing = await api.Landing.GetAsync(storage, YLandingBlockType.PersonalPlaylists);
var block = landing.Result?.Blocks?.FirstOrDefault(b => b.Type == YLandingBlockType.PersonalPlaylists);
var landing = await Api.Landing.GetAsync(YLandingBlockType.PersonalPlaylists);
var block = landing?.Blocks?.FirstOrDefault(b => b.Type == YLandingBlockType.PersonalPlaylists);
if (block?.Entities == null)
return new List<YResponse<YPlaylist>>();
return new List<YPlaylist>();
var tasks = block.Entities
.OfType<YLandingEntityPersonalPlaylist>()
.Select(e => api.Playlist.GetAsync(storage, e.Data?.Data));
.Select(e => GetAsync(e.Data?.Data?.Owner?.Uid ?? Api.Storage.User.Uid, e.Data?.Data?.Kind ?? ""))
.Where(t => t != null)
.ToList();
return new List<YResponse<YPlaylist>>(await Task.WhenAll(tasks));
var results = await Task.WhenAll(tasks);
return results.Where(p => p != null).ToList()!;
}
/// <summary>Получает избранные плейлисты.</summary>
public Task<YResponse<List<YPlaylist>>> FavoritesAsync(AuthStorage storage)
=> new YGetPlaylistFavoritesBuilder(api, storage).Build(null!).GetResponseAsync();
public Task<YPlaylist?> GetAsync(string user, string kind)
=> new YGetPlaylistBuilder(Api).ExecuteAsync((user, kind));
/// <summary>Получает плейлист дня.</summary>
public Task<YResponse<YPlaylist>> OfTheDayAsync(AuthStorage storage)
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.PlaylistOfTheDay);
public Task<YPlaylist?> GetAsync(string uuid)
=> new YGetPlaylistByUuidBuilder(Api).ExecuteAsync(uuid);
/// <summary>Получает плейлист «Дежавю».</summary>
public Task<YResponse<YPlaylist>> DejaVuAsync(AuthStorage storage)
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.NeverHeard);
public Task<YPlaylist?> GetAsync(YPlaylist playlist)
=> GetAsync(playlist.Owner.Uid, playlist.Kind);
/// <summary>Получает плейлист «Премьера».</summary>
public Task<YResponse<YPlaylist>> PremiereAsync(AuthStorage storage)
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.RecentTracks);
public Task<List<YPlaylist>?> GetAsync(IEnumerable<(string user, string kind)> ids)
=> new YGetPlaylistsBuilder(Api).ExecuteAsync(ids);
/// <summary>Получает плейлист «Тайник».</summary>
public Task<YResponse<YPlaylist>> MissedAsync(AuthStorage storage)
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.MissedLikes);
public Task<List<YPlaylist>?> FavoritesAsync()
=> new YGetPlaylistFavoritesBuilder(Api).ExecuteAsync(null!);
/// <summary>Получает плейлист «Кинопоиск».</summary>
public Task<YResponse<YPlaylist>> KinopoiskAsync(AuthStorage storage)
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.Kinopoisk);
public async Task<YPlaylist?> OfTheDayAsync()
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.PlaylistOfTheDay.ToString());
private async Task<YResponse<YPlaylist>> GetPersonalPlaylistAsync(AuthStorage storage, YGeneratedPlaylistType type)
{
var list = await GetPersonalPlaylistsAsync(storage);
return list.FirstOrDefault(e => string.Equals(e.Result?.GeneratedPlaylistType, type.ToString(), StringComparison.CurrentCultureIgnoreCase))
?? throw new Exception($"Плейлист типа {type} не найден.");
}
public async Task<YPlaylist?> DejaVuAsync()
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.NeverHeard.ToString());
/// <summary>Получает плейлист по идентификатору пользователя и типа.</summary>
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, string user, string kind)
=> new YGetPlaylistBuilder(api, storage).Build((user, kind)).GetResponseAsync();
public async Task<YPlaylist?> PremiereAsync()
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.RecentTracks.ToString());
/// <summary>Получает плейлист по UUID.</summary>
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, string uuid)
=> new YGetPlaylistByUuidBuilder(api, storage).Build(uuid).GetResponseAsync();
public async Task<YPlaylist?> MissedAsync()
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.MissedLikes.ToString());
/// <summary>Получает несколько плейлистов по списку пар (пользователь, тип).</summary>
public Task<YResponse<List<YPlaylist>>> GetAsync(AuthStorage storage, IEnumerable<(string user, string kind)> ids)
=> new YGetPlaylistsBuilder(api, storage).Build(ids).GetResponseAsync();
public async Task<YPlaylist?> KinopoiskAsync()
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.Kinopoisk.ToString());
/// <summary>Получает плейлист по объекту плейлиста (обновляет его треки).</summary>
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, YPlaylist playlist)
=> new YGetPlaylistBuilder(api, storage).Build((playlist.Owner.Uid, playlist.Kind)).GetResponseAsync();
public Task<YPlaylist?> CreateAsync(string name)
=> new YPlaylistCreateBuilder(Api).ExecuteAsync(name);
/// <summary>Создаёт новый плейлист с заданным именем.</summary>
public Task<YResponse<YPlaylist>> CreateAsync(AuthStorage storage, string name)
=> new YPlaylistCreateBuilder(api, storage).Build(name).GetResponseAsync();
public Task<YPlaylist?> RenameAsync(string kind, string name)
=> new YPlaylistRenameBuilder(Api).ExecuteAsync((kind, name));
/// <summary>Переименовывает плейлист.</summary>
public Task<YResponse<YPlaylist>> RenameAsync(AuthStorage storage, string kind, string name)
=> new YPlaylistRenameBuilder(api, storage).Build((kind, name)).GetResponseAsync();
public Task<YPlaylist?> RenameAsync(YPlaylist playlist, string name)
=> RenameAsync(playlist.Kind, name);
/// <summary>Переименовывает плейлист.</summary>
public Task<YResponse<YPlaylist>> RenameAsync(AuthStorage storage, YPlaylist playlist, string name)
=> RenameAsync(storage, playlist.Kind, name);
/// <summary>Удаляет плейлист.</summary>
public async Task<bool> DeleteAsync(AuthStorage storage, string kind)
public async Task<bool> DeleteAsync(string kind)
{
try
{
await new YPlaylistRemoveBuilder(api, storage).Build(kind).GetResponseAsync();
await new YPlaylistRemoveBuilder(Api).ExecuteAsync(kind);
return true;
}
catch (Exception ex)
catch
{
// Логирование ошибки можно добавить через ILogger
return false;
}
}
/// <summary>Удаляет плейлист.</summary>
public Task<bool> DeleteAsync(AuthStorage storage, YPlaylist playlist)
=> DeleteAsync(storage, playlist.Kind);
public Task<bool> DeleteAsync(YPlaylist playlist)
=> DeleteAsync(playlist.Kind);
/// <summary>Добавляет треки в начало плейлиста.</summary>
public async Task<YResponse<YPlaylist>> InsertTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YTrack> tracks)
public async Task<YPlaylist?> InsertTracksAsync(YPlaylist playlist, IEnumerable<YTrack> tracks)
{
var change = await ChangePlaylistAsync(storage, playlist, new List<YPlaylistChange>
var change = await new YPlaylistChangeBuilder(Api).ExecuteAsync((playlist, new[]
{
new()
new YPlaylistChange
{
Operation = YPlaylistChangeType.Insert,
At = 0,
Tracks = tracks.Select(t => t.GetKey())
}
});
return await GetAsync(storage, change.Result);
}));
return change != null ? await GetAsync(change) : null;
}
/// <summary>Удаляет треки из плейлиста.</summary>
public Task<YResponse<YPlaylist>> DeleteTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YTrack> tracks)
public async Task<YPlaylist?> DeleteTracksAsync(YPlaylist playlist, IEnumerable<YTrack> tracks)
{
var distinctTracks = tracks.Distinct().ToList();
var changes = distinctTracks
var indices = distinctTracks
.Select(t => playlist.Tracks?.FindIndex(ct => ct.Track?.GetKey() == t.GetKey()) ?? -1)
.Where(i => i != -1)
.Select(i => new YPlaylistChange
{
Operation = YPlaylistChangeType.Delete,
From = i,
To = i + 1,
Tracks = new List<YTrackAlbumPair> { playlist.Tracks![i].Track!.GetKey() }
})
.ToList();
return ChangePlaylistAsync(storage, playlist, changes);
}
var changes = indices.Select(i => new YPlaylistChange
{
Operation = YPlaylistChangeType.Delete,
From = i,
To = i + 1,
Tracks = new[] { playlist.Tracks![i].Track!.GetKey() }
});
private Task<YResponse<YPlaylist>> ChangePlaylistAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YPlaylistChange> changes)
=> new YPlaylistChangeBuilder(api, storage).Build((playlist, changes)).GetResponseAsync();
var change = await new YPlaylistChangeBuilder(Api).ExecuteAsync((playlist, changes));
return change != null ? await GetAsync(change) : null;
}
}

View File

@@ -1,72 +1,22 @@
using YandexMusic.API.Common;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Models.Queue;
using YandexMusic.API.Requests.Queue;
namespace YandexMusic.API;
/// <summary>
/// API для взаимодействия с очередями
/// </summary>
public partial class YQueueAPI : YCommonAPI
/// <summary>API для работы с очередями воспроизведения.</summary>
public class YQueueAPI : YCommonAPI
{
public YQueueAPI(YandexMusicApi yandex) : base(yandex)
{
}
public YQueueAPI(YandexMusicApi api) : base(api) { }
/// <summary>
/// Получение всех очередей треков с разных устройств для синхронизации между ними
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="device">Устройство</param>
/// <returns></returns>
public Task<YResponse<YQueueItemsContainer>> ListAsync(AuthStorage storage, string? device = null)
{
return new YQueuesListBuilder(api, storage)
.Build(device)
.GetResponseAsync();
}
public Task<YQueueItemsContainer?> ListAsync(string? device = null)
=> new YQueuesListBuilder(Api, device).ExecuteAsync(null!);
/// <summary>
/// Получение очереди
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="queueId">Идентификатор очереди</param>
/// <returns></returns>
public Task<YResponse<YQueue>> GetAsync(AuthStorage storage, string queueId)
{
return new YGetQueueBuilder(api, storage)
.Build(queueId)
.GetResponseAsync();
}
public Task<YQueue?> GetAsync(string queueId)
=> new YGetQueueBuilder(Api).ExecuteAsync(queueId);
/// <summary>
/// Создание новой очереди треков
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="queue">Очередь треков</param>
/// <param name="device">Устройство</param>
/// <returns></returns>
public Task<YResponse<YNewQueue>> CreateAsync(AuthStorage storage, YQueue queue, string? device = null)
{
return new YQueueCreateBuilder(api, storage, device)
.Build(queue)
.GetResponseAsync();
}
public Task<YNewQueue?> CreateAsync(YQueue queue, string? device = null)
=> new YQueueCreateBuilder(Api, device).ExecuteAsync(queue);
/// <summary>
/// Установка текущего индекса проигрываемого трека в очереди треков
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="queueId">Идентификатор очереди</param>
/// <param name="currentIndex">Текущий индекс</param>
/// <param name="isInteractive">Флаг интерактивности</param>
/// <param name="device">Устройство</param>
/// <returns></returns>
public Task<YResponse<YUpdatedQueue>> UpdatePositionAsync(AuthStorage storage, string queueId, int currentIndex, bool isInteractive, string device = null)
{
return new YQueueUpdatePositionBuilder(api, storage, device)
.Build((queueId, currentIndex, isInteractive))
.GetResponseAsync();
}
}
public Task<YUpdatedQueue?> UpdatePositionAsync(string queueId, int currentIndex, bool isInteractive, string? device = null)
=> new YQueueUpdatePositionBuilder(Api, device).ExecuteAsync((queueId, currentIndex, isInteractive));
}

View File

@@ -1,113 +1,37 @@
using YandexMusic.API.Common;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Models.Radio;
using YandexMusic.API.Models.Track;
using YandexMusic.API.Requests.Radio;
namespace YandexMusic.API;
/// <summary>
/// API для взаимодействия с радио
/// </summary>
public partial class YRadioAPI : YCommonAPI
/// <summary>API для работы с радио.</summary>
public class YRadioAPI : YCommonAPI
{
public YRadioAPI(YandexMusicApi yandex) : base(yandex)
{
}
public YRadioAPI(YandexMusicApi api) : base(api) { }
/// <summary>
/// Получение списка рекомендованных радиостанций
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public Task<YResponse<YStationsDashboard>> GetStationsDashboardAsync(AuthStorage storage)
{
return new YGetStationsDashboardBuilder(api, storage)
.Build(null)
.GetResponseAsync();
}
public Task<YStationsDashboard?> GetStationsDashboardAsync()
=> new YGetStationsDashboardBuilder(Api).ExecuteAsync(null!);
/// <summary>
/// Получение списка радиостанций
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public Task<YResponse<List<YStation>>> GetStationsAsync(AuthStorage storage)
{
return new YGetStationsBuilder(api, storage)
.Build(null)
.GetResponseAsync();
}
public Task<List<YStation>?> GetStationsAsync()
=> new YGetStationsBuilder(Api).ExecuteAsync(null!);
/// <summary>
/// Получение информации о радиостанции
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="type">Тип</param>
/// <param name="tag">Тэг</param>
/// <returns></returns>
public Task<YResponse<List<YStation>>> GetStationAsync(AuthStorage storage, string type, string tag)
{
return new YGetStationBuilder(api, storage)
.Build((type, tag))
.GetResponseAsync();
}
public Task<List<YStation>?> GetStationAsync(string type, string tag)
=> new YGetStationBuilder(Api).ExecuteAsync((type, tag));
/// <summary>
/// Получение информации о радиостанции
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="id">Идентификатор станции</param>
/// <returns></returns>
public Task<YResponse<List<YStation>>> GetStationAsync(AuthStorage storage, YStationId id)
{
return GetStationAsync(storage, id.Type, id.Tag);
}
public Task<List<YStation>?> GetStationAsync(YStationId id)
=> GetStationAsync(id.Type, id.Tag);
/// <summary>
/// Получение последовательности треков радиостанции
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="station">Радиостанция</param>
/// <param name="prevTrackId">Идентификатор предыдущего трека</param>
/// <returns></returns>
public Task<YResponse<YStationSequence>> GetStationTracksAsync(AuthStorage storage, YStation station, string prevTrackId = "")
{
return new YGetStationTracksBuilder(api, storage)
.Build((station.Station, prevTrackId))
.GetResponseAsync();
}
/// <summary>
/// Установка настроек подбора треков
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="station">Радиостанция</param>
/// <param name="settings">Настройки</param>
/// <returns></returns>
public Task<YResponse<string>> SetStationSettings2Async(AuthStorage storage, YStation station, YStationSettings2 settings)
{
return new YSetSettings2Builder(api, storage)
.Build((station.Station, settings))
.GetResponseAsync();
}
/// <summary>
/// Отправка обратной связи на действия при прослушивании радио
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="station">Радиостанция</param>
/// <param name="type">Тип обратной связи</param>
/// <param name="track">Трек</param>
/// <param name="batchId">Уникальный идентификатор партии треков. Возвращается при получении треков</param>
/// <param name="totalPlayedSeconds">Сколько было проиграно секунд трека перед действием</param>
/// <returns></returns>
public Task<string> SendStationFeedBackAsync(AuthStorage storage, YStation station, YStationFeedbackType type, YTrack track = null, string batchId = "", double totalPlayedSeconds = 0)
{
return new YSetStationFeedbackBuilder(api, storage)
.Build((type, station, track, batchId, totalPlayedSeconds))
.GetResponseAsync();
}
public Task<YStationSequence?> GetStationTracksAsync(YStation station, string prevTrackId = "")
=> new YGetStationTracksBuilder(Api).ExecuteAsync((station.Station, prevTrackId));
public Task<string?> SetStationSettings2Async(YStation station, YStationSettings2 settings)
=> new YSetSettings2Builder(Api).ExecuteAsync((station.Station, settings));
public Task<string?> SendStationFeedbackAsync(
YStation station,
YStationFeedbackType type,
YTrack? track = null,
string batchId = "",
double totalPlayedSeconds = 0)
=> new YSetStationFeedbackBuilder(Api).ExecuteAsync((type, station, track, batchId, totalPlayedSeconds));
}

View File

@@ -1,139 +1,38 @@
using YandexMusic.API.Common;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Models.Search;
using YandexMusic.API.Requests.Search;
namespace YandexMusic.API;
/// <summary>
/// API для поиска
/// </summary>
public partial class YSearchAPI : YCommonAPI
/// <summary>API для поиска.</summary>
public class YSearchAPI : YCommonAPI
{
public YSearchAPI(YandexMusicApi api) : base(api) { }
public YSearchAPI(YandexMusicApi yandex) : base(yandex)
{
}
public Task<YSearch?> TrackAsync(string trackName, int page = 0, int pageSize = 20)
=> SearchAsync(trackName, YSearchType.Track, page, pageSize);
/// <summary>
/// Поиск по трекам
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="trackName">Имя трека</param>
/// <param name="pageNumber">Номер страницы</param>
/// <param name="pageSize">Размер страницы</param>
/// <returns></returns>
public Task<YResponse<YSearch>> TrackAsync(AuthStorage storage, string trackName, int pageNumber = 0, int pageSize = 20)
{
return SearchAsync(storage, trackName, YSearchType.Track, pageNumber, pageSize);
}
public Task<YSearch?> AlbumsAsync(string albumName, int page = 0, int pageSize = 20)
=> SearchAsync(albumName, YSearchType.Album, page, pageSize);
/// <summary>
/// Поиск по альбомам
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="albumName">Имя альбома</param>
/// <param name="pageNumber">Номер страницы</param>
/// <param name="pageSize">Размер страницы</param>
/// <returns></returns>
public Task<YResponse<YSearch>> AlbumsAsync(AuthStorage storage, string albumName, int pageNumber = 0, int pageSize = 20)
{
return SearchAsync(storage, albumName, YSearchType.Album, pageNumber, pageSize);
}
public Task<YSearch?> ArtistAsync(string artistName, int page = 0, int pageSize = 20)
=> SearchAsync(artistName, YSearchType.Artist, page, pageSize);
/// <summary>
/// Поиск по артисту
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="artistName">Имя артиста</param>
/// <param name="pageNumber">Номер страницы</param>
/// <param name="pageSize">Размер страницы</param>
/// <returns></returns>
public Task<YResponse<YSearch>> ArtistAsync(AuthStorage storage, string artistName, int pageNumber = 0, int pageSize = 20)
{
return SearchAsync(storage, artistName, YSearchType.Artist, pageNumber, pageSize);
}
public Task<YSearch?> PlaylistAsync(string playlistName, int page = 0, int pageSize = 20)
=> SearchAsync(playlistName, YSearchType.Playlist, page, pageSize);
/// <summary>
/// Поиск по плейлистам
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="playlistName">Имя плейлиста</param>
/// <param name="pageNumber">Номер страницы</param>
/// <param name="pageSize">Размер страницы</param>
/// <returns></returns>
public Task<YResponse<YSearch>> PlaylistAsync(AuthStorage storage, string playlistName, int pageNumber = 0, int pageSize = 20)
{
return SearchAsync(storage, playlistName, YSearchType.Playlist, pageNumber, pageSize);
}
public Task<YSearch?> PodcastEpisodeAsync(string podcastName, int page = 0, int pageSize = 20)
=> SearchAsync(podcastName, YSearchType.PodcastEpisode, page, pageSize);
/// <summary>
/// Поиск по плейлистам
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="podcastName">Имя подкаста</param>
/// <param name="pageNumber">Номер страницы</param>
/// <param name="pageSize">Размер страницы</param>
/// <returns></returns>
public Task<YResponse<YSearch>> PodcastEpisodeAsync(AuthStorage storage, string podcastName, int pageNumber = 0, int pageSize = 20)
{
return SearchAsync(storage, podcastName, YSearchType.PodcastEpisode, pageNumber, pageSize);
}
public Task<YSearch?> VideosAsync(string videoName, int page = 0, int pageSize = 20)
=> SearchAsync(videoName, YSearchType.Video, page, pageSize);
/// <summary>
/// Поиск по видео
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="videoName">Имя видео</param>
/// <param name="pageNumber">Номер страницы</param>
/// <param name="pageSize">Размер страницы</param>
/// <returns></returns>
public Task<YResponse<YSearch>> VideosAsync(AuthStorage storage, string videoName, int pageNumber = 0, int pageSize = 20)
{
return SearchAsync(storage, videoName, YSearchType.Video, pageNumber, pageSize);
}
/// <summary>
/// Поиск по пользователям
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="userName">Имя пользователя</param>
/// <param name="pageNumber">Номер страницы</param>
/// <param name="pageSize">Размер страницы</param>
/// <returns></returns>
public Task<YResponse<YSearch>> UsersAsync(AuthStorage storage, string userName, int pageNumber = 0, int pageSize = 20)
{
return SearchAsync(storage, userName, YSearchType.User, pageNumber, pageSize);
}
/// <summary>
/// Поиск
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="searchText">Поисковый запрос</param>
/// <param name="searchType">Тип поиска</param>
/// <param name="page">Страница</param>
/// <param name="pageSize">Размер страницы</param>
/// <returns></returns>
public Task<YResponse<YSearch>> SearchAsync(AuthStorage storage, string searchText, YSearchType searchType, int page = 0, int pageSize = 20)
{
return new YSearchBuilder(api, storage)
.Build((searchText, searchType, page, pageSize))
.GetResponseAsync();
}
/// <summary>
/// Подсказка
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="searchText">Поисковый запрос</param>
/// <returns></returns>
public Task<YResponse<YSearchSuggest>> SuggestAsync(AuthStorage storage, string searchText)
{
return new YSearchSuggestBuilder(api, storage)
.Build(searchText)
.GetResponseAsync();
}
public Task<YSearch?> UsersAsync(string userName, int page = 0, int pageSize = 20)
=> SearchAsync(userName, YSearchType.User, page, pageSize);
public Task<YSearch?> SearchAsync(string searchText, YSearchType searchType, int page = 0, int pageSize = 20)
=> new YSearchBuilder(Api).ExecuteAsync((searchText, searchType, page, pageSize));
public Task<YSearchSuggest?> GetSearchSuggestionsAsync(string searchText)
=> new YSearchSuggestBuilder(Api).ExecuteAsync(searchText);
}

Some files were not shown because too many files have changed in this diff Show More