Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c5dca1491 | ||
| 815283a776 | |||
| 526353d679 | |||
| eb1eba0162 | |||
|
|
34261d02a9 | ||
|
|
5f761d4fe8 | ||
|
|
b6f78da9c8 | ||
|
|
0bbaac5689 | ||
|
|
a7caf829d3 |
607
ARCHITECTURE.md
Normal file
607
ARCHITECTURE.md
Normal 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
410
CONTRIBUTING.md
Normal 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
446
FAQ.md
Normal 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
536
QUICKSTART.md
Normal 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
437
README.md
Normal 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)
|
||||
111
YandexMusic.API/API/YAuthAPI.cs
Normal file
111
YandexMusic.API/API/YAuthAPI.cs
Normal 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!);
|
||||
}
|
||||
230
YandexMusic.API/API/YPassportAPI.cs
Normal file
230
YandexMusic.API/API/YPassportAPI.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@ public class YTrackAPI : YCommonAPI
|
||||
return $"https://{host}/get-{codec}/{sign}/{ts}{path}";
|
||||
}
|
||||
|
||||
public Task<YTrack?> GetAsync(string trackId)
|
||||
=> GetAsync(trackId);
|
||||
public async Task<YTrack?> GetAsync(string trackId)
|
||||
=> (await GetAsync([trackId]))?.FirstOrDefault();
|
||||
|
||||
public Task<List<YTrack>?> GetAsync(IEnumerable<string> trackIds)
|
||||
=> new YGetTracksBuilder(Api).ExecuteAsync(trackIds);
|
||||
|
||||
@@ -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!);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Net;
|
||||
using YandexMusic.API.Models.Account;
|
||||
|
||||
namespace YandexMusic.API.Common;
|
||||
@@ -7,6 +8,15 @@ namespace YandexMusic.API.Common;
|
||||
/// </summary>
|
||||
public class AuthStorage
|
||||
{
|
||||
private CookieContainer _cookieContainer;
|
||||
|
||||
public AuthStorage(CookieContainer cookieContainer)
|
||||
{
|
||||
_cookieContainer = cookieContainer;
|
||||
}
|
||||
|
||||
public CookieContainer CookieContainer => _cookieContainer;
|
||||
|
||||
/// <summary>
|
||||
/// Флаг, указывающий, авторизован ли пользователь.
|
||||
/// </summary>
|
||||
@@ -35,7 +45,17 @@ public class AuthStorage
|
||||
/// <summary>
|
||||
/// Внутренние данные авторизации (CSRF, track_id и т.д.).
|
||||
/// </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>
|
||||
/// Устанавливает флаг авторизации и сохраняет информацию об аккаунте.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using YandexMusic.API.Models.Album;
|
||||
|
||||
namespace YandexMusic.API.Extensions.API;
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>
|
||||
/// Методы-расширения для альбома.
|
||||
@@ -15,7 +15,7 @@ public static class YAlbumExtensions
|
||||
if (album.Volumes != null)
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@ public static class YAlbumExtensions
|
||||
/// Добавляет альбом в список лайкнутых.
|
||||
/// </summary>
|
||||
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>
|
||||
public static async Task<string?> RemoveLikeAsync(this YAlbum album)
|
||||
=> await album.Context.API.Library.RemoveAlbumLikeAsync(album);
|
||||
=> await album.Context.Api.Library.RemoveAlbumLikeAsync(album);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using YandexMusic.API.Models.Artist;
|
||||
using YandexMusic.API.Models.Track;
|
||||
|
||||
namespace YandexMusic.API.Extensions.API;
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>
|
||||
/// Методы-расширения для исполнителя.
|
||||
@@ -12,29 +12,29 @@ public static class YArtistExtensions
|
||||
/// Получает расширенную информацию об исполнителе.
|
||||
/// </summary>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
public static async Task<string?> RemoveLikeAsync(this YArtist artist)
|
||||
=> await artist.Context.API.Library.RemoveArtistLikeAsync(artist);
|
||||
=> await artist.Context.Api.Library.RemoveArtistLikeAsync(artist);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using YandexMusic.API.Models.Playlist;
|
||||
using YandexMusic.API.Models.Track;
|
||||
|
||||
namespace YandexMusic.API.Extensions.API;
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>
|
||||
/// Методы-расширения для плейлиста.
|
||||
@@ -18,44 +18,44 @@ public static class YPlaylistExtensions
|
||||
{
|
||||
if (playlist.Tracks != null)
|
||||
return playlist;
|
||||
return await playlist.Context.API.Playlist.GetAsync(playlist);
|
||||
return await playlist.Context.Api.Playlist.GetAsync(playlist);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавляет плейлист в список лайкнутых.
|
||||
/// </summary>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
/// Загружает трек в плейлист (только для владельца).
|
||||
@@ -63,7 +63,7 @@ public static class YPlaylistExtensions
|
||||
public static async Task<bool> UploadTrackAsync(this YPlaylist playlist, string filePath, string fileName)
|
||||
{
|
||||
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";
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using YandexMusic.API.Models.Radio;
|
||||
using YandexMusic.API.Models.Track;
|
||||
|
||||
namespace YandexMusic.API.Extensions.API;
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>
|
||||
/// Методы-расширения для радиостанции.
|
||||
@@ -12,17 +12,17 @@ public static class YStationResultExtensions
|
||||
/// Получает список треков для радиостанции.
|
||||
/// </summary>
|
||||
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>
|
||||
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>
|
||||
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);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using YandexMusic.API.Models.Track;
|
||||
|
||||
namespace YandexMusic.API.Extensions.API;
|
||||
namespace YandexMusic.API;
|
||||
|
||||
/// <summary>
|
||||
/// Методы-расширения для трека.
|
||||
@@ -11,53 +11,53 @@ public static class YTrackExtensions
|
||||
/// Получает прямую ссылку на скачивание трека.
|
||||
/// </summary>
|
||||
public static Task<string?> GetLinkAsync(this YTrack track)
|
||||
=> track.Context.API.Track.GetFileLinkAsync(track);
|
||||
=> track.Context.Api.Track.GetFileLinkAsync(track);
|
||||
|
||||
/// <summary>
|
||||
/// Сохраняет трек в файл.
|
||||
/// </summary>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
public static async Task<YTrackSimilar?> SimilarAsync(this YTrack track)
|
||||
=> await track.Context.API.Track.GetSimilarAsync(track);
|
||||
=> await track.Context.Api.Track.GetSimilarAsync(track);
|
||||
}
|
||||
49
YandexMusic.API/Extensions/EnumHelper.cs
Normal file
49
YandexMusic.API/Extensions/EnumHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
public class YAuthEmpty
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
9
YandexMusic.API/Models/Account/YAuthQrState.cs
Normal file
9
YandexMusic.API/Models/Account/YAuthQrState.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace YandexMusic.API.Models.Account;
|
||||
|
||||
public enum YAuthQrState
|
||||
{
|
||||
[EnumMember(Value = "otp_auth_finished")]
|
||||
OtpAuthFinished,
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace YandexMusic.API.Models.Account;
|
||||
namespace YandexMusic.API.Models.Account;
|
||||
|
||||
public class YAuthToken
|
||||
{
|
||||
[JsonPropertyName("csfr_token")]
|
||||
public string CsfrToken { get; set; }
|
||||
|
||||
[JsonPropertyName("track_id")]
|
||||
public string TrackId { get; set; }
|
||||
|
||||
[JsonPropertyName("process_uuid")]
|
||||
public string SessionTrackId { get; set; }
|
||||
|
||||
public string ProcessUuid { get; set; }
|
||||
|
||||
public Dictionary<string, string> Cookie { get; set; } = new();
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace YandexMusic.API.Models.Common;
|
||||
public class YExecutionContext
|
||||
{
|
||||
/// <summary>Экземпляр основного API.</summary>
|
||||
public YandexMusicApi API { get; internal set; } = null!;
|
||||
public YandexMusicApi Api { get; internal set; } = null!;
|
||||
|
||||
/// <summary>Хранилище данных авторизации.</summary>
|
||||
public AuthStorage Storage { get; internal set; } = null!;
|
||||
|
||||
@@ -38,7 +38,7 @@ public class YExecutionContextConverter : JsonConverter<object>
|
||||
var obj = JsonSerializer.Deserialize(ref reader, typeToConvert, innerOptions);
|
||||
if (obj is YBaseModel baseModel)
|
||||
{
|
||||
baseModel.Context = new YExecutionContext { API = _api, Storage = _storage };
|
||||
baseModel.Context = new YExecutionContext { Api = _api, Storage = _storage };
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace YandexMusic.API.Models.Common;
|
||||
|
||||
[XmlRoot("download-info")]
|
||||
public class YStorageDownloadFile
|
||||
{
|
||||
[XmlElement("host")]
|
||||
public string Host { get; set; }
|
||||
[XmlElement("path")]
|
||||
public string Path { get; set; }
|
||||
[XmlElement("s")]
|
||||
public string S { get; set; }
|
||||
[XmlElement("ts")]
|
||||
public string Ts { get; set; }
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using YandexMusic.API.Extensions;
|
||||
using YandexMusic.API.Models.Landing.Entity.Entities;
|
||||
|
||||
namespace YandexMusic.API.Models.Landing.Entity;
|
||||
@@ -26,7 +27,8 @@ public class YLandingEntityConverter : JsonConverter<List<YLandingEntity>>
|
||||
|
||||
var typeProp = root.GetProperty("type");
|
||||
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}");
|
||||
|
||||
YLandingEntity? entity = null;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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")]
|
||||
public string TrackId { get; set; }
|
||||
@@ -1,12 +1,14 @@
|
||||
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")]
|
||||
public int DefaultUid { get; set; }
|
||||
|
||||
[JsonPropertyName("retpath")]
|
||||
public string RetPath { get; set; }
|
||||
|
||||
[JsonPropertyName("track_id")]
|
||||
12
YandexMusic.API/Models/Passport/YAuthQrStatus.cs
Normal file
12
YandexMusic.API/Models/Passport/YAuthQrStatus.cs
Normal 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;
|
||||
}
|
||||
39
YandexMusic.API/Models/Passport/YCheckAvailabilityResult.cs
Normal file
39
YandexMusic.API/Models/Passport/YCheckAvailabilityResult.cs
Normal 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; }
|
||||
}
|
||||
12
YandexMusic.API/Models/Passport/YMasterInfo.cs
Normal file
12
YandexMusic.API/Models/Passport/YMasterInfo.cs
Normal 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;
|
||||
}
|
||||
42
YandexMusic.API/Models/Passport/YMultistepStart.cs
Normal file
42
YandexMusic.API/Models/Passport/YMultistepStart.cs
Normal 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; }
|
||||
}
|
||||
48
YandexMusic.API/Models/Passport/YPassportAccount.cs
Normal file
48
YandexMusic.API/Models/Passport/YPassportAccount.cs
Normal 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; }
|
||||
}
|
||||
11
YandexMusic.API/Models/Passport/YPassportName.cs
Normal file
11
YandexMusic.API/Models/Passport/YPassportName.cs
Normal 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;
|
||||
}
|
||||
11
YandexMusic.API/Models/Passport/YPassportPerson.cs
Normal file
11
YandexMusic.API/Models/Passport/YPassportPerson.cs
Normal 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;
|
||||
}
|
||||
12
YandexMusic.API/Models/Passport/YPassportSession.cs
Normal file
12
YandexMusic.API/Models/Passport/YPassportSession.cs
Normal 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;
|
||||
}
|
||||
12
YandexMusic.API/Models/Passport/YPassportSessionStatus.cs
Normal file
12
YandexMusic.API/Models/Passport/YPassportSessionStatus.cs
Normal 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; }
|
||||
}
|
||||
21
YandexMusic.API/Models/Passport/YPassportUser.cs
Normal file
21
YandexMusic.API/Models/Passport/YPassportUser.cs
Normal 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; }
|
||||
}
|
||||
7
YandexMusic.API/Models/Passport/YPushApp.cs
Normal file
7
YandexMusic.API/Models/Passport/YPushApp.cs
Normal 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;
|
||||
}
|
||||
18
YandexMusic.API/Models/Passport/YSendPushResult.cs
Normal file
18
YandexMusic.API/Models/Passport/YSendPushResult.cs
Normal 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();
|
||||
}
|
||||
54
YandexMusic.API/Models/Passport/YSuggestAccount.cs
Normal file
54
YandexMusic.API/Models/Passport/YSuggestAccount.cs
Normal 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; }
|
||||
}
|
||||
15
YandexMusic.API/Models/Passport/YSuggestByPhoneResult.cs
Normal file
15
YandexMusic.API/Models/Passport/YSuggestByPhoneResult.cs
Normal 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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
18
YandexMusic.API/Models/Passport/YValidateSquatter.cs
Normal file
18
YandexMusic.API/Models/Passport/YValidateSquatter.cs
Normal 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;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
|
||||
|
||||
namespace YandexMusic.API.Requests.Account;
|
||||
|
||||
internal class YGetAuthAppPasswordBuilder : YAuthRequestBuilder<YAuthBase?, string>
|
||||
internal class YGetAuthAppPasswordBuilder : YPassportRequestBuilder<YAuthBase?, string>
|
||||
{
|
||||
public YGetAuthAppPasswordBuilder(YandexMusicApi api) : base(api) { }
|
||||
protected override string Method => WebRequestMethods.Http.Post;
|
||||
|
||||
@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
|
||||
|
||||
namespace YandexMusic.API.Requests.Account;
|
||||
|
||||
internal class YGetAuthCaptchaBuilder : YAuthRequestBuilder<YAuthCaptcha?, object>
|
||||
internal class YGetAuthCaptchaBuilder : YPassportRequestBuilder<YAuthCaptcha?, object>
|
||||
{
|
||||
public YGetAuthCaptchaBuilder(YandexMusicApi api) : base(api) { }
|
||||
protected override string Method => WebRequestMethods.Http.Post;
|
||||
|
||||
@@ -1,14 +1,55 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using YandexMusic.API.Models.Account;
|
||||
using YandexMusic.API.Requests.Common;
|
||||
|
||||
namespace YandexMusic.API.Requests.Account;
|
||||
|
||||
internal class YGetAuthCookiesBuilder : YAuthRequestBuilder<YAccessToken?, object>
|
||||
internal class YGetAuthCookiesBuilder : YPassportRequestBuilder<YAccessToken?, object>
|
||||
{
|
||||
public YGetAuthCookiesBuilder(YandexMusicApi api) : base(api) { }
|
||||
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 HttpContent? GetContent(object _)
|
||||
=> 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);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
|
||||
|
||||
namespace YandexMusic.API.Requests.Account;
|
||||
|
||||
internal class YGetAuthLetterBuilder : YAuthRequestBuilder<YAuthLetter?, object>
|
||||
internal class YGetAuthLetterBuilder : YPassportRequestBuilder<YAuthLetter?, object>
|
||||
{
|
||||
public YGetAuthLetterBuilder(YandexMusicApi api) : base(api) { }
|
||||
protected override string Method => WebRequestMethods.Http.Post;
|
||||
|
||||
@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
|
||||
|
||||
namespace YandexMusic.API.Requests.Account;
|
||||
|
||||
internal class YGetAuthLoginCaptchaBuilder : YAuthRequestBuilder<YAuthBase?, string>
|
||||
internal class YGetAuthLoginCaptchaBuilder : YPassportRequestBuilder<YAuthBase?, string>
|
||||
{
|
||||
public YGetAuthLoginCaptchaBuilder(YandexMusicApi api) : base(api) { }
|
||||
protected override string Method => WebRequestMethods.Http.Post;
|
||||
|
||||
@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
|
||||
|
||||
namespace YandexMusic.API.Requests.Account;
|
||||
|
||||
internal class YGetAuthLoginLetterBuilder : YAuthRequestBuilder<YAuthLetterStatus?, object>
|
||||
internal class YGetAuthLoginLetterBuilder : YPassportRequestBuilder<YAuthLetterStatus?, object>
|
||||
{
|
||||
public YGetAuthLoginLetterBuilder(YandexMusicApi api) : base(api) { }
|
||||
protected override string Method => WebRequestMethods.Http.Post;
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using YandexMusic.API.Models.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) { }
|
||||
protected override string Method => WebRequestMethods.Http.Post;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
|
||||
|
||||
namespace YandexMusic.API.Requests.Account;
|
||||
|
||||
internal class YGetLoginInfoBuilder : YAuthRequestBuilder<YLoginInfo?, object>
|
||||
internal class YGetLoginInfoBuilder : YPassportRequestBuilder<YLoginInfo?, object>
|
||||
{
|
||||
public YGetLoginInfoBuilder(YandexMusicApi api) : base(api) { }
|
||||
protected override string Method => WebRequestMethods.Http.Get;
|
||||
|
||||
@@ -5,7 +5,7 @@ using YandexMusic.API.Models.Account;
|
||||
|
||||
namespace YandexMusic.API.Requests.Account;
|
||||
|
||||
internal class YGetShortAccountInfoBuilder : YAuthRequestBuilder<YShortAccountInfo?, object>
|
||||
internal class YGetShortAccountInfoBuilder : YPassportRequestBuilder<YShortAccountInfo?, object>
|
||||
{
|
||||
public YGetShortAccountInfoBuilder(YandexMusicApi api) : base(api) { }
|
||||
protected override string Method => WebRequestMethods.Http.Get;
|
||||
|
||||
@@ -4,7 +4,7 @@ using YandexMusic.API.Models.Account;
|
||||
|
||||
namespace YandexMusic.API.Requests.Account;
|
||||
|
||||
internal class YPostAuthStats : YAuthRequestBuilder<YAuthEmpty?, object>
|
||||
internal class YPostAuthStats : YPassportRequestBuilder<YAuthEmpty?, object>
|
||||
{
|
||||
public YPostAuthStats(YandexMusicApi api) : base(api) { }
|
||||
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" } });
|
||||
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
||||
{
|
||||
headers.Add("X-Csrf-Token", Api.Storage.AuthToken.CsfrToken);
|
||||
headers.Add("Process-Uuid", Api.Storage.AuthToken.ProcessUuid);
|
||||
headers.Add("X-Csrf-Token", Api.Storage.HeaderToken.CsfrToken);
|
||||
headers.Add("Process-Uuid", Api.Storage.HeaderToken.ProcessUuid);
|
||||
}
|
||||
}
|
||||
@@ -12,5 +12,8 @@ internal class YGetArtistTrackBuilder : YMusicRequestBuilder<YTracksPage?, (stri
|
||||
protected override Dictionary<string, string> GetSubstitutions((string id, int page, int pageSize) tuple)
|
||||
=> new() { { "artistId", tuple.id } };
|
||||
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() },
|
||||
};
|
||||
}
|
||||
@@ -12,5 +12,6 @@ internal class YConstants
|
||||
{
|
||||
public const string MusicUrl = "https://api.music.yandex.net";
|
||||
public const string PassportUrl = "https://passport.yandex.ru/";
|
||||
public const string MobilePassportUrl = "https://mobileproxy.passport.yandex.net";
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,17 @@ namespace YandexMusic.API.Requests.Common;
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
@@ -43,6 +53,8 @@ internal abstract class YJsonRequestBuilder<TResponse, TParams> : YRequestBuilde
|
||||
}
|
||||
}
|
||||
|
||||
protected string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет запрос и возвращает десериализованный объект типа TResponse.
|
||||
/// </summary>
|
||||
@@ -51,4 +63,4 @@ internal abstract class YJsonRequestBuilder<TResponse, TParams> : YRequestBuilde
|
||||
using var response = await ExecuteRawAsync(parameters);
|
||||
return await DeserializeAsync(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,20 @@ using YandexMusic.API.Requests.Common;
|
||||
namespace YandexMusic.API.Requests;
|
||||
|
||||
/// <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 YAuthRequestBuilder(YandexMusicApi api) : base(api) { }
|
||||
protected YPassportRequestBuilder(YandexMusicApi api) : base(api) { }
|
||||
|
||||
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
||||
{
|
||||
base.SetCustomHeaders(headers);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using YandexMusic.API.Common;
|
||||
@@ -23,7 +21,8 @@ internal abstract class YRequestBuilder<TParams>
|
||||
/// <summary>Шаблон пути (может содержать плейсхолдеры вида {id}).</summary>
|
||||
protected abstract string PathTemplate { get; }
|
||||
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
/// <summary>Определяет, нужно ли добавлять заголовок Authorization для этого запроса.</summary>
|
||||
protected virtual bool ShouldAddAuthorization => true;
|
||||
|
||||
/// <summary>Основной экземпляр API.</summary>
|
||||
protected YandexMusicApi Api { get; }
|
||||
@@ -34,12 +33,6 @@ internal abstract class YRequestBuilder<TParams>
|
||||
protected YRequestBuilder(YandexMusicApi api)
|
||||
{
|
||||
Api = api;
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||
};
|
||||
}
|
||||
|
||||
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.AcceptEncoding), "gzip");
|
||||
if (!string.IsNullOrEmpty(Storage.Token))
|
||||
if (ShouldAddAuthorization && !string.IsNullOrEmpty(Storage.Token))
|
||||
msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.Authorization), $"OAuth {Storage.Token}");
|
||||
SetCustomHeaders(msg.Headers);
|
||||
return msg;
|
||||
@@ -120,8 +113,6 @@ internal abstract class YRequestBuilder<TParams>
|
||||
protected virtual HttpContent? GetContent(TParams parameters) => null;
|
||||
protected virtual void SetCustomHeaders(HttpRequestHeaders headers) { }
|
||||
|
||||
protected string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
|
||||
|
||||
/// <summary>Выполняет запрос и возвращает десериализованный ответ.</summary>
|
||||
public async Task<HttpResponseMessage?> ExecuteRawAsync(TParams parameters)
|
||||
{
|
||||
|
||||
47
YandexMusic.API/Requests/Common/YXmlRequestBuilder.cs
Normal file
47
YandexMusic.API/Requests/Common/YXmlRequestBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
24
YandexMusic.API/Requests/Passport/YCheckPushCodeBuilder.cs
Normal file
24
YandexMusic.API/Requests/Passport/YCheckPushCodeBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
19
YandexMusic.API/Requests/Passport/YCheckSessionBuilder.cs
Normal file
19
YandexMusic.API/Requests/Passport/YCheckSessionBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
13
YandexMusic.API/Requests/Passport/YCreateTrackBuilder.cs
Normal file
13
YandexMusic.API/Requests/Passport/YCreateTrackBuilder.cs
Normal 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", "" } });
|
||||
}
|
||||
22
YandexMusic.API/Requests/Passport/YGetAuthLoginQRBuilder.cs
Normal file
22
YandexMusic.API/Requests/Passport/YGetAuthLoginQRBuilder.cs
Normal 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 }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,21 @@ using System.Net.Http.Headers;
|
||||
using YandexMusic.API.Models.Account;
|
||||
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) { }
|
||||
protected override string BaseUrl => YConstants.Endpoints.MobilePassportUrl;
|
||||
protected override string Method => WebRequestMethods.Http.Post;
|
||||
protected override string PathTemplate => "/1/token";
|
||||
protected override HttpContent? GetContent(object _)
|
||||
protected override HttpContent? GetContent(string passportToken)
|
||||
=> new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
{ "client_id", YConstants.ClientId },
|
||||
{ "client_secret", YConstants.ClientSecret },
|
||||
{ "grant_type", "x-token" },
|
||||
{ "access_token", Api.Storage.AccessToken.AccessToken }
|
||||
{ "access_token", passportToken }
|
||||
});
|
||||
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
||||
{
|
||||
17
YandexMusic.API/Requests/Passport/YGetQrStatus.cs
Normal file
17
YandexMusic.API/Requests/Passport/YGetQrStatus.cs
Normal 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,
|
||||
});
|
||||
}
|
||||
20
YandexMusic.API/Requests/Passport/YGetSessionBuilder.cs
Normal file
20
YandexMusic.API/Requests/Passport/YGetSessionBuilder.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
35
YandexMusic.API/Requests/Passport/YMultistepStartBuilder.cs
Normal file
35
YandexMusic.API/Requests/Passport/YMultistepStartBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
23
YandexMusic.API/Requests/Passport/YRfcOtpBuilder.cs
Normal file
23
YandexMusic.API/Requests/Passport/YRfcOtpBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
26
YandexMusic.API/Requests/Passport/YSendPushBuilder.cs
Normal file
26
YandexMusic.API/Requests/Passport/YSendPushBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
23
YandexMusic.API/Requests/Passport/YSuggestByPhoneBuilder.cs
Normal file
23
YandexMusic.API/Requests/Passport/YSuggestByPhoneBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -6,27 +6,30 @@ using YandexMusic.API.Requests.Common;
|
||||
namespace YandexMusic.API.Requests.Track;
|
||||
|
||||
/// <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) { }
|
||||
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 PathTemplate => "{src}";
|
||||
protected override string PathTemplate => "";
|
||||
|
||||
protected override Dictionary<string, string> GetSubstitutions(string src)
|
||||
=> new() { { "src", src.Split('?')[0] } };
|
||||
=> new() { { "src", src } };
|
||||
|
||||
protected override NameValueCollection GetQueryParams(string src)
|
||||
{
|
||||
var query = new NameValueCollection { { "format", "json" } };
|
||||
var query = new NameValueCollection();
|
||||
var parts = src.Split('?');
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
foreach (var param in parts[1].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;
|
||||
|
||||
@@ -8,11 +8,11 @@ namespace YandexMusic.API.Requests.Ugc;
|
||||
internal class YUgcUploadBuilder : YJsonRequestBuilder<YResponse<string>?, (string postTargetLink, byte[] fileBytes)>
|
||||
{
|
||||
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 PathTemplate => "{postTargetLink}";
|
||||
protected override string PathTemplate => "";
|
||||
|
||||
protected override Dictionary<string, string> GetSubstitutions((string postTargetLink, byte[] fileBytes) tuple)
|
||||
=> new() { { "postTargetLink", tuple.postTargetLink } };
|
||||
|
||||
@@ -6,9 +6,9 @@ namespace YandexMusic.API;
|
||||
public class YandexMusicApi
|
||||
{
|
||||
/// <summary>HttpClient, используемый для всех запросов.</summary>
|
||||
public HttpClient HttpClient { get; }
|
||||
internal HttpClient HttpClient { get; }
|
||||
/// <summary>Хранилище данных авторизации.</summary>
|
||||
public AuthStorage Storage { get; }
|
||||
internal AuthStorage Storage { get; }
|
||||
|
||||
/// <summary>API для работы с альбомами.</summary>
|
||||
public YAlbumAPI Album { get; internal set; } = null!;
|
||||
@@ -33,11 +33,13 @@ public class YandexMusicApi
|
||||
/// <summary>API для работы с очередями.</summary>
|
||||
public YQueueAPI Queue { get; internal set; } = null!;
|
||||
/// <summary>API для работы с пользователем и авторизацией.</summary>
|
||||
public YUserAPI User { get; internal set; } = null!;
|
||||
public YAuthAPI Auth { get; internal set; } = null!;
|
||||
/// <summary>API для загрузки пользовательского контента.</summary>
|
||||
public YUgcAPI UserGeneratedContent { get; internal set; } = null!;
|
||||
/// <summary>API для работы с протоколом Ynison (WebSocket).</summary>
|
||||
public YYnisonAPI Ynison { get; internal set; } = null!;
|
||||
/// <summary>API для работы с яндекс пасспорт.</summary>
|
||||
public YPassportAPI Passport { get; internal set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр API.
|
||||
@@ -49,19 +51,20 @@ public class YandexMusicApi
|
||||
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
Storage = storage ?? throw new ArgumentNullException(nameof(storage));
|
||||
|
||||
Album = new YAlbumAPI(this);
|
||||
Artist = new YArtistAPI(this);
|
||||
Label = new YLabelAPI(this);
|
||||
Landing = new YLandingAPI(this);
|
||||
Library = new YLibraryAPI(this);
|
||||
Playlist = new YPlaylistAPI(this);
|
||||
Pins = new YPinsAPI(this);
|
||||
Radio = new YRadioAPI(this);
|
||||
Search = new YSearchAPI(this);
|
||||
Track = new YTrackAPI(this);
|
||||
Queue = new YQueueAPI(this);
|
||||
User = new YUserAPI(this);
|
||||
UserGeneratedContent = new YUgcAPI(this);
|
||||
Ynison = new YYnisonAPI(this);
|
||||
Album = new(this);
|
||||
Artist = new(this);
|
||||
Label = new(this);
|
||||
Landing = new(this);
|
||||
Library = new(this);
|
||||
Playlist = new(this);
|
||||
Pins = new(this);
|
||||
Radio = new(this);
|
||||
Search = new(this);
|
||||
Track = new(this);
|
||||
Queue = new(this);
|
||||
Auth = new(this);
|
||||
UserGeneratedContent = new(this);
|
||||
Ynison = new(this);
|
||||
Passport = new(this);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<Solution>
|
||||
<Project Path="YaMusicCli/YaMusicCli.csproj" Id="5cce354e-7517-4a94-9584-197daa3ad6a4" />
|
||||
<Project Path="YandexMusic.API/YandexMusic.API.csproj" />
|
||||
<Project Path="YandexMusic/YandexMusic.csproj" Id="044fcef4-86d2-4cc9-9f7e-a577c19ae5c3" />
|
||||
<Project Path="YandexMusic/YandexMusic.csproj" />
|
||||
</Solution>
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
using YandexMusic.API;
|
||||
using System.Net;
|
||||
using YandexMusic.API;
|
||||
using YandexMusic.API.Common;
|
||||
using YandexMusic.API.Common.Ynison;
|
||||
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;
|
||||
|
||||
@@ -23,7 +12,6 @@ public class YandexMusicClient : IDisposable
|
||||
private readonly YandexMusicApi _api;
|
||||
private readonly AuthStorage _storage;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly bool _ownsHttpClient;
|
||||
private YnisonPlayer? _player;
|
||||
|
||||
/// <summary>Хранилище авторизации.</summary>
|
||||
@@ -41,363 +29,49 @@ public class YandexMusicClient : IDisposable
|
||||
/// <summary>HttpClient, используемый клиентом (можно получить для настройки кук).</summary>
|
||||
public HttpClient HttpClient => _httpClient;
|
||||
|
||||
/// <summary>Создаёт новый экземпляр клиента с собственным HttpClient.</summary>
|
||||
public YandexMusicClient() : this(YandexMusicHttpClientFactory.CreateDefault())
|
||||
{
|
||||
_ownsHttpClient = true;
|
||||
}
|
||||
/// <summary>API Яндекс Музыки.</summary>
|
||||
public YandexMusicApi Api => _api;
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт новый экземпляр клиента с указанным HttpClient.
|
||||
/// </summary>
|
||||
/// <param name="httpClient">Экземпляр HttpClient (должен быть настроен с нужными куками, таймаутами).</param>
|
||||
/// <param name="ownsHttpClient">Если true, клиент будет отвечать за освобождение HttpClient при Dispose.</param>
|
||||
public YandexMusicClient(HttpClient httpClient)
|
||||
/// <summary>Создаёт новый экземпляр клиента с собственным HttpClient.</summary>
|
||||
public YandexMusicClient(
|
||||
CookieContainer? cookieContainer = null,
|
||||
IWebProxy? proxy = null,
|
||||
TimeSpan? timeout = null,
|
||||
string? userAgent = null
|
||||
)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_storage = new AuthStorage();
|
||||
if (cookieContainer == null) cookieContainer = new CookieContainer();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#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()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_httpClient?.Dispose();
|
||||
_player?.Dispose();
|
||||
if (_ownsHttpClient)
|
||||
_httpClient.Dispose();
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user