9 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
79 changed files with 3808 additions and 674 deletions

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

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

@@ -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

@@ -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

@@ -26,8 +26,8 @@ public class YTrackAPI : YCommonAPI
return $"https://{host}/get-{codec}/{sign}/{ts}{path}"; return $"https://{host}/get-{codec}/{sign}/{ts}{path}";
} }
public Task<YTrack?> GetAsync(string trackId) public async Task<YTrack?> GetAsync(string trackId)
=> GetAsync(trackId); => (await GetAsync([trackId]))?.FirstOrDefault();
public Task<List<YTrack>?> GetAsync(IEnumerable<string> trackIds) public Task<List<YTrack>?> GetAsync(IEnumerable<string> trackIds)
=> new YGetTracksBuilder(Api).ExecuteAsync(trackIds); => new YGetTracksBuilder(Api).ExecuteAsync(trackIds);

View File

@@ -1,159 +0,0 @@
using System.Security.Authentication;
using System.Text.RegularExpressions;
using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Account;
namespace YandexMusic.API;
/// <summary>API для работы с пользователем и авторизации.</summary>
public class YUserAPI : YCommonAPI
{
public YUserAPI(YandexMusicApi api) : base(api) { }
private 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.AuthToken = new YAuthToken
{
CsfrToken = csrfMatch.Groups[1].Value,
ProcessUuid = processMatch.Groups[1].Value
};
await new YPostAuthStats(Api).ExecuteAsync(null!);
return true;
}
private 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;
Api.Storage.AccessToken = accessToken;
Api.Storage.Token = accessToken.AccessToken;
var shortInfo = await new YGetShortAccountInfoBuilder(Api).ExecuteAsync(null!);
if (shortInfo?.Status != YAuthStatus.Ok || string.IsNullOrWhiteSpace(shortInfo.Uid))
throw new Exception("Не удалось подтвердить авторизацию");
return true;
}
public async Task AuthorizeAsync(string token)
{
if (string.IsNullOrEmpty(token))
throw new Exception("Токен не может быть пустым");
Api.Storage.Token = token;
var authInfo = await new YGetAuthInfoBuilder(Api).ExecuteAsync(null!);
if (authInfo?.Account?.Uid == null)
throw new Exception("Пользователь не авторизован");
Api.Storage.SetAuthorized(authInfo.Account, token);
}
public Task<YAccountResult?> GetUserAuthAsync()
=> new YGetAuthInfoBuilder(Api).ExecuteAsync(null!);
public async Task<YAuthTypes?> CreateAuthSessionAsync(string userName)
{
if (!await GetCsrfTokenAsync())
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;
}
public async Task<string?> GetAuthQRLinkAsync()
{
if (!await GetCsrfTokenAsync())
throw new Exception("Не удалось инициализировать сессию");
var qr = await new YGetAuthQRBuilder(Api).ExecuteAsync(null!);
if (qr?.Status != YAuthStatus.Ok || string.IsNullOrEmpty(qr.TrackId))
return null;
Api.Storage.AuthToken = new YAuthToken
{
TrackId = qr.TrackId,
CsfrToken = qr.CsrfToken
};
return $"https://passport.yandex.ru/auth/magic/code/?track_id={qr.TrackId}";
}
public async Task<YAuthQRStatus?> AuthorizeByQRAsync()
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
var status = await new YGetAuthLoginQRBuilder(Api).ExecuteAsync(null!);
if (status?.Status == YAuthStatus.Ok && await LoginByCookiesAsync())
return status;
throw new AuthenticationException("Ошибка авторизации по QR");
}
public Task<YAuthCaptcha?> GetCaptchaAsync()
{
if (Api.Storage.AuthToken == null)
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
return new YGetAuthCaptchaBuilder(Api).ExecuteAsync(null!);
}
public Task<YAuthBase?> AuthorizeByCaptchaAsync(string captchaValue)
{
if (Api.Storage.AuthToken == null)
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
return new YGetAuthLoginCaptchaBuilder(Api).ExecuteAsync(captchaValue);
}
public Task<YAuthLetter?> GetAuthLetterAsync()
=> new YGetAuthLetterBuilder(Api).ExecuteAsync(null!);
public async Task<bool> AuthorizeByLetterAsync()
{
var status = await new YGetAuthLoginLetterBuilder(Api).ExecuteAsync(null!);
if (status?.Status != YAuthStatus.Ok || !status.MagicLinkConfirmed)
throw new Exception("Письмо не подтверждено");
return await LoginByCookiesAsync();
}
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 && await LoginByCookiesAsync())
return result;
throw new AuthenticationException("Ошибка авторизации по паролю");
}
public async Task<YAccessToken?> GetAccessTokenAsync()
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализована");
var token = await new YGetMusicTokenBuilder(Api).ExecuteAsync(null!);
if (token?.AccessToken != null)
Api.Storage.Token = token.AccessToken;
return token;
}
public Task<YLoginInfo?> GetLoginInfoAsync()
=> new YGetLoginInfoBuilder(Api).ExecuteAsync(null!);
}

View File

@@ -1,3 +1,4 @@
using System.Net;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Common; namespace YandexMusic.API.Common;
@@ -7,6 +8,15 @@ namespace YandexMusic.API.Common;
/// </summary> /// </summary>
public class AuthStorage public class AuthStorage
{ {
private CookieContainer _cookieContainer;
public AuthStorage(CookieContainer cookieContainer)
{
_cookieContainer = cookieContainer;
}
public CookieContainer CookieContainer => _cookieContainer;
/// <summary> /// <summary>
/// Флаг, указывающий, авторизован ли пользователь. /// Флаг, указывающий, авторизован ли пользователь.
/// </summary> /// </summary>
@@ -35,7 +45,17 @@ public class AuthStorage
/// <summary> /// <summary>
/// Внутренние данные авторизации (CSRF, track_id и т.д.). /// Внутренние данные авторизации (CSRF, track_id и т.д.).
/// </summary> /// </summary>
internal YAuthToken AuthToken { get; set; } = new(); public YAuthToken? HeaderToken { get; set; } = new();
/// <summary>
/// Внутренние данные авторизации (CSRF, track_id и т.д.).
/// </summary>
public YAuthToken? AuthToken { get; set; } = new();
/// <summary>
/// Страна, используемая для авторизации (по умолчанию "ru"). Может влиять на язык интерфейса и доступные методы авторизации.
/// </summary>
public object Country { get; set; } = "ru";
/// <summary> /// <summary>
/// Устанавливает флаг авторизации и сохраняет информацию об аккаунте. /// Устанавливает флаг авторизации и сохраняет информацию об аккаунте.

View File

@@ -1,6 +1,6 @@
using YandexMusic.API.Models.Album; using YandexMusic.API.Models.Album;
namespace YandexMusic.API.Extensions.API; namespace YandexMusic.API;
/// <summary> /// <summary>
/// Методы-расширения для альбома. /// Методы-расширения для альбома.
@@ -15,7 +15,7 @@ public static class YAlbumExtensions
if (album.Volumes != null) if (album.Volumes != null)
return album; return album;
var result = await album.Context.API.Album.GetAsync(album.Id); var result = await album.Context.Api.Album.GetAsync(album.Id);
return result ?? album; return result ?? album;
} }
@@ -23,11 +23,11 @@ public static class YAlbumExtensions
/// Добавляет альбом в список лайкнутых. /// Добавляет альбом в список лайкнутых.
/// </summary> /// </summary>
public static async Task<string?> AddLikeAsync(this YAlbum album) public static async Task<string?> AddLikeAsync(this YAlbum album)
=> await album.Context.API.Library.AddAlbumLikeAsync(album); => await album.Context.Api.Library.AddAlbumLikeAsync(album);
/// <summary> /// <summary>
/// Удаляет альбом из списка лайкнутых. /// Удаляет альбом из списка лайкнутых.
/// </summary> /// </summary>
public static async Task<string?> RemoveLikeAsync(this YAlbum album) public static async Task<string?> RemoveLikeAsync(this YAlbum album)
=> await album.Context.API.Library.RemoveAlbumLikeAsync(album); => await album.Context.Api.Library.RemoveAlbumLikeAsync(album);
} }

View File

@@ -1,7 +1,7 @@
using YandexMusic.API.Models.Artist; using YandexMusic.API.Models.Artist;
using YandexMusic.API.Models.Track; using YandexMusic.API.Models.Track;
namespace YandexMusic.API.Extensions.API; namespace YandexMusic.API;
/// <summary> /// <summary>
/// Методы-расширения для исполнителя. /// Методы-расширения для исполнителя.
@@ -12,29 +12,29 @@ public static class YArtistExtensions
/// Получает расширенную информацию об исполнителе. /// Получает расширенную информацию об исполнителе.
/// </summary> /// </summary>
public static async Task<YArtistBriefInfo?> BriefInfoAsync(this YArtist artist) public static async Task<YArtistBriefInfo?> BriefInfoAsync(this YArtist artist)
=> await artist.Context.API.Artist.GetAsync(artist.Id); => await artist.Context.Api.Artist.GetAsync(artist.Id);
/// <summary> /// <summary>
/// Получает страницу треков исполнителя. /// Получает страницу треков исполнителя.
/// </summary> /// </summary>
public static async Task<YTracksPage?> GetTracksAsync(this YArtist artist, int page = 0, int pageSize = 20) public static async Task<YTracksPage?> GetTracksAsync(this YArtist artist, int page = 0, int pageSize = 20)
=> await artist.Context.API.Artist.GetTracksAsync(artist.Id, page, pageSize); => await artist.Context.Api.Artist.GetTracksAsync(artist.Id, page, pageSize);
/// <summary> /// <summary>
/// Получает все треки исполнителя. /// Получает все треки исполнителя.
/// </summary> /// </summary>
public static async Task<List<YTrack>?> GetAllTracksAsync(this YArtist artist) public static async Task<List<YTrack>?> GetAllTracksAsync(this YArtist artist)
=> (await artist.Context.API.Artist.GetAllTracksAsync(artist.Id))?.Tracks; => (await artist.Context.Api.Artist.GetAllTracksAsync(artist.Id))?.Tracks;
/// <summary> /// <summary>
/// Добавляет исполнителя в список лайкнутых. /// Добавляет исполнителя в список лайкнутых.
/// </summary> /// </summary>
public static async Task<string?> AddLikeAsync(this YArtist artist) public static async Task<string?> AddLikeAsync(this YArtist artist)
=> await artist.Context.API.Library.AddArtistLikeAsync(artist); => await artist.Context.Api.Library.AddArtistLikeAsync(artist);
/// <summary> /// <summary>
/// Удаляет исполнителя из списка лайкнутых. /// Удаляет исполнителя из списка лайкнутых.
/// </summary> /// </summary>
public static async Task<string?> RemoveLikeAsync(this YArtist artist) public static async Task<string?> RemoveLikeAsync(this YArtist artist)
=> await artist.Context.API.Library.RemoveArtistLikeAsync(artist); => await artist.Context.Api.Library.RemoveArtistLikeAsync(artist);
} }

View File

@@ -1,7 +1,7 @@
using YandexMusic.API.Models.Playlist; using YandexMusic.API.Models.Playlist;
using YandexMusic.API.Models.Track; using YandexMusic.API.Models.Track;
namespace YandexMusic.API.Extensions.API; namespace YandexMusic.API;
/// <summary> /// <summary>
/// Методы-расширения для плейлиста. /// Методы-расширения для плейлиста.
@@ -18,44 +18,44 @@ public static class YPlaylistExtensions
{ {
if (playlist.Tracks != null) if (playlist.Tracks != null)
return playlist; return playlist;
return await playlist.Context.API.Playlist.GetAsync(playlist); return await playlist.Context.Api.Playlist.GetAsync(playlist);
} }
/// <summary> /// <summary>
/// Добавляет плейлист в список лайкнутых. /// Добавляет плейлист в список лайкнутых.
/// </summary> /// </summary>
public static async Task<string?> AddLikeAsync(this YPlaylist playlist) public static async Task<string?> AddLikeAsync(this YPlaylist playlist)
=> await playlist.Context.API.Library.AddPlaylistLikeAsync(playlist); => await playlist.Context.Api.Library.AddPlaylistLikeAsync(playlist);
/// <summary> /// <summary>
/// Удаляет плейлист из списка лайкнутых. /// Удаляет плейлист из списка лайкнутых.
/// </summary> /// </summary>
public static async Task<string?> RemoveLikeAsync(this YPlaylist playlist) public static async Task<string?> RemoveLikeAsync(this YPlaylist playlist)
=> await playlist.Context.API.Library.RemovePlaylistLikeAsync(playlist); => await playlist.Context.Api.Library.RemovePlaylistLikeAsync(playlist);
/// <summary> /// <summary>
/// Переименовывает плейлист (только для владельца). /// Переименовывает плейлист (только для владельца).
/// </summary> /// </summary>
public static async Task<YPlaylist?> RenameAsync(this YPlaylist playlist, string newName) public static async Task<YPlaylist?> RenameAsync(this YPlaylist playlist, string newName)
=> IsOwner(playlist) ? await playlist.Context.API.Playlist.RenameAsync(playlist, newName) : playlist; => IsOwner(playlist) ? await playlist.Context.Api.Playlist.RenameAsync(playlist, newName) : playlist;
/// <summary> /// <summary>
/// Удаляет плейлист (только для владельца). /// Удаляет плейлист (только для владельца).
/// </summary> /// </summary>
public static async Task<bool> DeleteAsync(this YPlaylist playlist) public static async Task<bool> DeleteAsync(this YPlaylist playlist)
=> IsOwner(playlist) && await playlist.Context.API.Playlist.DeleteAsync(playlist); => IsOwner(playlist) && await playlist.Context.Api.Playlist.DeleteAsync(playlist);
/// <summary> /// <summary>
/// Вставляет треки в начало плейлиста (только для владельца). /// Вставляет треки в начало плейлиста (только для владельца).
/// </summary> /// </summary>
public static async Task<YPlaylist?> InsertTracksAsync(this YPlaylist playlist, params YTrack[] tracks) public static async Task<YPlaylist?> InsertTracksAsync(this YPlaylist playlist, params YTrack[] tracks)
=> IsOwner(playlist) ? await playlist.Context.API.Playlist.InsertTracksAsync(playlist, tracks) : playlist; => IsOwner(playlist) ? await playlist.Context.Api.Playlist.InsertTracksAsync(playlist, tracks) : playlist;
/// <summary> /// <summary>
/// Удаляет треки из плейлиста (только для владельца). /// Удаляет треки из плейлиста (только для владельца).
/// </summary> /// </summary>
public static async Task<YPlaylist?> RemoveTracksAsync(this YPlaylist playlist, params YTrack[] tracks) public static async Task<YPlaylist?> RemoveTracksAsync(this YPlaylist playlist, params YTrack[] tracks)
=> IsOwner(playlist) ? await playlist.Context.API.Playlist.DeleteTracksAsync(playlist, tracks) : playlist; => IsOwner(playlist) ? await playlist.Context.Api.Playlist.DeleteTracksAsync(playlist, tracks) : playlist;
/// <summary> /// <summary>
/// Загружает трек в плейлист (только для владельца). /// Загружает трек в плейлист (только для владельца).
@@ -63,7 +63,7 @@ public static class YPlaylistExtensions
public static async Task<bool> UploadTrackAsync(this YPlaylist playlist, string filePath, string fileName) public static async Task<bool> UploadTrackAsync(this YPlaylist playlist, string filePath, string fileName)
{ {
if (!IsOwner(playlist)) return false; if (!IsOwner(playlist)) return false;
var result = await playlist.Context.API.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, filePath); var result = await playlist.Context.Api.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, filePath);
return result == "CREATED"; return result == "CREATED";
} }
} }

View File

@@ -1,7 +1,7 @@
using YandexMusic.API.Models.Radio; using YandexMusic.API.Models.Radio;
using YandexMusic.API.Models.Track; using YandexMusic.API.Models.Track;
namespace YandexMusic.API.Extensions.API; namespace YandexMusic.API;
/// <summary> /// <summary>
/// Методы-расширения для радиостанции. /// Методы-расширения для радиостанции.
@@ -12,17 +12,17 @@ public static class YStationResultExtensions
/// Получает список треков для радиостанции. /// Получает список треков для радиостанции.
/// </summary> /// </summary>
public static async Task<List<YSequenceItem>?> GetTracksAsync(this YStation station, string prevTrackId = "") public static async Task<List<YSequenceItem>?> GetTracksAsync(this YStation station, string prevTrackId = "")
=> (await station.Context.API.Radio.GetStationTracksAsync(station, prevTrackId))?.Sequence; => (await station.Context.Api.Radio.GetStationTracksAsync(station, prevTrackId))?.Sequence;
/// <summary> /// <summary>
/// Устанавливает настройки станции. /// Устанавливает настройки станции.
/// </summary> /// </summary>
public static async Task<string?> SetSettings2Async(this YStation station, YStationSettings2 settings) public static async Task<string?> SetSettings2Async(this YStation station, YStationSettings2 settings)
=> await station.Context.API.Radio.SetStationSettings2Async(station, settings); => await station.Context.Api.Radio.SetStationSettings2Async(station, settings);
/// <summary> /// <summary>
/// Отправляет обратную связь о прослушивании. /// Отправляет обратную связь о прослушивании.
/// </summary> /// </summary>
public static Task<string?> SendFeedbackAsync(this YStation station, YStationFeedbackType type, YTrack? track = null, string batchId = "", double totalPlayedSeconds = 0) public static Task<string?> SendFeedbackAsync(this YStation station, YStationFeedbackType type, YTrack? track = null, string batchId = "", double totalPlayedSeconds = 0)
=> station.Context.API.Radio.SendStationFeedbackAsync(station, type, track, batchId, totalPlayedSeconds); => station.Context.Api.Radio.SendStationFeedbackAsync(station, type, track, batchId, totalPlayedSeconds);
} }

View File

@@ -1,6 +1,6 @@
using YandexMusic.API.Models.Track; using YandexMusic.API.Models.Track;
namespace YandexMusic.API.Extensions.API; namespace YandexMusic.API;
/// <summary> /// <summary>
/// Методы-расширения для трека. /// Методы-расширения для трека.
@@ -11,53 +11,53 @@ public static class YTrackExtensions
/// Получает прямую ссылку на скачивание трека. /// Получает прямую ссылку на скачивание трека.
/// </summary> /// </summary>
public static Task<string?> GetLinkAsync(this YTrack track) public static Task<string?> GetLinkAsync(this YTrack track)
=> track.Context.API.Track.GetFileLinkAsync(track); => track.Context.Api.Track.GetFileLinkAsync(track);
/// <summary> /// <summary>
/// Сохраняет трек в файл. /// Сохраняет трек в файл.
/// </summary> /// </summary>
public static Task SaveAsync(this YTrack track, string filePath) public static Task SaveAsync(this YTrack track, string filePath)
=> track.Context.API.Track.ExtractToFileAsync(track, filePath); => track.Context.Api.Track.ExtractToFileAsync(track, filePath);
/// <summary> /// <summary>
/// Добавляет трек в список лайкнутых. /// Добавляет трек в список лайкнутых.
/// </summary> /// </summary>
public static async Task<int?> AddLikeAsync(this YTrack track) public static async Task<int?> AddLikeAsync(this YTrack track)
=> await track.Context.API.Library.AddTrackLikeAsync(track); => await track.Context.Api.Library.AddTrackLikeAsync(track);
/// <summary> /// <summary>
/// Удаляет трек из списка лайкнутых. /// Удаляет трек из списка лайкнутых.
/// </summary> /// </summary>
public static async Task<int?> RemoveLikeAsync(this YTrack track) public static async Task<int?> RemoveLikeAsync(this YTrack track)
=> await track.Context.API.Library.RemoveTrackLikeAsync(track); => await track.Context.Api.Library.RemoveTrackLikeAsync(track);
/// <summary> /// <summary>
/// Добавляет трек в список дизлайкнутых. /// Добавляет трек в список дизлайкнутых.
/// </summary> /// </summary>
public static async Task<int?> AddDislikeAsync(this YTrack track) public static async Task<int?> AddDislikeAsync(this YTrack track)
=> await track.Context.API.Library.AddTrackDislikeAsync(track); => await track.Context.Api.Library.AddTrackDislikeAsync(track);
/// <summary> /// <summary>
/// Удаляет трек из списка дизлайкнутых. /// Удаляет трек из списка дизлайкнутых.
/// </summary> /// </summary>
public static async Task<int?> RemoveDislikeAsync(this YTrack track) public static async Task<int?> RemoveDislikeAsync(this YTrack track)
=> await track.Context.API.Library.RemoveTrackDislikeAsync(track); => await track.Context.Api.Library.RemoveTrackDislikeAsync(track);
/// <summary> /// <summary>
/// Отправляет информацию о воспроизведении трека. /// Отправляет информацию о воспроизведении трека.
/// </summary> /// </summary>
public static Task<string?> SendPlayTrackInfoAsync(this YTrack track, string from, bool fromCache = false, string playId = "", string playlistId = "", double totalPlayedSeconds = 0, double endPositionSeconds = 0) public static Task<string?> SendPlayTrackInfoAsync(this YTrack track, string from, bool fromCache = false, string playId = "", string playlistId = "", double totalPlayedSeconds = 0, double endPositionSeconds = 0)
=> track.Context.API.Track.SendPlayTrackInfoAsync(track, from, fromCache, playId, playlistId, totalPlayedSeconds, endPositionSeconds); => track.Context.Api.Track.SendPlayTrackInfoAsync(track, from, fromCache, playId, playlistId, totalPlayedSeconds, endPositionSeconds);
/// <summary> /// <summary>
/// Получает дополнительную информацию о треке. /// Получает дополнительную информацию о треке.
/// </summary> /// </summary>
public static async Task<YTrackSupplement?> SupplementAsync(this YTrack track) public static async Task<YTrackSupplement?> SupplementAsync(this YTrack track)
=> await track.Context.API.Track.GetSupplementAsync(track); => await track.Context.Api.Track.GetSupplementAsync(track);
/// <summary> /// <summary>
/// Получает похожие треки. /// Получает похожие треки.
/// </summary> /// </summary>
public static async Task<YTrackSimilar?> SimilarAsync(this YTrack track) public static async Task<YTrackSimilar?> SimilarAsync(this YTrack track)
=> await track.Context.API.Track.GetSimilarAsync(track); => await track.Context.Api.Track.GetSimilarAsync(track);
} }

View File

@@ -0,0 +1,49 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.Serialization;
namespace YandexMusic.API.Extensions;
public static class EnumHelper
{
private static readonly ConcurrentDictionary<Type, Dictionary<string, object>> _enumMaps = new();
/// <summary>
/// Пытается преобразовать строковое значение в enum с учётом атрибутов [EnumMember].
/// </summary>
/// <typeparam name="T">Тип enum</typeparam>
/// <param name="memberValue">Строковое значение из JSON или другого источника</param>
/// <param name="ignoreCase">Учитывать регистр (по умолчанию true)</param>
/// <param name="result">Результат преобразования, если успешно, иначе default</param>
/// <returns>true если преобразование удалось, иначе false</returns>
public static bool TryEnumFromMemberValue<T>(string memberValue, bool ignoreCase, out T result) where T : struct, Enum
{
result = default;
if (string.IsNullOrEmpty(memberValue))
return false;
var type = typeof(T);
// Получаем или создаём кэш для данного enum
var map = _enumMaps.GetOrAdd(type, t =>
{
var dict = new Dictionary<string, object>(ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
foreach (var field in t.GetFields(BindingFlags.Static | BindingFlags.Public))
{
var attr = field.GetCustomAttribute<EnumMemberAttribute>();
var key = attr?.Value ?? field.Name;
dict[key] = field.GetValue(null)!;
}
return dict;
});
// Ищем по кэшу
if (map.TryGetValue(memberValue, out var value))
{
result = (T)value;
return true;
}
// fallback на обычный Enum.TryParse (без учёта EnumMember)
return Enum.TryParse(memberValue, ignoreCase, out result);
}
}

View File

@@ -0,0 +1,9 @@
using System.Runtime.Serialization;
namespace YandexMusic.API.Models.Account;
public enum YAuthQrState
{
[EnumMember(Value = "otp_auth_finished")]
OtpAuthFinished,
}

View File

@@ -1,16 +1,13 @@
using System.Text.Json.Serialization; namespace YandexMusic.API.Models.Account;
namespace YandexMusic.API.Models.Account;
public class YAuthToken public class YAuthToken
{ {
[JsonPropertyName("csfr_token")]
public string CsfrToken { get; set; } public string CsfrToken { get; set; }
[JsonPropertyName("track_id")]
public string TrackId { get; set; } public string TrackId { get; set; }
[JsonPropertyName("process_uuid")] public string SessionTrackId { get; set; }
public string ProcessUuid { get; set; } public string ProcessUuid { get; set; }
public Dictionary<string, string> Cookie { get; set; } = new(); public Dictionary<string, string> Cookie { get; set; } = new();

View File

@@ -9,7 +9,7 @@ namespace YandexMusic.API.Models.Common;
public class YExecutionContext public class YExecutionContext
{ {
/// <summary>Экземпляр основного API.</summary> /// <summary>Экземпляр основного API.</summary>
public YandexMusicApi API { get; internal set; } = null!; public YandexMusicApi Api { get; internal set; } = null!;
/// <summary>Хранилище данных авторизации.</summary> /// <summary>Хранилище данных авторизации.</summary>
public AuthStorage Storage { get; internal set; } = null!; public AuthStorage Storage { get; internal set; } = null!;

View File

@@ -38,7 +38,7 @@ public class YExecutionContextConverter : JsonConverter<object>
var obj = JsonSerializer.Deserialize(ref reader, typeToConvert, innerOptions); var obj = JsonSerializer.Deserialize(ref reader, typeToConvert, innerOptions);
if (obj is YBaseModel baseModel) if (obj is YBaseModel baseModel)
{ {
baseModel.Context = new YExecutionContext { API = _api, Storage = _storage }; baseModel.Context = new YExecutionContext { Api = _api, Storage = _storage };
} }
return obj; return obj;
} }

View File

@@ -1,9 +1,16 @@
using System.Xml.Serialization;
namespace YandexMusic.API.Models.Common; namespace YandexMusic.API.Models.Common;
[XmlRoot("download-info")]
public class YStorageDownloadFile public class YStorageDownloadFile
{ {
[XmlElement("host")]
public string Host { get; set; } public string Host { get; set; }
[XmlElement("path")]
public string Path { get; set; } public string Path { get; set; }
[XmlElement("s")]
public string S { get; set; } public string S { get; set; }
[XmlElement("ts")]
public string Ts { get; set; } public string Ts { get; set; }
} }

View File

@@ -1,5 +1,6 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using YandexMusic.API.Extensions;
using YandexMusic.API.Models.Landing.Entity.Entities; using YandexMusic.API.Models.Landing.Entity.Entities;
namespace YandexMusic.API.Models.Landing.Entity; namespace YandexMusic.API.Models.Landing.Entity;
@@ -26,7 +27,8 @@ public class YLandingEntityConverter : JsonConverter<List<YLandingEntity>>
var typeProp = root.GetProperty("type"); var typeProp = root.GetProperty("type");
var typeStr = typeProp.GetString(); var typeStr = typeProp.GetString();
if (!Enum.TryParse<YLandingEntityType>(typeStr, true, out var entityType))
if (!EnumHelper.TryEnumFromMemberValue<YLandingEntityType>(typeStr, true, out var entityType))
throw new JsonException($"Неизвестный тип сущности: {typeStr}"); throw new JsonException($"Неизвестный тип сущности: {typeStr}");
YLandingEntity? entity = null; YLandingEntity? entity = null;

View File

@@ -1,8 +1,9 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Models.Account; namespace YandexMusic.API.Models.Passport;
public class YAuthQR : YAuthBase public class YPassportTrack : YAuthBase
{ {
[JsonPropertyName("track_id")] [JsonPropertyName("track_id")]
public string TrackId { get; set; } public string TrackId { get; set; }

View File

@@ -1,12 +1,14 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Models.Account; namespace YandexMusic.API.Models.Passport;
public class YAuthQRStatus : YAuthBase public class YAuthQrSession
{ {
[JsonPropertyName("default_uid")] [JsonPropertyName("default_uid")]
public int DefaultUid { get; set; } public int DefaultUid { get; set; }
[JsonPropertyName("retpath")]
public string RetPath { get; set; } public string RetPath { get; set; }
[JsonPropertyName("track_id")] [JsonPropertyName("track_id")]

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YAuthQRStatus
{
[JsonPropertyName("state")]
public string? State { get; set; } = null;
[JsonPropertyName("trackId")]
public string TrackId { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YCheckAvailabilityResult
{
[JsonPropertyName("antifraudScore")]
public string AntifraudScore { get; set; } = string.Empty;
[JsonPropertyName("hasAvailableAccounts")]
public bool HasAvailableAccounts { get; set; }
[JsonPropertyName("flnFlowRequired")]
public bool FlnFlowRequired { get; set; }
[JsonPropertyName("can_use_push")]
public bool CanUsePush { get; set; }
[JsonPropertyName("can_use_webauthn")]
public bool CanUseWebauthn { get; set; }
[JsonPropertyName("has_master")]
public bool HasMaster { get; set; }
[JsonPropertyName("is_session_mastered")]
public bool IsSessionMastered { get; set; }
[JsonPropertyName("does_master_have_free_slots")]
public bool DoesMasterHaveFreeSlots { get; set; }
[JsonPropertyName("allowed_registration_flows")]
public List<object> AllowedRegistrationFlows { get; set; } = new();
[JsonPropertyName("SuggestBy")]
public string SuggestBy { get; set; } = string.Empty;
[JsonPropertyName("master_info")]
public YMasterInfo? MasterInfo { get; set; }
}

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YMasterInfo
{
[JsonPropertyName("firstname")]
public string FirstName { get; set; } = string.Empty;
[JsonPropertyName("lastname")]
public string LastName { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,42 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YMultistepStart
{
[JsonPropertyName("track_id")]
public string TrackId { get; set; } = string.Empty;
[JsonPropertyName("can_authorize")]
public bool CanAuthorize { get; set; }
[JsonPropertyName("can_register")]
public bool CanRegister { get; set; }
[JsonPropertyName("is_rfc_2fa_enabled")]
public bool IsRfc2faEnabled { get; set; }
[JsonPropertyName("allowed_account_types")]
public List<string> AllowedAccountTypes { get; set; } = new();
[JsonPropertyName("location_id")]
public string LocationId { get; set; } = string.Empty;
[JsonPropertyName("primary_alias_type")]
public int PrimaryAliasType { get; set; }
[JsonPropertyName("auth_methods")]
public List<string> AuthMethods { get; set; } = new();
[JsonPropertyName("preferred_auth_method")]
public string PreferredAuthMethod { get; set; } = string.Empty;
[JsonPropertyName("csrf_token")]
public string CsrfToken { get; set; } = string.Empty;
[JsonPropertyName("error")]
public string? Error { get; set; }
[JsonPropertyName("errors")]
public List<string>? Errors { get; set; }
}

View File

@@ -0,0 +1,48 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YPassportAccount
{
[JsonPropertyName("avatar_url")]
public string AvatarUrl { get; set; } = string.Empty;
[JsonPropertyName("display_login")]
public string DisplayLogin { get; set; } = string.Empty;
[JsonPropertyName("display_name")]
public YPassportName? DisplayName { get; set; }
[JsonPropertyName("has_master")]
public bool HasMaster { get; set; }
[JsonPropertyName("has_plus")]
public bool HasPlus { get; set; }
[JsonPropertyName("has_secure_phone")]
public bool HasSecurePhone { get; set; }
[JsonPropertyName("is_2fa_enabled")]
public bool Is2faEnabled { get; set; }
[JsonPropertyName("is_rfc_2fa_enabled")]
public bool IsRfc2faEnabled { get; set; }
[JsonPropertyName("is_sms_2fa_enabled")]
public bool IsSms2faEnabled { get; set; }
[JsonPropertyName("is_workspace_user")]
public bool IsWorkspaceUser { get; set; }
[JsonPropertyName("is_yandexoid")]
public bool IsYandexoid { get; set; }
public bool Login { get; set; }
public YPassportPerson? Person { get; set; }
[JsonPropertyName("secure_phone_id")]
public int SecurePhoneId { get; set; }
public int Uid { get; set; }
}

View File

@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YPassportName
{
[JsonPropertyName("default_avatar")]
public string DefaultAvatar { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,11 @@
namespace YandexMusic.API.Models.Passport;
public class YPassportPerson
{
public string Birthday { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public int Gender { get; set; }
public string Language { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YPassportSession
{
[JsonPropertyName("track_id")]
public string TrackId { get; set; } = string.Empty;
[JsonPropertyName("default_uid")]
public string DefaultUid { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YPassportSessionStatus
{
[JsonPropertyName("session_is_correct")]
public bool SessionIsCorrect { get; set; }
[JsonPropertyName("session_has_users")]
public bool SessionHasUsers { get; set; }
}

View File

@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YPassportUser
{
[JsonPropertyName("track_id")]
public string TrackId { get; set; } = string.Empty;
[JsonPropertyName("state")]
public string State { get; set; } = string.Empty;
[JsonPropertyName("account")]
public YPassportAccount? Account { get; set; }
[JsonPropertyName("error")]
public string? Error { get; set; }
[JsonPropertyName("errors")]
public List<string>? Errors { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace YandexMusic.API.Models.Passport;
public class YPushApp
{
public string App { get; set; } = string.Empty;
public string Platform { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YSendPushResult
{
[JsonPropertyName("pushes_devices_list")]
public List<object> PushesDevicesList { get; set; } = new();
[JsonPropertyName("deny_resend_until")]
public int DenyResendUntil { get; set; }
[JsonPropertyName("is_push_silent")]
public bool IsPushSilent { get; set; }
[JsonPropertyName("apps_for_bright_push")]
public List<YPushApp> AppsForBrightPush { get; set; } = new();
}

View File

@@ -0,0 +1,54 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YSuggestAccount
{
[JsonPropertyName("allowed_auth_flows")]
public List<string> AllowedAuthFlows { get; set; } = new();
[JsonPropertyName("avatar_url")]
public string AvatarUrl { get; set; } = string.Empty;
[JsonPropertyName("default_avatar")]
public string DefaultAvatar { get; set; } = string.Empty;
[JsonPropertyName("display_name")]
public YPassportName? DisplayName { get; set; }
[JsonPropertyName("has_bank_card")]
public bool HasBankCard { get; set; }
[JsonPropertyName("has_family")]
public bool HasFamily { get; set; }
[JsonPropertyName("has_master")]
public bool HasMaster { get; set; }
[JsonPropertyName("has_plus")]
public bool HasPlus { get; set; }
[JsonPropertyName("is_communal")]
public bool IsCommunal { get; set; }
[JsonPropertyName("location_id")]
public string LocationId { get; set; } = string.Empty;
[JsonPropertyName("login")]
public string Login { get; set; } = string.Empty;
[JsonPropertyName("primary_alias_type")]
public int PrimaryAliasType { get; set; }
[JsonPropertyName("priority")]
public int Priority { get; set; }
[JsonPropertyName("uid")]
public long Uid { get; set; }
[JsonPropertyName("shields")]
public List<string> Shields { get; set; } = new();
[JsonPropertyName("require_additional_sms_to_login")]
public bool RequireAdditionalSmsToLogin { get; set; }
}

View File

@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YSuggestByPhoneResult
{
[JsonPropertyName("accounts")]
public List<YSuggestAccount> Accounts { get; set; } = new();
[JsonPropertyName("allowed_registration_flows")]
public List<string> AllowedRegistrationFlows { get; set; } = new();
[JsonPropertyName("uid_from_bb")]
public string UidFromBb { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Models.Passport;
public class YValidatePhoneNumberResult
{
[JsonPropertyName("phone_number")]
public YPhoneNumber? PhoneNumber { get; set; }
[JsonPropertyName("valid_for_flash_call")]
public bool ValidForFlashCall { get; set; }
[JsonPropertyName("location_id")]
public string LocationId { get; set; } = string.Empty;
[JsonPropertyName("valid_for_viber")]
public bool ValidForViber { get; set; }
[JsonPropertyName("valid_for_whatsapp")]
public bool ValidForWhatsapp { get; set; }
[JsonPropertyName("valid_for_telegram")]
public bool ValidForTelegram { get; set; }
[JsonPropertyName("valid_for_sms")]
public bool ValidForSms { get; set; }
[JsonPropertyName("track_id")]
public string TrackId { get; set; } = string.Empty;
[JsonPropertyName("error")]
public string? Error { get; set; }
[JsonPropertyName("errors")]
public List<string>? Errors { get; set; }
}

View File

@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YValidateSquatter
{
[JsonPropertyName("require_flow_with_fio")]
public bool RequireFlowWithFio { get; set; }
[JsonPropertyName("require_flow_with_auth_hint")]
public bool RequireFlowWithAuthHint { get; set; }
[JsonPropertyName("auth_hint_question_id")]
public string AuthHintQuestionId { get; set; } = string.Empty;
[JsonPropertyName("suggestBy")]
public string SuggestBy { get; set; } = string.Empty;
}

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthAppPasswordBuilder : YAuthRequestBuilder<YAuthBase?, string> internal class YGetAuthAppPasswordBuilder : YPassportRequestBuilder<YAuthBase?, string>
{ {
public YGetAuthAppPasswordBuilder(YandexMusicApi api) : base(api) { } public YGetAuthAppPasswordBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthCaptchaBuilder : YAuthRequestBuilder<YAuthCaptcha?, object> internal class YGetAuthCaptchaBuilder : YPassportRequestBuilder<YAuthCaptcha?, object>
{ {
public YGetAuthCaptchaBuilder(YandexMusicApi api) : base(api) { } public YGetAuthCaptchaBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;

View File

@@ -1,14 +1,55 @@
using System.Net; using System.Net;
using System.Net.Http.Headers;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common; using YandexMusic.API.Requests.Common;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthCookiesBuilder : YAuthRequestBuilder<YAccessToken?, object> internal class YGetAuthCookiesBuilder : YPassportRequestBuilder<YAccessToken?, object>
{ {
public YGetAuthCookiesBuilder(YandexMusicApi api) : base(api) { } public YGetAuthCookiesBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;
protected override string BaseUrl => YConstants.Endpoints.MobilePassportUrl;
protected override string PathTemplate => "1/bundle/oauth/token_by_sessionid"; protected override string PathTemplate => "1/bundle/oauth/token_by_sessionid";
protected override HttpContent? GetContent(object _) protected override HttpContent? GetContent(object _)
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "client_id", YConstants.XClientId }, { "client_secret", YConstants.XClientSecret } }); => new FormUrlEncodedContent(new Dictionary<string, string> { { "client_id", YConstants.XClientId }, { "client_secret", YConstants.XClientSecret } });
protected override void SetCustomHeaders(HttpRequestHeaders headers)
{
base.SetCustomHeaders(headers);
headers.Add("ya-client-host", "passport.yandex.ru");
var cookieString = GetCookieString();
if (!string.IsNullOrEmpty(cookieString))
headers.Add("Ya-Client-Cookie", cookieString);
}
private string GetCookieString()
{
var container = Storage.CookieContainer;
if (container == null) return string.Empty;
var uris = new[]
{
new Uri("https://yandex.ru"),
new Uri("https://passport.yandex.ru"),
new Uri("https://mobileproxy.passport.yandex.net")
};
var cookies = new List<string>();
foreach (var uri in uris)
{
var cookieCollection = container.GetCookies(uri);
foreach (Cookie cookie in cookieCollection)
{
cookies.Add($"{cookie.Name}={cookie.Value}");
}
}
var distinct = cookies
.Select(c => c.Split('=')[0])
.Distinct()
.Select(name => cookies.First(c => c.StartsWith(name + "=")))
.ToList();
return string.Join("; ", distinct);
}
} }

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthLetterBuilder : YAuthRequestBuilder<YAuthLetter?, object> internal class YGetAuthLetterBuilder : YPassportRequestBuilder<YAuthLetter?, object>
{ {
public YGetAuthLetterBuilder(YandexMusicApi api) : base(api) { } public YGetAuthLetterBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthLoginCaptchaBuilder : YAuthRequestBuilder<YAuthBase?, string> internal class YGetAuthLoginCaptchaBuilder : YPassportRequestBuilder<YAuthBase?, string>
{ {
public YGetAuthLoginCaptchaBuilder(YandexMusicApi api) : base(api) { } public YGetAuthLoginCaptchaBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthLoginLetterBuilder : YAuthRequestBuilder<YAuthLetterStatus?, object> internal class YGetAuthLoginLetterBuilder : YPassportRequestBuilder<YAuthLetterStatus?, object>
{ {
public YGetAuthLoginLetterBuilder(YandexMusicApi api) : base(api) { } public YGetAuthLoginLetterBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;

View File

@@ -1,23 +0,0 @@
using System.Net;
using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account;
internal class YGetAuthLoginQRBuilder : YAuthRequestBuilder<YAuthQRStatus, string>
{
public YGetAuthLoginQRBuilder(YandexMusicApi yandex) : base(yandex)
{
}
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "auth/new/magic/status/";
protected override HttpContent GetContent(string tuple)
{
return new FormUrlEncodedContent(new Dictionary<string, string> {
{ "csrf_token", Api.Storage.AuthToken.CsfrToken },
{ "track_id", Api.Storage.AuthToken.TrackId }
});
}
}

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthLoginUserBuilder : YAuthRequestBuilder<YAuthTypes?, (string token, string login)> internal class YGetAuthLoginUserBuilder : YPassportRequestBuilder<YAuthTypes?, (string token, string login)>
{ {
public YGetAuthLoginUserBuilder(YandexMusicApi api) : base(api) { } public YGetAuthLoginUserBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;

View File

@@ -1,19 +0,0 @@
using System.Net;
using System.Net.Http.Headers;
using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account;
internal class YGetAuthQRBuilder : YAuthRequestBuilder<YAuthQR?, object>
{
public YGetAuthQRBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/auth/password/submit";
protected override HttpContent? GetContent(object _)
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "retpath", "" } });
protected override void SetCustomHeaders(HttpRequestHeaders headers)
{
headers.Add("X-Csrf-Token", Api.Storage.AuthToken.CsfrToken);
headers.Add("Process-Uuid", Api.Storage.AuthToken.ProcessUuid);
}
}

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetLoginInfoBuilder : YAuthRequestBuilder<YLoginInfo?, object> internal class YGetLoginInfoBuilder : YPassportRequestBuilder<YLoginInfo?, object>
{ {
public YGetLoginInfoBuilder(YandexMusicApi api) : base(api) { } public YGetLoginInfoBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Get; protected override string Method => WebRequestMethods.Http.Get;

View File

@@ -5,7 +5,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetShortAccountInfoBuilder : YAuthRequestBuilder<YShortAccountInfo?, object> internal class YGetShortAccountInfoBuilder : YPassportRequestBuilder<YShortAccountInfo?, object>
{ {
public YGetShortAccountInfoBuilder(YandexMusicApi api) : base(api) { } public YGetShortAccountInfoBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Get; protected override string Method => WebRequestMethods.Http.Get;

View File

@@ -4,7 +4,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YPostAuthStats : YAuthRequestBuilder<YAuthEmpty?, object> internal class YPostAuthStats : YPassportRequestBuilder<YAuthEmpty?, object>
{ {
public YPostAuthStats(YandexMusicApi api) : base(api) { } public YPostAuthStats(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;
@@ -13,7 +13,7 @@ internal class YPostAuthStats : YAuthRequestBuilder<YAuthEmpty?, object>
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "messageType", "CLIENT_READY" } }); => new FormUrlEncodedContent(new Dictionary<string, string> { { "messageType", "CLIENT_READY" } });
protected override void SetCustomHeaders(HttpRequestHeaders headers) protected override void SetCustomHeaders(HttpRequestHeaders headers)
{ {
headers.Add("X-Csrf-Token", Api.Storage.AuthToken.CsfrToken); headers.Add("X-Csrf-Token", Api.Storage.HeaderToken.CsfrToken);
headers.Add("Process-Uuid", Api.Storage.AuthToken.ProcessUuid); headers.Add("Process-Uuid", Api.Storage.HeaderToken.ProcessUuid);
} }
} }

View File

@@ -12,5 +12,8 @@ internal class YGetArtistTrackBuilder : YMusicRequestBuilder<YTracksPage?, (stri
protected override Dictionary<string, string> GetSubstitutions((string id, int page, int pageSize) tuple) protected override Dictionary<string, string> GetSubstitutions((string id, int page, int pageSize) tuple)
=> new() { { "artistId", tuple.id } }; => new() { { "artistId", tuple.id } };
protected override NameValueCollection GetQueryParams((string id, int page, int pageSize) tuple) protected override NameValueCollection GetQueryParams((string id, int page, int pageSize) tuple)
=> new() { { "page", tuple.page.ToString() }, { "pageSize", tuple.pageSize.ToString() } }; => new() {
{ "page", tuple.page.ToString() },
{ "pageSize", tuple.pageSize.ToString() },
};
} }

View File

@@ -12,5 +12,6 @@ internal class YConstants
{ {
public const string MusicUrl = "https://api.music.yandex.net"; public const string MusicUrl = "https://api.music.yandex.net";
public const string PassportUrl = "https://passport.yandex.ru/"; public const string PassportUrl = "https://passport.yandex.ru/";
public const string MobilePassportUrl = "https://mobileproxy.passport.yandex.net";
} }
} }

View File

@@ -10,7 +10,17 @@ namespace YandexMusic.API.Requests.Common;
/// </summary> /// </summary>
internal abstract class YJsonRequestBuilder<TResponse, TParams> : YRequestBuilder<TParams> internal abstract class YJsonRequestBuilder<TResponse, TParams> : YRequestBuilder<TParams>
{ {
protected YJsonRequestBuilder(YandexMusicApi api) : base(api) { } private readonly JsonSerializerOptions _jsonOptions;
protected YJsonRequestBuilder(YandexMusicApi api) : base(api)
{
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
}
protected virtual async Task<TResponse?> DeserializeAsync(HttpResponseMessage response) protected virtual async Task<TResponse?> DeserializeAsync(HttpResponseMessage response)
{ {
@@ -43,6 +53,8 @@ internal abstract class YJsonRequestBuilder<TResponse, TParams> : YRequestBuilde
} }
} }
protected string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
/// <summary> /// <summary>
/// Выполняет запрос и возвращает десериализованный объект типа TResponse. /// Выполняет запрос и возвращает десериализованный объект типа TResponse.
/// </summary> /// </summary>

View File

@@ -4,15 +4,20 @@ using YandexMusic.API.Requests.Common;
namespace YandexMusic.API.Requests; namespace YandexMusic.API.Requests;
/// <summary>Базовый класс для запросов к Passport (passport.yandex.ru).</summary> /// <summary>Базовый класс для запросов к Passport (passport.yandex.ru).</summary>
internal abstract class YAuthRequestBuilder<TResponse, TParams> : YJsonRequestBuilder<TResponse, TParams> internal abstract class YPassportRequestBuilder<TResponse, TParams> : YJsonRequestBuilder<TResponse, TParams>
{ {
protected override string BaseUrl => YConstants.Endpoints.PassportUrl; protected override string BaseUrl => YConstants.Endpoints.PassportUrl;
protected YAuthRequestBuilder(YandexMusicApi api) : base(api) { } protected YPassportRequestBuilder(YandexMusicApi api) : base(api) { }
protected override void SetCustomHeaders(HttpRequestHeaders headers) protected override void SetCustomHeaders(HttpRequestHeaders headers)
{ {
base.SetCustomHeaders(headers); base.SetCustomHeaders(headers);
headers.Add("X-Requested-With", "XMLHttpRequest"); headers.Add("X-Requested-With", "XMLHttpRequest");
if (Api.Storage.HeaderToken != null)
{
headers.Add("X-Csrf-Token", Api.Storage.HeaderToken.CsfrToken);
headers.Add("Process-Uuid", Api.Storage.HeaderToken.ProcessUuid);
}
} }
} }

View File

@@ -2,8 +2,6 @@
using System.Net; using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Web; using System.Web;
using YandexMusic.API.Common; using YandexMusic.API.Common;
@@ -23,7 +21,8 @@ internal abstract class YRequestBuilder<TParams>
/// <summary>Шаблон пути (может содержать плейсхолдеры вида {id}).</summary> /// <summary>Шаблон пути (может содержать плейсхолдеры вида {id}).</summary>
protected abstract string PathTemplate { get; } protected abstract string PathTemplate { get; }
private readonly JsonSerializerOptions _jsonOptions; /// <summary>Определяет, нужно ли добавлять заголовок Authorization для этого запроса.</summary>
protected virtual bool ShouldAddAuthorization => true;
/// <summary>Основной экземпляр API.</summary> /// <summary>Основной экземпляр API.</summary>
protected YandexMusicApi Api { get; } protected YandexMusicApi Api { get; }
@@ -34,12 +33,6 @@ internal abstract class YRequestBuilder<TParams>
protected YRequestBuilder(YandexMusicApi api) protected YRequestBuilder(YandexMusicApi api)
{ {
Api = api; Api = api;
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
} }
private string FullUrl => $"{BaseUrl.TrimEnd('/')}/{PathTemplate.TrimStart('/')}"; private string FullUrl => $"{BaseUrl.TrimEnd('/')}/{PathTemplate.TrimStart('/')}";
@@ -66,7 +59,7 @@ internal abstract class YRequestBuilder<TParams>
}; };
msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.AcceptCharset), Encoding.UTF8.WebName); msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.AcceptCharset), Encoding.UTF8.WebName);
msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.AcceptEncoding), "gzip"); msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.AcceptEncoding), "gzip");
if (!string.IsNullOrEmpty(Storage.Token)) if (ShouldAddAuthorization && !string.IsNullOrEmpty(Storage.Token))
msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.Authorization), $"OAuth {Storage.Token}"); msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.Authorization), $"OAuth {Storage.Token}");
SetCustomHeaders(msg.Headers); SetCustomHeaders(msg.Headers);
return msg; return msg;
@@ -120,8 +113,6 @@ internal abstract class YRequestBuilder<TParams>
protected virtual HttpContent? GetContent(TParams parameters) => null; protected virtual HttpContent? GetContent(TParams parameters) => null;
protected virtual void SetCustomHeaders(HttpRequestHeaders headers) { } protected virtual void SetCustomHeaders(HttpRequestHeaders headers) { }
protected string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
/// <summary>Выполняет запрос и возвращает десериализованный ответ.</summary> /// <summary>Выполняет запрос и возвращает десериализованный ответ.</summary>
public async Task<HttpResponseMessage?> ExecuteRawAsync(TParams parameters) public async Task<HttpResponseMessage?> ExecuteRawAsync(TParams parameters)
{ {

View File

@@ -0,0 +1,47 @@
using System.Xml;
using System.Xml.Serialization;
namespace YandexMusic.API.Requests.Common;
/// <summary>
/// Строитель запросов с десериализацией XML-ответа в TResponse.
/// </summary>
internal abstract class YXmlRequestBuilder<TResponse, TParams> : YRequestBuilder<TParams>
{
protected YXmlRequestBuilder(YandexMusicApi api) : base(api) { }
/// <summary>
/// Десериализует XML-ответ в объект типа TResponse.
/// </summary>
protected virtual async Task<TResponse?> DeserializeAsync(HttpResponseMessage response)
{
var xml = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
// Для XML-ошибок можно создать отдельную модель, но для простоты выбрасываем исключение
throw new Exception($"Ошибка HTTP {response.StatusCode}: {xml}");
}
try
{
using var stringReader = new StringReader(xml);
using var xmlReader = XmlReader.Create(stringReader, new XmlReaderSettings { Async = true });
var serializer = new XmlSerializer(typeof(TResponse));
return (TResponse?)serializer.Deserialize(xmlReader);
}
catch (Exception ex)
{
throw new Exception($"Ошибка десериализации XML: {ex.Message}\nXML: {xml}", ex);
}
}
/// <summary>
/// Выполняет запрос и возвращает десериализованный объект типа TResponse.
/// </summary>
public async Task<TResponse?> ExecuteAsync(TParams parameters)
{
using var response = await ExecuteRawAsync(parameters);
return await DeserializeAsync(response);
}
}

View File

@@ -0,0 +1,27 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YCheckPhoneAvailabilityBuilder : YPassportRequestBuilder<YCheckAvailabilityResult?, string>
{
public YCheckPhoneAvailabilityBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/suggest/check_availability";
protected override HttpContent? GetContent(string phone)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
phone_number = phone,
can_use_anmon = true,
check_for_push = true,
push_suggest_log_all_subscriptions = false
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,24 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Passport;
internal class YCheckPushCodeBuilder : YPassportRequestBuilder<YAuthEmpty?, string>
{
public YCheckPushCodeBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/auth/check-push-code";
protected override HttpContent? GetContent(string code)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
code
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,19 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YCheckSessionBuilder : YPassportRequestBuilder<YPassportSessionStatus?, object>
{
public YCheckSessionBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/sessions/check_session";
protected override HttpContent? GetContent(object _)
{
var data = new { track_id = Api.Storage.AuthToken.TrackId };
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,13 @@
using System.Net;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YCreateTrackBuilder : YPassportRequestBuilder<YPassportTrack?, object>
{
public YCreateTrackBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/auth/password/submit";
protected override HttpContent? GetContent(object _)
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "retpath", "" } });
}

View File

@@ -0,0 +1,22 @@
using System.Net;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YGetAuthLoginQRBuilder : YPassportRequestBuilder<YAuthQrSession, string>
{
public YGetAuthLoginQRBuilder(YandexMusicApi yandex) : base(yandex)
{
}
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/sessions/get_session";
protected override HttpContent GetContent(string tuple)
{
return new FormUrlEncodedContent(new Dictionary<string, string> {
{ "track_id", Api.Storage.AuthToken.SessionTrackId }
});
}
}

View File

@@ -3,20 +3,21 @@ using System.Net.Http.Headers;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common; using YandexMusic.API.Requests.Common;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Passport;
internal class YGetMusicTokenBuilder : YAuthRequestBuilder<YAccessToken?, object> internal class YGetMusicTokenBuilder : YPassportRequestBuilder<YAccessToken?, string>
{ {
public YGetMusicTokenBuilder(YandexMusicApi api) : base(api) { } public YGetMusicTokenBuilder(YandexMusicApi api) : base(api) { }
protected override string BaseUrl => YConstants.Endpoints.MobilePassportUrl;
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "/1/token"; protected override string PathTemplate => "/1/token";
protected override HttpContent? GetContent(object _) protected override HttpContent? GetContent(string passportToken)
=> new FormUrlEncodedContent(new Dictionary<string, string> => new FormUrlEncodedContent(new Dictionary<string, string>
{ {
{ "client_id", YConstants.ClientId }, { "client_id", YConstants.ClientId },
{ "client_secret", YConstants.ClientSecret }, { "client_secret", YConstants.ClientSecret },
{ "grant_type", "x-token" }, { "grant_type", "x-token" },
{ "access_token", Api.Storage.AccessToken.AccessToken } { "access_token", passportToken }
}); });
protected override void SetCustomHeaders(HttpRequestHeaders headers) protected override void SetCustomHeaders(HttpRequestHeaders headers)
{ {

View File

@@ -0,0 +1,17 @@
using System.Net;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YGetQrStatus : YPassportRequestBuilder<YAuthQRStatus?, object>
{
public YGetQrStatus(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/auth/magic/code/status";
protected override HttpContent? GetContent(object _)
=> new FormUrlEncodedContent(new Dictionary<string, string>
{
["csrf_token"] = Api.Storage.AuthToken.CsfrToken,
["track_id"] = Api.Storage.AuthToken.TrackId,
});
}

View File

@@ -0,0 +1,20 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YGetSessionBuilder : YPassportRequestBuilder<YPassportSession?, object>
{
public YGetSessionBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/sessions/get_session";
protected override HttpContent? GetContent(object _)
{
var data = new { track_id = Api.Storage.AuthToken.TrackId };
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,23 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YMultiStepPasswordBuilder : YPassportRequestBuilder<YPassportUser?, string>
{
public YMultiStepPasswordBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/auth/multistep_password";
protected override HttpContent? GetContent(string password)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
password
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,35 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YMultistepStartBuilder : YPassportRequestBuilder<YMultistepStart?, string>
{
public YMultistepStartBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/auth/multistep_start";
protected override HttpContent? GetContent(string login)
{
var data = new
{
login,
track_id = Api.Storage.AuthToken.TrackId,
display_language = "ru",
retpath = string.Empty,
can_send_push_code = true,
check_for_xtokens_for_pictures = false,
force_check_for_protocols = true,
app_id = "ru.yandex.music",
am_version_name = "7.50.2(750024597)",
app_platform = "android",
app_version_name = "2026.02.3 #135rur",
device_id = Api.Storage.DeviceId,
device_connection_type = "9"
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,23 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YRfcOtpBuilder : YPassportRequestBuilder<YPassportUser?, string>
{
public YRfcOtpBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/auth/rfc-otp";
protected override HttpContent? GetContent(string otp)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
otp
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,26 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YSendPushBuilder : YPassportRequestBuilder<YSendPushResult?, string>
{
public YSendPushBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/auth/suggest-send-push";
protected override HttpContent? GetContent(string phone)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
phone_number = phone,
can_use_anmon = true,
force_show_code_in_notification = "1",
country = Api.Storage.Country
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,23 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YSuggestByPhoneBuilder : YPassportRequestBuilder<YSuggestByPhoneResult?, object>
{
public YSuggestByPhoneBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/suggest/by_phone";
protected override HttpContent? GetContent(object _)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
can_use_anmon = true
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,24 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YValidatePhoneNumberBuilder : YPassportRequestBuilder<YValidatePhoneNumberResult?, string>
{
public YValidatePhoneNumberBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/validate/phone_number";
protected override HttpContent? GetContent(string phone)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
phone_number = phone,
country = Api.Storage.Country
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,25 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YValidateSquatterBuilder : YPassportRequestBuilder<YValidateSquatter?, string>
{
public YValidateSquatterBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/validate/squatter";
protected override HttpContent? GetContent(string phone)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
phone_number = phone,
scenario = "auth",
can_use_anmon = true
};
return JsonContent.Create(data);
}
}

View File

@@ -6,27 +6,30 @@ using YandexMusic.API.Requests.Common;
namespace YandexMusic.API.Requests.Track; namespace YandexMusic.API.Requests.Track;
/// <summary>Особый запрос не к api.music.yandex.net, а к произвольному URL.</summary> /// <summary>Особый запрос не к api.music.yandex.net, а к произвольному URL.</summary>
internal class YStorageDownloadFileBuilder : YJsonRequestBuilder<YStorageDownloadFile?, string> internal class YStorageDownloadFileBuilder : YXmlRequestBuilder<YStorageDownloadFile?, string>
{ {
public YStorageDownloadFileBuilder(YandexMusicApi api) : base(api) { } public YStorageDownloadFileBuilder(YandexMusicApi api) : base(api) { }
protected override string BaseUrl => ""; // не используется, т.к. URL берётся из параметра protected override bool ShouldAddAuthorization => false;
protected override string BaseUrl => "{src}"; // не используется, т.к. URL берётся из параметра
protected override string Method => WebRequestMethods.Http.Get; protected override string Method => WebRequestMethods.Http.Get;
protected override string PathTemplate => "{src}"; protected override string PathTemplate => "";
protected override Dictionary<string, string> GetSubstitutions(string src) protected override Dictionary<string, string> GetSubstitutions(string src)
=> new() { { "src", src.Split('?')[0] } }; => new() { { "src", src } };
protected override NameValueCollection GetQueryParams(string src) protected override NameValueCollection GetQueryParams(string src)
{ {
var query = new NameValueCollection { { "format", "json" } }; var query = new NameValueCollection();
var parts = src.Split('?'); var parts = src.Split('?');
if (parts.Length > 1) if (parts.Length > 1)
{ {
foreach (var param in parts[1].Split('&')) foreach (var param in parts[1].Split('&'))
{ {
var kv = param.Split('='); var kv = param.Split('=');
if (kv.Length == 2) query.Add(kv[0], kv[1]); if (kv.Length >= 2) query.Add(kv[0], kv[1]);
} }
} }
return query; return query;

View File

@@ -8,11 +8,11 @@ namespace YandexMusic.API.Requests.Ugc;
internal class YUgcUploadBuilder : YJsonRequestBuilder<YResponse<string>?, (string postTargetLink, byte[] fileBytes)> internal class YUgcUploadBuilder : YJsonRequestBuilder<YResponse<string>?, (string postTargetLink, byte[] fileBytes)>
{ {
public YUgcUploadBuilder(YandexMusicApi api) : base(api) { } public YUgcUploadBuilder(YandexMusicApi api) : base(api) { }
protected override string BaseUrl => ""; protected override string BaseUrl => "{postTargetLink}";
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "{postTargetLink}"; protected override string PathTemplate => "";
protected override Dictionary<string, string> GetSubstitutions((string postTargetLink, byte[] fileBytes) tuple) protected override Dictionary<string, string> GetSubstitutions((string postTargetLink, byte[] fileBytes) tuple)
=> new() { { "postTargetLink", tuple.postTargetLink } }; => new() { { "postTargetLink", tuple.postTargetLink } };

View File

@@ -6,9 +6,9 @@ namespace YandexMusic.API;
public class YandexMusicApi public class YandexMusicApi
{ {
/// <summary>HttpClient, используемый для всех запросов.</summary> /// <summary>HttpClient, используемый для всех запросов.</summary>
public HttpClient HttpClient { get; } internal HttpClient HttpClient { get; }
/// <summary>Хранилище данных авторизации.</summary> /// <summary>Хранилище данных авторизации.</summary>
public AuthStorage Storage { get; } internal AuthStorage Storage { get; }
/// <summary>API для работы с альбомами.</summary> /// <summary>API для работы с альбомами.</summary>
public YAlbumAPI Album { get; internal set; } = null!; public YAlbumAPI Album { get; internal set; } = null!;
@@ -33,11 +33,13 @@ public class YandexMusicApi
/// <summary>API для работы с очередями.</summary> /// <summary>API для работы с очередями.</summary>
public YQueueAPI Queue { get; internal set; } = null!; public YQueueAPI Queue { get; internal set; } = null!;
/// <summary>API для работы с пользователем и авторизацией.</summary> /// <summary>API для работы с пользователем и авторизацией.</summary>
public YUserAPI User { get; internal set; } = null!; public YAuthAPI Auth { get; internal set; } = null!;
/// <summary>API для загрузки пользовательского контента.</summary> /// <summary>API для загрузки пользовательского контента.</summary>
public YUgcAPI UserGeneratedContent { get; internal set; } = null!; public YUgcAPI UserGeneratedContent { get; internal set; } = null!;
/// <summary>API для работы с протоколом Ynison (WebSocket).</summary> /// <summary>API для работы с протоколом Ynison (WebSocket).</summary>
public YYnisonAPI Ynison { get; internal set; } = null!; public YYnisonAPI Ynison { get; internal set; } = null!;
/// <summary>API для работы с яндекс пасспорт.</summary>
public YPassportAPI Passport { get; internal set; } = null!;
/// <summary> /// <summary>
/// Инициализирует новый экземпляр API. /// Инициализирует новый экземпляр API.
@@ -49,19 +51,20 @@ public class YandexMusicApi
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
Storage = storage ?? throw new ArgumentNullException(nameof(storage)); Storage = storage ?? throw new ArgumentNullException(nameof(storage));
Album = new YAlbumAPI(this); Album = new(this);
Artist = new YArtistAPI(this); Artist = new(this);
Label = new YLabelAPI(this); Label = new(this);
Landing = new YLandingAPI(this); Landing = new(this);
Library = new YLibraryAPI(this); Library = new(this);
Playlist = new YPlaylistAPI(this); Playlist = new(this);
Pins = new YPinsAPI(this); Pins = new(this);
Radio = new YRadioAPI(this); Radio = new(this);
Search = new YSearchAPI(this); Search = new(this);
Track = new YTrackAPI(this); Track = new(this);
Queue = new YQueueAPI(this); Queue = new(this);
User = new YUserAPI(this); Auth = new(this);
UserGeneratedContent = new YUgcAPI(this); UserGeneratedContent = new(this);
Ynison = new YYnisonAPI(this); Ynison = new(this);
Passport = new(this);
} }
} }

View File

@@ -1,5 +1,5 @@
<Solution> <Solution>
<Project Path="YaMusicCli/YaMusicCli.csproj" Id="5cce354e-7517-4a94-9584-197daa3ad6a4" /> <Project Path="YaMusicCli/YaMusicCli.csproj" Id="5cce354e-7517-4a94-9584-197daa3ad6a4" />
<Project Path="YandexMusic.API/YandexMusic.API.csproj" /> <Project Path="YandexMusic.API/YandexMusic.API.csproj" />
<Project Path="YandexMusic/YandexMusic.csproj" Id="044fcef4-86d2-4cc9-9f7e-a577c19ae5c3" /> <Project Path="YandexMusic/YandexMusic.csproj" />
</Solution> </Solution>

View File

@@ -1,19 +1,8 @@
using YandexMusic.API; using System.Net;
using YandexMusic.API;
using YandexMusic.API.Common; using YandexMusic.API.Common;
using YandexMusic.API.Common.Ynison; using YandexMusic.API.Common.Ynison;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Models.Album;
using YandexMusic.API.Models.Artist;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Models.Feed;
using YandexMusic.API.Models.Landing;
using YandexMusic.API.Models.Landing.Entity.Entities.Context;
using YandexMusic.API.Models.Library;
using YandexMusic.API.Models.Playlist;
using YandexMusic.API.Models.Queue;
using YandexMusic.API.Models.Radio;
using YandexMusic.API.Models.Search;
using YandexMusic.API.Models.Track;
namespace YandexMusic; namespace YandexMusic;
@@ -23,7 +12,6 @@ public class YandexMusicClient : IDisposable
private readonly YandexMusicApi _api; private readonly YandexMusicApi _api;
private readonly AuthStorage _storage; private readonly AuthStorage _storage;
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly bool _ownsHttpClient;
private YnisonPlayer? _player; private YnisonPlayer? _player;
/// <summary>Хранилище авторизации.</summary> /// <summary>Хранилище авторизации.</summary>
@@ -41,363 +29,49 @@ public class YandexMusicClient : IDisposable
/// <summary>HttpClient, используемый клиентом (можно получить для настройки кук).</summary> /// <summary>HttpClient, используемый клиентом (можно получить для настройки кук).</summary>
public HttpClient HttpClient => _httpClient; public HttpClient HttpClient => _httpClient;
/// <summary>Создаёт новый экземпляр клиента с собственным HttpClient.</summary> /// <summary>API Яндекс Музыки.</summary>
public YandexMusicClient() : this(YandexMusicHttpClientFactory.CreateDefault()) public YandexMusicApi Api => _api;
{
_ownsHttpClient = true;
}
/// <summary> /// <summary>Создаёт новый экземпляр клиента с собственным HttpClient.</summary>
/// Создаёт новый экземпляр клиента с указанным HttpClient. public YandexMusicClient(
/// </summary> CookieContainer? cookieContainer = null,
/// <param name="httpClient">Экземпляр HttpClient (должен быть настроен с нужными куками, таймаутами).</param> IWebProxy? proxy = null,
/// <param name="ownsHttpClient">Если true, клиент будет отвечать за освобождение HttpClient при Dispose.</param> TimeSpan? timeout = null,
public YandexMusicClient(HttpClient httpClient) string? userAgent = null
)
{ {
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); if (cookieContainer == null) cookieContainer = new CookieContainer();
_storage = new AuthStorage();
var handler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = true,
CookieContainer = cookieContainer,
AllowAutoRedirect = true,
MaxAutomaticRedirections = 10,
Proxy = proxy,
UseProxy = proxy != null
};
var client = new HttpClient(handler, disposeHandler: true)
{
Timeout = timeout ?? TimeSpan.FromSeconds(30)
};
// Стандартный User-Agent, похожий на браузерный
client.DefaultRequestHeaders.Add("User-Agent",
userAgent ?? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
client.DefaultRequestHeaders.Add("Accept", "*/*");
client.DefaultRequestHeaders.Add("Accept-Language", "ru-RU,ru;q=0.9,en;q=0.8");
_httpClient = client;
_storage = new AuthStorage(cookieContainer);
_api = new YandexMusicApi(_httpClient, _storage); _api = new YandexMusicApi(_httpClient, _storage);
} }
#region Авторизация
/// <summary>Авторизация по готовому OAuth-токену.</summary>
public async Task<bool> Authorize(string token)
{
await _api.User.AuthorizeAsync(token);
return _storage.IsAuthorized;
}
/// <summary>Создание сеанса и получение доступных методов авторизации.</summary>
public Task<YAuthTypes?> CreateAuthSession(string userName)
=> _api.User.CreateAuthSessionAsync(userName);
/// <summary>Получение ссылки на QR-код для авторизации.</summary>
public Task<string?> GetAuthQRLink()
=> _api.User.GetAuthQRLinkAsync();
/// <summary>Авторизация по QR-коду (после сканирования).</summary>
public Task<YAuthQRStatus?> AuthorizeByQR()
=> _api.User.AuthorizeByQRAsync();
/// <summary>Получение капчи.</summary>
public Task<YAuthCaptcha?> GetCaptcha()
=> _api.User.GetCaptchaAsync();
/// <summary>Авторизация с вводом капчи.</summary>
public Task<YAuthBase?> AuthorizeByCaptcha(string captcha)
=> _api.User.AuthorizeByCaptchaAsync(captcha);
/// <summary>Запрос письма для авторизации.</summary>
public Task<YAuthLetter?> GetAuthLetter()
=> _api.User.GetAuthLetterAsync();
/// <summary>Подтверждение авторизации по письму.</summary>
public Task<bool> AuthorizeByLetter()
=> _api.User.AuthorizeByLetterAsync();
/// <summary>Авторизация по паролю приложения.</summary>
public Task<YAuthBase?> AuthorizeByAppPassword(string password)
=> _api.User.AuthorizeByAppPasswordAsync(password);
/// <summary>Получение AccessToken после успешной авторизации.</summary>
public Task<YAccessToken?> GetAccessToken()
=> _api.User.GetAccessTokenAsync();
/// <summary>Получение информации о пользователе через логин Яндекса.</summary>
public Task<YLoginInfo?> GetLoginInfo()
=> _api.User.GetLoginInfoAsync();
#endregion
#region Треки
/// <summary>Получает трек по идентификатору.</summary>
public async Task<YTrack?> GetTrackAsync(string id)
=> (await _api.Track.GetAsync(id));
/// <summary>Получает список треков по идентификаторам.</summary>
public async Task<List<YTrack>> GetTracksAsync(IEnumerable<string> ids)
=> (await _api.Track.GetAsync(ids)) ?? new List<YTrack>();
#endregion
#region Альбомы
/// <summary>Получает альбом по идентификатору.</summary>
public Task<YAlbum?> GetAlbumAsync(string id)
=> _api.Album.GetAsync(id);
/// <summary>Получает список альбомов по идентификаторам.</summary>
public async Task<List<YAlbum>> GetAlbumsAsync(IEnumerable<string> ids)
=> (await _api.Album.GetAsync(ids)) ?? new List<YAlbum>();
#endregion
#region Главная страница
/// <summary>Получает персональные блоки лендинга.</summary>
public Task<YLanding?> GetLandingAsync(params YLandingBlockType[] blocks)
=> _api.Landing.GetAsync(blocks);
/// <summary>Получает ленту событий.</summary>
public Task<YFeed?> GetFeedAsync()
=> _api.Landing.GetFeedAsync();
/// <summary>Получает детский лендинг.</summary>
public Task<YChildrenLanding?> GetChildrenLandingAsync()
=> _api.Landing.GetChildrenLandingAsync();
#endregion
#region Исполнители
/// <summary>Получает информацию об исполнителе.</summary>
public Task<YArtistBriefInfo?> GetArtistAsync(string id)
=> _api.Artist.GetAsync(id);
/// <summary>Получает список исполнителей.</summary>
public async Task<List<YArtist>> GetArtistsAsync(IEnumerable<string> ids)
=> (await _api.Artist.GetAsync(ids)) ?? new List<YArtist>();
#endregion
#region Плейлисты
/// <summary>Получает плейлист по пользователю и идентификатору.</summary>
public Task<YPlaylist?> GetPlaylistAsync(string user, string id)
=> _api.Playlist.GetAsync(user, id);
/// <summary>Получает плейлист по UUID.</summary>
public Task<YPlaylist?> GetPlaylistAsync(string uuid)
=> _api.Playlist.GetAsync(uuid);
/// <summary>Получает список плейлистов по списку пар (пользователь, идентификатор).</summary>
public async Task<List<YPlaylist>> GetPlaylistsAsync(IEnumerable<(string user, string id)> ids)
=> (await _api.Playlist.GetAsync(ids)) ?? new List<YPlaylist>();
/// <summary>Получает персональные плейлисты (с главной страницы).</summary>
public Task<List<YPlaylist>> GetPersonalPlaylistsAsync()
=> _api.Playlist.GetPersonalPlaylistsAsync();
/// <summary>Получает избранные плейлисты.</summary>
public async Task<List<YPlaylist>> GetFavoritesAsync()
=> (await _api.Playlist.FavoritesAsync()) ?? new List<YPlaylist>();
/// <summary>Получает плейлист «Дежавю».</summary>
public Task<YPlaylist?> GetDejaVuAsync()
=> _api.Playlist.DejaVuAsync();
/// <summary>Получает плейлист «Тайник».</summary>
public Task<YPlaylist?> GetMissedAsync()
=> _api.Playlist.MissedAsync();
/// <summary>Получает плейлист дня.</summary>
public Task<YPlaylist?> GetOfTheDayAsync()
=> _api.Playlist.OfTheDayAsync();
/// <summary>Получает плейлист «Кинопоиск».</summary>
public Task<YPlaylist?> GetKinopoiskAsync()
=> _api.Playlist.KinopoiskAsync();
/// <summary>Получает плейлист «Премьера».</summary>
public Task<YPlaylist?> GetPremiereAsync()
=> _api.Playlist.PremiereAsync();
/// <summary>Создаёт новый плейлист с заданным именем.</summary>
public Task<YPlaylist?> CreatePlaylistAsync(string name)
=> _api.Playlist.CreateAsync(name);
#endregion
#region Поиск
/// <summary>Выполняет поиск.</summary>
public Task<YSearch?> SearchAsync(string searchText, YSearchType searchType, int page = 0, int pageSize = 20)
=> _api.Search.SearchAsync(searchText, searchType, page, pageSize);
/// <summary>Получает подсказки для поискового запроса.</summary>
public Task<YSearchSuggest?> GetSearchSuggestionsAsync(string searchText)
=> _api.Search.GetSearchSuggestionsAsync(searchText);
#endregion
#region Библиотека
/// <summary>Получает лайкнутые треки.</summary>
public async Task<List<YTrack>> GetLikedTracksAsync()
{
var likes = await _api.Library.GetLikedTracksAsync();
var ids = likes?.Library?.Tracks.Select(t => t.Id).ToArray() ?? Array.Empty<string>();
if (ids.Length == 0) return new List<YTrack>();
var tracks = await _api.Track.GetAsync(ids);
return tracks ?? new List<YTrack>();
}
/// <summary>Получает дизлайкнутые треки.</summary>
public async Task<List<YTrack>> GetDislikedTracksAsync()
{
var dislikes = await _api.Library.GetDislikedTracksAsync();
var ids = dislikes?.Library?.Tracks.Select(t => t.Id).ToArray() ?? Array.Empty<string>();
if (ids.Length == 0) return new List<YTrack>();
var tracks = await _api.Track.GetAsync(ids);
return tracks ?? new List<YTrack>();
}
/// <summary>Получает лайкнутые альбомы.</summary>
public async Task<List<YAlbum>> GetLikedAlbumsAsync()
{
var albums = await _api.Library.GetLikedAlbumsAsync();
var ids = albums?.Select(a => a.Id).ToArray() ?? Array.Empty<string>();
if (ids.Length == 0) return new List<YAlbum>();
var result = await _api.Album.GetAsync(ids);
return result ?? new List<YAlbum>();
}
/// <summary>Получает лайкнутых исполнителей.</summary>
public async Task<List<YArtist>> GetLikedArtistsAsync()
{
var artists = await _api.Library.GetLikedArtistsAsync();
var ids = artists?.Select(a => a.Id).ToArray() ?? Array.Empty<string>();
if (ids.Length == 0) return new List<YArtist>();
var result = await _api.Artist.GetAsync(ids);
return result ?? new List<YArtist>();
}
/// <summary>Получает дизлайкнутых исполнителей.</summary>
public async Task<List<YArtist>> GetDislikedArtistsAsync()
{
var artists = await _api.Library.GetDislikedArtistsAsync();
var ids = artists?.Select(a => a.Id).ToArray() ?? Array.Empty<string>();
if (ids.Length == 0) return new List<YArtist>();
var result = await _api.Artist.GetAsync(ids);
return result ?? new List<YArtist>();
}
/// <summary>Получает лайкнутые плейлисты.</summary>
public async Task<List<YPlaylist>> GetLikedPlaylistsAsync()
{
var playlists = await _api.Library.GetLikedPlaylistsAsync();
var ids = playlists?
.Select(p => (p.Playlist.Uid, p.Playlist.Kind))
.ToArray() ?? Array.Empty<(string, string)>();
if (ids.Length == 0) return new List<YPlaylist>();
var result = await _api.Playlist.GetAsync(ids);
return result ?? new List<YPlaylist>();
}
/// <summary>Получает список недавно прослушанных треков.</summary>
public async Task<List<YRecentlyListened>> GetRecentlyListenedAsync(
IEnumerable<YPlayContextType> contextTypes,
int trackCount = 50,
int contextCount = 10)
{
var response = await _api.Library.GetRecentlyListenedAsync(contextTypes, trackCount, contextCount);
return response?.Contexts ?? new List<YRecentlyListened>();
}
#endregion
#region Радио
/// <summary>Получает список рекомендованных радиостанций.</summary>
public async Task<List<YStation>> GetRadioDashboardAsync()
{
var dashboard = await _api.Radio.GetStationsDashboardAsync();
return dashboard?.Stations ?? new List<YStation>();
}
/// <summary>Получает список всех радиостанций.</summary>
public async Task<List<YStation>> GetRadioStationsAsync()
=> (await _api.Radio.GetStationsAsync()) ?? new List<YStation>();
/// <summary>Получает информацию о радиостанции по идентификатору.</summary>
public async Task<YStation?> GetRadioStationAsync(YStationId id)
=> (await _api.Radio.GetStationAsync(id))?.FirstOrDefault();
#endregion
#region Очереди
/// <summary>Получает все очереди с разных устройств.</summary>
public Task<YQueueItemsContainer?> GetQueuesAsync(string? device = null)
=> _api.Queue.ListAsync(device);
/// <summary>Получает очередь по идентификатору.</summary>
public Task<YQueue?> GetQueueAsync(string queueId)
=> _api.Queue.GetAsync(queueId);
/// <summary>Создаёт новую очередь.</summary>
public Task<YNewQueue?> CreateQueueAsync(YQueue queue, string? device = null)
=> _api.Queue.CreateAsync(queue, device);
/// <summary>Обновляет позицию в очереди.</summary>
public Task<YUpdatedQueue?> UpdateQueuePositionAsync(string queueId, int currentIndex, bool isInteractive, string? device = null)
=> _api.Queue.UpdatePositionAsync(queueId, currentIndex, isInteractive, device);
#endregion
#region Загрузка треков (UGC)
/// <summary>Загружает трек из файла в плейлист.</summary>
public Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, string filePath)
=> _api.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, filePath);
/// <summary>Загружает трек из потока в плейлист.</summary>
public Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, Stream stream)
=> _api.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, stream);
/// <summary>Загружает трек из массива байтов в плейлист.</summary>
public Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, byte[] file)
=> _api.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, file);
#endregion
#region Лейблы
/// <summary>Получает альбомы лейбла с пагинацией.</summary>
public async Task<List<YAlbum>> GetAlbumsByLabelAsync(YLabel label, int page = 0)
=> (await _api.Label.GetAlbumsByLabelAsync(label, page))?.Albums ?? new List<YAlbum>();
/// <summary>Получает артистов лейбла с пагинацией.</summary>
public async Task<List<YArtist>> GetArtistsByLabelAsync(YLabel label, int page = 0)
=> (await _api.Label.GetArtistsByLabelAsync(label, page))?.Artists ?? new List<YArtist>();
#endregion
#region Ynison (WebSocket плеер)
/// <summary>Подключается к Ynison и запускает синхронизацию состояния плеера.</summary>
public async Task ConnectYnisonAsync()
{
if (_player == null)
_player = _api.Ynison.GetPlayer();
await _player.ConnectAsync();
}
/// <summary>Отключается от Ynison.</summary>
public async Task DisconnectYnisonAsync()
{
if (_player != null)
await _player.DisconnectAsync();
}
#endregion
#region IDisposable
private bool _disposed;
/// <summary>Освобождает ресурсы.</summary>
public void Dispose() public void Dispose()
{ {
if (_disposed) return; _httpClient?.Dispose();
_player?.Dispose(); _player?.Dispose();
if (_ownsHttpClient)
_httpClient.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
} }
#endregion
} }