12 Commits

Author SHA1 Message Date
FrigaT
8c5dca1491 Добавлен парсинг EnumMember
All checks were successful
Release / pack-and-publish (release) Successful in 32s
2026-04-23 17:16:18 +03:00
815283a776 readme 2026-04-21 12:51:23 +03:00
526353d679 Переделано воспроизведение аудио
All checks were successful
Release / pack-and-publish (release) Successful in 36s
2026-04-21 11:14:36 +03:00
eb1eba0162 Полный рефакторинг api. Вынесено отдельно api passport 2026-04-20 23:30:01 +03:00
FrigaT
34261d02a9 Авторизация через паспорт
All checks were successful
Release / pack-and-publish (release) Successful in 31s
2026-04-20 15:58:27 +03:00
FrigaT
5f761d4fe8 Добавлена авторизация через паспорт 2026-04-20 15:56:38 +03:00
FrigaT
b6f78da9c8 dispose
All checks were successful
Release / pack-and-publish (release) Successful in 37s
2026-04-20 14:47:43 +03:00
FrigaT
0bbaac5689 Переделан способ авторизации по qr
All checks were successful
Release / pack-and-publish (release) Successful in 1m5s
2026-04-20 14:31:47 +03:00
FrigaT
a7caf829d3 Открыл AuthToken
All checks were successful
Release / pack-and-publish (release) Successful in 34s
2026-04-19 20:19:41 +03:00
FrigaT
add7f08215 Добавлен вывод AuthStorage. Спрятаны внутренние api запросы
All checks were successful
Release / pack-and-publish (release) Successful in 32s
2026-04-19 17:41:30 +03:00
FrigaT
36e28ce3fe Полностью переписанное api
All checks were successful
Release / pack-and-publish (release) Successful in 36s
2026-04-19 17:00:05 +03:00
FrigaT
5541d0ad27 fix Artist api
All checks were successful
Release / pack-and-publish (release) Successful in 40s
2026-04-16 18:43:07 +03:00
161 changed files with 5055 additions and 3729 deletions

607
ARCHITECTURE.md Normal file
View File

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

410
CONTRIBUTING.md Normal file
View File

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

446
FAQ.md Normal file
View File

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

536
QUICKSTART.md Normal file
View File

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

437
README.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,307 +1,110 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Common; using YandexMusic.API.Models.Common;
using YandexMusic.API.Models.Track; using YandexMusic.API.Models.Track;
using YandexMusic.API.Requests.Track; using YandexMusic.API.Requests.Track;
namespace YandexMusic.API; namespace YandexMusic.API;
/// <summary> /// <summary>API для работы с треками (получение, загрузка, метаданные).</summary>
/// API для взаимодействия с треками public class YTrackAPI : YCommonAPI
/// </summary>
public partial class YTrackAPI : YCommonAPI
{ {
#region Вспомогательные функции public YTrackAPI(YandexMusicApi api) : base(api) { }
private string BuildLinkForDownload(YTrackDownloadInfo mainDownloadResponse, YStorageDownloadFile storageDownload) private static string BuildDownloadLink(YTrackDownloadInfo info, YStorageDownloadFile storageDownload)
{ {
string path = storageDownload.Path; var path = storageDownload.Path;
string host = storageDownload.Host; var host = storageDownload.Host;
string ts = storageDownload.Ts; var ts = storageDownload.Ts;
string s = storageDownload.S; var s = storageDownload.S;
string codec = mainDownloadResponse.Codec; var codec = info.Codec;
string secret = $"XGRlBW9FXlekgbPrRHuSiA{path.Substring(1, path.Length - 1)}{s}"; var secret = $"XGRlBW9FXlekgbPrRHuSiA{path[1..]}{s}";
MD5 md5 = MD5.Create(); var md5Hash = MD5.HashData(Encoding.UTF8.GetBytes(secret));
byte[] md5Hash = md5.ComputeHash(Encoding.UTF8.GetBytes(secret)); var hmacsha1 = new HMACSHA1(md5Hash);
HMACSHA1 hmacsha1 = new(); var sign = BitConverter.ToString(hmacsha1.ComputeHash(md5Hash)).Replace("-", "").ToLower();
byte[] hmasha1Hash = hmacsha1.ComputeHash(md5Hash); return $"https://{host}/get-{codec}/{sign}/{ts}{path}";
string sign = BitConverter.ToString(hmasha1Hash).Replace("-", "").ToLower();
string link = $"https://{host}/get-{codec}/{sign}/{ts}{path}";
return link;
} }
#endregion Вспомогательные функции public async Task<YTrack?> GetAsync(string trackId)
=> (await GetAsync([trackId]))?.FirstOrDefault();
public Task<List<YTrack>?> GetAsync(IEnumerable<string> trackIds)
=> new YGetTracksBuilder(Api).ExecuteAsync(trackIds);
public Task<List<YTrackDownloadInfo>?> GetMetadataForDownloadAsync(string trackKey, bool direct = false)
=> new YTrackDownloadInfoBuilder(Api).ExecuteAsync((trackKey, direct));
public Task<List<YTrackDownloadInfo>?> GetMetadataForDownloadAsync(YTrack track, bool direct = false)
=> GetMetadataForDownloadAsync(track.GetKey().ToString(), direct);
public YTrackAPI(YandexMusicApi yandex) : base(yandex) public Task<YStorageDownloadFile?> GetDownloadFileInfoAsync(YTrackDownloadInfo metadataInfo)
=> new YStorageDownloadFileBuilder(Api).ExecuteAsync(metadataInfo.DownloadInfoUrl);
public async Task<string?> GetFileLinkAsync(string trackKey)
{ {
var meta = await GetMetadataForDownloadAsync(trackKey);
var info = meta?.OrderByDescending(i => i.BitrateInKbps).FirstOrDefault(m => m.Codec == "mp3");
if (info == null) return null;
var storageDownload = await GetDownloadFileInfoAsync(info);
if (storageDownload == null) return null;
return BuildDownloadLink(info, storageDownload);
} }
/// <summary> public Task<string?> GetFileLinkAsync(YTrack track)
/// Получение треков => GetFileLinkAsync(track.GetKey().ToString());
/// </summary>
/// <param name="storage">Хранилище</param> public async Task ExtractToFileAsync(string trackKey, string filePath)
/// <param name="trackId">Идентификатор трека</param>
/// <returns></returns>
public Task<YResponse<List<YTrack>>> GetAsync(AuthStorage storage, string trackId)
{ {
return new YGetTracksBuilder(api, storage) var url = await GetFileLinkAsync(trackKey);
.Build(new[] { trackId }) if (string.IsNullOrEmpty(url)) throw new Exception("Не удалось получить ссылку на трек");
.GetResponseAsync(); using var response = await Api.HttpClient.GetAsync(url);
await using var fs = File.Create(filePath);
await response.Content.CopyToAsync(fs);
} }
/// <summary> public Task ExtractToFileAsync(YTrack track, string filePath)
/// Получение треков => ExtractToFileAsync(track.GetKey().ToString(), filePath);
/// </summary>
/// <param name="storage">Хранилище</param> public async Task<byte[]> ExtractDataAsync(string trackKey)
/// <param name="trackIds">Идентификаторы треков</param>
/// <returns></returns>
public Task<YResponse<List<YTrack>>> GetAsync(AuthStorage storage, IEnumerable<string> trackIds)
{ {
return new YGetTracksBuilder(api, storage) var url = await GetFileLinkAsync(trackKey);
.Build(trackIds) if (string.IsNullOrEmpty(url)) throw new Exception("Не удалось получить ссылку на трек");
.GetResponseAsync(); return await Api.HttpClient.GetByteArrayAsync(url);
} }
/// <summary> public Task<byte[]> ExtractDataAsync(YTrack track)
/// Получение метаданных для загрузки => ExtractDataAsync(track.GetKey().ToString());
/// </summary>
/// <param name="storage">Хранилище</param> public async Task<Stream> ExtractStreamAsync(string trackKey, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
/// <param name="trackKey">Ключ трека в формате {идентифактор трека:идентификатор альбома}</param>
/// <param name="direct">Должен ли ответ содержать прямую ссылку на загрузку</param>
/// <returns></returns>
public Task<YResponse<List<YTrackDownloadInfo>>> GetMetadataForDownloadAsync(AuthStorage storage, string trackKey, bool direct = false)
{ {
return new YTrackDownloadInfoBuilder(api, storage) var url = await GetFileLinkAsync(trackKey);
.Build((trackKey, direct)) if (string.IsNullOrEmpty(url)) throw new Exception("Не удалось получить ссылку на трек");
.GetResponseAsync(); var response = await Api.HttpClient.GetAsync(url, completionOption);
return await response.Content.ReadAsStreamAsync();
} }
/// <summary> public Task<Stream> ExtractStreamAsync(YTrack track, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
/// Получение метаданных для загрузки => ExtractStreamAsync(track.GetKey().ToString(), completionOption);
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="track">Трек</param>
/// <param name="direct">Должен ли ответ содержать прямую ссылку на загрузку</param>
/// <returns></returns>
public Task<YResponse<List<YTrackDownloadInfo>>> GetMetadataForDownloadAsync(AuthStorage storage, YTrack track, bool direct = false)
{
return GetMetadataForDownloadAsync(storage, track.GetKey().ToString(), direct);
}
/// <summary> public Task<string?> SendPlayTrackInfoAsync(
/// Получение информации для формирования ссылки для загрузки YTrack track,
/// </summary> string from,
/// <param name="storage">Хранилище</param> bool fromCache = false,
/// <param name="metadataInfo">Метаданные для загрузки</param> string playId = "",
/// <returns></returns> string playlistId = "",
public Task<YStorageDownloadFile> GetDownloadFileInfoAsync(AuthStorage storage, YTrackDownloadInfo metadataInfo) double totalPlayedSeconds = 0,
{ double endPositionSeconds = 0)
return new YStorageDownloadFileBuilder(api, storage) => new YSendTrackInfoBuilder(Api).ExecuteAsync((track, from, fromCache, playId, playlistId, totalPlayedSeconds, endPositionSeconds));
.Build(metadataInfo.DownloadInfoUrl)
.GetResponseAsync();
}
/// <summary> public Task<YTrackSupplement?> GetSupplementAsync(string trackId)
/// Получение ссылки для загрузки => new YGetTrackSupplementBuilder(Api).ExecuteAsync(trackId);
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
/// <returns></returns>
public async Task<string> GetFileLinkAsync(AuthStorage storage, string trackKey)
{
YResponse<List<YTrackDownloadInfo>> meta = await GetMetadataForDownloadAsync(storage, trackKey);
YTrackDownloadInfo info = meta.Result
.OrderByDescending(i => i.BitrateInKbps)
.First(m => m.Codec == "mp3");
YStorageDownloadFile storageDownload = await GetDownloadFileInfoAsync(storage, info);
return BuildLinkForDownload(info, storageDownload);
}
/// <summary> public Task<YTrackSupplement?> GetSupplementAsync(YTrack track)
/// Получение ссылки для загрузки => GetSupplementAsync(track.GetKey().ToString());
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="track">Трек</param>
/// <returns></returns>
public Task<string> GetFileLinkAsync(AuthStorage storage, YTrack track)
{
return GetFileLinkAsync(storage, track.GetKey().ToString());
}
/// <summary>
/// Отправка текущего состояния прослушиваемого трека
/// <param name="storage">Хранилище</param>
/// <param name="track">Трек</param>
/// <param name="from">Наименования клиента, с которого происходит прослушивание</param>
/// <param name="fromCache">Проигрывается ли трек с кеша</param>
/// <param name="playId">Уникальный идентификатор проигрывания</param>
/// <param name="playlistId">Уникальный идентификатор плейлиста, если таковой прослушивается</param>
/// <param name="totalPlayedSeconds">Сколько было всего воспроизведено трека в секундах</param>
/// <param name="endPositionSeconds">Окончательное значение воспроизведенных секунд</param>
/// </summary>
/// <returns></returns>
public Task<string> SendPlayTrackInfoAsync(AuthStorage storage, YTrack track, string from, bool fromCache = false, string playId = "", string playlistId = "", double totalPlayedSeconds = 0, double endPositionSeconds = 0)
{
return new YSendTrackInfoBuilder(api, storage)
.Build((track, from, fromCache, playId, playlistId, totalPlayedSeconds, endPositionSeconds))
.GetResponseAsync();
}
#region GetSupplement
/// <summary>
/// Получение дополнительной информации для трека
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="trackId">Идентификатор трека</param>
/// <returns></returns>
public Task<YResponse<YTrackSupplement>> GetSupplementAsync(AuthStorage storage, string trackId)
{
return new YGetTrackSupplementBuilder(api, storage)
.Build(trackId)
.GetResponseAsync();
}
/// <summary>
/// Получение дополнительной информации для трека
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="track">Трек</param>
/// <returns></returns>
public Task<YResponse<YTrackSupplement>> GetSupplementAsync(AuthStorage storage, YTrack track)
{
return new YGetTrackSupplementBuilder(api, storage)
.Build(track.GetKey().ToString())
.GetResponseAsync();
}
#endregion GetSupplement
#region GetSimilar
/// <summary>
/// Получение похожих треков
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="trackId">Идентификатор трека</param>
/// <returns></returns>
public Task<YResponse<YTrackSimilar>> GetSimilarAsync(AuthStorage storage, string trackId)
{
return new YGetTrackSimilarBuilder(api, storage)
.Build(trackId)
.GetResponseAsync();
}
/// <summary>
/// Получение похожих треков
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="track">Трек</param>
/// <returns></returns>
public Task<YResponse<YTrackSimilar>> GetSimilarAsync(AuthStorage storage, YTrack track)
{
return new YGetTrackSimilarBuilder(api, storage)
.Build(track.GetKey().ToString())
.GetResponseAsync();
}
#endregion GetSimilar
#region Получение данных трека
#region В файл
/// <summary>
/// Выгрузка в файл
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
/// <param name="filePath">Путь для файла</param>
public async Task ExtractToFileAsync(AuthStorage storage, string trackKey, string filePath)
{
string url = await GetFileLinkAsync(storage, trackKey);
await new DataDownloader(storage).ToFile(url, filePath);
}
/// <summary>
/// Выгрузка в файл
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="track">Трек</param>
/// <param name="filePath">Путь для файла</param>
public Task ExtractToFileAsync(AuthStorage storage, YTrack track, string filePath)
{
return ExtractToFileAsync(storage, track.GetKey().ToString(), filePath);
}
#endregion В файл
#region В массив байт
/// <summary>
/// Получение двоичного массива данных
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
/// <returns></returns>
public async Task<byte[]> ExtractDataAsync(AuthStorage storage, string trackKey)
{
string url = await GetFileLinkAsync(storage, trackKey);
return await new DataDownloader(storage).AsBytes(url);
}
/// <summary>
/// Получение двоичного массива данных
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="track">Трек</param>
/// <returns></returns>
public Task<byte[]> ExtractDataAsync(AuthStorage storage, YTrack track)
{
return ExtractDataAsync(storage, track.GetKey().ToString());
}
#endregion В массив байт
#region В поток
/// <summary>
/// Получение потока данных
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
/// <param name="httpCompletionOption">Параметры передачи управления при http запросе</param>
/// <returns></returns>
public async Task<Stream> ExtractStreamAsync(AuthStorage storage, string trackKey,
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
{
string url = await GetFileLinkAsync(storage, trackKey);
return await new DataDownloader(storage).AsStream(url, httpCompletionOption);
}
/// <summary>
/// Получение потока данных
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="track">Трек</param>
/// <param name="httpCompletionOption">Параметры передачи управления при http запросе</param>
/// <returns></returns>
public Task<Stream> ExtractStreamAsync(AuthStorage storage, YTrack track,
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
{
return ExtractStreamAsync(storage, track.GetKey().ToString(), httpCompletionOption);
}
#endregion В поток
#endregion Получение данных трека
public Task<YTrackSimilar?> GetSimilarAsync(string trackId)
=> new YGetTrackSimilarBuilder(Api).ExecuteAsync(trackId);
public Task<YTrackSimilar?> GetSimilarAsync(YTrack track)
=> GetSimilarAsync(track.GetKey().ToString());
} }

View File

@@ -1,71 +1,36 @@
using YandexMusic.API.Common; using YandexMusic.API.Models.Playlist;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Models.Playlist;
using YandexMusic.API.Models.Ugc; using YandexMusic.API.Models.Ugc;
using YandexMusic.API.Requests.Ugc; using YandexMusic.API.Requests.Ugc;
namespace YandexMusic.API; namespace YandexMusic.API;
public partial class YUgcAPI : YCommonAPI /// <summary>API для загрузки пользовательского контента (UGC).</summary>
public class YUgcAPI : YCommonAPI
{ {
public YUgcAPI(YandexMusicApi yandex) : base(yandex) public YUgcAPI(YandexMusicApi api) : base(api) { }
{
}
/// <summary> public Task<YUgcUpload?> GetUgcUploadLinkAsync(YPlaylist playlist, string fileName)
/// Получение ссылки на загрузчик трека => new YUgcGetUploadLinkBuilder(Api).ExecuteAsync((playlist, fileName));
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="playlist">Плейлист, куда будет загружен трек</param>
/// <param name="fileName">Название файла для загрузки</param>
public Task<YUgcUpload> GetUgcUploadLinkAsync(AuthStorage storage, YPlaylist playlist, string fileName)
{
return new YUgcGetUploadLinkBuilder(api, storage)
.Build((playlist, fileName))
.GetResponseAsync();
}
/// <summary> public async Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, string filePath)
/// Загрузка трека из файла
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="uploadLink">Ссылка на балансировщик для загрузки, можно получить из GetUgcUploadLinkAsync</param>
/// <param name="filePath">Загружаемый файл</param>
public Task<YResponse<string>> UploadUgcTrackAsync(AuthStorage storage, string uploadLink, string filePath)
{ {
if (!File.Exists(filePath)) if (!File.Exists(filePath))
throw new FileNotFoundException("Файл для загрузки не существует.", filePath); throw new FileNotFoundException("Файл не найден", filePath);
return await UploadTrackToPlaylistAsync(playlist, fileName, await File.ReadAllBytesAsync(filePath));
return UploadUgcTrackAsync(storage, uploadLink, File.Open(filePath, FileMode.Open));
} }
/// <summary> public async Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, Stream stream)
/// Загрузка трека из потока
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="uploadLink">Ссылка на балансировщик для загрузки, можно получить из GetUgcUploadLinkAsync</param>
/// <param name="stream">Поток с данными для загрузки</param>
public Task<YResponse<string>> UploadUgcTrackAsync(AuthStorage storage, string uploadLink, Stream stream)
{ {
if (stream == null) using var ms = new MemoryStream();
throw new NullReferenceException("Пустая ссылка на поток загрузки."); await stream.CopyToAsync(ms);
return await UploadTrackToPlaylistAsync(playlist, fileName, ms.ToArray());
using MemoryStream ms = new();
stream.CopyTo(ms);
return UploadUgcTrackAsync(storage, uploadLink, ms.ToArray());
} }
/// <summary> public async Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, byte[] file)
/// Загрузка трека из массива
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="uploadLink">Ссылка на балансировщик для загрузки, можно получить из GetUgcUploadLinkAsync</param>
/// <param name="file">Загружаемый трек в виде массив байтов</param>
public Task<YResponse<string>> UploadUgcTrackAsync(AuthStorage storage, string uploadLink, byte[] file)
{ {
return new YUgcUploadBuilder(api, storage) var uploadLink = await GetUgcUploadLinkAsync(playlist, fileName);
.Build((uploadLink, file)) if (uploadLink?.PostTarget == null) return null;
.GetResponseAsync(); var result = await new YUgcUploadBuilder(Api).ExecuteAsync((uploadLink.PostTarget, file));
return result?.Result;
} }
} }

View File

@@ -1,300 +0,0 @@
using System.Security.Authentication;
using System.Text.RegularExpressions;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Account;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Requests.Account;
namespace YandexMusic.API;
/// <summary>
/// API для пользователя
/// </summary>
public partial class YUserAPI : YCommonAPI
{
#region Вспомогательные функции
private async Task<bool> GetCsrfTokenAsync(AuthStorage storage)
{
using HttpResponseMessage authMethodsResponse = await new YGetAuthMethodsBuilder(api, storage)
.Build(null)
.GetResponseAsync();
if (!authMethodsResponse.IsSuccessStatusCode)
throw new HttpRequestException("Невозможно получить CFRF-токен.");
string responseString = await authMethodsResponse.Content
.ReadAsStringAsync();
Match match = Regex.Match(responseString, "\"csrf_token\" value=\"([^\"]+)\"");
if (!match.Success || match.Groups.Count < 2)
return false;
storage.AuthToken = new YAuthToken
{
CsfrToken = match.Groups[1].Value
};
return true;
}
private async Task<bool> LoginByCookiesAsync(AuthStorage storage)
{
if (storage.AuthToken == null)
throw new AuthenticationException("Невозможно инициализировать сессию входа.");
YAccessToken accessToken = await new YGetAuthCookiesBuilder(api, storage)
.Build(null)
.GetResponseAsync();
storage.IsAuthorized = !string.IsNullOrEmpty(accessToken.AccessToken);
storage.AccessToken = accessToken;
storage.Token = accessToken.AccessToken;
YShortAccountInfo validateTokenResponse = await new YGetShortAccountInifoBuilder(api, storage)
.Build(null)
.GetResponseAsync();
if (validateTokenResponse.Status != YAuthStatus.Ok)
throw new Exception("Вход в аккаунт не выполнен.");
storage.IsAuthorized = !string.IsNullOrWhiteSpace(validateTokenResponse.Uid);
return storage.IsAuthorized;
}
#endregion Вспомогательные функции
public YUserAPI(YandexMusicApi yandex) : base(yandex)
{
}
/// <summary>
/// Авторизация
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="token">Токен авторизации</param>
/// <returns></returns>
public async Task AuthorizeAsync(AuthStorage storage, string token)
{
if (string.IsNullOrEmpty(token))
throw new Exception("Задан пустой токен авторизации.");
storage.Token = token;
// Пытаемся получить информацию о пользователе
YResponse<YAccountResult> authInfo = await GetUserAuthAsync(storage);
// Если не авторизован, то авторизуем
if (string.IsNullOrEmpty(authInfo.Result.Account.Uid))
throw new Exception("Пользователь незалогинен.");
// Флаг авторизации
storage.IsAuthorized = true;
storage.User = authInfo.Result.Account;
}
/// <summary>
/// Получение информации об авторизации
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public Task<YResponse<YAccountResult>> GetUserAuthAsync(AuthStorage storage)
{
return new YGetAuthInfoBuilder(api, storage)
.Build(null)
.GetResponseAsync();
}
/// <summary>
/// Создание сеанса и получение доступных методов авторизации
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="userName">Имя пользователя</param>
/// <returns></returns>
public async Task<YAuthTypes> CreateAuthSessionAsync(AuthStorage storage, string userName)
{
if (!await GetCsrfTokenAsync(storage))
throw new Exception("Невозможно инициализировать сессию входа.");
YAuthTypes types = await new YGetAuthLoginUserBuilder(api, storage)
.Build((storage.AuthToken.CsfrToken, userName))
.GetResponseAsync();
storage.AuthToken.TrackId = types.TrackId;
return types;
}
/// <summary>
/// Получение ссылки на QR-код
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public async Task<string> GetAuthQRLinkAsync(AuthStorage storage)
{
if (!await GetCsrfTokenAsync(storage))
throw new Exception("Невозможно инициализировать сессию входа.");
YAuthQR result = await new YGetAuthQRBuilder(api, storage)
.Build(null)
.GetResponseAsync();
if (result.Status != YAuthStatus.Ok)
return string.Empty;
storage.AuthToken = new YAuthToken
{
TrackId = result.TrackId,
CsfrToken = result.CsrfToken
};
return $"https://passport.yandex.ru/auth/magic/code/?track_id={result.TrackId}";
}
/// <summary>
/// Авторизация по QR-коду
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public async Task<YAuthQRStatus> AuthorizeByQRAsync(AuthStorage storage)
{
if (storage.AuthToken == null)
throw new Exception("Не выполнен запрос на авторизацию по QR.");
try
{
YAuthQRStatus qrStatus = await new YGetAuthLoginQRBuilder(api, storage)
.Build(null)
.GetResponseAsync();
if (qrStatus.Status != YAuthStatus.Ok)
return qrStatus;
bool ok = await LoginByCookiesAsync(storage);
if (!ok)
throw new AuthenticationException("Ошибка авторизации по QR.");
return qrStatus;
}
catch (Exception ex)
{
throw new AuthenticationException("Ошибка авторизации по QR.", ex);
}
}
/// <summary>
/// Получение <see cref="YAuthCaptcha"/>
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public Task<YAuthCaptcha> GetCaptchaAsync(AuthStorage storage)
{
if (storage.AuthToken == null || string.IsNullOrWhiteSpace(storage.AuthToken.CsfrToken))
throw new AuthenticationException($"Не найдена сессия входа. Выполните {nameof(CreateAuthSessionAsync)} перед использованием.");
return new YGetAuthCaptchaBuilder(api, storage)
.Build(null)
.GetResponseAsync();
}
/// <summary>
/// Авторизация по captcha
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="captchaValue">Значение captcha</param>
/// <returns></returns>
public Task<YAuthBase> AuthorizeByCaptchaAsync(AuthStorage storage, string captchaValue)
{
if (storage.AuthToken == null || string.IsNullOrWhiteSpace(storage.AuthToken.CsfrToken))
throw new AuthenticationException($"Не найдена сессия входа. Выполните {nameof(CreateAuthSessionAsync)} перед использованием.");
return new YGetAuthLoginCaptchaBuilder(api, storage)
.Build(captchaValue)
.GetResponseAsync();
}
/// <summary>
/// Получение письма авторизации на почту пользователя
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public Task<YAuthLetter> GetAuthLetterAsync(AuthStorage storage)
{
return new YGetAuthLetterBuilder(api, storage)
.Build(null)
.GetResponseAsync();
}
/// <summary>
/// Авторизация после подтверждения входа через письмо
/// </summary>
/// <param name="storage">Хранилище</param>
/// <returns></returns>
public async Task<bool> AuthorizeByLetterAsync(AuthStorage storage)
{
YAuthLetterStatus status = await new YGetAuthLoginLetterBuilder(api, storage)
.Build(null)
.GetResponseAsync();
if (status.Status == YAuthStatus.Ok && !status.MagicLinkConfirmed)
throw new Exception("Не подтвержден вход посредством e-mail.");
return await LoginByCookiesAsync(storage);
}
/// <summary>
/// Авторизация с помощью пароля из приложения Яндекс
/// </summary>
/// <param name="storage">Хранилище</param>
/// <param name="password">Пароль</param>
/// <returns></returns>
public async Task<YAuthBase> AuthorizeByAppPasswordAsync(AuthStorage storage, string password)
{
if (storage.AuthToken == null || string.IsNullOrWhiteSpace(storage.AuthToken.CsfrToken))
throw new AuthenticationException($"Не найдена сессия входа. Выполните {nameof(CreateAuthSessionAsync)} перед использованием.");
YAuthBase response = await new YGetAuthAppPasswordBuilder(api, storage)
.Build(password)
.GetResponseAsync();
if (response.Status == YAuthStatus.Ok)
{
bool ok = await LoginByCookiesAsync(storage);
if (!ok)
throw new AuthenticationException("Ошибка авторизации.");
}
return response;
}
/// <summary>
/// Получение <see cref="YAccessToken"/> после авторизации с помощью QR, e-mail, пароля из приложения
/// </summary>
public async Task<YAccessToken> GetAccessTokenAsync(AuthStorage storage)
{
if (storage.AuthToken == null)
throw new Exception("Не найдена сессия входа.");
YAccessToken accessToken = await new YGetMusicTokenBuilder(api, storage)
.Build(null)
.GetResponseAsync();
storage.Token = accessToken.AccessToken;
return accessToken;
}
/// <summary>
/// Получение информации о пользователе через логин Яндекса
/// </summary>
public Task<YLoginInfo> GetLoginInfoAsync(AuthStorage storage)
{
return new YGetLoginInfoBuilder(api, storage)
.Build(null)
.GetResponseAsync();
}
}

View File

@@ -1,21 +1,16 @@
using YandexMusic.API.Common;
using YandexMusic.API.Common.Ynison; using YandexMusic.API.Common.Ynison;
namespace YandexMusic.API; namespace YandexMusic.API;
/// <summary>
/// API Ynison /// <summary>API для работы с Ynison (WebSocket-плеер).</summary>
/// </summary> public class YYnisonAPI : YCommonAPI
public partial class YYnisonAPI : YCommonAPI
{ {
public YYnisonAPI(YandexMusicApi yandex) : base(yandex) public YYnisonAPI(YandexMusicApi api) : base(api) { }
{
}
public YnisonPlayer GetPlayer(AuthStorage storage) public YnisonPlayer GetPlayer()
{ {
if (string.IsNullOrEmpty(storage.Token)) if (string.IsNullOrEmpty(Api.Storage.Token))
throw new Exception("Токен пользователя не задан."); throw new Exception("Токен пользователя не задан");
return new YnisonPlayer(Api, Api.Storage);
return new(api, storage);
} }
} }

View File

@@ -1,80 +1,81 @@
using System.Net; using System.Net;
using YandexMusic.API.Common.Providers;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common;
namespace YandexMusic.API.Common; namespace YandexMusic.API.Common;
/// <summary> /// <summary>
/// Хранилище данных пользователя /// Хранилище данных авторизации. Не содержит HTTP-зависимостей.
/// </summary> /// </summary>
public class AuthStorage public class AuthStorage
{ {
/// <summary> private CookieContainer _cookieContainer;
/// Http-контекст
/// </summary> public AuthStorage(CookieContainer cookieContainer)
public HttpContext Context { get; } {
_cookieContainer = cookieContainer;
}
public CookieContainer CookieContainer => _cookieContainer;
/// <summary> /// <summary>
/// Флаг авторизации /// Флаг, указывающий, авторизован ли пользователь.
/// </summary> /// </summary>
public bool IsAuthorized { get; internal set; } public bool IsAuthorized { get; internal set; }
/// <summary> /// <summary>
/// Идентификатор устройства /// Идентификатор устройства (используется в заголовках).
/// </summary> /// </summary>
public string DeviceId { get; set; } = "csharp"; public string DeviceId { get; set; } = "csharp";
/// <summary> /// <summary>
/// Токен авторизации /// OAuth-токен для доступа к API.
/// </summary> /// </summary>
public string Token { get; internal set; } public string Token { get; internal set; } = string.Empty;
/// <summary> /// <summary>
/// Аккаунт /// Информация об аккаунте пользователя.
/// </summary> /// </summary>
public YAccount User { get; set; } public YAccount User { get; internal set; } = new();
/// <summary> /// <summary>
/// Провайдер запросов /// Временный токен доступа (используется в некоторых сценариях авторизации).
/// </summary> /// </summary>
public IRequestProvider Provider { get; } public YAccessToken AccessToken { get; internal set; } = new();
/// <summary> /// <summary>
/// Токен доступа /// Внутренние данные авторизации (CSRF, track_id и т.д.).
/// </summary> /// </summary>
public YAccessToken AccessToken { get; set; } public YAuthToken? HeaderToken { get; set; } = new();
internal YAuthToken AuthToken { get; set; }
/// <summary> /// <summary>
/// Конструктор /// Внутренние данные авторизации (CSRF, track_id и т.д.).
/// </summary> /// </summary>
public AuthStorage(IRequestProvider provider) public YAuthToken? AuthToken { get; set; } = new();
/// <summary>
/// Страна, используемая для авторизации (по умолчанию "ru"). Может влиять на язык интерфейса и доступные методы авторизации.
/// </summary>
public object Country { get; set; } = "ru";
/// <summary>
/// Устанавливает флаг авторизации и сохраняет информацию об аккаунте.
/// </summary>
internal void SetAuthorized(YAccount user, string token)
{
User = user ?? throw new ArgumentNullException(nameof(user));
Token = token ?? throw new ArgumentNullException(nameof(token));
IsAuthorized = true;
}
/// <summary>
/// Сбрасывает состояние авторизации.
/// </summary>
internal void ResetAuthorization()
{ {
User = new YAccount(); User = new YAccount();
Context = new HttpContext(); Token = string.Empty;
Provider = provider; AccessToken = new YAccessToken();
AuthToken = new YAuthToken();
IsAuthorized = false;
} }
/// <summary>
/// Конструктор
/// </summary>
public AuthStorage()
{
User = new YAccount();
Context = new HttpContext();
Provider = new DefaultRequestProvider(this);
}
/// <summary>
/// Установка прокси для пользователия
/// </summary>
/// <param name="proxy">Прокси</param>
public void SetProxy(IWebProxy proxy)
{
Context.WebProxy = proxy;
}
} }

View File

@@ -1,43 +0,0 @@
using System.Net;
namespace YandexMusic.API.Common;
/// <summary>
/// Загрузчик файлов по ссылке
/// </summary>
public class DataDownloader
{
private AuthStorage authStorage;
private async Task<HttpContent> GetResponseContent(string url, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
{
HttpRequestMessage message = new(new HttpMethod(WebRequestMethods.Http.Get), url);
HttpResponseMessage response = await authStorage.Provider.GetWebResponseAsync(message, httpCompletionOption);
return response.Content;
}
public async Task<Stream> AsStream(string url, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
{
HttpContent content = await GetResponseContent(url, httpCompletionOption);
return await content.ReadAsStreamAsync();
}
public async Task<byte[]> AsBytes(string url)
{
HttpContent content = await GetResponseContent(url);
return await content.ReadAsByteArrayAsync();
}
public async Task ToFile(string url, string fileName)
{
using Stream stream = await AsStream(url);
using FileStream fs = File.Create(fileName);
await stream.CopyToAsync(fs);
}
public DataDownloader(AuthStorage storage)
{
authStorage = storage;
}
}

View File

@@ -1,61 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using YandexMusic.API.Converters;
using YandexMusic.API.Models.Common;
namespace YandexMusic.API.Common.Providers;
/// <summary>Базовый провайдер HTTP-запросов с общей логикой десериализации.</summary>
public abstract class CommonRequestProvider : IRequestProvider
{
/// <summary>Хранилище данных авторизации.</summary>
protected readonly AuthStorage storage;
/// <summary>Инициализирует новый экземпляр провайдера.</summary>
/// <param name="authStorage">Хранилище авторизации.</param>
protected CommonRequestProvider(AuthStorage authStorage)
{
storage = authStorage;
}
/// <summary>Выполняет HTTP-запрос и возвращает ответ.</summary>
public abstract Task<HttpResponseMessage> GetWebResponseAsync(
HttpRequestMessage message,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead);
/// <summary>Преобразует HTTP-ответ в объект типа T.</summary>
public virtual async Task<T> GetDataFromResponseAsync<T>(
YandexMusicApi api,
HttpResponseMessage response)
{
var json = await response.Content.ReadAsStringAsync();
JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
Converters = {
new JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower),
new IntToStringConverter(),
new StringToIntConverter(),
new YExecutionContextConverter(api, storage),
}
};
if (!response.IsSuccessStatusCode)
{
var error = JsonSerializer.Deserialize<YErrorResponse>(json, JsonOptions);
throw error ?? new Exception("Ошибка десериализации ответа с ошибкой.");
}
try
{
// Если нужен контекст выполнения, он добавляется через кастомный конвертер
return JsonSerializer.Deserialize<T>(json, JsonOptions)
?? throw new JsonException("Десериализация вернула null");
}
catch (Exception ex)
{
throw new Exception($"Ошибка десериализации: {ex.Message}", ex);
}
}
}

View File

@@ -1,45 +0,0 @@
using System.Net;
namespace YandexMusic.API.Common.Providers;
/// <summary>Стандартный провайдер HTTP-запросов с использованием HttpClient.</summary>
public class DefaultRequestProvider : CommonRequestProvider
{
/// <summary>Инициализирует новый экземпляр провайдера.</summary>
/// <param name="authStorage">Хранилище авторизации.</param>
public DefaultRequestProvider(AuthStorage authStorage) : base(authStorage) { }
/// <summary>Выполняет HTTP-запрос и возвращает ответ.</summary>
/// <param name="message">HTTP-запрос.</param>
/// <param name="completionOption">Опция завершения запроса.</param>
/// <returns>HTTP-ответ.</returns>
public override async Task<HttpResponseMessage> GetWebResponseAsync(
HttpRequestMessage message,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
using var handler = new SocketsHttpHandler
{
Proxy = storage.Context.WebProxy,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = true,
CookieContainer = storage.Context.Cookies,
AllowAutoRedirect = true,
MaxAutomaticRedirections = 10
};
using var client = new HttpClient(handler);
try
{
return await client.SendAsync(message, completionOption);
}
catch (HttpRequestException ex)
{
// Пытаемся извлечь тело ошибки, если оно доступно
if (ex.InnerException == null)
throw;
throw new Exception($"Ошибка HTTP-запроса: {ex.Message}", ex);
}
}
}

View File

@@ -1,25 +0,0 @@
namespace YandexMusic.API.Common.Providers
{
/// <summary>
/// Интерфейс для провайдеров обработки запросов
/// </summary>
public interface IRequestProvider
{
/// <summary>
/// Функция получения ответа
/// </summary>
/// <param name="message">Запрос</param>
/// <param name="completionOption">Опция завершения запроса</param>
/// <returns></returns>
Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead);
/// <summary>
/// Функция формирования ответа
/// </summary>
/// <typeparam name="T">Тип объекта с ответом</typeparam>
/// <param name="api">API</param>
/// <param name="response">Ответ</param>
/// <returns></returns>
Task<T> GetDataFromResponseAsync<T>(YandexMusicApi api, HttpResponseMessage response);
}
}

View File

@@ -0,0 +1,47 @@
using System.Net;
namespace YandexMusic.API.Common;
/// <summary>
/// Фабрика для создания стандартного HttpClient с поддержкой кук, прокси и автоматической декомпрессией.
/// </summary>
public static class YandexMusicHttpClientFactory
{
/// <summary>
/// Создаёт стандартный HttpClient с автоматическим управлением куками.
/// </summary>
/// <param name="proxy">Прокси-сервер (опционально).</param>
/// <param name="timeout">Таймаут запросов (по умолчанию 30 секунд).</param>
/// <param name="userAgent">User-Agent (по умолчанию как у браузера Chrome).</param>
/// <returns>Настроенный HttpClient.</returns>
public static HttpClient CreateDefault(
CookieContainer? cookieContainer = null,
IWebProxy? proxy = null,
TimeSpan? timeout = null,
string? userAgent = null)
{
var handler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = true,
CookieContainer = cookieContainer ?? new 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");
return client;
}
}

View File

@@ -1,4 +1,5 @@
using System.Net.WebSockets; using System.Net;
using System.Net.WebSockets;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using YandexMusic.API.Models.Track; using YandexMusic.API.Models.Track;
@@ -12,6 +13,7 @@ public class YnisonPlayer : IDisposable
{ {
private readonly JsonSerializerOptions _jsonOptions; private readonly JsonSerializerOptions _jsonOptions;
private readonly AuthStorage _storage; private readonly AuthStorage _storage;
private readonly IWebProxy? _proxy;
private YnisonWebSocket? _redirector; private YnisonWebSocket? _redirector;
private YnisonWebSocket? _state; private YnisonWebSocket? _state;
@@ -33,40 +35,36 @@ public class YnisonPlayer : IDisposable
/// <summary>Аргументы события получения состояния.</summary> /// <summary>Аргументы события получения состояния.</summary>
public class ReceiveEventArgs : EventArgs public class ReceiveEventArgs : EventArgs
{ {
/// <summary>Состояние плеера.</summary>
public YYnisonState State { get; init; } = null!; public YYnisonState State { get; init; } = null!;
} }
/// <summary>Аргументы события закрытия соединения.</summary> /// <summary>Аргументы события закрытия соединения.</summary>
public class CloseEventArgs : EventArgs public class CloseEventArgs : EventArgs
{ {
/// <summary>Статус закрытия.</summary>
public WebSocketCloseStatus? Status { get; init; } public WebSocketCloseStatus? Status { get; init; }
/// <summary>Описание причины закрытия.</summary>
public string? Description { get; init; } public string? Description { get; init; }
} }
internal YnisonPlayer(YandexMusicApi api, AuthStorage authStorage) internal YnisonPlayer(YandexMusicApi api, AuthStorage authStorage, IWebProxy? proxy = null)
{ {
API = api; API = api;
_storage = authStorage; _storage = authStorage;
_proxy = proxy;
_jsonOptions = new JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
{ {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(new UpperSnakeCaseNamingPolicy(), false) } Converters = { new JsonStringEnumConverter(new UpperSnakeCaseNamingPolicy(), false) }
}; };
_redirector = new YnisonWebSocket(); _redirector = new YnisonWebSocket(_proxy);
_state = new YnisonWebSocket(); _state = new YnisonWebSocket(_proxy);
} }
private string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions); private string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
private T Deserialize<T>(YYnisonMessageType messageType, string data) private T Deserialize<T>(YYnisonMessageType messageType, string data)
{ => JsonSerializer.Deserialize<T>(data, _jsonOptions)
return JsonSerializer.Deserialize<T>(data, _jsonOptions)
?? throw new JsonException("Десериализация вернула null"); ?? throw new JsonException("Десериализация вернула null");
}
private T DeserializeMessage<T>(YYnisonMessageType messageType, string data) private T DeserializeMessage<T>(YYnisonMessageType messageType, string data)
{ {
@@ -120,8 +118,8 @@ public class YnisonPlayer : IDisposable
if (index < 0 || index >= State.PlayerState.PlayerQueue.PlayableList.Count) if (index < 0 || index >= State.PlayerState.PlayerQueue.PlayableList.Count)
return null; return null;
var item = State.PlayerState.PlayerQueue.PlayableList[index]; var item = State.PlayerState.PlayerQueue.PlayableList[index];
var response = await API.Track.GetAsync(_storage, item.PlayableId); var response = await API.Track.GetAsync(item.PlayableId);
return response?.Result?.FirstOrDefault(); return response;
} }
private async Task UpdateStateAsync() private async Task UpdateStateAsync()

View File

@@ -1,4 +1,5 @@
using System.Net.WebSockets; using System.Net;
using System.Net.WebSockets;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@@ -7,14 +8,15 @@ namespace YandexMusic.API.Common.Ynison;
/// <summary>WebSocket-клиент для взаимодействия с протоколом Ynison.</summary> /// <summary>WebSocket-клиент для взаимодействия с протоколом Ynison.</summary>
public class YnisonWebSocket : IDisposable public class YnisonWebSocket : IDisposable
{ {
private readonly ClientWebSocket _socketClient = new(); private ClientWebSocket? _socketClient;
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
private CancellationToken _cancellationToken; private CancellationToken _cancellationToken;
private readonly StringBuilder _data = new(); private readonly StringBuilder _data = new();
private const int BufferSize = 4096; private const int BufferSize = 4096;
private readonly IWebProxy? _proxy;
/// <summary>Флаг, указывает, открыто ли соединение.</summary> /// <summary>Флаг, указывает, открыто ли соединение.</summary>
public bool IsConnected => _socketClient.State == WebSocketState.Open; public bool IsConnected => _socketClient?.State == WebSocketState.Open;
/// <summary>Событие получения сообщения.</summary> /// <summary>Событие получения сообщения.</summary>
public event EventHandler<ReceiveEventArgs>? OnReceive; public event EventHandler<ReceiveEventArgs>? OnReceive;
@@ -25,19 +27,25 @@ public class YnisonWebSocket : IDisposable
/// <summary>Аргументы события получения данных.</summary> /// <summary>Аргументы события получения данных.</summary>
public class ReceiveEventArgs : EventArgs public class ReceiveEventArgs : EventArgs
{ {
/// <summary>Полученные данные (JSON-строка).</summary>
public string Data { get; init; } = null!; public string Data { get; init; } = null!;
} }
/// <summary>Аргументы события закрытия соединения.</summary> /// <summary>Аргументы события закрытия соединения.</summary>
public class CloseEventArgs : EventArgs public class CloseEventArgs : EventArgs
{ {
/// <summary>Статус закрытия.</summary>
public WebSocketCloseStatus? Status { get; init; } public WebSocketCloseStatus? Status { get; init; }
/// <summary>Описание причины закрытия.</summary>
public string? Description { get; init; } public string? Description { get; init; }
} }
/// <summary>
/// Инициализирует новый экземпляр WebSocket-клиента.
/// </summary>
/// <param name="proxy">Прокси-сервер (опционально).</param>
public YnisonWebSocket(IWebProxy? proxy = null)
{
_proxy = proxy;
}
private static string GetProtocolData(string deviceId, string? redirectTicket) private static string GetProtocolData(string deviceId, string? redirectTicket)
{ {
var deviceInfo = new Dictionary<string, object> var deviceInfo = new Dictionary<string, object>
@@ -57,6 +65,9 @@ public class YnisonWebSocket : IDisposable
private async Task<string> ReadSocketContentAsync() private async Task<string> ReadSocketContentAsync()
{ {
if (_socketClient == null)
throw new InvalidOperationException("WebSocket не инициализирован");
var buffer = new byte[BufferSize]; var buffer = new byte[BufferSize];
WebSocketReceiveResult result; WebSocketReceiveResult result;
do do
@@ -68,17 +79,19 @@ public class YnisonWebSocket : IDisposable
} }
/// <summary>Подключается к WebSocket.</summary> /// <summary>Подключается к WebSocket.</summary>
/// <param name="storage">Хранилище авторизации.</param> /// <param name="storage">Хранилище авторизации (для токена и deviceId).</param>
/// <param name="url">URL WebSocket.</param> /// <param name="url">URL WebSocket.</param>
/// <param name="redirectTicket">Тикет перенаправления (опционально).</param> /// <param name="redirectTicket">Тикет перенаправления (опционально).</param>
public async Task ConnectAsync(AuthStorage storage, string url, string? redirectTicket = null) public async Task ConnectAsync(AuthStorage storage, string url, string? redirectTicket = null)
{ {
_socketClient = new ClientWebSocket();
_socketClient.Options.AddSubProtocol("Bearer"); _socketClient.Options.AddSubProtocol("Bearer");
var protocolData = GetProtocolData(storage.DeviceId, redirectTicket); var protocolData = GetProtocolData(storage.DeviceId, redirectTicket);
_socketClient.Options.SetRequestHeader("Sec-WebSocket-Protocol", $"Bearer, v2, {protocolData}"); _socketClient.Options.SetRequestHeader("Sec-WebSocket-Protocol", $"Bearer, v2, {protocolData}");
_socketClient.Options.SetRequestHeader("Origin", "https://music.yandex.ru"); _socketClient.Options.SetRequestHeader("Origin", "https://music.yandex.ru");
_socketClient.Options.SetRequestHeader("Authorization", $"OAuth {storage.Token}"); _socketClient.Options.SetRequestHeader("Authorization", $"OAuth {storage.Token}");
_socketClient.Options.Proxy = storage.Context.WebProxy; if (_proxy != null)
_socketClient.Options.Proxy = _proxy;
_cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token; _cancellationToken = _cancellationTokenSource.Token;
@@ -89,7 +102,7 @@ public class YnisonWebSocket : IDisposable
/// <summary>Начинает асинхронный приём сообщений.</summary> /// <summary>Начинает асинхронный приём сообщений.</summary>
public async Task BeginReceiveAsync() public async Task BeginReceiveAsync()
{ {
if (_socketClient.State != WebSocketState.Open) if (_socketClient == null || _socketClient.State != WebSocketState.Open)
return; return;
try try
@@ -116,9 +129,11 @@ public class YnisonWebSocket : IDisposable
} }
/// <summary>Отправляет JSON-сообщение.</summary> /// <summary>Отправляет JSON-сообщение.</summary>
/// <param name="json">JSON-строка.</param>
public async ValueTask SendAsync(string json) public async ValueTask SendAsync(string json)
{ {
if (_socketClient == null)
throw new InvalidOperationException("WebSocket не инициализирован");
var bytes = Encoding.UTF8.GetBytes(json); var bytes = Encoding.UTF8.GetBytes(json);
await _socketClient.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, _cancellationToken); await _socketClient.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, _cancellationToken);
} }
@@ -127,16 +142,14 @@ public class YnisonWebSocket : IDisposable
public async Task StopReceiveAsync() public async Task StopReceiveAsync()
{ {
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested) if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
{
await _cancellationTokenSource.CancelAsync(); await _cancellationTokenSource.CancelAsync();
}
} }
/// <summary>Освобождает ресурсы.</summary> /// <summary>Освобождает ресурсы.</summary>
public void Dispose() public void Dispose()
{ {
_cancellationTokenSource?.Dispose(); _cancellationTokenSource?.Dispose();
_socketClient.Dispose(); _socketClient?.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
} }

View File

@@ -3,7 +3,7 @@ using System.Text.Json.Serialization;
namespace YandexMusic.API.Converters; namespace YandexMusic.API.Converters;
public class IntToStringConverter : JsonConverter<string> internal class IntToStringConverter : JsonConverter<string>
{ {
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {

View File

@@ -3,7 +3,7 @@ using System.Text.Json.Serialization;
namespace YandexMusic.API.Converters; namespace YandexMusic.API.Converters;
public class StringToIntConverter : JsonConverter<int> internal class StringToIntConverter : JsonConverter<int>
{ {
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {

View File

@@ -1,29 +1,33 @@
using YandexMusic.API.Models.Album; using YandexMusic.API.Models.Album;
namespace YandexMusic.API.Extensions.API; namespace YandexMusic.API;
/// <summary> /// <summary>
/// Методы-расширения для альбома /// Методы-расширения для альбома.
/// </summary> /// </summary>
public static partial class YAlbumExtensions public static class YAlbumExtensions
{ {
/// <summary>
/// Получает полную информацию об альбоме вместе с треками (если ещё не загружены).
/// </summary>
public static async Task<YAlbum> WithTracksAsync(this YAlbum album) public static async Task<YAlbum> WithTracksAsync(this YAlbum album)
{ {
return album.Volumes != null if (album.Volumes != null)
? album return album;
: (await album.Context.API.Album.GetAsync(album.Context.Storage, album.Id))
.Result; var result = await album.Context.Api.Album.GetAsync(album.Id);
return result ?? album;
} }
public static async Task<string> AddLikeAsync(this YAlbum album) /// <summary>
{ /// Добавляет альбом в список лайкнутых.
return (await album.Context.API.Library.AddAlbumLikeAsync(album.Context.Storage, album)) /// </summary>
.Result; public static async Task<string?> AddLikeAsync(this YAlbum album)
} => await album.Context.Api.Library.AddAlbumLikeAsync(album);
public static async Task<string> RemoveLikeAsync(this YAlbum album) /// <summary>
{ /// Удаляет альбом из списка лайкнутых.
return (await album.Context.API.Library.RemoveAlbumLikeAsync(album.Context.Storage, album)) /// </summary>
.Result; public static async Task<string?> RemoveLikeAsync(this YAlbum album)
} => await album.Context.Api.Library.RemoveAlbumLikeAsync(album);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
using System.Net;
namespace YandexMusic.API.Extensions;
public static class HttpRequestHeaderExtensions
{
public static string GetName(this HttpRequestHeader header)
{
return header.ToString().SplitByCapitalLetter("-");
}
}

View File

@@ -1,49 +0,0 @@
using System.Text.RegularExpressions;
namespace YandexMusic.API.Extensions;
public static class StringExtensions
{
public static string ReplaceRegex(this string str, string regExpr, string replStr, RegexOptions options = RegexOptions.IgnoreCase)
{
return str == null
? string.Empty
: Regex.Replace(str, regExpr, replStr);
}
public static string SplitByCapitalLetter(this string str, string delimiter)
{
return string.Join(delimiter, Regex.Matches(str, @"([A-Z]+)(?=([A-Z][a-z]|$)) | [A-Z][a-z].+?(?=([A-Z]|$))", RegexOptions.IgnorePatternWhitespace)
.Cast<Match>()
.Select(m => m.ToString()));
}
/// <summary>
/// Проверяет соответствие регулярному выражению
/// </summary>
public static bool IsMatch(this string str, string pattern, RegexOptions options)
{
return Regex.IsMatch(str, pattern, options);
}
/// <summary>
/// Проверяет соответствие регулярному выражению
/// </summary>
public static bool IsMatch(this string str, string pattern)
{
return IsMatch(str, pattern, RegexOptions.IgnoreCase);
}
/// <summary>
/// Возвращает совпадения для регулярного выражения
/// </summary>
public static string[] GetMatches(this string str, string pattern, RegexOptions options = RegexOptions.IgnoreCase)
{
return str.IsMatch(pattern, options)
? Regex.Matches(str, pattern, options)
.Cast<Match>()
.Select(m => m.Value)
.ToArray()
: new string[] { };
}
}

View File

@@ -0,0 +1,5 @@
namespace YandexMusic.API.Models.Account;
public class YAuthEmpty
{
}

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
using System.Text.Json.Serialization;
using YandexMusic.API.Models.Album; using YandexMusic.API.Models.Album;
using YandexMusic.API.Models.Common; using YandexMusic.API.Models.Common;
using YandexMusic.API.Models.Common.Cover; using YandexMusic.API.Models.Common.Cover;
@@ -11,7 +10,6 @@ public class YArtistBriefInfo
{ {
public YButton ActionButton { get; set; } public YButton ActionButton { get; set; }
public List<YAlbum> Albums { get; set; } public List<YAlbum> Albums { get; set; }
[JsonConverter(typeof(YCoverConverter))]
public List<YCover> AllCovers { get; set; } public List<YCover> AllCovers { get; set; }
public List<YAlbum> AlsoAlbums { get; set; } public List<YAlbum> AlsoAlbums { get; set; }
public YArtist Artist { get; set; } public YArtist Artist { get; set; }

View File

@@ -1,33 +1,7 @@
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Common.Cover; namespace YandexMusic.API.Models.Common.Cover;
public class YCoverConverter : JsonConverter<YCover>
{
public override YCover? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject) return null;
using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;
var type = root.TryGetProperty("type", out var t) ? t.GetString() : null;
if (root.TryGetProperty("error", out _)) type = "error";
return type switch
{
"color" => JsonSerializer.Deserialize<YCoverColor>(root.GetRawText(), options),
"error" => JsonSerializer.Deserialize<YCoverError>(root.GetRawText(), options),
"from-artist-photos" or "from-album-cover" => JsonSerializer.Deserialize<YCoverImage>(root.GetRawText(), options),
"pic" => JsonSerializer.Deserialize<YCoverPic>(root.GetRawText(), options),
"mosaic" => JsonSerializer.Deserialize<YCoverMosaic>(root.GetRawText(), options),
_ => new YCover() { Type = YCoverType.Error }
};
}
public override void Write(Utf8JsonWriter writer, YCover value, JsonSerializerOptions options)
=> JsonSerializer.Serialize(writer, value, options);
}
[JsonConverter(typeof(YCoverConverter))] [JsonConverter(typeof(YCoverConverter))]
public class YCover public class YCover
{ {

View File

@@ -0,0 +1,29 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Common.Cover;
public class YCoverConverter : JsonConverter<YCover>
{
public override YCover? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject) return null;
using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;
var type = root.TryGetProperty("type", out var t) ? t.GetString() : null;
if (root.TryGetProperty("error", out _)) type = "error";
return type switch
{
"color" => JsonSerializer.Deserialize<YCoverColor>(root.GetRawText(), options),
"error" => JsonSerializer.Deserialize<YCoverError>(root.GetRawText(), options),
"from-artist-photos" or "from-album-cover" => JsonSerializer.Deserialize<YCoverImage>(root.GetRawText(), options),
"pic" => JsonSerializer.Deserialize<YCoverPic>(root.GetRawText(), options),
"mosaic" => JsonSerializer.Deserialize<YCoverMosaic>(root.GetRawText(), options),
_ => new YCover() { Type = YCoverType.Error }
};
}
public override void Write(Utf8JsonWriter writer, YCover value, JsonSerializerOptions options)
=> JsonSerializer.Serialize(writer, value, options);
}

View File

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

View File

@@ -1,24 +1,34 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using YandexMusic.API.Common; using YandexMusic.API.Common;
using YandexMusic.API.Models.Common;
namespace YandexMusic.API.Models.Common; namespace YandexMusic.API.Converters;
/// <summary>Конвертер для внедрения контекста выполнения (API и хранилище) в модели.</summary> /// <summary>
/// Конвертер для внедрения контекста выполнения (API и хранилище) в модели, наследуемые от YBaseModel.
/// </summary>
public class YExecutionContextConverter : JsonConverter<object> public class YExecutionContextConverter : JsonConverter<object>
{ {
private readonly YandexMusicApi _api; private readonly YandexMusicApi _api;
private readonly AuthStorage _storage; private readonly AuthStorage _storage;
/// <summary>
/// Инициализирует новый экземпляр конвертера.
/// </summary>
/// <param name="api">Экземпляр основного API.</param>
/// <param name="storage">Хранилище авторизации.</param>
public YExecutionContextConverter(YandexMusicApi api, AuthStorage storage) public YExecutionContextConverter(YandexMusicApi api, AuthStorage storage)
{ {
_api = api; _api = api;
_storage = storage; _storage = storage;
} }
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert) => public override bool CanConvert(Type typeToConvert) =>
typeof(YBaseModel).IsAssignableFrom(typeToConvert); typeof(YBaseModel).IsAssignableFrom(typeToConvert);
/// <inheritdoc />
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
// Убираем этот конвертер из опций, чтобы избежать рекурсии // Убираем этот конвертер из опций, чтобы избежать рекурсии
@@ -28,11 +38,12 @@ public class YExecutionContextConverter : JsonConverter<object>
var obj = JsonSerializer.Deserialize(ref reader, typeToConvert, innerOptions); var obj = JsonSerializer.Deserialize(ref reader, typeToConvert, innerOptions);
if (obj is YBaseModel baseModel) if (obj is YBaseModel baseModel)
{ {
baseModel.Context = new YExecutionContext { API = _api, Storage = _storage }; baseModel.Context = new YExecutionContext { Api = _api, Storage = _storage };
} }
return obj; return obj;
} }
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{ {
var innerOptions = new JsonSerializerOptions(options); var innerOptions = new JsonSerializerOptions(options);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,19 @@
using System.Net; using System.Net;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
[YPassportRequest(WebRequestMethods.Http.Post, "registration-validations/auth/multi_step/commit_password")] internal class YGetAuthAppPasswordBuilder : YPassportRequestBuilder<YAuthBase?, string>
internal class YGetAuthAppPasswordBuilder : YRequestBuilder<YAuthBase, string>
{ {
public YGetAuthAppPasswordBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetAuthAppPasswordBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Post;
} protected override string PathTemplate => "registration-validations/auth/multi_step/commit_password";
protected override HttpContent? GetContent(string password)
protected override HttpContent GetContent(string tuple) => new FormUrlEncodedContent(new Dictionary<string, string>
{ {
return new FormUrlEncodedContent(new Dictionary<string, string> { { "csrf_token", Api.Storage.AuthToken.CsfrToken },
{ "csrf_token", storage.AuthToken.CsfrToken }, { "track_id", Api.Storage.AuthToken.TrackId },
{ "track_id", storage.AuthToken.TrackId }, { "password", password },
{ "password", tuple },
{ "retpath", "https://passport.yandex.ru/am/finish?status=ok&from=Login" } { "retpath", "https://passport.yandex.ru/am/finish?status=ok&from=Login" }
}); });
}
} }

View File

@@ -1,29 +1,13 @@
using System.Net; using System.Net;
using System.Net.Http.Headers; using YandexMusic.API.Models.Account;
using YandexMusic.API.Common;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
[YPassportRequest(WebRequestMethods.Http.Post, "registration-validations/textcaptcha")] internal class YGetAuthCaptchaBuilder : YPassportRequestBuilder<YAuthCaptcha?, object>
internal class YGetAuthCaptchaBuilder : YRequestBuilder<Models.Account.YAuthCaptcha, string>
{ {
public YGetAuthCaptchaBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetAuthCaptchaBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Post;
} protected override string PathTemplate => "registration-validations/textcaptcha";
protected override HttpContent? GetContent(object _)
protected override HttpContent GetContent(string tuple) => new FormUrlEncodedContent(new Dictionary<string, string> { { "csrf_token", Api.Storage.AuthToken.CsfrToken }, { "track_id", Api.Storage.AuthToken.TrackId } });
{
return new FormUrlEncodedContent(new Dictionary<string, string> {
{ "csrf_token", storage.AuthToken.CsfrToken },
{ "track_id", storage.AuthToken.TrackId },
});
}
protected override void SetCustomHeaders(HttpRequestHeaders headers)
{
headers.Add("X-Requested-With", "XMLHttpRequest");
}
} }

View File

@@ -1,36 +1,55 @@
using System.Net; using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common; using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
[YMobileProxyRequest(WebRequestMethods.Http.Post, "1/bundle/oauth/token_by_sessionid")] internal class YGetAuthCookiesBuilder : YPassportRequestBuilder<YAccessToken?, object>
internal class YGetAuthCookiesBuilder : YRequestBuilder<YAccessToken, string>
{ {
public YGetAuthCookiesBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) 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) protected override void SetCustomHeaders(HttpRequestHeaders headers)
{ {
CookieCollection cookieCollection = new() { base.SetCustomHeaders(headers);
storage.Context.Cookies.GetCookies(new Uri("https://yandex.ru/")), headers.Add("ya-client-host", "passport.yandex.ru");
storage.Context.Cookies.GetCookies(new Uri("https://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")
}; };
headers.Add("Ya-Client-Cookie", string.Join(";", cookieCollection.Select(c => $"{c.Name}={c.Value}"))); var cookies = new List<string>();
headers.Add("Ya-Client-Host", "passport.yandex.ru"); foreach (var uri in uris)
} {
var cookieCollection = container.GetCookies(uri);
foreach (Cookie cookie in cookieCollection)
{
cookies.Add($"{cookie.Name}={cookie.Value}");
}
}
protected override HttpContent GetContent(string tuple) var distinct = cookies
{ .Select(c => c.Split('=')[0])
return new FormUrlEncodedContent(new Dictionary<string, string> { .Distinct()
{ "client_id", YConstants.XClientId }, .Select(name => cookies.First(c => c.StartsWith(name + "=")))
{ "client_secret", YConstants.XClientSecret } .ToList();
});
return string.Join("; ", distinct);
} }
} }

View File

@@ -1,17 +1,11 @@
using System.Net; using System.Net;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
[YApiRequest(WebRequestMethods.Http.Get, "account/status")] internal class YGetAuthInfoBuilder : YMusicRequestBuilder<YAccountResult?, object>
public class YGetAuthInfoBuilder : YRequestBuilder<YResponse<YAccountResult>, object>
{ {
public YGetAuthInfoBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetAuthInfoBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Get;
} protected override string PathTemplate => "account/status";
} }

View File

@@ -1,29 +1,13 @@
using System.Net; using System.Net;
using YandexMusic.API.Models.Account;
using YandexMusic.API.Common;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
[YPassportRequest(WebRequestMethods.Http.Post, "registration-validations/auth/send_magic_letter")] internal class YGetAuthLetterBuilder : YPassportRequestBuilder<YAuthLetter?, object>
internal class YGetAuthLetterBuilder : YRequestBuilder<Models.Account.YAuthLetter, string>
{ {
public YGetAuthLetterBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetAuthLetterBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Post;
} protected override string PathTemplate => "registration-validations/auth/send_magic_letter";
protected override HttpContent? GetContent(object _)
protected override HttpContent GetContent(string tuple) => new FormUrlEncodedContent(new Dictionary<string, string> { { "csrf_token", Api.Storage.AuthToken.CsfrToken }, { "track_id", Api.Storage.AuthToken.TrackId } });
{
if (storage.AuthToken == null)
{
throw new Exception("Не найдена сессия входа.");
}
return new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "csrf_token", storage.AuthToken.CsfrToken },
{ "track_id", storage.AuthToken.TrackId },
});
}
} }

View File

@@ -1,31 +1,13 @@
using System.Net; using System.Net;
using System.Net.Http.Headers;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
[YPassportRequest(WebRequestMethods.Http.Post, "registration-validations/checkHuman")] internal class YGetAuthLoginCaptchaBuilder : YPassportRequestBuilder<YAuthBase?, string>
internal class YGetAuthLoginCaptchaBuilder : YRequestBuilder<YAuthBase, string>
{ {
public YGetAuthLoginCaptchaBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetAuthLoginCaptchaBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Post;
} protected override string PathTemplate => "registration-validations/checkHuman";
protected override HttpContent? GetContent(string captchaAnswer)
protected override HttpContent GetContent(string tuple) => new FormUrlEncodedContent(new Dictionary<string, string> { { "csrf_token", Api.Storage.AuthToken.CsfrToken }, { "track_id", Api.Storage.AuthToken.TrackId }, { "answer", captchaAnswer } });
{
return new FormUrlEncodedContent(new Dictionary<string, string> {
{ "csrf_token", storage.AuthToken.CsfrToken },
{ "track_id", storage.AuthToken.TrackId },
{ "answer", tuple }
});
}
protected override void SetCustomHeaders(HttpRequestHeaders headers)
{
headers.Add("X-Requested-With", "XMLHttpRequest");
}
} }

View File

@@ -1,24 +1,13 @@
using System.Net; using System.Net;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
[YPassportRequest(WebRequestMethods.Http.Post, "auth/letter/status/")] internal class YGetAuthLoginLetterBuilder : YPassportRequestBuilder<YAuthLetterStatus?, object>
internal class YGetAuthLoginLetterBuilder : YRequestBuilder<YAuthLetterStatus, string>
{ {
public YGetAuthLoginLetterBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetAuthLoginLetterBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Post;
} protected override string PathTemplate => "auth/letter/status/";
protected override HttpContent? GetContent(object _)
protected override HttpContent GetContent(string tuple) => new FormUrlEncodedContent(new Dictionary<string, string> { { "csrf_token", Api.Storage.AuthToken.CsfrToken }, { "track_id", Api.Storage.AuthToken.TrackId } });
{
return new FormUrlEncodedContent(new Dictionary<string, string> {
{ "csrf_token", storage.AuthToken.CsfrToken },
{ "track_id", storage.AuthToken.TrackId },
});
}
} }

View File

@@ -1,24 +0,0 @@
using System.Net;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account;
[YPassportRequest(WebRequestMethods.Http.Post, "auth/new/magic/status/")]
internal class YGetAuthLoginQRBuilder : YRequestBuilder<YAuthQRStatus, string>
{
public YGetAuthLoginQRBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
{
}
protected override HttpContent GetContent(string tuple)
{
return new FormUrlEncodedContent(new Dictionary<string, string> {
{ "csrf_token", storage.AuthToken.CsfrToken },
{ "track_id", storage.AuthToken.TrackId }
});
}
}

View File

@@ -1,24 +1,13 @@
using System.Net; using System.Net;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
[YPassportRequest(WebRequestMethods.Http.Post, "registration-validations/auth/multi_step/start")] internal class YGetAuthLoginUserBuilder : YPassportRequestBuilder<YAuthTypes?, (string token, string login)>
internal class YGetAuthLoginUserBuilder : YRequestBuilder<YAuthTypes, (string token, string login)>
{ {
public YGetAuthLoginUserBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetAuthLoginUserBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Post;
} protected override string PathTemplate => "registration-validations/auth/multi_step/start";
protected override HttpContent? GetContent((string token, string login) tuple)
protected override HttpContent GetContent((string token, string login) tuple) => new FormUrlEncodedContent(new Dictionary<string, string> { { "csrf_token", tuple.token }, { "login", tuple.login } });
{
return new FormUrlEncodedContent(new Dictionary<string, string> {
{ "csrf_token", tuple.token },
{ "login", tuple.login }
});
}
} }

View File

@@ -1,23 +1,13 @@
using System.Collections.Specialized; using System.Net;
using System.Net;
using YandexMusic.API.Common;
using YandexMusic.API.Requests.Common; using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
[YPassportRequest(WebRequestMethods.Http.Get, "am")] internal class YGetAuthMethodsBuilder : YRequestBuilder<object>
internal class YGetAuthMethodsBuilder : YRequestBuilder<HttpResponseMessage, string>
{ {
public YGetAuthMethodsBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetAuthMethodsBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Get;
} protected override string PathTemplate => "am";
protected override NameValueCollection GetQueryParams(string tuple) protected override string BaseUrl => YConstants.Endpoints.PassportUrl;
{
return new NameValueCollection {
{ "app_platform", "android" }
};
}
} }

View File

@@ -1,25 +0,0 @@
using System.Net;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account;
[YPassportRequest(WebRequestMethods.Http.Post, "registration-validations/auth/password/submit")]
internal class YGetAuthQRBuilder : YRequestBuilder<YAuthQR, string>
{
public YGetAuthQRBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
{
}
protected override HttpContent GetContent(string tuple)
{
return new FormUrlEncodedContent(new Dictionary<string, string> {
{ "csrf_token", storage.AuthToken.CsfrToken },
{ "retpath", "https://passport.yandex.ru/profile" },
{ "with_code", "1" },
});
}
}

View File

@@ -1,16 +1,11 @@
using System.Net; using System.Net;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
[YLoginRequest(WebRequestMethods.Http.Get, "info")] internal class YGetLoginInfoBuilder : YPassportRequestBuilder<YLoginInfo?, object>
public class YGetLoginInfoBuilder : YRequestBuilder<YLoginInfo, object>
{ {
public YGetLoginInfoBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetLoginInfoBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Get;
} protected override string PathTemplate => "info";
} }

View File

@@ -1,35 +0,0 @@
using System.Net;
using System.Net.Http.Headers;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account;
[YOAuthMobile(WebRequestMethods.Http.Post, "/1/token")]
internal class YGetMusicTokenBuilder : YRequestBuilder<YAccessToken, string>
{
public YGetMusicTokenBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
{
}
protected override HttpContent GetContent(string tuple)
{
return new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "client_id", YConstants.ClientId },
{ "client_secret", YConstants.ClientSecret },
{ "grant_type", "x-token" },
{ "access_token", storage.AccessToken.AccessToken }
});
}
protected override void SetCustomHeaders(HttpRequestHeaders headers)
{
headers.Remove("Authorization");
base.SetCustomHeaders(headers);
}
}

View File

@@ -1,30 +1,19 @@
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Net; using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
[YMobileProxyRequest(WebRequestMethods.Http.Get, "/1/bundle/account/short_info/")] internal class YGetShortAccountInfoBuilder : YPassportRequestBuilder<YShortAccountInfo?, object>
internal class YGetShortAccountInifoBuilder : YRequestBuilder<YShortAccountInfo, object>
{ {
public YGetShortAccountInifoBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetShortAccountInfoBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Get;
} protected override string PathTemplate => "1/bundle/account/short_info/";
protected override NameValueCollection GetQueryParams(object _)
protected override NameValueCollection GetQueryParams(object tuple) => new() { { "avatar_size", "islands-300" } };
{
return new NameValueCollection {
{ "avatar_size", "islands-300" }
};
}
protected override void SetCustomHeaders(HttpRequestHeaders headers) protected override void SetCustomHeaders(HttpRequestHeaders headers)
{ {
headers.Add("Ya-Consumer-Authorization", $"OAuth {storage.AccessToken.AccessToken}"); headers.Add("Ya-Consumer-Authorization", $"OAuth {Api.Storage.AccessToken.AccessToken}");
} }
} }

View File

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

View File

@@ -1,24 +1,13 @@
using System.Net; using System.Net;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Album; using YandexMusic.API.Models.Album;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Album; namespace YandexMusic.API.Requests.Album;
[YApiRequest(WebRequestMethods.Http.Get, "albums/{albumId}/with-tracks")] internal class YGetAlbumBuilder : YMusicRequestBuilder<YAlbum?, string>
public class YGetAlbumBuilder : YRequestBuilder<YResponse<YAlbum>, string>
{ {
public YGetAlbumBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetAlbumBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Get;
} protected override string PathTemplate => "albums/{albumId}/with-tracks";
protected override Dictionary<string, string> GetSubstitutions(string albumId) protected override Dictionary<string, string> GetSubstitutions(string albumId)
{ => new() { { "albumId", albumId } };
return new Dictionary<string, string> {
{ "albumId", albumId }
};
}
} }

View File

@@ -1,24 +1,13 @@
using System.Net; using System.Net;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Album; using YandexMusic.API.Models.Album;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Album; namespace YandexMusic.API.Requests.Album;
[YApiRequest(WebRequestMethods.Http.Post, "albums")] internal class YGetAlbumsBuilder : YMusicRequestBuilder<List<YAlbum>?, IEnumerable<string>>
public class YGetAlbumsBuilder : YRequestBuilder<YResponse<List<YAlbum>>, IEnumerable<string>>
{ {
public YGetAlbumsBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetAlbumsBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Post;
} protected override string PathTemplate => "albums";
protected override HttpContent? GetContent(IEnumerable<string> albumIds)
protected override HttpContent GetContent(IEnumerable<string> albumIds) => new FormUrlEncodedContent(new Dictionary<string, string> { { "album-ids", string.Join(",", albumIds) } });
{
return new FormUrlEncodedContent(new Dictionary<string, string> {
{ "album-ids", string.Join(",", albumIds) }
});
}
} }

View File

@@ -1,24 +1,13 @@
using System.Net; using System.Net;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Artist; using YandexMusic.API.Models.Artist;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Artist; namespace YandexMusic.API.Requests.Artist;
[YApiRequest(WebRequestMethods.Http.Get, "artists/{artistId}/brief-info")] internal class YGetArtistBuilder : YMusicRequestBuilder<YArtistBriefInfo?, string>
public class YGetArtistBuilder : YRequestBuilder<YResponse<YArtistBriefInfo>, string>
{ {
public YGetArtistBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetArtistBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Get;
} protected override string PathTemplate => "artists/{artistId}/brief-info";
protected override Dictionary<string, string> GetSubstitutions(string artistId) protected override Dictionary<string, string> GetSubstitutions(string artistId)
{ => new() { { "artistId", artistId } };
return new Dictionary<string, string> {
{ "artistId", artistId }
};
}
} }

View File

@@ -1,31 +1,19 @@
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Net; using System.Net;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Artist; using YandexMusic.API.Models.Artist;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Artist; namespace YandexMusic.API.Requests.Artist;
[YApiRequest(WebRequestMethods.Http.Get, "artists/{artistId}/tracks")] internal class YGetArtistTrackBuilder : YMusicRequestBuilder<YTracksPage?, (string id, int page, int pageSize)>
public class YGetArtistTrackBuilder : YRequestBuilder<YResponse<YTracksPage>, (string id, int page, int pageSize)>
{ {
public YGetArtistTrackBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) { } public YGetArtistTrackBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Get;
protected override string PathTemplate => "artists/{artistId}/tracks";
protected override Dictionary<string, string> GetSubstitutions((string id, int page, int pageSize) tuple) protected override Dictionary<string, string> GetSubstitutions((string id, int page, int pageSize) tuple)
{ => new() { { "artistId", tuple.id } };
return new Dictionary<string, string> {
{ "artistId", tuple.id },
};
}
protected override NameValueCollection GetQueryParams((string id, int page, int pageSize) tuple) protected override NameValueCollection GetQueryParams((string id, int page, int pageSize) tuple)
{ => new() {
return new NameValueCollection {
{ "page", tuple.page.ToString() }, { "page", tuple.page.ToString() },
{ "pageSize", tuple.pageSize.ToString() }, { "pageSize", tuple.pageSize.ToString() },
}; };
}
} }

View File

@@ -1,24 +1,13 @@
using System.Net; using System.Net;
using YandexMusic.API.Common;
using YandexMusic.API.Models.Artist; using YandexMusic.API.Models.Artist;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Requests.Common;
using YandexMusic.API.Requests.Common.Attributes;
namespace YandexMusic.API.Requests.Artist; namespace YandexMusic.API.Requests.Artist;
[YApiRequest(WebRequestMethods.Http.Post, "artists")] internal class YGetArtistsBuilder : YMusicRequestBuilder<List<YArtist>?, IEnumerable<string>>
public class YGetArtistsBuilder : YRequestBuilder<YResponse<List<YArtist>>, IEnumerable<string>>
{ {
public YGetArtistsBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) public YGetArtistsBuilder(YandexMusicApi api) : base(api) { }
{ protected override string Method => WebRequestMethods.Http.Post;
} protected override string PathTemplate => "artists";
protected override HttpContent? GetContent(IEnumerable<string> artistIds)
protected override HttpContent GetContent(IEnumerable<string> artistIds) => new FormUrlEncodedContent(new Dictionary<string, string> { { "artist-Ids", string.Join(",", artistIds) } });
{
return new FormUrlEncodedContent(new Dictionary<string, string> {
{ "artist-Ids", string.Join(",", artistIds) }
});
}
} }

View File

@@ -1,9 +0,0 @@
namespace YandexMusic.API.Requests.Common.Attributes;
public class YApiRequestAttribute : YBasePathRequestAttribute
{
public YApiRequestAttribute(string method, string url) : base(method, url)
{
basePath = "https://api.music.yandex.net";
}
}

View File

@@ -1,31 +0,0 @@
namespace YandexMusic.API.Requests.Common.Attributes;
/// <summary>
/// Атрибут запроса относительно базового адреса
/// </summary>
public class YBasePathRequestAttribute : YRequestAttribute
{
#region Поля
protected string basePath;
#endregion Поля
#region Свойства
public override string Url => GetFullUrl();
#endregion Свойства
#region Вспомогательные функции
private string GetFullUrl()
{
return $"{basePath.TrimEnd('/')}/{path.TrimStart('/')}";
}
#endregion Вспомогательные функции
public YBasePathRequestAttribute(string method, string url) : base(method, url)
{
}
}

View File

@@ -1,9 +0,0 @@
namespace YandexMusic.API.Requests.Common.Attributes;
public class YLoginRequestAttribute : YBasePathRequestAttribute
{
public YLoginRequestAttribute(string method, string url) : base(method, url)
{
basePath = "https://login.yandex.ru";
}
}

View File

@@ -1,9 +0,0 @@
namespace YandexMusic.API.Requests.Common.Attributes;
public class YMobileProxyRequestAttribute : YBasePathRequestAttribute
{
public YMobileProxyRequestAttribute(string method, string url) : base(method, url)
{
basePath = "https://mobileproxy.passport.yandex.net";
}
}

View File

@@ -1,9 +0,0 @@
namespace YandexMusic.API.Requests.Common.Attributes;
public class YOAuthMobileAttribute : YBasePathRequestAttribute
{
public YOAuthMobileAttribute(string method, string url) : base(method, url)
{
basePath = "https://oauth.mobile.yandex.net";
}
}

View File

@@ -1,9 +0,0 @@
namespace YandexMusic.API.Requests.Common.Attributes;
public class YOAuthRequestAttribute : YBasePathRequestAttribute
{
public YOAuthRequestAttribute(string method, string url) : base(method, url)
{
basePath = "https://oauth.yandex.ru";
}
}

View File

@@ -1,9 +0,0 @@
namespace YandexMusic.API.Requests.Common.Attributes;
public class YPassportRequestAttribute : YBasePathRequestAttribute
{
public YPassportRequestAttribute(string method, string url) : base(method, url)
{
basePath = "https://passport.yandex.ru";
}
}

View File

@@ -1,26 +0,0 @@
namespace YandexMusic.API.Requests.Common.Attributes;
/// <summary>
/// Атрибут запроса без привязки к базовому адресу
/// </summary>
public class YRequestAttribute : Attribute
{
#region Поля
protected string path;
#endregion Поля
#region Свойства
public string Method { get; }
public virtual string Url => path;
#endregion Свойства
public YRequestAttribute(string method, string url)
{
Method = method;
path = url;
}
}

View File

@@ -1,9 +0,0 @@
namespace YandexMusic.API.Requests.Common.Attributes;
public class YWebApiRequestAttribute : YBasePathRequestAttribute
{
public YWebApiRequestAttribute(string method, string url) : base(method, url)
{
basePath = "https://music.yandex.ru";
}
}

View File

@@ -1,25 +0,0 @@
using System.Net;
namespace YandexMusic.API.Requests.Common;
public class HttpContext
{
public CookieContainer Cookies;
public HttpContext()
{
Cookies = new CookieContainer();
}
public IWebProxy WebProxy { get; set; }
public long GetTimeInterval()
{
DateTime dt = TimeZoneInfo.ConvertTimeToUtc(DateTime.Now);
DateTime dt1970 = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
TimeSpan tsInterval = dt.Subtract(dt1970);
long iMilliseconds = Convert.ToInt64(tsInterval.TotalMilliseconds);
return iMilliseconds;
}
}

View File

@@ -7,4 +7,11 @@ internal class YConstants
public const string XClientId = "c0ebe342af7d48fbbbfcf2d2eedb8f9e"; public const string XClientId = "c0ebe342af7d48fbbbfcf2d2eedb8f9e";
public const string XClientSecret = "ad0a908f0aa341a182a37ecd75bc319e"; public const string XClientSecret = "ad0a908f0aa341a182a37ecd75bc319e";
internal static class Endpoints
{
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";
}
} }

View File

@@ -0,0 +1,66 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using YandexMusic.API.Converters;
using YandexMusic.API.Models.Common;
namespace YandexMusic.API.Requests.Common;
/// <summary>
/// Строитель запросов с десериализацией JSON-ответа в TResponse.
/// </summary>
internal abstract class YJsonRequestBuilder<TResponse, TParams> : YRequestBuilder<TParams>
{
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)
{
var json = await response.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
Converters = {
new JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower),
new IntToStringConverter(),
new StringToIntConverter(),
new YExecutionContextConverter(Api, Storage),
}
};
if (!response.IsSuccessStatusCode)
{
var error = JsonSerializer.Deserialize<YErrorResponse>(json, options);
throw error ?? new Exception($"Ошибка HTTP {response.StatusCode}: {json}");
}
try
{
return JsonSerializer.Deserialize<TResponse>(json, options);
}
catch (Exception ex)
{
throw new Exception($"Ошибка десериализации: {ex.Message}\nJSON: {json}", ex);
}
}
protected string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
/// <summary>
/// Выполняет запрос и возвращает десериализованный объект типа TResponse.
/// </summary>
public async Task<TResponse?> ExecuteAsync(TParams parameters)
{
using var response = await ExecuteRawAsync(parameters);
return await DeserializeAsync(response);
}
}

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