Compare commits
27 Commits
11d0b0d72f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c5dca1491 | ||
| 815283a776 | |||
| 526353d679 | |||
| eb1eba0162 | |||
|
|
34261d02a9 | ||
|
|
5f761d4fe8 | ||
|
|
b6f78da9c8 | ||
|
|
0bbaac5689 | ||
|
|
a7caf829d3 | ||
|
|
add7f08215 | ||
|
|
36e28ce3fe | ||
|
|
5541d0ad27 | ||
|
|
ea9f392896 | ||
|
|
6dcf39de56 | ||
|
|
50da85be57 | ||
|
|
699d38da74 | ||
|
|
1779ecaca9 | ||
|
|
8abc6c5074 | ||
|
|
b8f78a5856 | ||
|
|
21a0c5abe6 | ||
|
|
a40b36ef96 | ||
|
|
266aa2e181 | ||
|
|
3e5b9beaf1 | ||
|
|
6f01772bb7 | ||
|
|
dd338e3f57 | ||
|
|
ba9d97239e | ||
|
|
8444fc5f8e |
51
.gitea/workflows/release-package.yaml
Normal file
51
.gitea/workflows/release-package.yaml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pack-and-publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
|
- name: Set version from tag
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
TAG="${GITHUB_REF_NAME#v}"
|
||||||
|
echo "PACKAGE_VERSION=$TAG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Replace ProjectReference with PackageReference
|
||||||
|
run: |
|
||||||
|
sed -i "s#<ProjectReference Include=\"..\/YandexMusic.API\/YandexMusic.API.csproj\" />#<PackageReference Include=\"YandexMusic.API\" Version=\"${{ steps.version.outputs.PACKAGE_VERSION }}\" />#" YandexMusic/YandexMusic.csproj
|
||||||
|
|
||||||
|
- name: Build and Pack projects
|
||||||
|
run: |
|
||||||
|
mkdir -p artifacts
|
||||||
|
for proj in $PROJECTS; do
|
||||||
|
echo "Restoring $proj..."
|
||||||
|
dotnet restore $proj
|
||||||
|
echo "Building $proj..."
|
||||||
|
dotnet build $proj -c Release -p:Version=${{ steps.version.outputs.PACKAGE_VERSION }}
|
||||||
|
echo "Packing $proj..."
|
||||||
|
dotnet pack $proj -c Release --no-build -p:PackageVersion=${{ steps.version.outputs.PACKAGE_VERSION }} -o ./artifacts
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
PROJECTS: |
|
||||||
|
YandexMusic.API
|
||||||
|
YandexMusic
|
||||||
|
|
||||||
|
- name: Publish to NuGet
|
||||||
|
env:
|
||||||
|
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
|
||||||
|
run: dotnet nuget push ./artifacts/*.nupkg --source https://git.frigat.duckdns.org/api/packages/FrigaT/nuget/index.json --api-key $NUGET_API_KEY --skip-duplicate
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -360,4 +360,5 @@ MigrationBackup/
|
|||||||
.ionide/
|
.ionide/
|
||||||
|
|
||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
YaMusicCli/
|
||||||
|
|||||||
607
ARCHITECTURE.md
Normal file
607
ARCHITECTURE.md
Normal file
@@ -0,0 +1,607 @@
|
|||||||
|
# 🏗️ Архитектура YandexMusic
|
||||||
|
|
||||||
|
Полное описание архитектуры, компонентов и взаимодействия между слоями решения YandexMusic.
|
||||||
|
|
||||||
|
## 📋 Содержание
|
||||||
|
|
||||||
|
- [Обзор архитектуры](#обзор-архитектуры)
|
||||||
|
- [Структура слоёв](#структура-слоёв)
|
||||||
|
- [Основные компоненты](#основные-компоненты)
|
||||||
|
- [Потоки данных](#потоки-данных)
|
||||||
|
- [Паттерны проектирования](#паттерны-проектирования)
|
||||||
|
- [Модульная структура](#модульная-структура)
|
||||||
|
- [Расширяемость](#расширяемость)
|
||||||
|
|
||||||
|
## 🔍 Обзор архитектуры
|
||||||
|
|
||||||
|
### Многослойная архитектура
|
||||||
|
|
||||||
|
Проект построен на классической трёхслойной архитектуре с элементами DDD:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ CLI Layer (YaMusicCli) │ ← Точка входа
|
||||||
|
├────────────────────────────────────────┤
|
||||||
|
│ Client Layer (YandexMusic) │ ← Удобный интерфейс
|
||||||
|
├────────────────────────────────────────┤
|
||||||
|
│ API Layer (YandexMusic.API) │ ← Логика API
|
||||||
|
├────────────────────────────────────────┤
|
||||||
|
│ Data Layer (Models) │ ← Данные и сущности
|
||||||
|
├────────────────────────────────────────┤
|
||||||
|
│ Infrastructure Layer │ ← HTTP, Serialization
|
||||||
|
└────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ключевые принципы
|
||||||
|
|
||||||
|
- **Separation of Concerns** - каждый слой отвечает за одно
|
||||||
|
- **Dependency Injection** - слабая связанность через интерфейсы
|
||||||
|
- **Single Responsibility** - каждый класс решает одну задачу
|
||||||
|
- **Open/Closed Principle** - открыто для расширения, закрыто для модификации
|
||||||
|
- **Asynchronous First** - всё асинхронное по умолчанию
|
||||||
|
|
||||||
|
## 📦 Структура слоёв
|
||||||
|
|
||||||
|
### 1. CLI Layer (YaMusicCli)
|
||||||
|
|
||||||
|
**Назначение:** Командная строка интерфейс для пользователей.
|
||||||
|
|
||||||
|
```
|
||||||
|
YaMusicCli/
|
||||||
|
├── Program.cs # Точка входа, парсинг аргументов
|
||||||
|
└── Commands/ # Команды для CLI
|
||||||
|
├── SearchCommand
|
||||||
|
├── PlaylistCommand
|
||||||
|
└── TrackCommand
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответственность:**
|
||||||
|
|
||||||
|
- Парсинг команд пользователя
|
||||||
|
- Вывод результатов в консоль
|
||||||
|
- Обработка пользовательского ввода
|
||||||
|
- Форматирование данных для вывода
|
||||||
|
|
||||||
|
**Зависимости:** YandexMusic (Client Layer)
|
||||||
|
|
||||||
|
### 2. Client Layer (YandexMusic)
|
||||||
|
|
||||||
|
**Назначение:** Удобный оборачиватель (wrapper) для работы с API.
|
||||||
|
|
||||||
|
```
|
||||||
|
YandexMusic/
|
||||||
|
└── YandexMusicClient.cs
|
||||||
|
├── Initialization (конструктор)
|
||||||
|
├── Authorization (авторизация)
|
||||||
|
├── Properties (доступ к API и хранилищу)
|
||||||
|
└── Cleanup (dispose)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Основной класс:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class YandexMusicClient : IDisposable
|
||||||
|
{
|
||||||
|
// Инициализация
|
||||||
|
public YandexMusicClient(
|
||||||
|
CookieContainer? cookieContainer = null,
|
||||||
|
IWebProxy? proxy = null,
|
||||||
|
TimeSpan? timeout = null,
|
||||||
|
string? userAgent = null)
|
||||||
|
|
||||||
|
// Авторизация
|
||||||
|
public async Task AuthorizeAsync(string login, string password)
|
||||||
|
public async Task AuthorizeByTokenAsync(string token)
|
||||||
|
|
||||||
|
// Свойства доступа
|
||||||
|
public YandexMusicApi Api { get; } // API Яндекс Музыки
|
||||||
|
public AuthStorage AuthStorage { get; } // Хранилище данных
|
||||||
|
public HttpClient HttpClient { get; } // HttpClient
|
||||||
|
public YAccount Account { get; } // Информация об аккаунте
|
||||||
|
public bool IsAuthorized { get; } // Статус авторизации
|
||||||
|
public YnisonPlayer? Ynison { get; } // WebSocket плеер
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответственность:**
|
||||||
|
|
||||||
|
- Упрощение работы с API
|
||||||
|
- Управление HttpClient и cookies
|
||||||
|
- Обработка авторизации
|
||||||
|
- Инициализация компонентов
|
||||||
|
|
||||||
|
**Зависимости:** YandexMusic.API (API Layer)
|
||||||
|
|
||||||
|
### 3. API Layer (YandexMusic.API)
|
||||||
|
|
||||||
|
**Назначение:** Низкоуровневой доступ к API Яндекс Музыки.
|
||||||
|
|
||||||
|
```
|
||||||
|
YandexMusic.API/
|
||||||
|
├── YandexMusicApi.cs # Главный класс - фасад
|
||||||
|
├── API/ # API классы по функциям
|
||||||
|
│ ├── YAlbumAPI
|
||||||
|
│ ├── YArtistAPI
|
||||||
|
│ ├── YTrackAPI
|
||||||
|
│ ├── YPlaylistAPI
|
||||||
|
│ ├── YRadioAPI
|
||||||
|
│ ├── YSearchAPI
|
||||||
|
│ └── ... (остальные API)
|
||||||
|
├── Requests/ # Построители запросов
|
||||||
|
│ ├── Common/
|
||||||
|
│ ├── Album/
|
||||||
|
│ ├── Artist/
|
||||||
|
│ └── ... (по категориям)
|
||||||
|
├── Models/ # Модели данных
|
||||||
|
│ ├── Common/
|
||||||
|
│ ├── Album/
|
||||||
|
│ ├── Track/
|
||||||
|
│ └── ... (по типам)
|
||||||
|
└── Common/ # Вспомогательные компоненты
|
||||||
|
├── AuthStorage
|
||||||
|
├── Providers/
|
||||||
|
├── Encryptor
|
||||||
|
└── Ynison/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Главный класс:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class YandexMusicApi
|
||||||
|
{
|
||||||
|
// API Методы
|
||||||
|
public YAlbumAPI Album { get; }
|
||||||
|
public YArtistAPI Artist { get; }
|
||||||
|
public YTrackAPI Track { get; }
|
||||||
|
public YPlaylistAPI Playlist { get; }
|
||||||
|
public YRadioAPI Radio { get; }
|
||||||
|
public YSearchAPI Search { get; }
|
||||||
|
// ... и так далее для всех веток API
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**API Классы:**
|
||||||
|
|
||||||
|
Каждый API класс наследуется от `YCommonAPI`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public abstract class YCommonAPI
|
||||||
|
{
|
||||||
|
protected readonly YandexMusicApi api;
|
||||||
|
protected YCommonAPI(YandexMusicApi yandex) => api = yandex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пример конкретного API
|
||||||
|
public class YTrackAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YTrackAPI(YandexMusicApi yandex) : base(yandex) { }
|
||||||
|
|
||||||
|
public async Task<YTrack?> GetTrackAsync(string trackId) { }
|
||||||
|
public async Task<IEnumerable<YTrack>> GetTracksAsync(IEnumerable<string> ids) { }
|
||||||
|
// ... остальные методы
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответственность:**
|
||||||
|
|
||||||
|
- Построение HTTP запросов
|
||||||
|
- Вызов провайдера запросов
|
||||||
|
- Десериализация ответов
|
||||||
|
- Обработка ошибок API
|
||||||
|
|
||||||
|
### 4. Data Layer (Models)
|
||||||
|
|
||||||
|
**Назначение:** Модели данных, отражающие структуру API Яндекс Музыки.
|
||||||
|
|
||||||
|
```
|
||||||
|
Models/
|
||||||
|
├── Common/ # Общие модели
|
||||||
|
│ ├── YBaseModel # Базовая модель с контекстом
|
||||||
|
│ ├── YResponse<T> # Ответ от API
|
||||||
|
│ ├── YError # Ошибка
|
||||||
|
│ └── ... (остальные)
|
||||||
|
├── Album/ # Модели альбомов
|
||||||
|
│ ├── YAlbum
|
||||||
|
│ └── YAlbumMenuItem
|
||||||
|
├── Track/ # Модели треков
|
||||||
|
│ ├── YTrack
|
||||||
|
│ ├── YTrackSupplement
|
||||||
|
│ └── YTrackSimilar
|
||||||
|
├── Playlist/ # Модели плейлистов
|
||||||
|
│ ├── YPlaylist
|
||||||
|
│ ├── YPlaylistChange
|
||||||
|
│ └── YPlaylistMadeFor
|
||||||
|
└── ... (остальные модели)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Базовая модель:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public abstract class YBaseModel
|
||||||
|
{
|
||||||
|
[JsonIgnore]
|
||||||
|
public YExecutionContext? Context { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пример модели
|
||||||
|
public class YTrack : YBaseModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("duration_ms")]
|
||||||
|
public int DurationMs { get; set; }
|
||||||
|
|
||||||
|
// ... остальные свойства
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответственность:**
|
||||||
|
|
||||||
|
- Представление данных API
|
||||||
|
- System.Text.Json сериализация/десериализация
|
||||||
|
- Хранение контекста выполнения (опционально)
|
||||||
|
|
||||||
|
### 5. Infrastructure Layer
|
||||||
|
|
||||||
|
**Назначение:** Низкоуровневая инфраструктура для работы с HTTP и сериализацией.
|
||||||
|
|
||||||
|
```
|
||||||
|
Common/
|
||||||
|
├── Providers/ # Провайдеры запросов
|
||||||
|
│ ├── IRequestProvider # Интерфейс
|
||||||
|
│ ├── DefaultRequestProvider
|
||||||
|
│ ├── CommonRequestProvider
|
||||||
|
│ └── MockRequestProvider
|
||||||
|
├── AuthStorage # Управление авторизацией
|
||||||
|
├── Encryptor # Шифрование данных
|
||||||
|
├── DataDownloader # Загрузка файлов
|
||||||
|
└── Ynison/ # WebSocket
|
||||||
|
├── YnisonPlayer
|
||||||
|
└── YnisonWebSocket
|
||||||
|
```
|
||||||
|
|
||||||
|
**IRequestProvider:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IRequestProvider
|
||||||
|
{
|
||||||
|
// Выполняет HTTP запрос
|
||||||
|
Task<HttpResponseMessage> GetWebResponseAsync(
|
||||||
|
HttpRequestMessage message,
|
||||||
|
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead);
|
||||||
|
|
||||||
|
// Преобразует ответ в модель
|
||||||
|
Task<T> GetDataFromResponseAsync<T>(
|
||||||
|
YandexMusicApi api,
|
||||||
|
HttpResponseMessage response);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Потоки данных
|
||||||
|
|
||||||
|
### Типичный поток запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
1. User Action
|
||||||
|
↓
|
||||||
|
2. YandexMusicClient Method
|
||||||
|
↓
|
||||||
|
3. YandexMusicApi.SomeAPI.SomeMethodAsync()
|
||||||
|
↓
|
||||||
|
4. YRequestBuilder<T> builds HttpRequestMessage
|
||||||
|
↓
|
||||||
|
5. IRequestProvider.GetWebResponseAsync()
|
||||||
|
↓
|
||||||
|
6. HttpClient sends request
|
||||||
|
↓
|
||||||
|
7. Server responds with HttpResponseMessage
|
||||||
|
↓
|
||||||
|
8. IRequestProvider.GetDataFromResponseAsync<T>()
|
||||||
|
↓
|
||||||
|
9. System.Text.Json deserializes to Model<T>
|
||||||
|
↓
|
||||||
|
10. Model<T> returned to caller
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример: Получение трека
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 1. Пользователь вызывает метод
|
||||||
|
var track = await client.Api.Track.GetTrackAsync("trackId123");
|
||||||
|
|
||||||
|
// 2. YTrackAPI.GetTrackAsync() создаёт запрос
|
||||||
|
var builder = new YGetTracksBuilder(api);
|
||||||
|
var message = builder.Build("trackId123");
|
||||||
|
|
||||||
|
// 3. Запрос отправляется через провайдер
|
||||||
|
var response = await authStorage.Provider.GetWebResponseAsync(message);
|
||||||
|
|
||||||
|
// 4. Ответ преобразуется в модель
|
||||||
|
var track = await provider.GetDataFromResponseAsync<YTrack>(api, response);
|
||||||
|
|
||||||
|
// 5. Модель возвращается пользователю
|
||||||
|
return track;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Паттерны проектирования
|
||||||
|
|
||||||
|
### 1. Facade Pattern (YandexMusicApi)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Фасад предоставляет простой интерфейс к сложной подсистеме
|
||||||
|
public class YandexMusicApi
|
||||||
|
{
|
||||||
|
public YAlbumAPI Album { get; } // Скрывает сложность
|
||||||
|
public YArtistAPI Artist { get; } // инициализации
|
||||||
|
public YTrackAPI Track { get; } // каждого компонента
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Builder Pattern (Request Builders)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Построитель для конструирования запросов
|
||||||
|
public class YSearchBuilder : YRequestBuilder<YSearch>
|
||||||
|
{
|
||||||
|
private string _query;
|
||||||
|
private int _page = 0;
|
||||||
|
|
||||||
|
public YSearchBuilder Query(string q) { _query = q; return this; }
|
||||||
|
public YSearchBuilder Page(int p) { _page = p; return this; }
|
||||||
|
public override HttpRequestMessage Build() { /* ... */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Использование
|
||||||
|
var search = new YSearchBuilder()
|
||||||
|
.Query("Beatles")
|
||||||
|
.Page(1)
|
||||||
|
.Build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Strategy Pattern (IRequestProvider)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Интерфейс позволяет выбрать стратегию обработки запросов
|
||||||
|
public interface IRequestProvider
|
||||||
|
{
|
||||||
|
Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Разные реализации для разных сценариев
|
||||||
|
public class DefaultRequestProvider : IRequestProvider { }
|
||||||
|
public class MockRequestProvider : IRequestProvider { }
|
||||||
|
public class CustomRequestProvider : IRequestProvider { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Dependency Injection
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Зависимости передаются через конструктор
|
||||||
|
public class YTrackAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
protected readonly YandexMusicApi api; // Injected
|
||||||
|
|
||||||
|
public YTrackAPI(YandexMusicApi yandex) => api = yandex;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Template Method Pattern
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Базовый класс определяет структуру
|
||||||
|
public abstract class YCommonAPI
|
||||||
|
{
|
||||||
|
protected readonly YandexMusicApi api;
|
||||||
|
protected YCommonAPI(YandexMusicApi yandex) => api = yandex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подклассы реализуют специфичные методы
|
||||||
|
public class YAlbumAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
// Реализация методов для альбомов
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧩 Модульная структура
|
||||||
|
|
||||||
|
### Логическое разделение на модули
|
||||||
|
|
||||||
|
```
|
||||||
|
Core Module (YandexMusic.API)
|
||||||
|
├── Authentication Module
|
||||||
|
│ ├── AuthStorage
|
||||||
|
│ └── Authorization methods
|
||||||
|
├── Request Module
|
||||||
|
│ ├── IRequestProvider
|
||||||
|
│ ├── Request builders
|
||||||
|
│ └── HttpContext
|
||||||
|
├── Model Module
|
||||||
|
│ └── All data models
|
||||||
|
└── API Module
|
||||||
|
├── YAlbumAPI
|
||||||
|
├── YTrackAPI
|
||||||
|
├── YPlaylistAPI
|
||||||
|
└── ... (each API endpoint)
|
||||||
|
|
||||||
|
Client Module (YandexMusic)
|
||||||
|
├── YandexMusicClient
|
||||||
|
└── Client configuration
|
||||||
|
|
||||||
|
CLI Module (YaMusicCli)
|
||||||
|
├── Program.cs
|
||||||
|
└── CLI commands
|
||||||
|
```
|
||||||
|
|
||||||
|
### Зависимости между модулями
|
||||||
|
|
||||||
|
```
|
||||||
|
CLI Module
|
||||||
|
↓ depends on
|
||||||
|
Client Module
|
||||||
|
↓ depends on
|
||||||
|
Core Module
|
||||||
|
├── Authentication Module
|
||||||
|
├── Request Module
|
||||||
|
├── Model Module
|
||||||
|
└── API Module
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 Расширяемость
|
||||||
|
|
||||||
|
### Как добавить новый API метод
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 1. Создайте builder в Requests/YourCategory/
|
||||||
|
public class YGetYourResourceBuilder : YRequestBuilder<YYourResource>
|
||||||
|
{
|
||||||
|
public override HttpRequestMessage Build()
|
||||||
|
{
|
||||||
|
// Реализуйте построение запроса
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Добавьте метод в соответствующий API класс
|
||||||
|
public class YYourResourceAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public async Task<YYourResource?> GetYourResourceAsync(string id)
|
||||||
|
{
|
||||||
|
var builder = new YGetYourResourceBuilder(api);
|
||||||
|
var request = builder.Build();
|
||||||
|
return await new YRequest<YYourResource?>(request, api, api.AuthStorage).GetResponseAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Используйте новый метод
|
||||||
|
var resource = await api.YourResource.GetYourResourceAsync("id123");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Как создать custom RequestProvider
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class MyCustomRequestProvider : IRequestProvider
|
||||||
|
{
|
||||||
|
private readonly AuthStorage _storage;
|
||||||
|
|
||||||
|
public MyCustomRequestProvider(AuthStorage storage)
|
||||||
|
{
|
||||||
|
_storage = storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HttpResponseMessage> GetWebResponseAsync(
|
||||||
|
HttpRequestMessage message,
|
||||||
|
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||||
|
{
|
||||||
|
// Ваша логика обработки запроса
|
||||||
|
using var handler = new HttpClientHandler();
|
||||||
|
using var client = new HttpClient(handler);
|
||||||
|
return await client.SendAsync(message, completionOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> GetDataFromResponseAsync<T>(
|
||||||
|
YandexMusicApi api,
|
||||||
|
HttpResponseMessage response)
|
||||||
|
{
|
||||||
|
// Ваша логика десериализации
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
return JsonSerializer.Deserialize<T>(content) ?? default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Использование
|
||||||
|
var provider = new MyCustomRequestProvider(storage);
|
||||||
|
var storage = new AuthStorage(provider);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Как создать extension method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class YPlaylistExtensions
|
||||||
|
{
|
||||||
|
public static async Task<IEnumerable<YTrack>> GetAllTracksAsync(
|
||||||
|
this YPlaylist playlist,
|
||||||
|
YandexMusicApi api)
|
||||||
|
{
|
||||||
|
// Расширение функциональности существующего класса
|
||||||
|
var allTracks = new List<YTrack>();
|
||||||
|
var currentPage = 0;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var page = await api.Playlist.GetPlaylistAsync(
|
||||||
|
playlist.Uid,
|
||||||
|
page: currentPage);
|
||||||
|
|
||||||
|
if (page?.Tracks?.Count == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
allTracks.AddRange(page.Tracks ?? []);
|
||||||
|
currentPage++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allTracks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Диаграмма взаимодействия компонентов
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────┐
|
||||||
|
│ CLI Layer │
|
||||||
|
│ (YaMusicCli) │
|
||||||
|
└────────┬─────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ Client Layer │
|
||||||
|
│ (YandexMusicClient) │
|
||||||
|
└────────┬─────────────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌────────────────────────────────────┐
|
||||||
|
│ API Layer │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ YandexMusicApi (Facade) │ │
|
||||||
|
│ └──┬──────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──┴─────────┬─────────────────┐│
|
||||||
|
│ ↓ ↓ ↓│
|
||||||
|
│ YTrackAPI YPlaylistAPI YSearchAPI │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └──────┬─────┴────────┬────────┘ │
|
||||||
|
└─────────┼──────────────┼───────────┘
|
||||||
|
│ │
|
||||||
|
↓ ↓
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ IRequestProvider │
|
||||||
|
│ (Strategy Pattern) │
|
||||||
|
└──────┬───────────────────┘
|
||||||
|
│
|
||||||
|
┌──┴───┬──────────┐
|
||||||
|
↓ ↓ ↓
|
||||||
|
Default Common Mock
|
||||||
|
Provider Provider Provider
|
||||||
|
│ │ │
|
||||||
|
└──────┼────────┘
|
||||||
|
↓
|
||||||
|
┌──────────────────┐
|
||||||
|
│ HttpClient │
|
||||||
|
└──────┬───────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌──────────────────┐
|
||||||
|
│ Network Layer │
|
||||||
|
└──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Безопасность архитектуры
|
||||||
|
|
||||||
|
1. **Инкапсуляция** - приватные поля и контролируемый доступ
|
||||||
|
2. **Валидация** - все входные параметры валидируются
|
||||||
|
3. **Null-safety** - полная поддержка nullable reference types
|
||||||
|
4. **Async-safe** - асинхронные операции без blocking
|
||||||
|
5. **Encryption** - встроенное шифрование для чувствительных данных
|
||||||
410
CONTRIBUTING.md
Normal file
410
CONTRIBUTING.md
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
# 🤝 Руководство для участников
|
||||||
|
|
||||||
|
Спасибо за интерес к проекту YandexMusic! Этот документ содержит рекомендации для тех, кто хочет внести свой вклад в развитие проекта.
|
||||||
|
|
||||||
|
## 📋 Содержание
|
||||||
|
|
||||||
|
- [Кодекс поведения](#кодекс-поведения)
|
||||||
|
- [Как начать](#как-начать)
|
||||||
|
- [Процесс разработки](#процесс-разработки)
|
||||||
|
- [Стандарты кода](#стандарты-кода)
|
||||||
|
- [Commit сообщения](#commit-сообщения)
|
||||||
|
- [Pull Requests](#pull-requests)
|
||||||
|
- [Отчёты об ошибках](#отчёты-об-ошибках)
|
||||||
|
- [Предложения по улучшениям](#предложения-по-улучшениям)
|
||||||
|
|
||||||
|
## 🤐 Кодекс поведения
|
||||||
|
|
||||||
|
- Будьте уважительны к другим участникам
|
||||||
|
- Не допускайте дискриминацию, оскорбления и враждебного поведения
|
||||||
|
- Критикуйте идеи, а не людей
|
||||||
|
- Решайте конфликты конструктивно
|
||||||
|
|
||||||
|
## 🚀 Как начать
|
||||||
|
|
||||||
|
### 1. Подготовка окружения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Клонируем репозиторий
|
||||||
|
git clone https://git.frigat.duckdns.org/FrigaT/YandexMusic.git
|
||||||
|
cd YandexMusic
|
||||||
|
|
||||||
|
# Создаём ветку для своей работы
|
||||||
|
git checkout -b feature/my-feature
|
||||||
|
|
||||||
|
# Восстанавливаем зависимости
|
||||||
|
dotnet restore
|
||||||
|
|
||||||
|
# Собираем проект
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Установка инструментов
|
||||||
|
|
||||||
|
- **Visual Studio 2026 Enterprise** (рекомендуется)
|
||||||
|
- **Visual Studio Code** + C# extension (альтернатива)
|
||||||
|
- **.NET 10 SDK**
|
||||||
|
|
||||||
|
### 3. Запуск тестов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Процесс разработки
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Выберите issue или создайте новый
|
||||||
|
2. Создайте feature-ветку
|
||||||
|
3. Внесите изменения
|
||||||
|
4. Напишите/обновите тесты
|
||||||
|
5. Убедитесь что код собирается и тесты проходят
|
||||||
|
6. Создайте Pull Request
|
||||||
|
7. Ждите review
|
||||||
|
8. Исправьте замечания если нужно
|
||||||
|
9. Merge в master
|
||||||
|
```
|
||||||
|
|
||||||
|
### Версионирование веток
|
||||||
|
|
||||||
|
```
|
||||||
|
master → Production версия
|
||||||
|
├── feature/* → Новые функции
|
||||||
|
├── bugfix/* → Исправления ошибок
|
||||||
|
├── refactor/* → Рефакторинг кода
|
||||||
|
└── docs/* → Обновления документации
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📐 Стандарты кода
|
||||||
|
|
||||||
|
### Общие правила
|
||||||
|
|
||||||
|
- **Язык:** C# 12
|
||||||
|
- **Платформа:** .NET 10
|
||||||
|
- **Стиль:** Microsoft C# Coding Conventions
|
||||||
|
- **Документация:** Все публичные члены должны иметь XML документацию на русском
|
||||||
|
|
||||||
|
### Пример документированного кода
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace YandexMusic.API.API;
|
||||||
|
|
||||||
|
/// <summary>API для работы с альбомами.</summary>
|
||||||
|
public class YAlbumAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
/// <summary>Инициализирует новый экземпляр.</summary>
|
||||||
|
/// <param name="yandex">Экземпляр основного API.</param>
|
||||||
|
public YAlbumAPI(YandexMusicApi yandex) : base(yandex) { }
|
||||||
|
|
||||||
|
/// <summary>Получает информацию об альбоме по идентификатору.</summary>
|
||||||
|
/// <param name="albumId">Идентификатор альбома</param>
|
||||||
|
/// <returns>Модель альбома или null если не найден</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Если albumId null</exception>
|
||||||
|
public async Task<YAlbum?> GetAlbumAsync(string albumId)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(albumId);
|
||||||
|
|
||||||
|
// Реализация
|
||||||
|
return await Task.FromResult<YAlbum?>(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Правила именования
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Классы: PascalCase
|
||||||
|
public class YandexMusicApi { }
|
||||||
|
|
||||||
|
// Методы: PascalCase с Async суффиксом для асинхронных
|
||||||
|
public async Task<YTrack?> GetTrackAsync(string trackId) { }
|
||||||
|
|
||||||
|
// Свойства: PascalCase
|
||||||
|
public YandexMusicApi Api { get; }
|
||||||
|
|
||||||
|
// Приватные поля: _camelCase
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
// Параметры: camelCase
|
||||||
|
public void DoSomething(string userName, int userId) { }
|
||||||
|
|
||||||
|
// Локальные переменные: camelCase
|
||||||
|
var trackList = new List<YTrack>();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
Используйте `.editorconfig` для автоматического форматирования:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Отступы - 4 пробела
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# Новая строка для фигурных скобок
|
||||||
|
csharp_new_line_before_open_brace = all
|
||||||
|
|
||||||
|
# Используйте var где возможно
|
||||||
|
csharp_style_var_for_built_in_types = true
|
||||||
|
|
||||||
|
# Null-forgiving operator с осторожностью
|
||||||
|
csharp_style_null_forgiving_operator = false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nullable Reference Types
|
||||||
|
|
||||||
|
Всегда включен `<Nullable>enable</Nullable>`. Правила:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ Хорошо - явно указано может быть null
|
||||||
|
public string? GetUserName() { }
|
||||||
|
|
||||||
|
// ✅ Хорошо - явно не null
|
||||||
|
public string GetTitle() => "Title";
|
||||||
|
|
||||||
|
// ❌ Плохо - неявная nullable ссылка
|
||||||
|
public object GetSomething() { }
|
||||||
|
|
||||||
|
// ✅ Хорошо - используйте null-coalescing
|
||||||
|
var result = value ?? defaultValue;
|
||||||
|
|
||||||
|
// ✅ Хорошо - используйте null-conditional
|
||||||
|
var count = list?.Count ?? 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Асинхронное программирование
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ Хорошо - async/await
|
||||||
|
public async Task<YTrack?> GetTrackAsync(string id)
|
||||||
|
{
|
||||||
|
return await api.Track.GetTrackAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Плохо - Task.Result блокирует поток
|
||||||
|
public YTrack? GetTrack(string id)
|
||||||
|
{
|
||||||
|
return api.Track.GetTrackAsync(id).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Хорошо - ConfigureAwait(false) в библиотеках
|
||||||
|
public async Task<YTrack?> GetTrackAsync(string id)
|
||||||
|
{
|
||||||
|
return await api.Track.GetTrackAsync(id).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обработка ошибок
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ Хорошо
|
||||||
|
public async Task<YTrack?> GetTrackAsync(string trackId)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(trackId);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(trackId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _provider.GetTrackAsync(trackId);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Ошибка при получении трека");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Плохо - молча игнорируем ошибки
|
||||||
|
public async Task<YTrack?> GetTrackAsync(string trackId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _provider.GetTrackAsync(trackId);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Структура файла
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.API;
|
||||||
|
|
||||||
|
/// <summary>API для работы с треками.</summary>
|
||||||
|
public class YTrackAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
/// <summary>Инициализирует новый экземпляр.</summary>
|
||||||
|
public YTrackAPI(YandexMusicApi yandex) : base(yandex) { }
|
||||||
|
|
||||||
|
/// <summary>Получает трек.</summary>
|
||||||
|
public async Task<YTrack?> GetTrackAsync(string trackId)
|
||||||
|
{
|
||||||
|
// реализация
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Получает несколько треков.</summary>
|
||||||
|
public async Task<IEnumerable<YTrack>> GetTracksAsync(IEnumerable<string> trackIds)
|
||||||
|
{
|
||||||
|
// реализация
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Commit сообщения
|
||||||
|
|
||||||
|
### Формат
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>(<scope>): <subject>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type
|
||||||
|
|
||||||
|
- `feat:` - новая функция
|
||||||
|
- `fix:` - исправление ошибки
|
||||||
|
- `docs:` - обновление документации
|
||||||
|
- `style:` - форматирование кода (не влияет на функциональность)
|
||||||
|
- `refactor:` - переписывание кода (не меняет функциональность)
|
||||||
|
- `perf:` - улучшение производительности
|
||||||
|
- `test:` - добавление или обновление тестов
|
||||||
|
- `ci:` - изменения в CI/CD
|
||||||
|
- `chore:` - обновление зависимостей, версии и т.д.
|
||||||
|
|
||||||
|
### Примеры
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "feat(track-api): add support for track recommendations"
|
||||||
|
|
||||||
|
git commit -m "fix(auth): handle refresh token expiration correctly"
|
||||||
|
|
||||||
|
git commit -m "docs(readme): update installation instructions"
|
||||||
|
|
||||||
|
git commit -m "refactor(models): simplify YTrack class structure"
|
||||||
|
|
||||||
|
git commit -m "test(playlist): add tests for playlist operations"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔀 Pull Requests
|
||||||
|
|
||||||
|
### Перед созданием PR
|
||||||
|
|
||||||
|
- [ ] Код собирается без ошибок: `dotnet build`
|
||||||
|
- [ ] Все тесты проходят: `dotnet test`
|
||||||
|
- [ ] Код отформатирован согласно стилю
|
||||||
|
- [ ] Добавлена XML документация для всех публичных членов
|
||||||
|
- [ ] Обновлена документация если нужно
|
||||||
|
|
||||||
|
### Шаблон PR
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Описание
|
||||||
|
Краткое описание внесённых изменений.
|
||||||
|
|
||||||
|
## Тип изменения
|
||||||
|
- [ ] Новая функция (non-breaking change)
|
||||||
|
- [ ] Исправление ошибки (non-breaking change)
|
||||||
|
- [ ] Breaking change (поясните почему)
|
||||||
|
- [ ] Обновление документации
|
||||||
|
|
||||||
|
## Как это было протестировано?
|
||||||
|
Опишите шаги для тестирования.
|
||||||
|
|
||||||
|
## Связанные Issues
|
||||||
|
Closes #issue_number
|
||||||
|
|
||||||
|
## Чеклист
|
||||||
|
- [ ] Код следует стилю проекта
|
||||||
|
- [ ] Добавлена документация
|
||||||
|
- [ ] Нет новых warnings
|
||||||
|
- [ ] Тесты проходят
|
||||||
|
- [ ] Изменения совместимы с существующим кодом
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Отчёты об ошибках
|
||||||
|
|
||||||
|
### Шаблон Issue
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Описание ошибки
|
||||||
|
Четкое и краткое описание проблемы.
|
||||||
|
|
||||||
|
## Шаги воспроизведения
|
||||||
|
1. Сделайте...
|
||||||
|
2. Затем...
|
||||||
|
3. Ошибка воспроизведится
|
||||||
|
|
||||||
|
## Ожидаемое поведение
|
||||||
|
Что должно было происходить.
|
||||||
|
|
||||||
|
## Фактическое поведение
|
||||||
|
Что происходит на самом деле.
|
||||||
|
|
||||||
|
## Окружение
|
||||||
|
- OS: Windows 11
|
||||||
|
- .NET Version: 10.0
|
||||||
|
- Visual Studio: 2026 Enterprise
|
||||||
|
- YandexMusic Version: 0.0.1
|
||||||
|
|
||||||
|
## Дополнительный контекст
|
||||||
|
Скриптстек, логи, код примера.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 Предложения по улучшениям
|
||||||
|
|
||||||
|
### Шаблон Feature Request
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Описание
|
||||||
|
Четкое описание желаемой функции.
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
Как бы выглядел код с использованием этой функции?
|
||||||
|
|
||||||
|
## Альтернативы
|
||||||
|
Есть ли другие способы решить эту проблему?
|
||||||
|
|
||||||
|
## Дополнительный контекст
|
||||||
|
Почему это нужно добавить?
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Области для внесения вклада
|
||||||
|
|
||||||
|
### High Priority
|
||||||
|
|
||||||
|
- [ ] Документация API методов
|
||||||
|
- [ ] Unit тесты для API классов
|
||||||
|
- [ ] Обработка ошибок
|
||||||
|
- [ ] Оптимизация производительности
|
||||||
|
|
||||||
|
### Medium Priority
|
||||||
|
|
||||||
|
- [ ] Примеры использования
|
||||||
|
- [ ] Интеграционные тесты
|
||||||
|
- [ ] Улучшение логирования
|
||||||
|
- [ ] Расширение функциональности
|
||||||
|
|
||||||
|
### Low Priority
|
||||||
|
|
||||||
|
- [ ] Рефакторинг кода
|
||||||
|
- [ ] Улучшение читаемости
|
||||||
|
- [ ] Документация
|
||||||
|
|
||||||
|
## 📞 Контакты
|
||||||
|
|
||||||
|
Вопросы? Свяжитесь с разработчиком:
|
||||||
|
|
||||||
|
- **Issues:** [https://git.frigat.duckdns.org/FrigaT/YandexMusic/issues]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Спасибо за то, что делаете YandexMusic лучше! 🚀
|
||||||
446
FAQ.md
Normal file
446
FAQ.md
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
# ❓ FAQ - Часто задаваемые вопросы
|
||||||
|
|
||||||
|
Ответы на самые частые вопросы по использованию YandexMusic.
|
||||||
|
|
||||||
|
## 📋 Содержание
|
||||||
|
|
||||||
|
- [Общие вопросы](#общие-вопросы)
|
||||||
|
- [Установка и настройка](#установка-и-настройка)
|
||||||
|
- [Использование](#использование)
|
||||||
|
- [Авторизация](#авторизация)
|
||||||
|
- [Ошибки и проблемы](#ошибки-и-проблемы)
|
||||||
|
- [Производительность](#производительность)
|
||||||
|
- [Разработка](#разработка)
|
||||||
|
|
||||||
|
## 📖 Общие вопросы
|
||||||
|
|
||||||
|
### Q: Что такое YandexMusic?
|
||||||
|
|
||||||
|
**A:** YandexMusic — это .NET 10 библиотека для работы с неофициальным API Яндекс Музыки. Она позволяет искать музыку, управлять плейлистами, получать информацию об альбомах и исполнителях, а также работать с WebSocket через протокол Ynison.
|
||||||
|
|
||||||
|
### Q: Это официальная библиотека?
|
||||||
|
|
||||||
|
**A:** Нет, это неофициальная библиотека. Используйте её на свой риск и соблюдайте Terms of Service Яндекс Музыки.
|
||||||
|
|
||||||
|
### Q: На каких платформах работает?
|
||||||
|
|
||||||
|
**A:** На любых платформах, поддерживающих .NET 10:
|
||||||
|
- Windows (10/11)
|
||||||
|
- Linux (Ubuntu, Debian, Red Hat, etc.)
|
||||||
|
- macOS (Intel & Apple Silicon)
|
||||||
|
|
||||||
|
### Q: Нужна ли авторизация?
|
||||||
|
|
||||||
|
**A:** Нет, многие операции работают без авторизации (поиск, получение информации о треках). Но для некоторых операций (работа с лайками, плейлистами) требуется авторизация.
|
||||||
|
|
||||||
|
### Q: Где я могу сообщить об ошибке?
|
||||||
|
|
||||||
|
**A:** Создайте issue на GitHub:
|
||||||
|
- https://git.frigat.duckdns.org/FrigaT/YandexMusic/issues
|
||||||
|
|
||||||
|
## 🚀 Установка и настройка
|
||||||
|
|
||||||
|
### Q: Какие требования?
|
||||||
|
|
||||||
|
**A:**
|
||||||
|
- .NET 10 SDK или выше
|
||||||
|
- C# 12 совместимый компилятор
|
||||||
|
- Интернет соединение
|
||||||
|
|
||||||
|
### Q: Как установить библиотеку?
|
||||||
|
|
||||||
|
**A:** Несколько способов:
|
||||||
|
|
||||||
|
1. **Через NuGet:**
|
||||||
|
```bash
|
||||||
|
dotnet add package YandexMusic
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Из GitHub:**
|
||||||
|
```bash
|
||||||
|
git clone https://git.frigat.duckdns.org/FrigaT/YandexMusic.git
|
||||||
|
cd YandexMusic
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Добавить ссылку на проект:**
|
||||||
|
```bash
|
||||||
|
dotnet add reference ../YandexMusic/YandexMusic.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Как обновить библиотеку?
|
||||||
|
|
||||||
|
**A:** Через NuGet:
|
||||||
|
```bash
|
||||||
|
dotnet package update YandexMusic
|
||||||
|
```
|
||||||
|
|
||||||
|
Или если вы клонировали репозиторий:
|
||||||
|
```bash
|
||||||
|
git pull origin master
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Какие зависимости нужны?
|
||||||
|
|
||||||
|
**A:** Только встроенные в .NET 10:
|
||||||
|
- System.Net.Http
|
||||||
|
- System.Text.Json
|
||||||
|
- System.Net.WebSockets
|
||||||
|
|
||||||
|
Нет дополнительных NuGet пакетов!
|
||||||
|
|
||||||
|
## 💻 Использование
|
||||||
|
|
||||||
|
### Q: Как начать использовать?
|
||||||
|
|
||||||
|
**A:** Самый простой способ:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using YandexMusic;
|
||||||
|
|
||||||
|
var client = new YandexMusicClient();
|
||||||
|
var results = await client.Api.Search.SearchAsync("Beatles");
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробнее в [QUICKSTART.md](QUICKSTART.md).
|
||||||
|
|
||||||
|
### Q: Как получить информацию о треке?
|
||||||
|
|
||||||
|
**A:**
|
||||||
|
```csharp
|
||||||
|
var track = await client.Api.Track.GetTrackAsync("trackId");
|
||||||
|
Console.WriteLine($"{track?.Title} - {track?.Artists?.FirstOrDefault()?.Title}");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Как найти ID трека/альбома/плейлиста?
|
||||||
|
|
||||||
|
**A:**
|
||||||
|
- В URL страницы на music.yandex.ru
|
||||||
|
- Через API при поиске
|
||||||
|
- На официальном сайте в адресной строке
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
- Трек: `https://music.yandex.ru/album/123/track/456` → ID: `456`
|
||||||
|
- Альбом: `https://music.yandex.ru/album/123` → ID: `123`
|
||||||
|
- Плейлист: `https://music.yandex.ru/playlist/123` → ID: `123`
|
||||||
|
|
||||||
|
### Q: Почему я получаю null?
|
||||||
|
|
||||||
|
**A:** Несколько причин:
|
||||||
|
|
||||||
|
1. Неправильный ID
|
||||||
|
2. Ресурс был удален
|
||||||
|
3. Нет доступа (требуется авторизация)
|
||||||
|
4. Сервер вернул ошибку
|
||||||
|
|
||||||
|
Всегда проверяйте результат:
|
||||||
|
```csharp
|
||||||
|
var track = await client.Api.Track.GetTrackAsync(id);
|
||||||
|
if (track == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Трек не найден или нет доступа");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Как работать с пагинацией?
|
||||||
|
|
||||||
|
**A:** Используйте параметр `page`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var page1 = await client.Api.Search.SearchAsync("query", page: 0);
|
||||||
|
var page2 = await client.Api.Search.SearchAsync("query", page: 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Как получить большое количество данных?
|
||||||
|
|
||||||
|
**A:** Используйте цикл с пагинацией:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var allResults = new List<YTrack>();
|
||||||
|
int page = 0;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var search = await client.Api.Search.SearchAsync("query", page: page);
|
||||||
|
if (search?.Tracks?.Results?.Count == 0) break;
|
||||||
|
|
||||||
|
allResults.AddRange(search.Tracks.Results ?? []);
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Авторизация
|
||||||
|
|
||||||
|
### Q: Как авторизироваться?
|
||||||
|
|
||||||
|
**A:** Два способа:
|
||||||
|
|
||||||
|
1. **Через логин и пароль:**
|
||||||
|
```csharp
|
||||||
|
await client.AuthorizeAsync("email@gmail.com", "password");
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Через токен:**
|
||||||
|
```csharp
|
||||||
|
await client.AuthorizeByTokenAsync("your-oauth-token");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Где получить OAuth токен?
|
||||||
|
|
||||||
|
**A:** Вы можете:
|
||||||
|
1. Авторизироваться через логин/пароль (библиотека получит токен автоматически)
|
||||||
|
2. Получить токен через официальное приложение Яндекса
|
||||||
|
3. Использовать токен из DevTools браузера
|
||||||
|
|
||||||
|
### Q: Как проверить авторизирован ли я?
|
||||||
|
|
||||||
|
**A:**
|
||||||
|
```csharp
|
||||||
|
if (client.IsAuthorized)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Авторизирован: {client.Account.User?.DisplayName}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("Требуется авторизация");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Сохраняется ли авторизация?
|
||||||
|
|
||||||
|
**A:** По умолчанию нет. Вы должны авторизироваться при каждом запуске.
|
||||||
|
|
||||||
|
Если хотите сохранить токен:
|
||||||
|
```csharp
|
||||||
|
// После авторизации
|
||||||
|
string token = client.AuthStorage.Token;
|
||||||
|
|
||||||
|
// Сохранить в файл, БД и т.д.
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// При следующем запуске
|
||||||
|
var client = new YandexMusicClient();
|
||||||
|
await client.AuthorizeByTokenAsync(savedToken);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Безопасно ли хранить пароль?
|
||||||
|
|
||||||
|
**A:** **Нет!** Никогда не сохраняйте пароль в открытом виде. Лучше:
|
||||||
|
1. Используйте OAuth токены
|
||||||
|
2. Сохраняйте токены в безопасном хранилище
|
||||||
|
3. Используйте переменные окружения
|
||||||
|
|
||||||
|
## ⚠️ Ошибки и проблемы
|
||||||
|
|
||||||
|
### Q: Получаю `HttpRequestException`
|
||||||
|
|
||||||
|
**A:** Проблемы с сетью или сервером:
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var track = await client.Api.Track.GetTrackAsync(id);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Сетевая ошибка: {ex.Message}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверьте:
|
||||||
|
- Интернет соединение
|
||||||
|
- Доступность сервера Яндекса
|
||||||
|
- Firewall/прокси настройки
|
||||||
|
|
||||||
|
### Q: Получаю `JsonException`
|
||||||
|
|
||||||
|
**A:** Проблема с десериализацией JSON. Обновите библиотеку или сообщите об issue.
|
||||||
|
|
||||||
|
### Q: Получаю 401 Unauthorized
|
||||||
|
|
||||||
|
**A:** Проблема с авторизацией:
|
||||||
|
```csharp
|
||||||
|
// Проверьте токен
|
||||||
|
if (!client.IsAuthorized)
|
||||||
|
{
|
||||||
|
await client.AuthorizeAsync("email", "password");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Или обновите токен
|
||||||
|
await client.AuthorizeByTokenAsync(newToken);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Получаю 429 Too Many Requests
|
||||||
|
|
||||||
|
**A:** Вы отправляете слишком много запросов. Добавьте задержку:
|
||||||
|
```csharp
|
||||||
|
for (int i = 0; i < ids.Count; i++)
|
||||||
|
{
|
||||||
|
var track = await client.Api.Track.GetTrackAsync(ids[i]);
|
||||||
|
if (i < ids.Count - 1) await Task.Delay(100); // 100ms задержка
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Результаты пусты/null
|
||||||
|
|
||||||
|
**A:** Несколько причин:
|
||||||
|
|
||||||
|
1. **Неправильный ID:**
|
||||||
|
```csharp
|
||||||
|
var track = await client.Api.Track.GetTrackAsync("invalid-id");
|
||||||
|
// Будет null если ID неверный
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Нет результатов поиска:**
|
||||||
|
```csharp
|
||||||
|
var results = await client.Api.Search.SearchAsync("абракадабра");
|
||||||
|
// results?.Tracks?.Results будет пусто
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Требуется авторизация:**
|
||||||
|
```csharp
|
||||||
|
var library = await client.Api.Library.GetLibraryAsync();
|
||||||
|
// Вернёт null если не авторизирован
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Почему медленно работает?
|
||||||
|
|
||||||
|
**A:** Несколько причин:
|
||||||
|
1. Медленное интернет соединение
|
||||||
|
2. Сервер Яндекса перегружен
|
||||||
|
3. Получаете большое количество данных
|
||||||
|
4. Блокирующие операции вместо async
|
||||||
|
|
||||||
|
Используйте асинхронные операции:
|
||||||
|
```csharp
|
||||||
|
// ✅ Хорошо
|
||||||
|
var track = await client.Api.Track.GetTrackAsync(id);
|
||||||
|
|
||||||
|
// ❌ Плохо
|
||||||
|
var track = client.Api.Track.GetTrackAsync(id).Result;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Производительность
|
||||||
|
|
||||||
|
### Q: Как оптимизировать производительность?
|
||||||
|
|
||||||
|
**A:** Несколько советов:
|
||||||
|
|
||||||
|
1. **Используйте пакетные операции:**
|
||||||
|
```csharp
|
||||||
|
// ✅ Хорошо - один запрос
|
||||||
|
var tracks = await client.Api.Track.GetTracksAsync(["id1", "id2", "id3"]);
|
||||||
|
|
||||||
|
// ❌ Плохо - три запроса
|
||||||
|
foreach (var id in ids)
|
||||||
|
{
|
||||||
|
var track = await client.Api.Track.GetTrackAsync(id);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Кешируйте результаты:**
|
||||||
|
```csharp
|
||||||
|
private Dictionary<string, YTrack?> cache = new();
|
||||||
|
|
||||||
|
public async Task<YTrack?> GetCached(string id)
|
||||||
|
{
|
||||||
|
if (cache.TryGetValue(id, out var cached)) return cached;
|
||||||
|
var track = await client.Api.Track.GetTrackAsync(id);
|
||||||
|
cache[id] = track;
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Запускайте запросы параллельно:**
|
||||||
|
```csharp
|
||||||
|
var (albums, artists) = await (
|
||||||
|
Task.Run(() => client.Api.Album.GetAlbumsAsync(ids)),
|
||||||
|
Task.Run(() => client.Api.Artist.GetArtistsAsync(ids))
|
||||||
|
).AsAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Добавляйте задержку между запросами:**
|
||||||
|
```csharp
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
var track = await client.Api.Track.GetTrackAsync(id);
|
||||||
|
await Task.Delay(100); // 100ms между запросами
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Есть ли какие-то лимиты?
|
||||||
|
|
||||||
|
**A:** Яндекс Музыка имеет лимиты:
|
||||||
|
- Количество запросов в секунду
|
||||||
|
- Размер результатов поиска
|
||||||
|
- Размер файлов для загрузки
|
||||||
|
|
||||||
|
Уважайте эти лимиты и не создавайте нагрузку на сервер.
|
||||||
|
|
||||||
|
## 🔧 Разработка
|
||||||
|
|
||||||
|
### Q: Как стать разработчиком?
|
||||||
|
|
||||||
|
**A:** Читайте [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
|
### Q: Как создать pull request?
|
||||||
|
|
||||||
|
**A:**
|
||||||
|
1. Форкните репозиторий
|
||||||
|
2. Создайте ветку: `git checkout -b feature/my-feature`
|
||||||
|
3. Сделайте изменения
|
||||||
|
4. Коммитьте: `git commit -m "feat: описание"`
|
||||||
|
5. Пушьте: `git push origin feature/my-feature`
|
||||||
|
6. Создайте Pull Request
|
||||||
|
|
||||||
|
### Q: Как собрать проект?
|
||||||
|
|
||||||
|
**A:**
|
||||||
|
```bash
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Как запустить тесты?
|
||||||
|
|
||||||
|
**A:**
|
||||||
|
```bash
|
||||||
|
dotnet test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Есть ли стиль кода?
|
||||||
|
|
||||||
|
**A:** Да, используется Microsoft C# Coding Conventions. Смотрите [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
|
### Q: Как добавить новый API метод?
|
||||||
|
|
||||||
|
**A:** Прочитайте [ARCHITECTURE.md](ARCHITECTURE.md) раздел "Как добавить новый API метод".
|
||||||
|
|
||||||
|
### Q: Как создать тест?
|
||||||
|
|
||||||
|
**A:** Создайте файл в папке Tests:
|
||||||
|
```csharp
|
||||||
|
using Xunit;
|
||||||
|
using YandexMusic.API;
|
||||||
|
|
||||||
|
public class YTrackAPITests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTrackAsync_WithValidId_ReturnsTrack()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var api = new YandexMusicApi();
|
||||||
|
string trackId = "valid-id";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await api.Track.GetTrackAsync(trackId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🆘 Не нашли ответ?
|
||||||
|
|
||||||
|
Создайте вопрос через GitHub Issues:
|
||||||
|
https://git.frigat.duckdns.org/FrigaT/YandexMusic/issues
|
||||||
536
QUICKSTART.md
Normal file
536
QUICKSTART.md
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
# ⚡ Быстрый старт - YandexMusic
|
||||||
|
|
||||||
|
5-минутное введение в использование YandexMusic для начинающих.
|
||||||
|
|
||||||
|
## 📋 Содержание
|
||||||
|
|
||||||
|
- [Установка](#установка)
|
||||||
|
- [Ваш первый запрос](#ваш-первый-запрос)
|
||||||
|
- [Авторизация](#авторизация)
|
||||||
|
- [Основные операции](#основные-операции)
|
||||||
|
- [WebSocket (Ynison)](#websocket-ynison)
|
||||||
|
- [Обработка ошибок](#обработка-ошибок)
|
||||||
|
- [Дальнейшее обучение](#дальнейшее-обучение)
|
||||||
|
|
||||||
|
## 📦 Установка
|
||||||
|
|
||||||
|
### 1. Требования
|
||||||
|
|
||||||
|
- .NET 10 SDK
|
||||||
|
- C# 12 совместимый компилятор
|
||||||
|
|
||||||
|
### 2. Создание проекта
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создаём консольное приложение
|
||||||
|
dotnet new console -n MyMusicApp
|
||||||
|
cd MyMusicApp
|
||||||
|
|
||||||
|
# Добавляем ссылку на YandexMusic (если опубликовано в NuGet)
|
||||||
|
dotnet add package YandexMusic
|
||||||
|
|
||||||
|
# ИЛИ добавляем ссылку на проект локально
|
||||||
|
dotnet add reference ../YandexMusic/YandexMusic.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Восстановление зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet restore
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✨ Ваш первый запрос
|
||||||
|
|
||||||
|
### Простой поиск (без авторизации)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using YandexMusic;
|
||||||
|
using YandexMusic.API;
|
||||||
|
|
||||||
|
// Создаём клиент
|
||||||
|
var client = new YandexMusicClient();
|
||||||
|
|
||||||
|
// Ищем музыку
|
||||||
|
var results = await client.Api.Search.SearchAsync("Ленинград");
|
||||||
|
|
||||||
|
// Выводим первые результаты
|
||||||
|
if (results?.Tracks?.Results != null)
|
||||||
|
{
|
||||||
|
foreach (var track in results.Tracks.Results.Take(5))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"🎵 {track.Title} - {string.Join(", ", track.Artists?.Select(a => a.Title) ?? [])}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Получение информации о треке
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var track = await client.Api.Track.GetTrackAsync("trackId");
|
||||||
|
|
||||||
|
if (track != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Трек: {track.Title}");
|
||||||
|
Console.WriteLine($"Исполнитель: {track.Artists?.FirstOrDefault()?.Title}");
|
||||||
|
Console.WriteLine($"Альбом: {track.Album?.Title}");
|
||||||
|
Console.WriteLine($"Длительность: {track.DurationMs / 1000} сек");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Авторизация
|
||||||
|
|
||||||
|
### Способ 1: Через логин и пароль
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var client = new YandexMusicClient();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Авторизуемся
|
||||||
|
await client.AuthorizeAsync("your-email@gmail.com", "your-password");
|
||||||
|
|
||||||
|
Console.WriteLine($"✅ Авторизирован: {client.Account.User?.DisplayName}");
|
||||||
|
Console.WriteLine($"💰 Подписка: {client.Account.Plus?.HasPlus}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ Ошибка: {ex.Message}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Способ 2: Через токен
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var client = new YandexMusicClient();
|
||||||
|
|
||||||
|
// Если у вас уже есть токен
|
||||||
|
await client.AuthorizeByTokenAsync("your-oauth-token");
|
||||||
|
|
||||||
|
Console.WriteLine($"✅ Авторизирован через токен");
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Основные операции
|
||||||
|
|
||||||
|
### 1. Поиск
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Поиск треков
|
||||||
|
var search = await client.Api.Search.SearchAsync("Beatles");
|
||||||
|
var tracks = search?.Tracks?.Results ?? [];
|
||||||
|
foreach (var t in tracks.Take(10))
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {t.Title}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поиск альбомов
|
||||||
|
var albumSearch = await client.Api.Search.SearchAsync("Abbey Road", type: "album");
|
||||||
|
|
||||||
|
// Поиск исполнителей
|
||||||
|
var artistSearch = await client.Api.Search.SearchAsync("John Lennon", type: "artist");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Альбомы
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Получение альбома
|
||||||
|
var album = await client.Api.Album.GetAlbumAsync("albumId");
|
||||||
|
Console.WriteLine($"Альбом: {album?.Title} ({album?.Year})");
|
||||||
|
Console.WriteLine($"Жанр: {album?.Genre}");
|
||||||
|
|
||||||
|
// Список треков в альбоме
|
||||||
|
foreach (var track in album?.Tracks ?? [])
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {track.Position}. {track.Title}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение нескольких альбомов
|
||||||
|
var albums = await client.Api.Album.GetAlbumsAsync(["albumId1", "albumId2"]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Исполнители
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Получение исполнителя
|
||||||
|
var artist = await client.Api.Artist.GetArtistAsync("artistId");
|
||||||
|
Console.WriteLine($"Исполнитель: {artist?.Title}");
|
||||||
|
Console.WriteLine($"Жанр: {artist?.Genre}");
|
||||||
|
|
||||||
|
// Фото исполнителя
|
||||||
|
if (artist?.Cover?.Pic != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Фото: {artist.Cover.Pic}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Информация об исполнителе
|
||||||
|
Console.WriteLine($"Альбомов: {artist?.Albums?.Count}");
|
||||||
|
Console.WriteLine($"Треков: {artist?.Tracks?.Count}");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Плейлисты
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Получение плейлиста
|
||||||
|
var playlist = await client.Api.Playlist.GetPlaylistAsync("playlistId");
|
||||||
|
Console.WriteLine($"Плейлист: {playlist?.Title}");
|
||||||
|
Console.WriteLine($"Описание: {playlist?.Description}");
|
||||||
|
Console.WriteLine($"Треков: {playlist?.Tracks?.Count}");
|
||||||
|
|
||||||
|
// Список треков
|
||||||
|
foreach (var track in playlist?.Tracks ?? [])
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {track.Title}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение информации о плейлисте через UUID
|
||||||
|
var playlistByUuid = await client.Api.Playlist.GetPlaylistByUuidAsync("uuid");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Треки
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Получение одного трека
|
||||||
|
var track = await client.Api.Track.GetTrackAsync("trackId");
|
||||||
|
|
||||||
|
// Получение нескольких треков
|
||||||
|
var tracks = await client.Api.Track.GetTracksAsync(["id1", "id2", "id3"]);
|
||||||
|
|
||||||
|
// Дополнительная информация о треке
|
||||||
|
var supplement = await client.Api.Track.GetTrackSupplementAsync("trackId");
|
||||||
|
|
||||||
|
// Похожие треки
|
||||||
|
var similar = await client.Api.Track.GetTrackSimilarAsync("trackId");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Радио
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Получение станций
|
||||||
|
var stations = await client.Api.Radio.GetStationsAsync();
|
||||||
|
|
||||||
|
foreach (var station in stations?.Stations ?? [])
|
||||||
|
{
|
||||||
|
Console.WriteLine($"📻 {station.Title}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Треки станции
|
||||||
|
var stationTracks = await client.Api.Radio.GetStationTracksAsync("stationId");
|
||||||
|
foreach (var track in stationTracks?.Sequence ?? [])
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {track.Track?.Title}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Библиотека (Лайки)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Получение лайков
|
||||||
|
var library = await client.Api.Library.GetLibraryAsync();
|
||||||
|
|
||||||
|
Console.WriteLine($"Лайки: {library?.Library?.Count}");
|
||||||
|
|
||||||
|
// Добавление трека в лайки
|
||||||
|
await client.Api.Library.LibraryAddAsync("trackId");
|
||||||
|
|
||||||
|
// Удаление из лайков
|
||||||
|
await client.Api.Library.LibraryRemoveAsync("trackId");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Главная страница (Landing)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Получение рекомендаций
|
||||||
|
var landing = await client.Api.Landing.GetLandingAsync("home");
|
||||||
|
|
||||||
|
foreach (var block in landing?.Blocks ?? [])
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Блок: {block.Title}");
|
||||||
|
|
||||||
|
foreach (var entity in block.Entities ?? [])
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {entity.Title}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 WebSocket (Ynison)
|
||||||
|
|
||||||
|
### Подключение к Ynison
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var client = new YandexMusicClient();
|
||||||
|
|
||||||
|
// Авторизуемся
|
||||||
|
await client.AuthorizeAsync("email@gmail.com", "password");
|
||||||
|
|
||||||
|
// Получаем плеер
|
||||||
|
var player = client.Ynison;
|
||||||
|
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
// Подключаемся
|
||||||
|
await player.ConnectAsync();
|
||||||
|
Console.WriteLine("✅ Подключены к Ynison");
|
||||||
|
|
||||||
|
// Используем плеер...
|
||||||
|
|
||||||
|
// Отключаемся
|
||||||
|
await player.DisconnectAsync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Обработка ошибок
|
||||||
|
|
||||||
|
### Базовая обработка
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var track = await client.Api.Track.GetTrackAsync("trackId");
|
||||||
|
if (track == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Трек не найден");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Ошибка сети: {ex.Message}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Неожиданная ошибка: {ex.Message}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка авторизации
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
if (!client.IsAuthorized)
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ Требуется авторизация");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполняем авторизованные операции
|
||||||
|
var account = await client.Api.User.GetAccountAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Валидация входных параметров
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task GetTrackInfo(string trackId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(trackId))
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ ID трека не может быть пустым");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var track = await client.Api.Track.GetTrackAsync(trackId);
|
||||||
|
if (track == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ Трек с ID {trackId} не найден");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"✅ {track.Title}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Полные примеры
|
||||||
|
|
||||||
|
### Пример 1: Поиск и загрузка информации
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using YandexMusic;
|
||||||
|
|
||||||
|
// Создание клиента
|
||||||
|
var client = new YandexMusicClient();
|
||||||
|
|
||||||
|
// Поиск
|
||||||
|
Console.Write("Введите исполнителя: ");
|
||||||
|
string query = Console.ReadLine() ?? "Beatles";
|
||||||
|
|
||||||
|
var search = await client.Api.Search.SearchAsync(query);
|
||||||
|
var artists = search?.Artists?.Results ?? [];
|
||||||
|
|
||||||
|
if (artists.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Исполнители не найдены");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выбор первого результата
|
||||||
|
var artist = artists.First();
|
||||||
|
Console.WriteLine($"Найден: {artist.Title}");
|
||||||
|
|
||||||
|
// Получение полной информации
|
||||||
|
var fullArtist = await client.Api.Artist.GetArtistAsync(artist.Id);
|
||||||
|
Console.WriteLine($"Жанр: {fullArtist?.Genre}");
|
||||||
|
Console.WriteLine($"Альбомов: {fullArtist?.Albums?.Count}");
|
||||||
|
|
||||||
|
// Список треков
|
||||||
|
Console.WriteLine("\nПопулярные треки:");
|
||||||
|
foreach (var track in fullArtist?.Tracks?.Take(5) ?? [])
|
||||||
|
{
|
||||||
|
Console.WriteLine($" 🎵 {track.Title}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример 2: Работа с плейлистом
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using YandexMusic;
|
||||||
|
|
||||||
|
var client = new YandexMusicClient();
|
||||||
|
|
||||||
|
// ID плейлиста Яндекса
|
||||||
|
string playlistId = "1130499373";
|
||||||
|
|
||||||
|
// Получение плейлиста
|
||||||
|
var playlist = await client.Api.Playlist.GetPlaylistAsync(playlistId);
|
||||||
|
|
||||||
|
Console.WriteLine($"📋 {playlist?.Title}");
|
||||||
|
Console.WriteLine($"Описание: {playlist?.Description}");
|
||||||
|
Console.WriteLine($"Треков: {playlist?.Tracks?.Count}");
|
||||||
|
|
||||||
|
// Вывод треков с номерами
|
||||||
|
Console.WriteLine("\nТреки:");
|
||||||
|
int i = 1;
|
||||||
|
foreach (var track in playlist?.Tracks ?? [])
|
||||||
|
{
|
||||||
|
var artists = string.Join(", ", track.Artists?.Select(a => a.Title) ?? []);
|
||||||
|
Console.WriteLine($"{i}. {track.Title} - {artists}");
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Общая длительность
|
||||||
|
var totalSeconds = (playlist?.Tracks ?? []).Sum(t => t.DurationMs ?? 0) / 1000;
|
||||||
|
var hours = totalSeconds / 3600;
|
||||||
|
var minutes = (totalSeconds % 3600) / 60;
|
||||||
|
Console.WriteLine($"\nОбщая длительность: {hours}:{minutes:D2}");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример 3: Авторизация и библиотека
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using YandexMusic;
|
||||||
|
|
||||||
|
var client = new YandexMusicClient();
|
||||||
|
|
||||||
|
Console.Write("Email: ");
|
||||||
|
string email = Console.ReadLine() ?? "";
|
||||||
|
|
||||||
|
Console.Write("Password: ");
|
||||||
|
string password = Console.ReadLine() ?? "";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await client.AuthorizeAsync(email, password);
|
||||||
|
Console.WriteLine($"✅ Добро пожаловать, {client.Account.User?.DisplayName}!");
|
||||||
|
|
||||||
|
if (client.IsAuthorized)
|
||||||
|
{
|
||||||
|
// Получение лайков
|
||||||
|
var library = await client.Api.Library.GetLibraryAsync();
|
||||||
|
Console.WriteLine($"\n❤️ Ваши лайки: {library?.Library?.Count} треков");
|
||||||
|
|
||||||
|
// Список лайков
|
||||||
|
if (library?.Library?.Count > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("\nПервые 5 лайков:");
|
||||||
|
var trackIds = library.Library.Select(l => l.TrackId).Take(5);
|
||||||
|
var tracks = await client.Api.Track.GetTracksAsync(trackIds);
|
||||||
|
|
||||||
|
foreach (var track in tracks)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" ❤️ {track?.Title}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ Ошибка: {ex.Message}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 Дальнейшее обучение
|
||||||
|
|
||||||
|
### Документация
|
||||||
|
|
||||||
|
- **[README.md](README.md)** - Полное описание проекта
|
||||||
|
- **[ARCHITECTURE.md](ARCHITECTURE.md)** - Архитектура и паттерны
|
||||||
|
- **[YandexMusic.API/README.md](YandexMusic.API/README.md)** - Низкоуровневый API
|
||||||
|
- **[CONTRIBUTING.md](CONTRIBUTING.md)** - Как внести вклад
|
||||||
|
|
||||||
|
### Полезные ресурсы
|
||||||
|
|
||||||
|
- [.NET 10 Documentation](https://learn.microsoft.com/dotnet/)
|
||||||
|
- [C# 12 Features](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12)
|
||||||
|
- [System.Text.Json Guide](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json)
|
||||||
|
|
||||||
|
### Где найти ID
|
||||||
|
|
||||||
|
**Трека:** `https://music.yandex.ru/album/123456/track/789012`
|
||||||
|
- Album ID: `123456`
|
||||||
|
- Track ID: `789012`
|
||||||
|
|
||||||
|
**Плейлиста:** `https://music.yandex.ru/playlist/1130499373`
|
||||||
|
- Playlist ID: `1130499373`
|
||||||
|
|
||||||
|
**Исполнителя:** `https://music.yandex.ru/artist/123456`
|
||||||
|
- Artist ID: `123456`
|
||||||
|
|
||||||
|
## ⚡ Советы и трюки
|
||||||
|
|
||||||
|
### 1. Работа с async/await
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Получение нескольких ресурсов параллельно
|
||||||
|
var (albums, artists, tracks) = await (
|
||||||
|
client.Api.Album.GetAlbumsAsync(ids),
|
||||||
|
client.Api.Artist.GetArtistsAsync(ids),
|
||||||
|
client.Api.Track.GetTracksAsync(ids)
|
||||||
|
).AsAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Обработка больших списков
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Пагинация для больших результатов
|
||||||
|
var allPlaylists = new List<YPlaylist>();
|
||||||
|
int page = 0;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var playlists = await client.Api.Playlist.GetPlaylistsAsync(page: page);
|
||||||
|
if (playlists?.Playlists?.Count == 0) break;
|
||||||
|
|
||||||
|
allPlaylists.AddRange(playlists.Playlists ?? []);
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Кеширование результатов
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private readonly Dictionary<string, YTrack?> _trackCache = new();
|
||||||
|
|
||||||
|
public async Task<YTrack?> GetTrackAsync(string id)
|
||||||
|
{
|
||||||
|
if (_trackCache.TryGetValue(id, out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
var track = await client.Api.Track.GetTrackAsync(id);
|
||||||
|
_trackCache[id] = track;
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Следующий шаг:** Прочитайте [ARCHITECTURE.md](ARCHITECTURE.md) для понимания внутреннего устройства.
|
||||||
|
|
||||||
|
**Нужна помощь?** Посмотрите [README.md](README.md) или создайте issue.
|
||||||
|
|
||||||
|
Удачи в разработке! 🚀
|
||||||
437
README.md
Normal file
437
README.md
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
# 🎵 YandexMusic - Complete Solution
|
||||||
|
|
||||||
|
Полнофункциональное решение для работы с неофициальным API Яндекс Музыки на базе .NET 10. Содержит библиотеку API, оборачиватель клиента и CLI приложение.
|
||||||
|
|
||||||
|
## 📋 Содержание
|
||||||
|
|
||||||
|
- [Описание](#описание)
|
||||||
|
- [Структура решения](#структура-решения)
|
||||||
|
- [Требования](#требования)
|
||||||
|
- [Установка](#установка)
|
||||||
|
- [Быстрый старт](#быстрый-старт)
|
||||||
|
- [Проекты](#проекты)
|
||||||
|
- [Примеры использования](#примеры-использования)
|
||||||
|
- [Архитектура](#архитектура)
|
||||||
|
- [Лицензия](#лицензия)
|
||||||
|
|
||||||
|
## 📖 Описание
|
||||||
|
|
||||||
|
**YandexMusic** — это комплексное решение для взаимодействия с API Яндекс Музыки на платформе .NET 10. Решение состоит из двух ключевых компонентов:
|
||||||
|
|
||||||
|
1. **YandexMusic.API** — низкоуровневая библиотека для прямого взаимодействия с API
|
||||||
|
2. **YandexMusic** — удобный оборачиватель (wrapper) с клиентом `YandexMusicClient`
|
||||||
|
|
||||||
|
### 🎯 Основные возможности
|
||||||
|
|
||||||
|
- ✅ **Полная асинхронная архитектура** — Async/await на всех уровнях
|
||||||
|
- ✅ **Типобезопасный код** — Nullable reference types, full type safety
|
||||||
|
- ✅ **Модульная структура** — Легко расширяемые компоненты
|
||||||
|
- ✅ **Современный стек** — .NET 10, C# 12, System.Text.Json
|
||||||
|
- ✅ **Полная документация на русском** — XML docs для всех публичных членов
|
||||||
|
- ✅ **WebSocket поддержка** — Протокол Ynison для real-time синхронизации
|
||||||
|
- ✅ **Гибкая конфигурация** — Поддержка прокси, cookies, custom headers
|
||||||
|
- ✅ **Отладка встроена** — Debug settings для логирования и анализа
|
||||||
|
|
||||||
|
## 🏗️ Структура решения
|
||||||
|
|
||||||
|
```
|
||||||
|
YandexMusic/
|
||||||
|
├── YandexMusic.API/ # Низкоуровневая библиотека API
|
||||||
|
│ ├── API/ # Классы для разных веток API
|
||||||
|
│ ├── Models/ # Модели данных
|
||||||
|
│ ├── Requests/ # Построители запросов
|
||||||
|
│ ├── Common/ # Вспомогательные компоненты
|
||||||
|
│ ├── Extensions/ # Методы расширения
|
||||||
|
│ ├── YandexMusicApi.cs # Главный класс API
|
||||||
|
│ ├── YandexMusic.API.csproj
|
||||||
|
│ └── README.md # Документация по API
|
||||||
|
│
|
||||||
|
├── YandexMusic/ # Оборачиватель и клиент
|
||||||
|
│ ├── YandexMusicClient.cs # Основной класс клиента
|
||||||
|
│ ├── YandexMusic.csproj
|
||||||
|
│ └── YandexMusicClient.cs # Реализация клиента
|
||||||
|
│
|
||||||
|
└── README.md # Этот файл
|
||||||
|
```
|
||||||
|
|
||||||
|
### Зависимости между проектами
|
||||||
|
|
||||||
|
```
|
||||||
|
YandexMusic (Client wrapper)
|
||||||
|
↓
|
||||||
|
YandexMusic.API (Core library)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Требования
|
||||||
|
|
||||||
|
- **.NET 10** или выше
|
||||||
|
- **C# 12** или выше
|
||||||
|
- **Visual Studio 2026** (рекомендуется) или Visual Studio Code
|
||||||
|
|
||||||
|
### Системные требования
|
||||||
|
|
||||||
|
- Windows 10/11, Linux, macOS (любая ОС с .NET 10)
|
||||||
|
- Минимум 512 MB RAM
|
||||||
|
- Интернет соединение для работы с API
|
||||||
|
|
||||||
|
## 🚀 Установка
|
||||||
|
|
||||||
|
### Клонирование репозитория
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.frigat.duckdns.org/FrigaT/YandexMusic.git
|
||||||
|
cd YandexMusic
|
||||||
|
```
|
||||||
|
|
||||||
|
### Восстановление зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet restore
|
||||||
|
```
|
||||||
|
|
||||||
|
### Сборка решения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск тестов (если есть)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚡ Быстрый старт
|
||||||
|
|
||||||
|
### 1. Как клиент (рекомендуется для большинства случаев)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using YandexMusic;
|
||||||
|
|
||||||
|
// Создание клиента
|
||||||
|
var client = new YandexMusicClient();
|
||||||
|
|
||||||
|
// Получение информации о треке
|
||||||
|
var track = await client.Api.Track.GetTrackAsync("trackId123");
|
||||||
|
Console.WriteLine($"Трек: {track?.Title}");
|
||||||
|
|
||||||
|
// Поиск музыки
|
||||||
|
var results = await client.Api.Search.SearchAsync("Ленинград");
|
||||||
|
Console.WriteLine($"Найдено результатов: {results?.Tracks?.Results?.Count}");
|
||||||
|
|
||||||
|
// Работа с плейлистами
|
||||||
|
var playlists = await client.Api.Playlist.GetPlaylistAsync("playlistId");
|
||||||
|
Console.WriteLine($"Плейлист: {playlists?.Title}");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Низкоуровневой API (расширенная работа)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using YandexMusic.API;
|
||||||
|
|
||||||
|
// Создание API
|
||||||
|
var api = new YandexMusicApi();
|
||||||
|
|
||||||
|
// Прямая работа с API
|
||||||
|
var track = await api.Track.GetTrackAsync("trackId");
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Проекты
|
||||||
|
|
||||||
|
### YandexMusic.API
|
||||||
|
|
||||||
|
Низкоуровневая библиотека, предоставляющая полный доступ к API Яндекс Музыки.
|
||||||
|
|
||||||
|
**Основные компоненты:**
|
||||||
|
|
||||||
|
- `YandexMusicApi` — главный класс, содержит все API классы
|
||||||
|
- `AuthStorage` — управление авторизацией и cookies
|
||||||
|
- `IRequestProvider` — интерфейс для обработки HTTP запросов
|
||||||
|
- `YCommonAPI` — базовый класс для всех API веток
|
||||||
|
|
||||||
|
**API Классы:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class YandexMusicApi
|
||||||
|
{
|
||||||
|
public YAlbumAPI Album { get; } // Альбомы
|
||||||
|
public YArtistAPI Artist { get; } // Исполнители
|
||||||
|
public YLabelAPI Label { get; } // Лейблы
|
||||||
|
public YLandingAPI Landing { get; } // Рекомендации
|
||||||
|
public YLibraryAPI Library { get; } // Библиотека
|
||||||
|
public YPlaylistAPI Playlist { get; } // Плейлисты
|
||||||
|
public YPinsAPI Pins { get; } // Закреплённые
|
||||||
|
public YRadioAPI Radio { get; } // Радио
|
||||||
|
public YSearchAPI Search { get; } // Поиск
|
||||||
|
public YTrackAPI Track { get; } // Треки
|
||||||
|
public YQueueAPI Queue { get; } // Очередь
|
||||||
|
public YUserAPI User { get; } // Пользователь
|
||||||
|
public YUgcAPI UserGeneratedContent { get; } // UGC
|
||||||
|
public YYnisonAPI Ynison { get; } // WebSocket
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Особенности:**
|
||||||
|
|
||||||
|
- 300+ моделей данных для полного покрытия API
|
||||||
|
- Асинхронные методы для всех операций
|
||||||
|
- Поддержка WebSocket (Ynison)
|
||||||
|
- Встроенная обработка ошибок
|
||||||
|
- System.Text.Json для сериализации
|
||||||
|
|
||||||
|
**Документация:** [YandexMusic.API/README.md](YandexMusic.API/README.md)
|
||||||
|
|
||||||
|
### YandexMusic
|
||||||
|
|
||||||
|
Удобный оборачиватель (wrapper) над низкоуровневой библиотекой с клиентом `YandexMusicClient`.
|
||||||
|
|
||||||
|
**Основной класс:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class YandexMusicClient : IDisposable
|
||||||
|
{
|
||||||
|
// Свойства
|
||||||
|
public AuthStorage AuthStorage { get; }
|
||||||
|
public YAccount Account { get; }
|
||||||
|
public bool IsAuthorized { get; }
|
||||||
|
public YnisonPlayer? Ynison { get; }
|
||||||
|
public HttpClient HttpClient { get; }
|
||||||
|
public YandexMusicApi Api { get; }
|
||||||
|
|
||||||
|
// Методы авторизации
|
||||||
|
public Task AuthorizeAsync(string login, string password);
|
||||||
|
public Task AuthorizeByTokenAsync(string token);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Возможности:**
|
||||||
|
|
||||||
|
- Интеграция с собственным HttpClient
|
||||||
|
- Управление cookies и прокси
|
||||||
|
- Встроенная авторизация
|
||||||
|
- WebSocket плеер Ynison
|
||||||
|
- Удобное API через свойство `Api`
|
||||||
|
|
||||||
|
**Использование:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// С пользовательскими настройками
|
||||||
|
var client = new YandexMusicClient(
|
||||||
|
cookieContainer: new CookieContainer(),
|
||||||
|
proxy: new WebProxy("http://proxy:8080"),
|
||||||
|
timeout: TimeSpan.FromSeconds(30),
|
||||||
|
userAgent: "Custom Agent"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Авторизация
|
||||||
|
await client.AuthorizeAsync("login@gmail.com", "password");
|
||||||
|
|
||||||
|
// Использование
|
||||||
|
var playlists = await client.Api.Playlist.GetPlaylistAsync("123");
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 Примеры использования
|
||||||
|
|
||||||
|
### Пример 1: Поиск и получение информации
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using YandexMusic;
|
||||||
|
|
||||||
|
var client = new YandexMusicClient();
|
||||||
|
|
||||||
|
// Поиск треков
|
||||||
|
var search = await client.Api.Search.SearchAsync("The Beatles");
|
||||||
|
var track = search?.Tracks?.Results?.FirstOrDefault();
|
||||||
|
|
||||||
|
if (track != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"🎵 {track.Title}");
|
||||||
|
Console.WriteLine($"👤 {string.Join(", ", track.Artists?.Select(a => a.Title) ?? [])}");
|
||||||
|
Console.WriteLine($"⏱️ {track.DurationMs / 1000} сек");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример 2: Работа с плейлистами
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Получение плейлиста
|
||||||
|
var playlist = await client.Api.Playlist.GetPlaylistAsync("playlistId");
|
||||||
|
Console.WriteLine($"Плейлист: {playlist?.Title}");
|
||||||
|
Console.WriteLine($"Треков: {playlist?.Tracks?.Count}");
|
||||||
|
|
||||||
|
// Вывод треков
|
||||||
|
foreach (var track in playlist?.Tracks ?? [])
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {track.Title}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример 3: Работа с альбомами
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Получение альбома
|
||||||
|
var album = await client.Api.Album.GetAlbumAsync("albumId");
|
||||||
|
|
||||||
|
Console.WriteLine($"Альбом: {album?.Title}");
|
||||||
|
Console.WriteLine($"Исполнитель: {album?.Artists?.FirstOrDefault()?.Title}");
|
||||||
|
Console.WriteLine($"Год: {album?.Year}");
|
||||||
|
Console.WriteLine($"Жанр: {album?.Genre}");
|
||||||
|
|
||||||
|
// Вывод треков в альбоме
|
||||||
|
foreach (var track in album?.Tracks ?? [])
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {track.Position}. {track.Title}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример 4: Авторизация
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var client = new YandexMusicClient();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Авторизация через логин и пароль
|
||||||
|
await client.AuthorizeAsync("your-email@gmail.com", "your-password");
|
||||||
|
|
||||||
|
Console.WriteLine($"✅ Авторизирован: {client.Account.User?.DisplayName}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ Ошибка авторизации: {ex.Message}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример 5: WebSocket Ynison
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Подключение к Ynison (если авторизирован)
|
||||||
|
var player = client.Ynison;
|
||||||
|
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
await player.ConnectAsync();
|
||||||
|
Console.WriteLine("✅ Подключено к Ynison");
|
||||||
|
|
||||||
|
// Использование плеера...
|
||||||
|
|
||||||
|
await player.DisconnectAsync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏛️ Архитектура
|
||||||
|
|
||||||
|
### Слои
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ YandexMusic (Client Wrapper) │ ← Удобный клиент
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ YandexMusic.API (Core Library) │ ← Низкоуровневой API
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ HttpClient, System.Text.Json │ ← .NET Framework
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ключевые компоненты YandexMusic.API
|
||||||
|
|
||||||
|
```
|
||||||
|
YandexMusicApi (Main Entry Point)
|
||||||
|
├── API Classes (YAlbumAPI, YTrackAPI, etc.)
|
||||||
|
│ └── Requests (YGetAlbumBuilder, YSearchBuilder, etc.)
|
||||||
|
│
|
||||||
|
├── Models (YAlbum, YTrack, YPlaylist, etc.)
|
||||||
|
│ └── Common Models (YBaseModel, YResponse, etc.)
|
||||||
|
│
|
||||||
|
├── AuthStorage (Authorization Management)
|
||||||
|
│ └── IRequestProvider (HTTP Request Handling)
|
||||||
|
│ ├── DefaultRequestProvider
|
||||||
|
│ ├── CommonRequestProvider
|
||||||
|
│ └── MockRequestProvider
|
||||||
|
│
|
||||||
|
└── Extensions & Utilities
|
||||||
|
├── HttpRequestHeaderExtensions
|
||||||
|
├── StringExtensions
|
||||||
|
├── Encryptor (для шифрования)
|
||||||
|
└── DataDownloader
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обработка запросов
|
||||||
|
|
||||||
|
```
|
||||||
|
Request Builder (YRequestBuilder<T>)
|
||||||
|
↓
|
||||||
|
HttpRequestMessage
|
||||||
|
↓
|
||||||
|
IRequestProvider.GetWebResponseAsync()
|
||||||
|
↓
|
||||||
|
HttpResponseMessage
|
||||||
|
↓
|
||||||
|
System.Text.Json Deserialization
|
||||||
|
↓
|
||||||
|
Model<T>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Конфигурация
|
||||||
|
|
||||||
|
### Настройка HttpClient
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var client = new YandexMusicClient(
|
||||||
|
cookieContainer: new CookieContainer(),
|
||||||
|
proxy: new WebProxy("http://127.0.0.1:8080"),
|
||||||
|
timeout: TimeSpan.FromSeconds(30),
|
||||||
|
userAgent: "MyCustomAgent/1.0"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Статистика проекта
|
||||||
|
|
||||||
|
| Метрика | Значение |
|
||||||
|
|---------|----------|
|
||||||
|
| Проектов | 3 |
|
||||||
|
| Целевая платформа | .NET 10 |
|
||||||
|
| Язык C# | 12 |
|
||||||
|
| Основных API методов | 50+ |
|
||||||
|
| Моделей данных | 300+ |
|
||||||
|
| Документированных членов | 100% |
|
||||||
|
| Асинхронных методов | 100% |
|
||||||
|
|
||||||
|
## 🔐 Безопасность
|
||||||
|
|
||||||
|
- ✅ Использование HTTPS для всех запросов
|
||||||
|
- ✅ Поддержка прокси для безопасности
|
||||||
|
- ✅ Встроенное шифрование для чувствительных данных
|
||||||
|
- ✅ Управление cookies и сессиями
|
||||||
|
- ✅ Валидация всех входных данных
|
||||||
|
|
||||||
|
**⚠️ Важно:** Это неофициальная библиотека. Используйте её на свой риск и соблюдайте Terms of Service Яндекс Музыки.
|
||||||
|
|
||||||
|
## 🚦 Статус проекта
|
||||||
|
|
||||||
|
- ✅ **Стабильный** — Основная функциональность работает
|
||||||
|
- 🔄 **Активная разработка** — Регулярные обновления
|
||||||
|
- 📝 **Документирован** — Полная документация на русском
|
||||||
|
|
||||||
|
## 📝 Лицензия
|
||||||
|
|
||||||
|
Это неофициальная библиотека для работы с API Яндекс Музыки.
|
||||||
|
|
||||||
|
**Дисклеймер:** Автор не несет ответственности за неправомерное использование данной библиотеки. Используйте её в соответствии с Terms of Service Яндекс Музыки.
|
||||||
|
|
||||||
|
## 👨💻 Автор
|
||||||
|
|
||||||
|
**FrigaT** - Разработчик
|
||||||
|
|
||||||
|
## 🤝 Поддержка
|
||||||
|
|
||||||
|
Для вопросов, багов и предложений:
|
||||||
|
- 🐛 Issues: [https://git.frigat.duckdns.org/FrigaT/YandexMusic/issues]
|
||||||
|
- 💬 Discussions: [https://git.frigat.duckdns.org/FrigaT/YandexMusic/discussions]
|
||||||
|
|
||||||
|
## 📚 Дополнительная информация
|
||||||
|
|
||||||
|
- [YandexMusic.API/README.md](YandexMusic.API/README.md) — Документация по низкоуровневому API
|
||||||
|
- [Официальный сайт Яндекс Музыки](https://music.yandex.ru)
|
||||||
|
- [.NET 10 Documentation](https://learn.microsoft.com/dotnet/)
|
||||||
|
- [C# 12 Features](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12)
|
||||||
@@ -1,38 +1,18 @@
|
|||||||
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;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>API для работы с альбомами.</summary>
|
||||||
/// API для взаимодействия с альбомами
|
|
||||||
/// </summary>
|
|
||||||
public class YAlbumAPI : YCommonAPI
|
public class YAlbumAPI : YCommonAPI
|
||||||
{
|
{
|
||||||
public YAlbumAPI(YandexMusicApi yandex) : base(yandex)
|
public YAlbumAPI(YandexMusicApi api) : base(api) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Получает альбом по идентификатору.</summary>
|
/// <summary>Получает альбом по идентификатору.</summary>
|
||||||
/// <param name="storage">Хранилище авторизации.</param>
|
public Task<YAlbum?> GetAsync(string albumId)
|
||||||
/// <param name="albumId">Идентификатор альбома.</param>
|
=> new YGetAlbumBuilder(Api).ExecuteAsync(albumId);
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YAlbum>> GetAsync(AuthStorage storage, string albumId)
|
|
||||||
{
|
|
||||||
return 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></returns>
|
|
||||||
public Task<YResponse<List<YAlbum>>> GetAsync(AuthStorage storage, IEnumerable<string> albumIds)
|
|
||||||
{
|
|
||||||
return new YGetAlbumsBuilder(api, storage)
|
|
||||||
.Build(albumIds)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
111
YandexMusic.API/API/YAuthAPI.cs
Normal file
111
YandexMusic.API/API/YAuthAPI.cs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
using System.Security.Authentication;
|
||||||
|
using YandexMusic.API.Models.Account;
|
||||||
|
using YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>API для работы с пользователем и авторизации.</summary>
|
||||||
|
public class YAuthAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YAuthAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
/// <summary>Авторизация по готовому музыкальному токену (OAuth).</summary>
|
||||||
|
public async Task AuthorizeAsync(string musicToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(musicToken))
|
||||||
|
throw new ArgumentException("Токен не может быть пустым", nameof(musicToken));
|
||||||
|
|
||||||
|
Api.Storage.Token = musicToken;
|
||||||
|
var authInfo = await new YGetAuthInfoBuilder(Api).ExecuteAsync(null!);
|
||||||
|
if (authInfo?.Account?.Uid == null)
|
||||||
|
throw new Exception("Пользователь не авторизован");
|
||||||
|
|
||||||
|
Api.Storage.SetAuthorized(authInfo.Account, musicToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Авторизация по паспортному токену (полученному, например, после QR-входа).</summary>
|
||||||
|
public async Task AuthorizeByPassportTokenAsync(string passportToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(passportToken))
|
||||||
|
throw new ArgumentException("Паспортный токен не может быть пустым", nameof(passportToken));
|
||||||
|
|
||||||
|
var musicToken = await Api.Passport.GetMusicTokenByPassportTokenAsync(passportToken);
|
||||||
|
if (musicToken?.AccessToken == null)
|
||||||
|
throw new Exception("Не удалось обменять паспортный токен на музыкальный");
|
||||||
|
|
||||||
|
await AuthorizeAsync(musicToken.AccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Получение информации о текущем аккаунте.</summary>
|
||||||
|
public Task<YAccountResult?> GetUserAuthAsync()
|
||||||
|
=> new YGetAuthInfoBuilder(Api).ExecuteAsync(null!);
|
||||||
|
|
||||||
|
/// <summary>Создание сессии авторизации (получение доступных методов входа).</summary>
|
||||||
|
public async Task<YAuthTypes?> CreateAuthSessionAsync(string userName)
|
||||||
|
{
|
||||||
|
if (!await Api.Passport.GetCsrfTokenAsync()) // вместо InitSessionAsync
|
||||||
|
throw new Exception("Не удалось инициализировать сессию");
|
||||||
|
|
||||||
|
var result = await new YGetAuthLoginUserBuilder(Api).ExecuteAsync((Api.Storage.AuthToken.CsfrToken, userName));
|
||||||
|
if (result?.TrackId != null)
|
||||||
|
Api.Storage.AuthToken.TrackId = result.TrackId;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Получение капчи.</summary>
|
||||||
|
public Task<YAuthCaptcha?> GetCaptchaAsync()
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
|
||||||
|
return new YGetAuthCaptchaBuilder(Api).ExecuteAsync(null!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Авторизация по капче.</summary>
|
||||||
|
public Task<YAuthBase?> AuthorizeByCaptchaAsync(string captchaValue)
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
|
||||||
|
return new YGetAuthLoginCaptchaBuilder(Api).ExecuteAsync(captchaValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Отправить письмо для авторизации.</summary>
|
||||||
|
public Task<YAuthLetter?> GetAuthLetterAsync()
|
||||||
|
=> new YGetAuthLetterBuilder(Api).ExecuteAsync(null!);
|
||||||
|
|
||||||
|
/// <summary>Подтверждение входа по письму и получение музыкального токена.</summary>
|
||||||
|
public async Task<bool> AuthorizeByLetterAsync()
|
||||||
|
{
|
||||||
|
var status = await new YGetAuthLoginLetterBuilder(Api).ExecuteAsync(null!);
|
||||||
|
if (status?.Status != YAuthStatus.Ok || !status.MagicLinkConfirmed)
|
||||||
|
throw new Exception("Письмо не подтверждено");
|
||||||
|
|
||||||
|
var musicToken = await Api.Passport.GetMusicTokenByCookiesAsync();
|
||||||
|
if (musicToken?.AccessToken == null)
|
||||||
|
throw new Exception("Не удалось получить музыкальный токен после подтверждения письма");
|
||||||
|
|
||||||
|
await AuthorizeAsync(musicToken.AccessToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Авторизация по паролю приложения Яндекс.</summary>
|
||||||
|
public async Task<YAuthBase?> AuthorizeByAppPasswordAsync(string password)
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
|
||||||
|
|
||||||
|
var result = await new YGetAuthAppPasswordBuilder(Api).ExecuteAsync(password);
|
||||||
|
if (result?.Status != YAuthStatus.Ok)
|
||||||
|
throw new AuthenticationException("Ошибка авторизации по паролю");
|
||||||
|
|
||||||
|
var musicToken = await Api.Passport.GetMusicTokenByCookiesAsync();
|
||||||
|
if (musicToken?.AccessToken == null)
|
||||||
|
throw new Exception("Не удалось получить музыкальный токен после ввода пароля");
|
||||||
|
|
||||||
|
await AuthorizeAsync(musicToken.AccessToken);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Получение информации о пользователе через логин.</summary>
|
||||||
|
public Task<YLoginInfo?> GetLoginInfoAsync()
|
||||||
|
=> new YGetLoginInfoBuilder(Api).ExecuteAsync(null!);
|
||||||
|
}
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Базовый класс для всех веток API.</summary>
|
||||||
/// Родительский класс для ветки API
|
public abstract class YCommonAPI
|
||||||
/// </summary>
|
|
||||||
public class YCommonAPI
|
|
||||||
{
|
{
|
||||||
protected YandexMusicApi api;
|
/// <summary>Основной экземпляр API.</summary>
|
||||||
|
protected YandexMusicApi Api { get; }
|
||||||
|
|
||||||
public YCommonAPI(YandexMusicApi yandex)
|
protected YCommonAPI(YandexMusicApi api) => Api = api;
|
||||||
{
|
}
|
||||||
api = yandex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
19
YandexMusic.API/API/YLabelAPI.cs
Normal file
19
YandexMusic.API/API/YLabelAPI.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using YandexMusic.API.Models.Common;
|
||||||
|
using YandexMusic.API.Models.Label;
|
||||||
|
using YandexMusic.API.Requests.Label;
|
||||||
|
|
||||||
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>API для работы с лейблами.</summary>
|
||||||
|
public class YLabelAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YLabelAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
/// <summary>Получает альбомы лейбла с пагинацией.</summary>
|
||||||
|
public Task<YLabelAlbums?> GetAlbumsByLabelAsync(YLabel label, int page = 0)
|
||||||
|
=> new YGetLabelAlbumsBuilder(Api).ExecuteAsync((label, page));
|
||||||
|
|
||||||
|
/// <summary>Получает артистов лейбла с пагинацией.</summary>
|
||||||
|
public Task<YLabelArtists?> GetArtistsByLabelAsync(YLabel label, int page = 0)
|
||||||
|
=> new YGetLabelArtistsBuilder(Api).ExecuteAsync((label, page));
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Label;
|
|
||||||
using YandexMusic.API.Requests.Label;
|
|
||||||
|
|
||||||
namespace YandexMusic.API
|
|
||||||
{
|
|
||||||
public partial class YLabelAPI : YCommonAPI
|
|
||||||
{
|
|
||||||
public YLabelAPI(YandexMusicApi yandex) : base(yandex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Постраничное получение альбомов лейбла
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="label">Лейбл</param>
|
|
||||||
/// <param name="page">Страница</param>
|
|
||||||
public Task<YResponse<YLabelAlbums>> GetAlbumsByLabelAsync(AuthStorage storage, YLabel label, int page)
|
|
||||||
{
|
|
||||||
return new YGetLabelAlbumsBuilder(api, storage)
|
|
||||||
.Build((label, page))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Постраничное получение артистов лейбла
|
|
||||||
/// </summary>
|
|
||||||
/// <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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
21
YandexMusic.API/API/YLandingAPI.cs
Normal file
21
YandexMusic.API/API/YLandingAPI.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using YandexMusic.API.Models.Feed;
|
||||||
|
using YandexMusic.API.Models.Landing;
|
||||||
|
using YandexMusic.API.Requests.Feed;
|
||||||
|
using YandexMusic.API.Requests.Landing;
|
||||||
|
|
||||||
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>API для работы с главной страницей (лендингом).</summary>
|
||||||
|
public class YLandingAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YLandingAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
public Task<YLanding?> GetAsync(params YLandingBlockType[] blocks)
|
||||||
|
=> new YGetLandingBuilder(Api).ExecuteAsync(blocks);
|
||||||
|
|
||||||
|
public Task<YFeed?> GetFeedAsync()
|
||||||
|
=> new YGetFeedBuilder(Api).ExecuteAsync(null!);
|
||||||
|
|
||||||
|
public Task<YChildrenLanding?> GetChildrenLandingAsync()
|
||||||
|
=> new YGetChildrenLandingBuilder(Api).ExecuteAsync(null!);
|
||||||
|
}
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Feed;
|
|
||||||
using YandexMusic.API.Models.Landing;
|
|
||||||
using YandexMusic.API.Requests.Feed;
|
|
||||||
using YandexMusic.API.Requests.Landing;
|
|
||||||
|
|
||||||
namespace YandexMusic.API
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// API для взаимодействия с главной страницей
|
|
||||||
/// </summary>
|
|
||||||
public partial class YLandingAPI : YCommonAPI
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
public YLandingAPI(YandexMusicApi yandex) : base(yandex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение персональных списков
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="blocks">Типы запрашиваемых блоков</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YLanding>> GetAsync(AuthStorage storage, params YLandingBlockType[] blocks)
|
|
||||||
{
|
|
||||||
if (blocks == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new YGetLandingBuilder(api, storage)
|
|
||||||
.Build(blocks)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение ленты
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YFeed>> GetFeedAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return new YGetFeedBuilder(api, storage)
|
|
||||||
.Build(null)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение лендинга детского раздела
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YChildrenLanding>> GetChildrenLandingAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return new YGetChildrenLandingBuilder(api, storage)
|
|
||||||
.Build(null)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
90
YandexMusic.API/API/YLibraryAPI.cs
Normal file
90
YandexMusic.API/API/YLibraryAPI.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
using YandexMusic.API.Models.Album;
|
||||||
|
using YandexMusic.API.Models.Artist;
|
||||||
|
using YandexMusic.API.Models.Common;
|
||||||
|
using YandexMusic.API.Models.Landing.Entity.Entities.Context;
|
||||||
|
using YandexMusic.API.Models.Library;
|
||||||
|
using YandexMusic.API.Models.Playlist;
|
||||||
|
using YandexMusic.API.Models.Track;
|
||||||
|
using YandexMusic.API.Requests.Library;
|
||||||
|
|
||||||
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>API для работы с библиотекой (лайки, дизлайки, недавно прослушанное).</summary>
|
||||||
|
public class YLibraryAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YLibraryAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
#region Лайки
|
||||||
|
|
||||||
|
public Task<YLibraryTracks?> GetLikedTracksAsync()
|
||||||
|
=> new YGetLibrarySectionBuilder<YLibraryTracks>(Api).ExecuteAsync((YLibrarySection.Tracks, YLibrarySectionType.Likes));
|
||||||
|
|
||||||
|
public Task<List<YLibraryAlbum>?> GetLikedAlbumsAsync()
|
||||||
|
=> new YGetLibrarySectionBuilder<List<YLibraryAlbum>>(Api).ExecuteAsync((YLibrarySection.Albums, YLibrarySectionType.Likes));
|
||||||
|
|
||||||
|
public Task<List<YArtist>?> GetLikedArtistsAsync()
|
||||||
|
=> new YGetLibrarySectionBuilder<List<YArtist>>(Api).ExecuteAsync((YLibrarySection.Artists, YLibrarySectionType.Likes));
|
||||||
|
|
||||||
|
public Task<List<YLibraryPlaylists>?> GetLikedPlaylistsAsync()
|
||||||
|
=> new YGetLibrarySectionBuilder<List<YLibraryPlaylists>>(Api).ExecuteAsync((YLibrarySection.Playlists, YLibrarySectionType.Likes));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Дизлайки
|
||||||
|
|
||||||
|
public Task<YLibraryTracks?> GetDislikedTracksAsync()
|
||||||
|
=> new YGetLibrarySectionBuilder<YLibraryTracks>(Api).ExecuteAsync((YLibrarySection.Tracks, YLibrarySectionType.Dislikes));
|
||||||
|
|
||||||
|
public Task<List<YArtist>?> GetDislikedArtistsAsync()
|
||||||
|
=> new YGetLibrarySectionBuilder<List<YArtist>>(Api).ExecuteAsync((YLibrarySection.Artists, YLibrarySectionType.Dislikes));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Добавление/удаление
|
||||||
|
|
||||||
|
public Task<int?> AddTrackLikeAsync(YTrack track)
|
||||||
|
=> new YLibraryAddBuilder<YRevision>(Api).ExecuteAsync((track.Id, YLibrarySection.Tracks, YLibrarySectionType.Likes))
|
||||||
|
.ContinueWith(t => t.Result?.Revision);
|
||||||
|
|
||||||
|
public Task<int?> RemoveTrackLikeAsync(YTrack track)
|
||||||
|
=> new YLibraryRemoveBuilder<YRevision>(Api).ExecuteAsync((track.Id, YLibrarySection.Tracks, YLibrarySectionType.Likes))
|
||||||
|
.ContinueWith(t => t.Result?.Revision);
|
||||||
|
|
||||||
|
public Task<int?> AddTrackDislikeAsync(YTrack track)
|
||||||
|
=> new YLibraryAddBuilder<YRevision>(Api).ExecuteAsync((track.Id, YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
|
||||||
|
.ContinueWith(t => t.Result?.Revision);
|
||||||
|
|
||||||
|
public Task<int?> RemoveTrackDislikeAsync(YTrack track)
|
||||||
|
=> new YLibraryRemoveBuilder<YRevision>(Api).ExecuteAsync((track.Id, YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
|
||||||
|
.ContinueWith(t => t.Result?.Revision);
|
||||||
|
|
||||||
|
public Task<string?> AddAlbumLikeAsync(YAlbum album)
|
||||||
|
=> new YLibraryAddBuilder<string>(Api).ExecuteAsync((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes));
|
||||||
|
|
||||||
|
public Task<string?> RemoveAlbumLikeAsync(YAlbum album)
|
||||||
|
=> new YLibraryRemoveBuilder<string>(Api).ExecuteAsync((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes));
|
||||||
|
|
||||||
|
public Task<string?> AddArtistLikeAsync(YArtist artist)
|
||||||
|
=> new YLibraryAddBuilder<string>(Api).ExecuteAsync((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes));
|
||||||
|
|
||||||
|
public Task<string?> RemoveArtistLikeAsync(YArtist artist)
|
||||||
|
=> new YLibraryRemoveBuilder<string>(Api).ExecuteAsync((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes));
|
||||||
|
|
||||||
|
public Task<string?> AddPlaylistLikeAsync(YPlaylist playlist)
|
||||||
|
=> new YLibraryAddBuilder<string>(Api).ExecuteAsync((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes));
|
||||||
|
|
||||||
|
public Task<string?> RemovePlaylistLikeAsync(YPlaylist playlist)
|
||||||
|
=> new YLibraryRemoveBuilder<string>(Api).ExecuteAsync((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Недавно прослушанное
|
||||||
|
|
||||||
|
public Task<YRecentlyListenedContext?> GetRecentlyListenedAsync(
|
||||||
|
IEnumerable<YPlayContextType> contextTypes,
|
||||||
|
int trackCount = 50,
|
||||||
|
int contextCount = 10)
|
||||||
|
=> new YGetLibraryRecentlyListenedBuilder(Api).ExecuteAsync((contextTypes, trackCount, contextCount));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Album;
|
|
||||||
using YandexMusic.API.Models.Artist;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Landing.Entity.Entities.Context;
|
|
||||||
using YandexMusic.API.Models.Library;
|
|
||||||
using YandexMusic.API.Models.Playlist;
|
|
||||||
using YandexMusic.API.Models.Track;
|
|
||||||
using YandexMusic.API.Requests.Library;
|
|
||||||
|
|
||||||
namespace YandexMusic.API
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// API для взаимодействия с библиотекой
|
|
||||||
/// </summary>
|
|
||||||
public partial class YLibraryAPI : YCommonAPI
|
|
||||||
{
|
|
||||||
#region Вспомогательные функции
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение секции библиотеки
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Тип объекта библиотеки</typeparam>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="section">Секция</param>
|
|
||||||
/// <param name="type">Тип</param>
|
|
||||||
/// <returns>Список объектов из секции</returns>
|
|
||||||
private Task<YResponse<T>> GetLibrarySection<T>(AuthStorage storage, YLibrarySection section, YLibrarySectionType type = YLibrarySectionType.Likes)
|
|
||||||
{
|
|
||||||
return new YGetLibrarySectionBuilder<T>(api, storage)
|
|
||||||
.Build((section, type))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Вспомогательные функции
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public YLibraryAPI(YandexMusicApi yandex) : base(yandex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Лайки
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение лайкнутых треков
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YLibraryTracks>> GetLikedTracksAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetLibrarySection<YLibraryTracks>(storage, YLibrarySection.Tracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение лайкнутых альбомов
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YLibraryAlbum>>> GetLikedAlbumsAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetLibrarySection<List<YLibraryAlbum>>(storage, YLibrarySection.Albums);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение лайкнутых исполнителей
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YArtist>>> GetLikedArtistsAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetLibrarySection<List<YArtist>>(storage, YLibrarySection.Artists);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение лайкнутых плейлистов
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YLibraryPlaylists>>> GetLikedPlaylistsAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetLibrarySection<List<YLibraryPlaylists>>(storage, YLibrarySection.Playlists);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Лайки
|
|
||||||
|
|
||||||
#region Дизлайки
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение дизлайкнутых треков
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YLibraryTracks>> GetDislikedTracksAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetLibrarySection<YLibraryTracks>(storage, YLibrarySection.Tracks, YLibrarySectionType.Dislikes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение дизлайкнутых исполнителей
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YArtist>>> GetDislikedArtistsAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetLibrarySection<List<YArtist>>(storage, YLibrarySection.Artists, YLibrarySectionType.Dislikes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Дизлайки
|
|
||||||
|
|
||||||
#region Добавление в списки лайков/дизлайков
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Добавить трек в список лайкнутых
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> AddTrackLikeAsync(AuthStorage storage, YTrack track)
|
|
||||||
{
|
|
||||||
return new YLibraryAddBuilder<YPlaylist>(api, storage)
|
|
||||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Удалить трек из списка лайкнутых
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YRevision>> RemoveTrackLikeAsync(AuthStorage storage, YTrack track)
|
|
||||||
{
|
|
||||||
return new YLibraryRemoveBuilder<YRevision>(api, storage)
|
|
||||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Добавить трек в список дизлайкнутых
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YRevision>> AddTrackDislikeAsync(AuthStorage storage, YTrack track)
|
|
||||||
{
|
|
||||||
return new YLibraryAddBuilder<YRevision>(api, storage)
|
|
||||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Удалить трек из списка дизлайкнутых
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YRevision>> RemoveTrackDislikeAsync(AuthStorage storage, YTrack track)
|
|
||||||
{
|
|
||||||
return new YLibraryRemoveBuilder<YRevision>(api, storage)
|
|
||||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Добавить альбом в список лайкнутых
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="album">Альбом</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> AddAlbumLikeAsync(AuthStorage storage, YAlbum album)
|
|
||||||
{
|
|
||||||
return new YLibraryAddBuilder<string>(api, storage)
|
|
||||||
.Build((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Удалить альбом из списка лайкнутых
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="album">Альбом</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> RemoveAlbumLikeAsync(AuthStorage storage, YAlbum album)
|
|
||||||
{
|
|
||||||
return new YLibraryRemoveBuilder<string>(api, storage)
|
|
||||||
.Build((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Добавить исполнителя в список лайкнутых
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="artist">Исполнитель</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> AddArtistLikeAsync(AuthStorage storage, YArtist artist)
|
|
||||||
{
|
|
||||||
return new YLibraryAddBuilder<string>(api, storage)
|
|
||||||
.Build((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Удалить исполнителя из списка лайкнутых
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="artist">Исполнитель</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> RemoveArtistLikeAsync(AuthStorage storage, YArtist artist)
|
|
||||||
{
|
|
||||||
return new YLibraryRemoveBuilder<string>(api, storage)
|
|
||||||
.Build((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Добавить плейлист в список лайкнутых
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="playlist">Плейлист</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> AddPlaylistLikeAsync(AuthStorage storage, YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return new YLibraryAddBuilder<string>(api, storage)
|
|
||||||
.Build((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Удалить плейлист из списка лайкнутых
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="playlist">Плейлист</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> RemovePlaylistLikeAsync(AuthStorage storage, YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return new YLibraryRemoveBuilder<string>(api, storage)
|
|
||||||
.Build((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Добавление/удаление в списки лайков/дизлайков
|
|
||||||
|
|
||||||
#region Получение списка "Вы недавно слушали"
|
|
||||||
|
|
||||||
public Task<YResponse<YRecentlyListenedContext>> GetRecentlyListenedAsync(AuthStorage storage, IEnumerable<YPlayContextType> contextTypes, int trackCount, int contextCount)
|
|
||||||
{
|
|
||||||
return new YGetLibraryRecentlyListenedBuilder(api, storage)
|
|
||||||
.Build((contextTypes, trackCount, contextCount))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Получение списка "Вы недавно слушали"
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
230
YandexMusic.API/API/YPassportAPI.cs
Normal file
230
YandexMusic.API/API/YPassportAPI.cs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
using System.Security.Authentication;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using YandexMusic.API.Models.Account;
|
||||||
|
using YandexMusic.API.Models.Passport;
|
||||||
|
using YandexMusic.API.Requests.Account;
|
||||||
|
using YandexMusic.API.Requests.Passport;
|
||||||
|
|
||||||
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>API для работы с яндекс паспортом</summary>
|
||||||
|
public class YPassportAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YPassportAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
public async Task<YAccessToken?> GetMusicTokenByCookiesAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(Api.Storage.AuthToken.TrackId))
|
||||||
|
{
|
||||||
|
if (!await GetCsrfTokenAsync())
|
||||||
|
throw new Exception("Не удалось инициализировать сессию");
|
||||||
|
await CreateTrackAsync(); // ваш приватный метод создания track_id
|
||||||
|
}
|
||||||
|
return await new YGetAuthCookiesBuilder(Api).ExecuteAsync(null!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YAccessToken?> GetMusicTokenByPassportTokenAsync(string passportToken)
|
||||||
|
=> await GetAccessTokenAsync(passportToken);
|
||||||
|
|
||||||
|
public async Task<string?> GetAuthQRLinkAsync()
|
||||||
|
{
|
||||||
|
if (!await GetCsrfTokenAsync())
|
||||||
|
throw new Exception("Не удалось инициализировать сессию");
|
||||||
|
|
||||||
|
await CreateTrackAsync();
|
||||||
|
|
||||||
|
return $"https://passport.yandex.ru/auth/magic/code/?track_id={Api.Storage.AuthToken.TrackId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YAuthQRStatus?> CheckQRStatusAsync()
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
|
||||||
|
var status = await new YGetQrStatus(Api).ExecuteAsync(null!);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(status?.TrackId))
|
||||||
|
{
|
||||||
|
Api.Storage.AuthToken.SessionTrackId = status.TrackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YAuthQrSession?> AuthorizeByQRAsync()
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Api.Storage.AuthToken.SessionTrackId))
|
||||||
|
throw new Exception("Токен сессии не инициализирован");
|
||||||
|
|
||||||
|
var status = await new YGetAuthLoginQRBuilder(Api).ExecuteAsync(null!);
|
||||||
|
if (status != null && status.DefaultUid != 0 && await LoginByCookiesAsync())
|
||||||
|
return status;
|
||||||
|
throw new AuthenticationException("Ошибка авторизации по QR");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Многоступенчатая авторизация: начало (передача логина).</summary>
|
||||||
|
public async Task<YMultistepStart?> MultistepStartAsync(string login)
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализирована. Вызовите GetAuthQRLinkAsync или CreateTrackAsync");
|
||||||
|
return await new YMultistepStartBuilder(Api).ExecuteAsync(login);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Многоступенчатая авторизация: ввод пароля.</summary>
|
||||||
|
public async Task<YPassportUser?> MultistepPasswordAsync(string password)
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
return await new YMultiStepPasswordBuilder(Api).ExecuteAsync(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Авторизация с помощью RFC OTP.</summary>
|
||||||
|
public async Task<YPassportUser?> RfcOtpPasswordAsync(string rfcOtp)
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
return await new YRfcOtpBuilder(Api).ExecuteAsync(rfcOtp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Создание сессии пользователя.</summary>
|
||||||
|
public async Task<YPassportSession?> CreateUserSessionAsync()
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
return await new YGetSessionBuilder(Api).ExecuteAsync(null!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Проверка состояния сессии.</summary>
|
||||||
|
public async Task<YPassportSessionStatus?> GetSessionStateAsync()
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
return await new YCheckSessionBuilder(Api).ExecuteAsync(null!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Проверка номера телефона (валидация).</summary>
|
||||||
|
public async Task<YValidatePhoneNumberResult?> ValidatePhoneNumberAsync(string phone)
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
return await new YValidatePhoneNumberBuilder(Api).ExecuteAsync(phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Проверка доступности номера.</summary>
|
||||||
|
public async Task<YCheckAvailabilityResult?> CheckPhoneAvailabilityAsync(string phone)
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
return await new YCheckPhoneAvailabilityBuilder(Api).ExecuteAsync(phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Запрос на отправку push-уведомления.</summary>
|
||||||
|
public async Task<YSendPushResult?> SuggestSendPushAsync(string phone)
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
return await new YSendPushBuilder(Api).ExecuteAsync(phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Проверка push-кода.</summary>
|
||||||
|
public async Task CheckPushCodeAsync(string code)
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
await new YCheckPushCodeBuilder(Api).ExecuteAsync(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Проверка на "сквоттера" (захват номера).</summary>
|
||||||
|
public async Task<YValidateSquatter?> ValidateSquatterAsync(string phone)
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
return await new YValidateSquatterBuilder(Api).ExecuteAsync(phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Получение списка аккаунтов по номеру телефона.</summary>
|
||||||
|
public async Task<YSuggestByPhoneResult?> SuggestByPhoneAsync()
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
return await new YSuggestByPhoneBuilder(Api).ExecuteAsync(null!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Вход по паролю (упрощённый, если не нужна многоступенчатость).</summary>
|
||||||
|
public async Task<YPassportUser?> LoginByPasswordAsync(string password)
|
||||||
|
{
|
||||||
|
// Сначала запускаем мультистеп (если track_id уже есть)
|
||||||
|
if (string.IsNullOrEmpty(Api.Storage.AuthToken.TrackId))
|
||||||
|
throw new Exception("TrackId не найден. Вызовите MultistepStartAsync сначала.");
|
||||||
|
return await MultistepPasswordAsync(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private async Task CreateTrackAsync()
|
||||||
|
{
|
||||||
|
if (!await GetCsrfTokenAsync())
|
||||||
|
throw new Exception("Невозможно инициализировать сессию входа.");
|
||||||
|
|
||||||
|
var track = await new YCreateTrackBuilder(Api).ExecuteAsync(null!);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(track?.TrackId) || string.IsNullOrWhiteSpace(track?.CsrfToken))
|
||||||
|
throw new Exception("Не удалось создать трек паспорта.");
|
||||||
|
|
||||||
|
Api.Storage.AuthToken.TrackId = track.TrackId;
|
||||||
|
Api.Storage.AuthToken.CsfrToken = track.CsrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<bool> GetCsrfTokenAsync()
|
||||||
|
{
|
||||||
|
using var response = await new YGetAuthMethodsBuilder(Api).ExecuteRawAsync(null!);
|
||||||
|
if (response == null || !response.IsSuccessStatusCode)
|
||||||
|
throw new HttpRequestException("Не удалось получить CSRF-токен");
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
var csrfMatch = Regex.Match(content, @"window\.__CSRF__\s*=\s*""([^""]+)""");
|
||||||
|
var processMatch = Regex.Match(content, @"'process_uuid'\s*:\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'");
|
||||||
|
|
||||||
|
if (!csrfMatch.Success || !processMatch.Success)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Api.Storage.HeaderToken = new YAuthToken
|
||||||
|
{
|
||||||
|
CsfrToken = csrfMatch.Groups[1].Value,
|
||||||
|
ProcessUuid = processMatch.Groups[1].Value
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<YAccessToken?> GetAccessTokenAsync(string passportToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(passportToken))
|
||||||
|
throw new Exception("Сессия не инициализована");
|
||||||
|
|
||||||
|
var token = await new YGetMusicTokenBuilder(Api).ExecuteAsync(passportToken);
|
||||||
|
if (token?.AccessToken != null)
|
||||||
|
Api.Storage.Token = token.AccessToken;
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal async Task<bool> LoginByCookiesAsync()
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new AuthenticationException("Сессия входа не инициализирована");
|
||||||
|
|
||||||
|
var accessToken = await new YGetAuthCookiesBuilder(Api).ExecuteAsync(null!);
|
||||||
|
if (accessToken == null || string.IsNullOrEmpty(accessToken.AccessToken))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await GetAccessTokenAsync(accessToken.AccessToken);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
YandexMusic.API/API/YPinsAPI.cs
Normal file
13
YandexMusic.API/API/YPinsAPI.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using YandexMusic.API.Models.Pins;
|
||||||
|
using YandexMusic.API.Requests.Pins;
|
||||||
|
|
||||||
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>API для работы с закреплёнными объектами (пинами).</summary>
|
||||||
|
public class YPinsAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YPinsAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
public Task<YPins?> GetAsync()
|
||||||
|
=> new YGetPinsBuilder(Api).ExecuteAsync(null!);
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Pins;
|
|
||||||
using YandexMusic.API.Requests.Pins;
|
|
||||||
|
|
||||||
namespace YandexMusic.API
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// API для взаимодействия с прикреплёнными объектами
|
|
||||||
/// </summary>
|
|
||||||
public partial class YPinsAPI : YCommonAPI
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
public YPinsAPI(YandexMusicApi yandex) : base(yandex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение списка прикреплённых объектов
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPins>> GetAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return new YGetPinsBuilder(api, storage)
|
|
||||||
.Build(null)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
119
YandexMusic.API/API/YPlaylistAPI.cs
Normal file
119
YandexMusic.API/API/YPlaylistAPI.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
using YandexMusic.API.Models.Landing;
|
||||||
|
using YandexMusic.API.Models.Landing.Entity.Entities;
|
||||||
|
using YandexMusic.API.Models.Playlist;
|
||||||
|
using YandexMusic.API.Models.Track;
|
||||||
|
using YandexMusic.API.Requests.Playlist;
|
||||||
|
|
||||||
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>API для работы с плейлистами.</summary>
|
||||||
|
public class YPlaylistAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YPlaylistAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
public async Task<List<YPlaylist>> GetPersonalPlaylistsAsync()
|
||||||
|
{
|
||||||
|
var landing = await Api.Landing.GetAsync(YLandingBlockType.PersonalPlaylists);
|
||||||
|
var block = landing?.Blocks?.FirstOrDefault(b => b.Type == YLandingBlockType.PersonalPlaylists);
|
||||||
|
if (block?.Entities == null)
|
||||||
|
return new List<YPlaylist>();
|
||||||
|
|
||||||
|
var tasks = block.Entities
|
||||||
|
.OfType<YLandingEntityPersonalPlaylist>()
|
||||||
|
.Select(e => GetAsync(e.Data?.Data?.Owner?.Uid ?? Api.Storage.User.Uid, e.Data?.Data?.Kind ?? ""))
|
||||||
|
.Where(t => t != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var results = await Task.WhenAll(tasks);
|
||||||
|
return results.Where(p => p != null).ToList()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<YPlaylist?> GetAsync(string user, string kind)
|
||||||
|
=> new YGetPlaylistBuilder(Api).ExecuteAsync((user, kind));
|
||||||
|
|
||||||
|
public Task<YPlaylist?> GetAsync(string uuid)
|
||||||
|
=> new YGetPlaylistByUuidBuilder(Api).ExecuteAsync(uuid);
|
||||||
|
|
||||||
|
public Task<YPlaylist?> GetAsync(YPlaylist playlist)
|
||||||
|
=> GetAsync(playlist.Owner.Uid, playlist.Kind);
|
||||||
|
|
||||||
|
public Task<List<YPlaylist>?> GetAsync(IEnumerable<(string user, string kind)> ids)
|
||||||
|
=> new YGetPlaylistsBuilder(Api).ExecuteAsync(ids);
|
||||||
|
|
||||||
|
public Task<List<YPlaylist>?> FavoritesAsync()
|
||||||
|
=> new YGetPlaylistFavoritesBuilder(Api).ExecuteAsync(null!);
|
||||||
|
|
||||||
|
public async Task<YPlaylist?> OfTheDayAsync()
|
||||||
|
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.PlaylistOfTheDay.ToString());
|
||||||
|
|
||||||
|
public async Task<YPlaylist?> DejaVuAsync()
|
||||||
|
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.NeverHeard.ToString());
|
||||||
|
|
||||||
|
public async Task<YPlaylist?> PremiereAsync()
|
||||||
|
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.RecentTracks.ToString());
|
||||||
|
|
||||||
|
public async Task<YPlaylist?> MissedAsync()
|
||||||
|
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.MissedLikes.ToString());
|
||||||
|
|
||||||
|
public async Task<YPlaylist?> KinopoiskAsync()
|
||||||
|
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.Kinopoisk.ToString());
|
||||||
|
|
||||||
|
public Task<YPlaylist?> CreateAsync(string name)
|
||||||
|
=> new YPlaylistCreateBuilder(Api).ExecuteAsync(name);
|
||||||
|
|
||||||
|
public Task<YPlaylist?> RenameAsync(string kind, string name)
|
||||||
|
=> new YPlaylistRenameBuilder(Api).ExecuteAsync((kind, name));
|
||||||
|
|
||||||
|
public Task<YPlaylist?> RenameAsync(YPlaylist playlist, string name)
|
||||||
|
=> RenameAsync(playlist.Kind, name);
|
||||||
|
|
||||||
|
public async Task<bool> DeleteAsync(string kind)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await new YPlaylistRemoveBuilder(Api).ExecuteAsync(kind);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> DeleteAsync(YPlaylist playlist)
|
||||||
|
=> DeleteAsync(playlist.Kind);
|
||||||
|
|
||||||
|
public async Task<YPlaylist?> InsertTracksAsync(YPlaylist playlist, IEnumerable<YTrack> tracks)
|
||||||
|
{
|
||||||
|
var change = await new YPlaylistChangeBuilder(Api).ExecuteAsync((playlist, new[]
|
||||||
|
{
|
||||||
|
new YPlaylistChange
|
||||||
|
{
|
||||||
|
Operation = YPlaylistChangeType.Insert,
|
||||||
|
At = 0,
|
||||||
|
Tracks = tracks.Select(t => t.GetKey())
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return change != null ? await GetAsync(change) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YPlaylist?> DeleteTracksAsync(YPlaylist playlist, IEnumerable<YTrack> tracks)
|
||||||
|
{
|
||||||
|
var distinctTracks = tracks.Distinct().ToList();
|
||||||
|
var indices = distinctTracks
|
||||||
|
.Select(t => playlist.Tracks?.FindIndex(ct => ct.Track?.GetKey() == t.GetKey()) ?? -1)
|
||||||
|
.Where(i => i != -1)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var changes = indices.Select(i => new YPlaylistChange
|
||||||
|
{
|
||||||
|
Operation = YPlaylistChangeType.Delete,
|
||||||
|
From = i,
|
||||||
|
To = i + 1,
|
||||||
|
Tracks = new[] { playlist.Tracks![i].Track!.GetKey() }
|
||||||
|
});
|
||||||
|
|
||||||
|
var change = await new YPlaylistChangeBuilder(Api).ExecuteAsync((playlist, changes));
|
||||||
|
return change != null ? await GetAsync(change) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Landing;
|
|
||||||
using YandexMusic.API.Models.Landing.Entity.Entities;
|
|
||||||
using YandexMusic.API.Models.Playlist;
|
|
||||||
using YandexMusic.API.Models.Track;
|
|
||||||
using YandexMusic.API.Requests.Playlist;
|
|
||||||
|
|
||||||
namespace YandexMusic.API
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// API для взамодействия с плейлистами
|
|
||||||
/// </summary>
|
|
||||||
public partial class YPlaylistAPI : YCommonAPI
|
|
||||||
{
|
|
||||||
#region Вспомогательные функции
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение персональных плейлистов
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="type">Тип</param>
|
|
||||||
/// <returns>Плейлист</returns>
|
|
||||||
private async Task<YResponse<YPlaylist>> GetPersonalPlaylist(AuthStorage storage, YGeneratedPlaylistType type)
|
|
||||||
{
|
|
||||||
List<YResponse<YPlaylist>> list = await GetPersonalPlaylistsAsync(storage);
|
|
||||||
return list.FirstOrDefault(e => string.Equals(e.Result.GeneratedPlaylistType, type.ToString(), StringComparison.CurrentCultureIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Изменение плейлиста
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="playlist">Плейлист</param>
|
|
||||||
/// <param name="changes">Список изменений</param>
|
|
||||||
/// <returns>Плейлист после изменений</returns>
|
|
||||||
private Task<YResponse<YPlaylist>> ChangePlaylist(AuthStorage storage, YPlaylist playlist, IEnumerable<YPlaylistChange> changes)
|
|
||||||
{
|
|
||||||
return new YPlaylistChangeBuilder(api, storage)
|
|
||||||
.Build((playlist, changes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<YTrack> RemoveIdentical(IEnumerable<YTrack> tracks)
|
|
||||||
{
|
|
||||||
return tracks.Distinct();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Вспомогательные функции
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public YPlaylistAPI(YandexMusicApi yandex) : base(yandex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Список с главной
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение списка персональных плейлистов
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<List<YResponse<YPlaylist>>> GetPersonalPlaylistsAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
YResponse<YLanding> landing = await api.Landing.GetAsync(storage, YLandingBlockType.PersonalPlaylists);
|
|
||||||
|
|
||||||
IEnumerable<Task<YResponse<YPlaylist>>> tasks = landing
|
|
||||||
.Result
|
|
||||||
.Blocks
|
|
||||||
.FirstOrDefault(b => b.Type == YLandingBlockType.PersonalPlaylists)
|
|
||||||
?.Entities
|
|
||||||
.Select(e => api.Playlist.GetAsync(storage, ((YLandingEntityPersonalPlaylist)e).Data?.Data));
|
|
||||||
|
|
||||||
return tasks == null
|
|
||||||
? new List<YResponse<YPlaylist>>()
|
|
||||||
: new List<YResponse<YPlaylist>>(await Task.WhenAll(tasks));
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Список с главной
|
|
||||||
|
|
||||||
#region Стандартные плейлисты
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Избранное
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YPlaylist>>> FavoritesAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return new YGetPlaylistFavoritesBuilder(api, storage)
|
|
||||||
.Build(null)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Плейлист дня
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> OfTheDayAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetPersonalPlaylist(storage, YGeneratedPlaylistType.PlaylistOfTheDay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Дежавю
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> DejaVuAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetPersonalPlaylist(storage, YGeneratedPlaylistType.NeverHeard);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Премьера
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> PremiereAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetPersonalPlaylist(storage, YGeneratedPlaylistType.RecentTracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Тайник
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> MissedAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetPersonalPlaylist(storage, YGeneratedPlaylistType.MissedLikes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Кинопоиск
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> KinopoiskAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetPersonalPlaylist(storage, YGeneratedPlaylistType.Kinopoisk);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Стандартные плейлисты
|
|
||||||
|
|
||||||
#region Получение плейлиста
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение плейлиста
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="user">Uid пользователя-владельца плейлиста</param>
|
|
||||||
/// <param name="kind">Тип</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, string user, string kind)
|
|
||||||
{
|
|
||||||
return new YGetPlaylistBuilder(api, storage)
|
|
||||||
.Build((user, kind))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение плейлиста по uuid
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="uuid">uuid</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, string uuid)
|
|
||||||
{
|
|
||||||
return new YGetPlaylistByUuidBuilder(api, storage)
|
|
||||||
.Build(uuid)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение плейлистов
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="ids">Список пар пользователь:тип</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YPlaylist>>> GetAsync(AuthStorage storage, IEnumerable<(string user, string kind)> ids)
|
|
||||||
{
|
|
||||||
return new YGetPlaylistsBuilder(api, storage)
|
|
||||||
.Build(ids)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение плейлиста
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="playlist">Описание плейлиста, для которого будут запрошены треки</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return new YGetPlaylistBuilder(api, storage)
|
|
||||||
.Build((playlist.Owner.Uid, playlist.Kind))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Получение плейлиста
|
|
||||||
|
|
||||||
#region Операции над плейлистами
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создание
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="name">Заголовок</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> CreateAsync(AuthStorage storage, string name)
|
|
||||||
{
|
|
||||||
return new YPlaylistCreateBuilder(api, storage)
|
|
||||||
.Build(name)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Переименование
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="kinds">Идентификатор плейлиста</param>
|
|
||||||
/// <param name="name">Заголовок</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> RenameAsync(AuthStorage storage, string kinds, string name)
|
|
||||||
{
|
|
||||||
return new YPlaylistRenameBuilder(api, storage)
|
|
||||||
.Build((kinds, name))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Переименование
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="playlist">Плейлист</param>
|
|
||||||
/// <param name="name">Заголовок</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> RenameAsync(AuthStorage storage, YPlaylist playlist, string name)
|
|
||||||
{
|
|
||||||
return RenameAsync(storage, playlist.Kind, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Удаление
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="kinds">Тип</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<bool> DeleteAsync(AuthStorage storage, string kinds)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await new YPlaylistRemoveBuilder(api, storage)
|
|
||||||
.Build(kinds)
|
|
||||||
.GetResponseAsync();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Удаление
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="playlist">Плейлист</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<bool> DeleteAsync(AuthStorage storage, YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return DeleteAsync(storage, playlist.Kind);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Добавление трека
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="playlist">Плейлист</param>
|
|
||||||
/// <param name="tracks">Треки для добавления</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<YResponse<YPlaylist>> InsertTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YTrack> tracks)
|
|
||||||
{
|
|
||||||
YResponse<YPlaylist> change = await ChangePlaylist(storage, playlist, new List<YPlaylistChange> {
|
|
||||||
new() {
|
|
||||||
Operation = YPlaylistChangeType.Insert,
|
|
||||||
At = 0,
|
|
||||||
Tracks = tracks.Select(t => t.GetKey())
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return await GetAsync(storage, change.Result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Удаление треков
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="playlist">Плейлист</param>
|
|
||||||
/// <param name="tracks">Треки для удаления</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> DeleteTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YTrack> tracks)
|
|
||||||
{
|
|
||||||
List<YPlaylistChange> changes = RemoveIdentical(tracks)
|
|
||||||
.Select(t => playlist.Tracks.Select(c => c.Track).ToList().IndexOf(t))
|
|
||||||
.Where(i => i != -1)
|
|
||||||
.Select(i =>
|
|
||||||
{
|
|
||||||
YTrackContainer t = playlist.Tracks[i];
|
|
||||||
return new YPlaylistChange
|
|
||||||
{
|
|
||||||
Operation = YPlaylistChangeType.Delete,
|
|
||||||
From = i,
|
|
||||||
To = i + 1,
|
|
||||||
Tracks = new List<YTrackAlbumPair> {
|
|
||||||
t.Track.GetKey()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return ChangePlaylist(storage, playlist, changes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Операции над плейлистами
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
22
YandexMusic.API/API/YQueueAPI.cs
Normal file
22
YandexMusic.API/API/YQueueAPI.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using YandexMusic.API.Models.Queue;
|
||||||
|
using YandexMusic.API.Requests.Queue;
|
||||||
|
|
||||||
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>API для работы с очередями воспроизведения.</summary>
|
||||||
|
public class YQueueAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YQueueAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
public Task<YQueueItemsContainer?> ListAsync(string? device = null)
|
||||||
|
=> new YQueuesListBuilder(Api, device).ExecuteAsync(null!);
|
||||||
|
|
||||||
|
public Task<YQueue?> GetAsync(string queueId)
|
||||||
|
=> new YGetQueueBuilder(Api).ExecuteAsync(queueId);
|
||||||
|
|
||||||
|
public Task<YNewQueue?> CreateAsync(YQueue queue, string? device = null)
|
||||||
|
=> new YQueueCreateBuilder(Api, device).ExecuteAsync(queue);
|
||||||
|
|
||||||
|
public Task<YUpdatedQueue?> UpdatePositionAsync(string queueId, int currentIndex, bool isInteractive, string? device = null)
|
||||||
|
=> new YQueueUpdatePositionBuilder(Api, device).ExecuteAsync((queueId, currentIndex, isInteractive));
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Queue;
|
|
||||||
using YandexMusic.API.Requests.Queue;
|
|
||||||
|
|
||||||
namespace YandexMusic.API
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// API для взаимодействия с очередями
|
|
||||||
/// </summary>
|
|
||||||
public partial class YQueueAPI : YCommonAPI
|
|
||||||
{
|
|
||||||
public YQueueAPI(YandexMusicApi yandex) : base(yandex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение всех очередей треков с разных устройств для синхронизации между ними
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="device">Устройство</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YQueueItemsContainer>> ListAsync(AuthStorage storage, string device = null)
|
|
||||||
{
|
|
||||||
return new YQueuesListBuilder(api, storage)
|
|
||||||
.Build(device)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение очереди
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="queueId">Идентификатор очереди</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YQueue>> GetAsync(AuthStorage storage, string queueId)
|
|
||||||
{
|
|
||||||
return new YGetQueueBuilder(api, storage)
|
|
||||||
.Build(queueId)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создание новой очереди треков
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="queue">Очередь треков</param>
|
|
||||||
/// <param name="device">Устройство</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YNewQueue>> CreateAsync(AuthStorage storage, YQueue queue, string device = null)
|
|
||||||
{
|
|
||||||
return new YQueueCreateBuilder(api, storage, device)
|
|
||||||
.Build(queue)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Установка текущего индекса проигрываемого трека в очереди треков
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="queueId">Идентификатор очереди</param>
|
|
||||||
/// <param name="currentIndex">Текущий индекс</param>
|
|
||||||
/// <param name="isInteractive">Флаг интерактивности</param>
|
|
||||||
/// <param name="device">Устройство</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YUpdatedQueue>> UpdatePositionAsync(AuthStorage storage, string queueId, int currentIndex, bool isInteractive, string device = null)
|
|
||||||
{
|
|
||||||
return new YQueueUpdatePositionBuilder(api, storage, device)
|
|
||||||
.Build((queueId, currentIndex, isInteractive))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
37
YandexMusic.API/API/YRadioAPI.cs
Normal file
37
YandexMusic.API/API/YRadioAPI.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using YandexMusic.API.Models.Radio;
|
||||||
|
using YandexMusic.API.Models.Track;
|
||||||
|
using YandexMusic.API.Requests.Radio;
|
||||||
|
|
||||||
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>API для работы с радио.</summary>
|
||||||
|
public class YRadioAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YRadioAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
public Task<YStationsDashboard?> GetStationsDashboardAsync()
|
||||||
|
=> new YGetStationsDashboardBuilder(Api).ExecuteAsync(null!);
|
||||||
|
|
||||||
|
public Task<List<YStation>?> GetStationsAsync()
|
||||||
|
=> new YGetStationsBuilder(Api).ExecuteAsync(null!);
|
||||||
|
|
||||||
|
public Task<List<YStation>?> GetStationAsync(string type, string tag)
|
||||||
|
=> new YGetStationBuilder(Api).ExecuteAsync((type, tag));
|
||||||
|
|
||||||
|
public Task<List<YStation>?> GetStationAsync(YStationId id)
|
||||||
|
=> GetStationAsync(id.Type, id.Tag);
|
||||||
|
|
||||||
|
public Task<YStationSequence?> GetStationTracksAsync(YStation station, string prevTrackId = "")
|
||||||
|
=> new YGetStationTracksBuilder(Api).ExecuteAsync((station.Station, prevTrackId));
|
||||||
|
|
||||||
|
public Task<string?> SetStationSettings2Async(YStation station, YStationSettings2 settings)
|
||||||
|
=> new YSetSettings2Builder(Api).ExecuteAsync((station.Station, settings));
|
||||||
|
|
||||||
|
public Task<string?> SendStationFeedbackAsync(
|
||||||
|
YStation station,
|
||||||
|
YStationFeedbackType type,
|
||||||
|
YTrack? track = null,
|
||||||
|
string batchId = "",
|
||||||
|
double totalPlayedSeconds = 0)
|
||||||
|
=> new YSetStationFeedbackBuilder(Api).ExecuteAsync((type, station, track, batchId, totalPlayedSeconds));
|
||||||
|
}
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Radio;
|
|
||||||
using YandexMusic.API.Models.Track;
|
|
||||||
using YandexMusic.API.Requests.Radio;
|
|
||||||
|
|
||||||
namespace YandexMusic.API
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// API для взаимодействия с радио
|
|
||||||
/// </summary>
|
|
||||||
public partial class YRadioAPI : YCommonAPI
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
public YRadioAPI(YandexMusicApi yandex) : base(yandex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение списка рекомендованных радиостанций
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YStationsDashboard>> GetStationsDashboardAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return new YGetStationsDashboardBuilder(api, storage)
|
|
||||||
.Build(null)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение списка радиостанций
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YStation>>> GetStationsAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return new YGetStationsBuilder(api, storage)
|
|
||||||
.Build(null)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение информации о радиостанции
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="type">Тип</param>
|
|
||||||
/// <param name="tag">Тэг</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YStation>>> GetStationAsync(AuthStorage storage, string type, string tag)
|
|
||||||
{
|
|
||||||
return new YGetStationBuilder(api, storage)
|
|
||||||
.Build((type, tag))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение информации о радиостанции
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="id">Идентификатор станции</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YStation>>> GetStationAsync(AuthStorage storage, YStationId id)
|
|
||||||
{
|
|
||||||
return GetStationAsync(storage, id.Type, id.Tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение последовательности треков радиостанции
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="station">Радиостанция</param>
|
|
||||||
/// <param name="prevTrackId">Идентификатор предыдущего трека</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YStationSequence>> GetStationTracksAsync(AuthStorage storage, YStation station, string prevTrackId = "")
|
|
||||||
{
|
|
||||||
return new YGetStationTracksBuilder(api, storage)
|
|
||||||
.Build((station.Station, prevTrackId))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Установка настроек подбора треков
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="station">Радиостанция</param>
|
|
||||||
/// <param name="settings">Настройки</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> SetStationSettings2Async(AuthStorage storage, YStation station, YStationSettings2 settings)
|
|
||||||
{
|
|
||||||
return new YSetSettings2Builder(api, storage)
|
|
||||||
.Build((station.Station, settings))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отправка обратной связи на действия при прослушивании радио
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="station">Радиостанция</param>
|
|
||||||
/// <param name="type">Тип обратной связи</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <param name="batchId">Уникальный идентификатор партии треков. Возвращается при получении треков</param>
|
|
||||||
/// <param name="totalPlayedSeconds">Сколько было проиграно секунд трека перед действием</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<string> SendStationFeedBackAsync(AuthStorage storage, YStation station, YStationFeedbackType type, YTrack track = null, string batchId = "", double totalPlayedSeconds = 0)
|
|
||||||
{
|
|
||||||
return new YSetStationFeedbackBuilder(api, storage)
|
|
||||||
.Build((type, station, track, batchId, totalPlayedSeconds))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
38
YandexMusic.API/API/YSearchAPI.cs
Normal file
38
YandexMusic.API/API/YSearchAPI.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using YandexMusic.API.Models.Common;
|
||||||
|
using YandexMusic.API.Models.Search;
|
||||||
|
using YandexMusic.API.Requests.Search;
|
||||||
|
|
||||||
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>API для поиска.</summary>
|
||||||
|
public class YSearchAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YSearchAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
public Task<YSearch?> TrackAsync(string trackName, int page = 0, int pageSize = 20)
|
||||||
|
=> SearchAsync(trackName, YSearchType.Track, page, pageSize);
|
||||||
|
|
||||||
|
public Task<YSearch?> AlbumsAsync(string albumName, int page = 0, int pageSize = 20)
|
||||||
|
=> SearchAsync(albumName, YSearchType.Album, page, pageSize);
|
||||||
|
|
||||||
|
public Task<YSearch?> ArtistAsync(string artistName, int page = 0, int pageSize = 20)
|
||||||
|
=> SearchAsync(artistName, YSearchType.Artist, page, pageSize);
|
||||||
|
|
||||||
|
public Task<YSearch?> PlaylistAsync(string playlistName, int page = 0, int pageSize = 20)
|
||||||
|
=> SearchAsync(playlistName, YSearchType.Playlist, page, pageSize);
|
||||||
|
|
||||||
|
public Task<YSearch?> PodcastEpisodeAsync(string podcastName, int page = 0, int pageSize = 20)
|
||||||
|
=> SearchAsync(podcastName, YSearchType.PodcastEpisode, page, pageSize);
|
||||||
|
|
||||||
|
public Task<YSearch?> VideosAsync(string videoName, int page = 0, int pageSize = 20)
|
||||||
|
=> SearchAsync(videoName, YSearchType.Video, page, pageSize);
|
||||||
|
|
||||||
|
public Task<YSearch?> UsersAsync(string userName, int page = 0, int pageSize = 20)
|
||||||
|
=> SearchAsync(userName, YSearchType.User, page, pageSize);
|
||||||
|
|
||||||
|
public Task<YSearch?> SearchAsync(string searchText, YSearchType searchType, int page = 0, int pageSize = 20)
|
||||||
|
=> new YSearchBuilder(Api).ExecuteAsync((searchText, searchType, page, pageSize));
|
||||||
|
|
||||||
|
public Task<YSearchSuggest?> GetSearchSuggestionsAsync(string searchText)
|
||||||
|
=> new YSearchSuggestBuilder(Api).ExecuteAsync(searchText);
|
||||||
|
}
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Search;
|
|
||||||
using YandexMusic.API.Requests.Search;
|
|
||||||
|
|
||||||
namespace YandexMusic.API
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// API для поиска
|
|
||||||
/// </summary>
|
|
||||||
public partial class YSearchAPI : YCommonAPI
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
public YSearchAPI(YandexMusicApi yandex) : base(yandex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Поиск по трекам
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="trackName">Имя трека</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> TrackAsync(AuthStorage storage, string trackName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, trackName, YSearchType.Track, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Поиск по альбомам
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="albumName">Имя альбома</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> AlbumsAsync(AuthStorage storage, string albumName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, albumName, YSearchType.Album, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Поиск по артисту
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="artistName">Имя артиста</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> ArtistAsync(AuthStorage storage, string artistName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, artistName, YSearchType.Artist, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Поиск по плейлистам
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="playlistName">Имя плейлиста</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> PlaylistAsync(AuthStorage storage, string playlistName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, playlistName, YSearchType.Playlist, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Поиск по плейлистам
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="podcastName">Имя подкаста</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> PodcastEpisodeAsync(AuthStorage storage, string podcastName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, podcastName, YSearchType.PodcastEpisode, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Поиск по видео
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="videoName">Имя видео</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> VideosAsync(AuthStorage storage, string videoName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, videoName, YSearchType.Video, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Поиск по пользователям
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="userName">Имя пользователя</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> UsersAsync(AuthStorage storage, string userName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, userName, YSearchType.User, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Поиск
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="searchText">Поисковый запрос</param>
|
|
||||||
/// <param name="searchType">Тип поиска</param>
|
|
||||||
/// <param name="page">Страница</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> SearchAsync(AuthStorage storage, string searchText, YSearchType searchType, int page = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return new YSearchBuilder(api, storage)
|
|
||||||
.Build((searchText, searchType, page, pageSize))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Подсказка
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="searchText">Поисковый запрос</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearchSuggest>> SuggestAsync(AuthStorage storage, string searchText)
|
|
||||||
{
|
|
||||||
return new YSearchSuggestBuilder(api, storage)
|
|
||||||
.Build(searchText)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
110
YandexMusic.API/API/YTrackAPI.cs
Normal file
110
YandexMusic.API/API/YTrackAPI.cs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using YandexMusic.API.Models.Common;
|
||||||
|
using YandexMusic.API.Models.Track;
|
||||||
|
using YandexMusic.API.Requests.Track;
|
||||||
|
|
||||||
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>API для работы с треками (получение, загрузка, метаданные).</summary>
|
||||||
|
public class YTrackAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YTrackAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
private static string BuildDownloadLink(YTrackDownloadInfo info, YStorageDownloadFile storageDownload)
|
||||||
|
{
|
||||||
|
var path = storageDownload.Path;
|
||||||
|
var host = storageDownload.Host;
|
||||||
|
var ts = storageDownload.Ts;
|
||||||
|
var s = storageDownload.S;
|
||||||
|
var codec = info.Codec;
|
||||||
|
|
||||||
|
var secret = $"XGRlBW9FXlekgbPrRHuSiA{path[1..]}{s}";
|
||||||
|
var md5Hash = MD5.HashData(Encoding.UTF8.GetBytes(secret));
|
||||||
|
var hmacsha1 = new HMACSHA1(md5Hash);
|
||||||
|
var sign = BitConverter.ToString(hmacsha1.ComputeHash(md5Hash)).Replace("-", "").ToLower();
|
||||||
|
return $"https://{host}/get-{codec}/{sign}/{ts}{path}";
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string?> GetFileLinkAsync(YTrack track)
|
||||||
|
=> GetFileLinkAsync(track.GetKey().ToString());
|
||||||
|
|
||||||
|
public async Task ExtractToFileAsync(string trackKey, string filePath)
|
||||||
|
{
|
||||||
|
var url = await GetFileLinkAsync(trackKey);
|
||||||
|
if (string.IsNullOrEmpty(url)) throw new Exception("Не удалось получить ссылку на трек");
|
||||||
|
using var response = await Api.HttpClient.GetAsync(url);
|
||||||
|
await using var fs = File.Create(filePath);
|
||||||
|
await response.Content.CopyToAsync(fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExtractToFileAsync(YTrack track, string filePath)
|
||||||
|
=> ExtractToFileAsync(track.GetKey().ToString(), filePath);
|
||||||
|
|
||||||
|
public async Task<byte[]> ExtractDataAsync(string trackKey)
|
||||||
|
{
|
||||||
|
var url = await GetFileLinkAsync(trackKey);
|
||||||
|
if (string.IsNullOrEmpty(url)) throw new Exception("Не удалось получить ссылку на трек");
|
||||||
|
return await Api.HttpClient.GetByteArrayAsync(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<byte[]> ExtractDataAsync(YTrack track)
|
||||||
|
=> ExtractDataAsync(track.GetKey().ToString());
|
||||||
|
|
||||||
|
public async Task<Stream> ExtractStreamAsync(string trackKey, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||||
|
{
|
||||||
|
var url = await GetFileLinkAsync(trackKey);
|
||||||
|
if (string.IsNullOrEmpty(url)) throw new Exception("Не удалось получить ссылку на трек");
|
||||||
|
var response = await Api.HttpClient.GetAsync(url, completionOption);
|
||||||
|
return await response.Content.ReadAsStreamAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Stream> ExtractStreamAsync(YTrack track, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||||
|
=> ExtractStreamAsync(track.GetKey().ToString(), completionOption);
|
||||||
|
|
||||||
|
public Task<string?> SendPlayTrackInfoAsync(
|
||||||
|
YTrack track,
|
||||||
|
string from,
|
||||||
|
bool fromCache = false,
|
||||||
|
string playId = "",
|
||||||
|
string playlistId = "",
|
||||||
|
double totalPlayedSeconds = 0,
|
||||||
|
double endPositionSeconds = 0)
|
||||||
|
=> new YSendTrackInfoBuilder(Api).ExecuteAsync((track, from, fromCache, playId, playlistId, totalPlayedSeconds, endPositionSeconds));
|
||||||
|
|
||||||
|
public Task<YTrackSupplement?> GetSupplementAsync(string trackId)
|
||||||
|
=> new YGetTrackSupplementBuilder(Api).ExecuteAsync(trackId);
|
||||||
|
|
||||||
|
public Task<YTrackSupplement?> GetSupplementAsync(YTrack track)
|
||||||
|
=> GetSupplementAsync(track.GetKey().ToString());
|
||||||
|
|
||||||
|
public Task<YTrackSimilar?> GetSimilarAsync(string trackId)
|
||||||
|
=> new YGetTrackSimilarBuilder(Api).ExecuteAsync(trackId);
|
||||||
|
|
||||||
|
public Task<YTrackSimilar?> GetSimilarAsync(YTrack track)
|
||||||
|
=> GetSimilarAsync(track.GetKey().ToString());
|
||||||
|
}
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Track;
|
|
||||||
using YandexMusic.API.Requests.Track;
|
|
||||||
|
|
||||||
namespace YandexMusic.API
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// API для взаимодействия с треками
|
|
||||||
/// </summary>
|
|
||||||
public partial class YTrackAPI : YCommonAPI
|
|
||||||
{
|
|
||||||
#region Вспомогательные функции
|
|
||||||
|
|
||||||
private string BuildLinkForDownload(YTrackDownloadInfo mainDownloadResponse, YStorageDownloadFile storageDownload)
|
|
||||||
{
|
|
||||||
string path = storageDownload.Path;
|
|
||||||
string host = storageDownload.Host;
|
|
||||||
string ts = storageDownload.Ts;
|
|
||||||
string s = storageDownload.S;
|
|
||||||
string codec = mainDownloadResponse.Codec;
|
|
||||||
|
|
||||||
string secret = $"XGRlBW9FXlekgbPrRHuSiA{path.Substring(1, path.Length - 1)}{s}";
|
|
||||||
MD5 md5 = MD5.Create();
|
|
||||||
byte[] md5Hash = md5.ComputeHash(Encoding.UTF8.GetBytes(secret));
|
|
||||||
HMACSHA1 hmacsha1 = new();
|
|
||||||
byte[] hmasha1Hash = hmacsha1.ComputeHash(md5Hash);
|
|
||||||
string sign = BitConverter.ToString(hmasha1Hash).Replace("-", "").ToLower();
|
|
||||||
|
|
||||||
string link = $"https://{host}/get-{codec}/{sign}/{ts}{path}";
|
|
||||||
|
|
||||||
return link;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Вспомогательные функции
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public YTrackAPI(YandexMusicApi yandex) : base(yandex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение треков
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="trackId">Идентификатор трека</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YTrack>>> GetAsync(AuthStorage storage, string trackId)
|
|
||||||
{
|
|
||||||
return new YGetTracksBuilder(api, storage)
|
|
||||||
.Build(new[] { trackId })
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение треков
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="trackIds">Идентификаторы треков</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YTrack>>> GetAsync(AuthStorage storage, IEnumerable<string> trackIds)
|
|
||||||
{
|
|
||||||
return new YGetTracksBuilder(api, storage)
|
|
||||||
.Build(trackIds)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение метаданных для загрузки
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <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)
|
|
||||||
.Build((trackKey, direct))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение метаданных для загрузки
|
|
||||||
/// </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>
|
|
||||||
/// Получение информации для формирования ссылки для загрузки
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="metadataInfo">Метаданные для загрузки</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YStorageDownloadFile> GetDownloadFileInfoAsync(AuthStorage storage, YTrackDownloadInfo metadataInfo)
|
|
||||||
{
|
|
||||||
return new YStorageDownloadFileBuilder(api, storage)
|
|
||||||
.Build(metadataInfo.DownloadInfoUrl)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение ссылки для загрузки
|
|
||||||
/// </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>
|
|
||||||
/// Получение ссылки для загрузки
|
|
||||||
/// </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 Получение данных трека
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
YandexMusic.API/API/YUgcAPI.cs
Normal file
36
YandexMusic.API/API/YUgcAPI.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using YandexMusic.API.Models.Playlist;
|
||||||
|
using YandexMusic.API.Models.Ugc;
|
||||||
|
using YandexMusic.API.Requests.Ugc;
|
||||||
|
|
||||||
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>API для загрузки пользовательского контента (UGC).</summary>
|
||||||
|
public class YUgcAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YUgcAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
public Task<YUgcUpload?> GetUgcUploadLinkAsync(YPlaylist playlist, string fileName)
|
||||||
|
=> new YUgcGetUploadLinkBuilder(Api).ExecuteAsync((playlist, fileName));
|
||||||
|
|
||||||
|
public async Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, string filePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException("Файл не найден", filePath);
|
||||||
|
return await UploadTrackToPlaylistAsync(playlist, fileName, await File.ReadAllBytesAsync(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, Stream stream)
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
await stream.CopyToAsync(ms);
|
||||||
|
return await UploadTrackToPlaylistAsync(playlist, fileName, ms.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, byte[] file)
|
||||||
|
{
|
||||||
|
var uploadLink = await GetUgcUploadLinkAsync(playlist, fileName);
|
||||||
|
if (uploadLink?.PostTarget == null) return null;
|
||||||
|
var result = await new YUgcUploadBuilder(Api).ExecuteAsync((uploadLink.PostTarget, file));
|
||||||
|
return result?.Result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Playlist;
|
|
||||||
using YandexMusic.API.Models.Ugc;
|
|
||||||
using YandexMusic.API.Requests.Ugc;
|
|
||||||
|
|
||||||
namespace YandexMusic.API
|
|
||||||
{
|
|
||||||
public partial class YUgcAPI : YCommonAPI
|
|
||||||
{
|
|
||||||
public YUgcAPI(YandexMusicApi yandex) : base(yandex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение ссылки на загрузчик трека
|
|
||||||
/// </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>
|
|
||||||
/// Загрузка трека из файла
|
|
||||||
/// </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))
|
|
||||||
throw new FileNotFoundException("Файл для загрузки не существует.", filePath);
|
|
||||||
|
|
||||||
return UploadUgcTrackAsync(storage, uploadLink, File.Open(filePath, FileMode.Open));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Загрузка трека из потока
|
|
||||||
/// </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)
|
|
||||||
throw new NullReferenceException("Пустая ссылка на поток загрузки.");
|
|
||||||
|
|
||||||
using MemoryStream ms = new();
|
|
||||||
stream.CopyTo(ms);
|
|
||||||
|
|
||||||
return UploadUgcTrackAsync(storage, uploadLink, ms.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Загрузка трека из массива
|
|
||||||
/// </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)
|
|
||||||
.Build((uploadLink, file))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,303 +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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
16
YandexMusic.API/API/YYnisonAPI.cs
Normal file
16
YandexMusic.API/API/YYnisonAPI.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using YandexMusic.API.Common.Ynison;
|
||||||
|
|
||||||
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>API для работы с Ynison (WebSocket-плеер).</summary>
|
||||||
|
public class YYnisonAPI : YCommonAPI
|
||||||
|
{
|
||||||
|
public YYnisonAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
public YnisonPlayer GetPlayer()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(Api.Storage.Token))
|
||||||
|
throw new Exception("Токен пользователя не задан");
|
||||||
|
return new YnisonPlayer(Api, Api.Storage);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,103 +1,81 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common.Debug;
|
|
||||||
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>
|
||||||
|
/// Хранилище данных авторизации. Не содержит HTTP-зависимостей.
|
||||||
|
/// </summary>
|
||||||
|
public class AuthStorage
|
||||||
{
|
{
|
||||||
/// <summary>
|
private CookieContainer _cookieContainer;
|
||||||
/// Хранилище данных пользователя
|
|
||||||
/// </summary>
|
public AuthStorage(CookieContainer cookieContainer)
|
||||||
public class AuthStorage
|
|
||||||
{
|
{
|
||||||
#region Свойства
|
_cookieContainer = cookieContainer;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
public CookieContainer CookieContainer => _cookieContainer;
|
||||||
/// Http-контекст
|
|
||||||
/// </summary>
|
|
||||||
public HttpContext Context { get; }
|
|
||||||
|
|
||||||
public DebugSettings Debug { get; set; }
|
/// <summary>
|
||||||
|
/// Флаг, указывающий, авторизован ли пользователь.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAuthorized { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Флаг авторизации
|
/// Идентификатор устройства (используется в заголовках).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsAuthorized { get; internal set; }
|
public string DeviceId { get; set; } = "csharp";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Идентификатор устройства
|
/// OAuth-токен для доступа к API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DeviceId { get; set; } = "csharp";
|
public string Token { get; internal set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Токен авторизации
|
/// Информация об аккаунте пользователя.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Token { get; internal set; }
|
public YAccount User { get; internal set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Аккаунт
|
/// Временный токен доступа (используется в некоторых сценариях авторизации).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public YAccount User { get; set; }
|
public YAccessToken AccessToken { get; internal set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Провайдер запросов
|
/// Внутренние данные авторизации (CSRF, track_id и т.д.).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IRequestProvider Provider { get; }
|
public YAuthToken? HeaderToken { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Токен доступа
|
/// Внутренние данные авторизации (CSRF, track_id и т.д.).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public YAccessToken AccessToken { get; set; }
|
public YAuthToken? AuthToken { get; set; } = new();
|
||||||
|
|
||||||
internal YAuthToken AuthToken { get; set; }
|
/// <summary>
|
||||||
|
/// Страна, используемая для авторизации (по умолчанию "ru"). Может влиять на язык интерфейса и доступные методы авторизации.
|
||||||
#endregion Свойства
|
/// </summary>
|
||||||
|
public object Country { get; set; } = "ru";
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Конструктор
|
|
||||||
/// </summary>
|
|
||||||
public AuthStorage(DebugSettings settings = null)
|
|
||||||
{
|
|
||||||
User = new YAccount();
|
|
||||||
Context = new HttpContext();
|
|
||||||
Debug = settings;
|
|
||||||
Provider = new DefaultRequestProvider(this);
|
|
||||||
|
|
||||||
if (Debug is { ClearDirectory: true })
|
|
||||||
{
|
|
||||||
Debug.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Конструктор
|
|
||||||
/// </summary>
|
|
||||||
public AuthStorage(IRequestProvider provider, DebugSettings settings = null)
|
|
||||||
{
|
|
||||||
User = new YAccount();
|
|
||||||
Context = new HttpContext();
|
|
||||||
Debug = settings;
|
|
||||||
Provider = provider;
|
|
||||||
|
|
||||||
if (Debug is { ClearDirectory: true })
|
|
||||||
{
|
|
||||||
Debug.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Установка прокси для пользователия
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="proxy">Прокси</param>
|
|
||||||
public void SetProxy(IWebProxy proxy)
|
|
||||||
{
|
|
||||||
Context.WebProxy = proxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// <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();
|
||||||
|
Token = string.Empty;
|
||||||
|
AccessToken = new YAccessToken();
|
||||||
|
AuthToken = new YAuthToken();
|
||||||
|
IsAuthorized = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,44 +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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +1,63 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace YandexMusic.API.Common
|
namespace YandexMusic.API.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Класс для шифровки
|
||||||
|
/// </summary>
|
||||||
|
public class Encryptor
|
||||||
{
|
{
|
||||||
/// <summary>
|
private readonly string IV = "encryption";
|
||||||
/// Класс для шифровки
|
private readonly byte[] IVHash;
|
||||||
/// </summary>
|
|
||||||
public class Encryptor
|
private readonly byte[] keyHash;
|
||||||
|
|
||||||
|
private readonly MD5 md5;
|
||||||
|
private readonly Aes aesAlg;
|
||||||
|
|
||||||
|
private byte[] GetHash(string value)
|
||||||
{
|
{
|
||||||
#region Поля
|
return md5.ComputeHash(Encoding.UTF8.GetBytes(value));
|
||||||
|
|
||||||
private readonly string IV = "encryption";
|
|
||||||
private readonly byte[] IVHash;
|
|
||||||
|
|
||||||
private readonly byte[] keyHash;
|
|
||||||
|
|
||||||
private readonly MD5 md5;
|
|
||||||
private readonly Aes aesAlg;
|
|
||||||
|
|
||||||
|
|
||||||
#endregion Поля
|
|
||||||
|
|
||||||
#region Вспомогательные функции
|
|
||||||
|
|
||||||
private byte[] GetHash(string value)
|
|
||||||
{
|
|
||||||
return md5.ComputeHash(Encoding.UTF8.GetBytes(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Вспомогательные функции
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public Encryptor(string key)
|
|
||||||
{
|
|
||||||
md5 = MD5.Create();
|
|
||||||
|
|
||||||
aesAlg = Aes.Create();
|
|
||||||
aesAlg.BlockSize = 128;
|
|
||||||
aesAlg.Padding = PaddingMode.PKCS7;
|
|
||||||
|
|
||||||
keyHash = GetHash(key);
|
|
||||||
IVHash = GetHash(IV);
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] Encrypt(byte[] data)
|
|
||||||
{
|
|
||||||
using MemoryStream ms = new();
|
|
||||||
using CryptoStream csEncrypt = new(ms, aesAlg.CreateEncryptor(keyHash, IVHash), CryptoStreamMode.Write);
|
|
||||||
|
|
||||||
csEncrypt.Write(data, 0, data.Length);
|
|
||||||
|
|
||||||
if (!csEncrypt.HasFlushedFinalBlock)
|
|
||||||
csEncrypt.FlushFinalBlock();
|
|
||||||
|
|
||||||
return ms.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] Decrypt(byte[] data)
|
|
||||||
{
|
|
||||||
using MemoryStream ms = new();
|
|
||||||
using CryptoStream csDecrypt = new(ms, aesAlg.CreateDecryptor(keyHash, IVHash), CryptoStreamMode.Write);
|
|
||||||
|
|
||||||
csDecrypt.Write(data, 0, data.Length);
|
|
||||||
|
|
||||||
if (!csDecrypt.HasFlushedFinalBlock)
|
|
||||||
csDecrypt.FlushFinalBlock();
|
|
||||||
|
|
||||||
return ms.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Encryptor(string key)
|
||||||
|
{
|
||||||
|
md5 = MD5.Create();
|
||||||
|
|
||||||
|
aesAlg = Aes.Create();
|
||||||
|
aesAlg.BlockSize = 128;
|
||||||
|
aesAlg.Padding = PaddingMode.PKCS7;
|
||||||
|
|
||||||
|
keyHash = GetHash(key);
|
||||||
|
IVHash = GetHash(IV);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] Encrypt(byte[] data)
|
||||||
|
{
|
||||||
|
using MemoryStream ms = new();
|
||||||
|
using CryptoStream csEncrypt = new(ms, aesAlg.CreateEncryptor(keyHash, IVHash), CryptoStreamMode.Write);
|
||||||
|
|
||||||
|
csEncrypt.Write(data, 0, data.Length);
|
||||||
|
|
||||||
|
if (!csEncrypt.HasFlushedFinalBlock)
|
||||||
|
csEncrypt.FlushFinalBlock();
|
||||||
|
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] Decrypt(byte[] data)
|
||||||
|
{
|
||||||
|
using MemoryStream ms = new();
|
||||||
|
using CryptoStream csDecrypt = new(ms, aesAlg.CreateDecryptor(keyHash, IVHash), CryptoStreamMode.Write);
|
||||||
|
|
||||||
|
csDecrypt.Write(data, 0, data.Length);
|
||||||
|
|
||||||
|
if (!csDecrypt.HasFlushedFinalBlock)
|
||||||
|
csDecrypt.FlushFinalBlock();
|
||||||
|
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
using YandexMusic.API.Models.Common;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Common.Providers
|
|
||||||
{
|
|
||||||
public class CommonRequestProvider : IRequestProvider
|
|
||||||
{
|
|
||||||
#region Поля
|
|
||||||
|
|
||||||
protected AuthStorage storage;
|
|
||||||
|
|
||||||
#endregion Поля
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public CommonRequestProvider(AuthStorage authStorage)
|
|
||||||
{
|
|
||||||
storage = authStorage;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region IRequestProvider
|
|
||||||
|
|
||||||
public virtual Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual async Task<T> GetDataFromResponseAsync<T>(YandexMusicApi api, HttpResponseMessage response)
|
|
||||||
{
|
|
||||||
string result = await response.Content.ReadAsStringAsync();
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
YErrorResponse exception = JsonConvert.DeserializeObject<YErrorResponse>(result);
|
|
||||||
throw exception ?? new Exception("Ошибка десериализации ответа с ошибкой.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
JsonSerializerSettings settings = new()
|
|
||||||
{
|
|
||||||
Converters = new List<JsonConverter> {
|
|
||||||
new YExecutionContextConverter(api, storage)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return storage.Debug != null
|
|
||||||
? storage.Debug.Deserialize<T>(response.RequestMessage?.RequestUri?.AbsolutePath, result, settings)
|
|
||||||
: JsonConvert.DeserializeObject<T>(result, settings);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new Exception($"Ошибка десериализации {ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion IRequestProvider
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Common.Providers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Стандартный провайдер запросов
|
|
||||||
/// </summary>
|
|
||||||
public class DefaultRequestProvider : CommonRequestProvider
|
|
||||||
{
|
|
||||||
#region Вспомогательные функции
|
|
||||||
|
|
||||||
private Exception ProcessException(Exception ex)
|
|
||||||
{
|
|
||||||
if (ex is not WebException webException)
|
|
||||||
return ex;
|
|
||||||
|
|
||||||
if (webException.Response is null)
|
|
||||||
return ex;
|
|
||||||
|
|
||||||
Stream s = webException.Response.GetResponseStream();
|
|
||||||
if (s is null)
|
|
||||||
return ex;
|
|
||||||
|
|
||||||
using StreamReader sr = new(s);
|
|
||||||
string result = sr.ReadToEnd();
|
|
||||||
|
|
||||||
YErrorResponse exception = JsonConvert.DeserializeObject<YErrorResponse>(result);
|
|
||||||
|
|
||||||
return exception ?? ex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Вспомогательные функции
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public DefaultRequestProvider(AuthStorage authStorage) : base(authStorage)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region IRequestProvider
|
|
||||||
|
|
||||||
public override Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message,
|
|
||||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
HttpClient client = new(new SocketsHttpHandler
|
|
||||||
{
|
|
||||||
Proxy = storage.Context.WebProxy,
|
|
||||||
AutomaticDecompression = DecompressionMethods.GZip,
|
|
||||||
UseCookies = true,
|
|
||||||
CookieContainer = storage.Context.Cookies,
|
|
||||||
});
|
|
||||||
|
|
||||||
return client.SendAsync(message, completionOption);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw ProcessException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion IRequestProvider
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
namespace YandexMusic.API.Common.Providers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Провайдер запросов данными из файла
|
|
||||||
/// </summary>
|
|
||||||
public class MockRequestProvider : CommonRequestProvider
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
public MockRequestProvider(AuthStorage authStorage) : base(authStorage)
|
|
||||||
{
|
|
||||||
storage = authStorage;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region IRequestProvider
|
|
||||||
|
|
||||||
public override Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message,
|
|
||||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion IRequestProvider
|
|
||||||
}
|
|
||||||
}
|
|
||||||
47
YandexMusic.API/Common/YandexMusicHttpClientFactory.cs
Normal file
47
YandexMusic.API/Common/YandexMusicHttpClientFactory.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
YandexMusic.API/Common/Ynison/UpperSnakeCaseNamingPolicy.cs
Normal file
43
YandexMusic.API/Common/Ynison/UpperSnakeCaseNamingPolicy.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Common.Ynison;
|
||||||
|
|
||||||
|
/// <summary>Политика именования в формате UPPER_SNAKE_CASE (все буквы верхнего регистра, слова через подчёркивание).</summary>
|
||||||
|
public class UpperSnakeCaseNamingPolicy : SnakeCaseNamingPolicy
|
||||||
|
{
|
||||||
|
/// <summary>Преобразует имя свойства в формат UPPER_SNAKE_CASE.</summary>
|
||||||
|
public override string ConvertName(string name)
|
||||||
|
{
|
||||||
|
var snakeCase = base.ConvertName(name);
|
||||||
|
return snakeCase.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Базовая политика именования в формате snake_case (все буквы нижнего регистра, слова через подчёркивание).</summary>
|
||||||
|
public class SnakeCaseNamingPolicy : JsonNamingPolicy
|
||||||
|
{
|
||||||
|
/// <summary>Преобразует имя свойства в формат snake_case.</summary>
|
||||||
|
public override string ConvertName(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
return name;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < name.Length; i++)
|
||||||
|
{
|
||||||
|
char c = name[i];
|
||||||
|
if (char.IsUpper(c))
|
||||||
|
{
|
||||||
|
if (i > 0)
|
||||||
|
sb.Append('_');
|
||||||
|
sb.Append(char.ToLowerInvariant(c));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace YandexMusic.API.Common.Ynison
|
|
||||||
{
|
|
||||||
public class UpperSnakeCaseNamingStrategy : SnakeCaseNamingStrategy
|
|
||||||
{
|
|
||||||
protected override string ResolvePropertyName(string name) => base.ResolvePropertyName(name).ToUpper();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,315 +1,181 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using YandexMusic.API.Models.Track;
|
using YandexMusic.API.Models.Track;
|
||||||
using YandexMusic.API.Models.Ynison;
|
using YandexMusic.API.Models.Ynison;
|
||||||
using YandexMusic.API.Models.Ynison.Messages;
|
using YandexMusic.API.Models.Ynison.Messages;
|
||||||
|
|
||||||
namespace YandexMusic.API.Common.Ynison
|
namespace YandexMusic.API.Common.Ynison;
|
||||||
|
|
||||||
|
/// <summary>Плеер для управления воспроизведением через протокол Ynison (WebSocket).</summary>
|
||||||
|
public class YnisonPlayer : IDisposable
|
||||||
{
|
{
|
||||||
public class YnisonPlayer : IDisposable
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
|
private readonly AuthStorage _storage;
|
||||||
|
private readonly IWebProxy? _proxy;
|
||||||
|
private YnisonWebSocket? _redirector;
|
||||||
|
private YnisonWebSocket? _state;
|
||||||
|
|
||||||
|
/// <summary>API Яндекс Музыки.</summary>
|
||||||
|
public YandexMusicApi API { get; }
|
||||||
|
|
||||||
|
/// <summary>Текущее состояние плеера.</summary>
|
||||||
|
public YYnisonState? State { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Текущий проигрываемый трек.</summary>
|
||||||
|
public YTrack? Current => GetCurrentAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
/// <summary>Событие получения нового состояния.</summary>
|
||||||
|
public event EventHandler<ReceiveEventArgs>? OnReceive;
|
||||||
|
|
||||||
|
/// <summary>Событие закрытия соединения.</summary>
|
||||||
|
public event EventHandler<CloseEventArgs>? OnClose;
|
||||||
|
|
||||||
|
/// <summary>Аргументы события получения состояния.</summary>
|
||||||
|
public class ReceiveEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
#region Поля
|
public YYnisonState State { get; init; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly JsonSerializerSettings jsonSettings = new()
|
/// <summary>Аргументы события закрытия соединения.</summary>
|
||||||
|
public class CloseEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
public WebSocketCloseStatus? Status { get; init; }
|
||||||
|
public string? Description { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal YnisonPlayer(YandexMusicApi api, AuthStorage authStorage, IWebProxy? proxy = null)
|
||||||
|
{
|
||||||
|
API = api;
|
||||||
|
_storage = authStorage;
|
||||||
|
_proxy = proxy;
|
||||||
|
_jsonOptions = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
Converters = new List<JsonConverter> {
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
new StringEnumConverter(new UpperSnakeCaseNamingStrategy())
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
},
|
Converters = { new JsonStringEnumConverter(new UpperSnakeCaseNamingPolicy(), false) }
|
||||||
|
};
|
||||||
|
_redirector = new YnisonWebSocket(_proxy);
|
||||||
|
_state = new YnisonWebSocket(_proxy);
|
||||||
|
}
|
||||||
|
|
||||||
NullValueHandling = NullValueHandling.Ignore,
|
private string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
|
||||||
ContractResolver = new DefaultContractResolver
|
|
||||||
|
private T Deserialize<T>(YYnisonMessageType messageType, string data)
|
||||||
|
=> JsonSerializer.Deserialize<T>(data, _jsonOptions)
|
||||||
|
?? throw new JsonException("Десериализация вернула null");
|
||||||
|
|
||||||
|
private T DeserializeMessage<T>(YYnisonMessageType messageType, string data)
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(data);
|
||||||
|
if (doc.RootElement.TryGetProperty("error", out _))
|
||||||
|
{
|
||||||
|
var error = Deserialize<YYnisonErrorMessage>(YYnisonMessageType.Error, data);
|
||||||
|
throw error ?? new Exception("Ошибка десериализации ответа с ошибкой.");
|
||||||
|
}
|
||||||
|
return Deserialize<T>(messageType, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string DefaultState()
|
||||||
|
{
|
||||||
|
var version = new YYnisonVersion
|
||||||
|
{
|
||||||
|
DeviceId = _storage.DeviceId,
|
||||||
|
Version = "0"
|
||||||
|
};
|
||||||
|
var fullState = new YYnisonUpdateFullStateMessage
|
||||||
|
{
|
||||||
|
UpdateFullState = new YYnisonFullState
|
||||||
{
|
{
|
||||||
// Важно! Унисон отдаёт данные в SnakeCase
|
Device = new YYnisonDevice
|
||||||
NamingStrategy = new SnakeCaseNamingStrategy()
|
{
|
||||||
|
Capabilities = new YYnisonDeviceCapabilities { CanBePlayer = true },
|
||||||
|
Info = new YYnisonDeviceInfo
|
||||||
|
{
|
||||||
|
DeviceId = _storage.DeviceId,
|
||||||
|
AppName = "Yandex Music API",
|
||||||
|
AppVersion = "0.0.1",
|
||||||
|
Type = "WEB",
|
||||||
|
Title = "YandexMusicAPI"
|
||||||
|
},
|
||||||
|
IsShadow = true
|
||||||
|
},
|
||||||
|
PlayerState = new YYnisonPlayerState
|
||||||
|
{
|
||||||
|
PlayerQueue = new YYnisonPlayerQueue { Version = version },
|
||||||
|
Status = new YYnisonPlayerStateStatus { Version = version }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
return SerializeJson(fullState);
|
||||||
|
}
|
||||||
|
|
||||||
private AuthStorage storage;
|
private async Task<YTrack?> GetCurrentAsync()
|
||||||
private YnisonWebSocket redirector;
|
{
|
||||||
private YnisonWebSocket state;
|
if (State == null) return null;
|
||||||
|
int index = State.PlayerState.PlayerQueue.CurrentPlayableIndex;
|
||||||
|
if (index < 0 || index >= State.PlayerState.PlayerQueue.PlayableList.Count)
|
||||||
|
return null;
|
||||||
|
var item = State.PlayerState.PlayerQueue.PlayableList[index];
|
||||||
|
var response = await API.Track.GetAsync(item.PlayableId);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Поля
|
private async Task UpdateStateAsync()
|
||||||
|
{
|
||||||
#region Свойства
|
if (State == null) return;
|
||||||
|
var update = new YYnisonUpdatePlayerStateMessage
|
||||||
/// <summary>
|
|
||||||
/// API
|
|
||||||
/// </summary>
|
|
||||||
public YandexMusicApi API { get; internal set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Состояние
|
|
||||||
/// </summary>
|
|
||||||
public YYnisonState State { get; internal set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Текущий проигрываемый трек
|
|
||||||
/// </summary>
|
|
||||||
public YTrack Current => GetCurrent();
|
|
||||||
|
|
||||||
#endregion Свойства
|
|
||||||
|
|
||||||
#region События
|
|
||||||
|
|
||||||
public class ReceiveEventArgs
|
|
||||||
{
|
{
|
||||||
public YYnisonState State { get; internal set; }
|
UpdatePlayerState = State.PlayerState
|
||||||
}
|
};
|
||||||
|
update.UpdatePlayerState.Status.Version = new YYnisonVersion { DeviceId = _storage.DeviceId };
|
||||||
|
update.UpdatePlayerState.PlayerQueue.Version = new YYnisonVersion { DeviceId = _storage.DeviceId };
|
||||||
|
if (_state != null)
|
||||||
|
await _state.SendAsync(SerializeJson(update));
|
||||||
|
}
|
||||||
|
|
||||||
public delegate void OnReceiveEventHandler(YnisonPlayer player, ReceiveEventArgs args);
|
/// <summary>Подключается к Ynison и начинает получение состояния.</summary>
|
||||||
|
public async Task ConnectAsync()
|
||||||
/// <summary>
|
{
|
||||||
/// Получение данных
|
if (_redirector == null) throw new ObjectDisposedException(nameof(YnisonPlayer));
|
||||||
/// </summary>
|
await _redirector.ConnectAsync(_storage, "wss://ynison.music.yandex.ru/redirector.YnisonRedirectService/GetRedirectToYnison");
|
||||||
public event OnReceiveEventHandler OnReceive;
|
_redirector.OnReceive += async (socket, data) =>
|
||||||
|
|
||||||
|
|
||||||
public class CloseEventArgs
|
|
||||||
{
|
{
|
||||||
public WebSocketCloseStatus? Status { get; set; }
|
var redirectInfo = Deserialize<YYnisonRedirect>(YYnisonMessageType.Redirect, data.Data);
|
||||||
public string Description { get; set; }
|
if (_state == null) return;
|
||||||
}
|
if (_state.IsConnected) return;
|
||||||
|
await _state.ConnectAsync(_storage, $"wss://{redirectInfo.Host}/ynison_state.YnisonStateService/PutYnisonState", redirectInfo.RedirectTicket);
|
||||||
public delegate void OnCloseEventHandler(YnisonPlayer player, CloseEventArgs args);
|
_state.OnReceive += (s, d) =>
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение данных
|
|
||||||
/// </summary>
|
|
||||||
public event OnCloseEventHandler OnClose;
|
|
||||||
|
|
||||||
#endregion События
|
|
||||||
|
|
||||||
#region Вспомогательные функции
|
|
||||||
|
|
||||||
private string SerializeJson(object data)
|
|
||||||
{
|
|
||||||
return JsonConvert.SerializeObject(data, jsonSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
private T Deserialize<T>(YYnisonMessageType messageType, string data)
|
|
||||||
{
|
|
||||||
return storage.Debug != null
|
|
||||||
? storage.Debug.Deserialize<T>($"Ynison{messageType}", data, jsonSettings)
|
|
||||||
: JsonConvert.DeserializeObject<T>(data, jsonSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
private T DeserializeMessage<T>(YYnisonMessageType messageType, string data)
|
|
||||||
{
|
|
||||||
JObject o = JObject.Parse(data);
|
|
||||||
// Сообщение с ошибкой
|
|
||||||
if (o.ContainsKey("error"))
|
|
||||||
{
|
{
|
||||||
YYnisonErrorMessage exception = Deserialize<YYnisonErrorMessage>(YYnisonMessageType.Error, data);
|
var message = DeserializeMessage<YYnisonState>(YYnisonMessageType.State, d.Data);
|
||||||
throw exception ?? new Exception("Ошибка десериализации ответа с ошибкой.");
|
State = message;
|
||||||
}
|
OnReceive?.Invoke(this, new ReceiveEventArgs { State = State });
|
||||||
|
|
||||||
return Deserialize<T>(messageType, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string DefaultState()
|
|
||||||
{
|
|
||||||
YYnisonVersion version = new()
|
|
||||||
{
|
|
||||||
DeviceId = storage.DeviceId,
|
|
||||||
Version = "0"
|
|
||||||
};
|
};
|
||||||
|
_state.OnClose += (s, args) =>
|
||||||
YYnisonUpdateFullStateMessage fullState = new()
|
|
||||||
{
|
{
|
||||||
UpdateFullState = new()
|
OnClose?.Invoke(this, new CloseEventArgs { Status = args.Status, Description = args.Description });
|
||||||
{
|
|
||||||
Device = new()
|
|
||||||
{
|
|
||||||
Capabilities = new()
|
|
||||||
{
|
|
||||||
CanBePlayer = true
|
|
||||||
},
|
|
||||||
Info = new()
|
|
||||||
{
|
|
||||||
DeviceId = storage.DeviceId,
|
|
||||||
AppName = "Yandex Music API",
|
|
||||||
AppVersion = "0.0.1",
|
|
||||||
Type = "WEB",
|
|
||||||
Title = "YandexMusicAPI"
|
|
||||||
},
|
|
||||||
IsShadow = true
|
|
||||||
},
|
|
||||||
PlayerState = new()
|
|
||||||
{
|
|
||||||
PlayerQueue = new()
|
|
||||||
{
|
|
||||||
Version = version
|
|
||||||
},
|
|
||||||
Status = new()
|
|
||||||
{
|
|
||||||
Version = version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
_ = _state.BeginReceiveAsync();
|
||||||
|
await _state.SendAsync(DefaultState());
|
||||||
|
};
|
||||||
|
await _redirector.BeginReceiveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
return SerializeJson(fullState);
|
/// <summary>Отключается от Ynison.</summary>
|
||||||
}
|
public async Task DisconnectAsync()
|
||||||
|
{
|
||||||
|
if (_state != null) await _state.StopReceiveAsync();
|
||||||
|
if (_redirector != null) await _redirector.StopReceiveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private YTrack GetCurrent()
|
/// <summary>Освобождает ресурсы.</summary>
|
||||||
{
|
public void Dispose()
|
||||||
if (State == null)
|
{
|
||||||
return null;
|
_redirector?.Dispose();
|
||||||
|
_state?.Dispose();
|
||||||
int index = State.PlayerState.PlayerQueue.CurrentPlayableIndex;
|
_redirector = null;
|
||||||
if (index < 0 || index > State.PlayerState.PlayerQueue.PlayableList.Count)
|
_state = null;
|
||||||
return null;
|
GC.SuppressFinalize(this);
|
||||||
|
|
||||||
YYnisonPlayableItem item = State.PlayerState.PlayerQueue.PlayableList[index];
|
|
||||||
|
|
||||||
return API.Track.Get(storage, item.PlayableId)
|
|
||||||
.Result
|
|
||||||
.FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateState()
|
|
||||||
{
|
|
||||||
YYnisonUpdatePlayerStateMessage update = new()
|
|
||||||
{
|
|
||||||
UpdatePlayerState = State.PlayerState
|
|
||||||
};
|
|
||||||
|
|
||||||
update.UpdatePlayerState.Status.Version = new()
|
|
||||||
{
|
|
||||||
DeviceId = storage.DeviceId
|
|
||||||
};
|
|
||||||
|
|
||||||
update.UpdatePlayerState.PlayerQueue.Version = new()
|
|
||||||
{
|
|
||||||
DeviceId = storage.DeviceId
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
state.Send(SerializeJson(update));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine(ex);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Вспомогательные функции
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region Подключение
|
|
||||||
|
|
||||||
public void Connect()
|
|
||||||
{
|
|
||||||
redirector.Connect(storage, "wss://ynison.music.yandex.ru/redirector.YnisonRedirectService/GetRedirectToYnison");
|
|
||||||
redirector.OnReceive += (socket, data) =>
|
|
||||||
{
|
|
||||||
YYnisonRedirect redirectInfo = Deserialize<YYnisonRedirect>(YYnisonMessageType.Redirect, data.Data);
|
|
||||||
|
|
||||||
if (state.IsConnected)
|
|
||||||
return;
|
|
||||||
|
|
||||||
state.Connect(storage, $"wss://{redirectInfo.Host}/ynison_state.YnisonStateService/PutYnisonState", redirectInfo.RedirectTicket);
|
|
||||||
state.OnReceive += (s, d) =>
|
|
||||||
{
|
|
||||||
YYnisonState message = DeserializeMessage<YYnisonState>(YYnisonMessageType.State, d.Data);
|
|
||||||
|
|
||||||
State = message;
|
|
||||||
|
|
||||||
OnReceive?.Invoke(this, new ReceiveEventArgs
|
|
||||||
{
|
|
||||||
State = State
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
state.OnClose += (s, args) =>
|
|
||||||
{
|
|
||||||
OnClose?.Invoke(this, new CloseEventArgs
|
|
||||||
{
|
|
||||||
Status = args.Status,
|
|
||||||
Description = args.Description
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
state.BeginReceive();
|
|
||||||
// Отправка изначального состояния
|
|
||||||
state.Send(DefaultState());
|
|
||||||
};
|
|
||||||
|
|
||||||
redirector.BeginReceive();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Disconnect()
|
|
||||||
{
|
|
||||||
state?.StopReceive();
|
|
||||||
redirector?.StopReceive();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Подключение
|
|
||||||
|
|
||||||
#region Плеер
|
|
||||||
|
|
||||||
/*
|
|
||||||
public void Play()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Next()
|
|
||||||
{
|
|
||||||
List<YYnisonPlayableItem> list = State.PlayerState.PlayerQueue.PlayableList;
|
|
||||||
|
|
||||||
if (State.PlayerState.PlayerQueue.EntityType == YYnisonEntityType.Radio)
|
|
||||||
{
|
|
||||||
YYnisonPlayableItem next = State.PlayerState.PlayerQueue.Queue.WaveQueue.RecommendedPlayableList
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
list.RemoveAt(0);
|
|
||||||
list.Add(next);
|
|
||||||
|
|
||||||
UpdateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (State.PlayerState.PlayerQueue.CurrentPlayableIndex < list.Count - 1)
|
|
||||||
{
|
|
||||||
State.PlayerState.PlayerQueue.CurrentPlayableIndex++;
|
|
||||||
UpdateState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Previous()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#endregion Плеер
|
|
||||||
|
|
||||||
internal YnisonPlayer(YandexMusicApi api, AuthStorage authStorage)
|
|
||||||
{
|
|
||||||
API = api;
|
|
||||||
storage = authStorage;
|
|
||||||
|
|
||||||
redirector = new();
|
|
||||||
state = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region IDisposable
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
redirector?.StopReceive();
|
|
||||||
redirector?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion IDisposable
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,179 +1,155 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net;
|
||||||
|
using System.Net.WebSockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace YandexMusic.API.Common.Ynison
|
namespace YandexMusic.API.Common.Ynison;
|
||||||
|
|
||||||
|
/// <summary>WebSocket-клиент для взаимодействия с протоколом Ynison.</summary>
|
||||||
|
public class YnisonWebSocket : IDisposable
|
||||||
{
|
{
|
||||||
public class YnisonWebSocket : IDisposable
|
private ClientWebSocket? _socketClient;
|
||||||
|
private CancellationTokenSource? _cancellationTokenSource;
|
||||||
|
private CancellationToken _cancellationToken;
|
||||||
|
private readonly StringBuilder _data = new();
|
||||||
|
private const int BufferSize = 4096;
|
||||||
|
private readonly IWebProxy? _proxy;
|
||||||
|
|
||||||
|
/// <summary>Флаг, указывает, открыто ли соединение.</summary>
|
||||||
|
public bool IsConnected => _socketClient?.State == WebSocketState.Open;
|
||||||
|
|
||||||
|
/// <summary>Событие получения сообщения.</summary>
|
||||||
|
public event EventHandler<ReceiveEventArgs>? OnReceive;
|
||||||
|
|
||||||
|
/// <summary>Событие закрытия соединения.</summary>
|
||||||
|
public event EventHandler<CloseEventArgs>? OnClose;
|
||||||
|
|
||||||
|
/// <summary>Аргументы события получения данных.</summary>
|
||||||
|
public class ReceiveEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
#region Поля
|
public string Data { get; init; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly JsonSerializerSettings jsonSettings = new()
|
/// <summary>Аргументы события закрытия соединения.</summary>
|
||||||
|
public class CloseEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
public WebSocketCloseStatus? Status { 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)
|
||||||
|
{
|
||||||
|
var deviceInfo = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
Converters = new List<JsonConverter> {
|
{ "app_name", "Chrome" },
|
||||||
new StringEnumConverter {
|
{ "type", 1 }
|
||||||
NamingStrategy = new CamelCaseNamingStrategy()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
NullValueHandling = NullValueHandling.Ignore
|
|
||||||
};
|
};
|
||||||
|
var protocol = new Dictionary<string, string>
|
||||||
private readonly ClientWebSocket socketClient = new();
|
|
||||||
|
|
||||||
private CancellationTokenSource cancellationTokenSource = new();
|
|
||||||
private CancellationToken cancellation;
|
|
||||||
|
|
||||||
private readonly StringBuilder data = new();
|
|
||||||
private readonly int size = 4096;
|
|
||||||
|
|
||||||
#endregion Поля
|
|
||||||
|
|
||||||
#region Свойства
|
|
||||||
|
|
||||||
public bool IsConnected => socketClient.State == WebSocketState.Open;
|
|
||||||
|
|
||||||
#endregion Свойства
|
|
||||||
|
|
||||||
#region События
|
|
||||||
|
|
||||||
public class ReceiveEventArgs
|
|
||||||
{
|
{
|
||||||
public string Data { get; internal set; }
|
{ "Ynison-Device-Id", deviceId },
|
||||||
}
|
{ "Ynison-Device-Info", JsonSerializer.Serialize(deviceInfo) }
|
||||||
|
};
|
||||||
|
if (!string.IsNullOrEmpty(redirectTicket))
|
||||||
|
protocol.Add("Ynison-Redirect-Ticket", redirectTicket);
|
||||||
|
return JsonSerializer.Serialize(protocol);
|
||||||
|
}
|
||||||
|
|
||||||
public delegate void OnReceiveEventHandler(YnisonWebSocket socket, ReceiveEventArgs args);
|
private async Task<string> ReadSocketContentAsync()
|
||||||
/// <summary>
|
{
|
||||||
/// Получение данных
|
if (_socketClient == null)
|
||||||
/// </summary>
|
throw new InvalidOperationException("WebSocket не инициализирован");
|
||||||
public event OnReceiveEventHandler OnReceive;
|
|
||||||
|
|
||||||
public class CloseEventArgs
|
var buffer = new byte[BufferSize];
|
||||||
|
WebSocketReceiveResult result;
|
||||||
|
do
|
||||||
{
|
{
|
||||||
public WebSocketCloseStatus? Status { get; set; }
|
result = await _socketClient.ReceiveAsync(new ArraySegment<byte>(buffer), _cancellationToken);
|
||||||
public string Description { get; set; }
|
_data.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
|
||||||
}
|
} while (!result.EndOfMessage);
|
||||||
|
return _data.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
public delegate void OnCloseEventHandler(YnisonWebSocket socket, CloseEventArgs args);
|
/// <summary>Подключается к WebSocket.</summary>
|
||||||
/// <summary>
|
/// <param name="storage">Хранилище авторизации (для токена и deviceId).</param>
|
||||||
/// Закрытие соединения
|
/// <param name="url">URL WebSocket.</param>
|
||||||
/// </summary>
|
/// <param name="redirectTicket">Тикет перенаправления (опционально).</param>
|
||||||
public event OnCloseEventHandler OnClose;
|
public async Task ConnectAsync(AuthStorage storage, string url, string? redirectTicket = null)
|
||||||
|
{
|
||||||
|
_socketClient = new ClientWebSocket();
|
||||||
|
_socketClient.Options.AddSubProtocol("Bearer");
|
||||||
|
var protocolData = GetProtocolData(storage.DeviceId, redirectTicket);
|
||||||
|
_socketClient.Options.SetRequestHeader("Sec-WebSocket-Protocol", $"Bearer, v2, {protocolData}");
|
||||||
|
_socketClient.Options.SetRequestHeader("Origin", "https://music.yandex.ru");
|
||||||
|
_socketClient.Options.SetRequestHeader("Authorization", $"OAuth {storage.Token}");
|
||||||
|
if (_proxy != null)
|
||||||
|
_socketClient.Options.Proxy = _proxy;
|
||||||
|
|
||||||
#endregion События
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
_cancellationToken = _cancellationTokenSource.Token;
|
||||||
|
|
||||||
#region Вспомогательные функции
|
await _socketClient.ConnectAsync(new Uri(url), CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
private string SerializeJson(object obj)
|
/// <summary>Начинает асинхронный приём сообщений.</summary>
|
||||||
|
public async Task BeginReceiveAsync()
|
||||||
|
{
|
||||||
|
if (_socketClient == null || _socketClient.State != WebSocketState.Open)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
return JsonConvert.SerializeObject(obj, jsonSettings);
|
while (!_cancellationToken.IsCancellationRequested && _socketClient.State == WebSocketState.Open)
|
||||||
}
|
|
||||||
|
|
||||||
private string GetProtocolData(string deviceId, string redirectTicket)
|
|
||||||
{
|
|
||||||
Dictionary<string, object> deviceInfo = new() {
|
|
||||||
{ "app_name", "Chrome" },
|
|
||||||
{ "type", 1 }
|
|
||||||
};
|
|
||||||
|
|
||||||
Dictionary<string, string> protocol = new() {
|
|
||||||
{ "Ynison-Device-Id", deviceId },
|
|
||||||
{ "Ynison-Device-Info", SerializeJson(deviceInfo) }
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(redirectTicket))
|
|
||||||
protocol.Add("Ynison-Redirect-Ticket", redirectTicket);
|
|
||||||
|
|
||||||
return SerializeJson(protocol);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> ReadSocketContent()
|
|
||||||
{
|
|
||||||
byte[] buffer = new byte[size];
|
|
||||||
WebSocketReceiveResult result;
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
{
|
||||||
result = await socketClient.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
var content = await ReadSocketContentAsync();
|
||||||
data.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
|
OnReceive?.Invoke(this, new ReceiveEventArgs { Data = content });
|
||||||
} while (!result.EndOfMessage);
|
_data.Clear();
|
||||||
|
}
|
||||||
return data.ToString();
|
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
#endregion Вспомогательные функции
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public bool Connect(AuthStorage storage, string url, string redirectTicket = null)
|
|
||||||
{
|
{
|
||||||
socketClient.Options.AddSubProtocol("Bearer");
|
// Ожидаемая отмена
|
||||||
|
|
||||||
socketClient.Options.SetRequestHeader("Sec-WebSocket-Protocol", $"Bearer, v2, {GetProtocolData(storage.DeviceId, redirectTicket)}");
|
|
||||||
socketClient.Options.SetRequestHeader("Origin", "https://music.yandex.ru");
|
|
||||||
socketClient.Options.SetRequestHeader("Authorization", $"OAuth {storage.Token}");
|
|
||||||
|
|
||||||
socketClient.Options.Proxy = storage.Context.WebProxy;
|
|
||||||
|
|
||||||
socketClient.ConnectAsync(new Uri(url), CancellationToken.None)
|
|
||||||
.GetAwaiter()
|
|
||||||
.GetResult();
|
|
||||||
|
|
||||||
cancellation = cancellationTokenSource.Token;
|
|
||||||
|
|
||||||
return socketClient.State == WebSocketState.Open;
|
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
public async Task BeginReceive()
|
|
||||||
{
|
{
|
||||||
if (socketClient.State != WebSocketState.Open)
|
var closeStatus = _socketClient.CloseStatus;
|
||||||
return;
|
var closeDesc = _socketClient.CloseStatusDescription;
|
||||||
|
OnClose?.Invoke(this, new CloseEventArgs { Status = closeStatus, Description = closeDesc });
|
||||||
do
|
if (_socketClient.State == WebSocketState.Open)
|
||||||
{
|
await _socketClient.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
|
||||||
string content = await ReadSocketContent();
|
|
||||||
OnReceive?.Invoke(this, new ReceiveEventArgs
|
|
||||||
{
|
|
||||||
Data = content
|
|
||||||
});
|
|
||||||
|
|
||||||
data.Clear();
|
|
||||||
} while (!cancellation.IsCancellationRequested && socketClient.State == WebSocketState.Open);
|
|
||||||
|
|
||||||
OnClose?.Invoke(this, new CloseEventArgs
|
|
||||||
{
|
|
||||||
Status = socketClient.CloseStatus,
|
|
||||||
Description = socketClient.CloseStatusDescription
|
|
||||||
});
|
|
||||||
|
|
||||||
await socketClient.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Отправляет JSON-сообщение.</summary>
|
||||||
|
public async ValueTask SendAsync(string json)
|
||||||
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
throw new InvalidOperationException("WebSocket не инициализирован");
|
||||||
|
|
||||||
public ValueTask Send(string json)
|
var bytes = Encoding.UTF8.GetBytes(json);
|
||||||
{
|
await _socketClient.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, _cancellationToken);
|
||||||
ReadOnlyMemory<byte> message = new(Encoding.UTF8.GetBytes(json));
|
}
|
||||||
return socketClient.SendAsync(message, WebSocketMessageType.Text, false, CancellationToken.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StopReceive()
|
/// <summary>Останавливает приём сообщений.</summary>
|
||||||
{
|
public async Task StopReceiveAsync()
|
||||||
if (socketClient.State != WebSocketState.Open)
|
{
|
||||||
return Task.CompletedTask;
|
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
|
||||||
|
await _cancellationTokenSource.CancelAsync();
|
||||||
|
}
|
||||||
|
|
||||||
cancellationTokenSource.Cancel(false);
|
/// <summary>Освобождает ресурсы.</summary>
|
||||||
|
public void Dispose()
|
||||||
return Task.CompletedTask;
|
{
|
||||||
}
|
_cancellationTokenSource?.Dispose();
|
||||||
|
_socketClient?.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
|
||||||
#region IDisposable
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
socketClient?.Dispose();
|
|
||||||
cancellationTokenSource?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion IDisposable
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
34
YandexMusic.API/Converters/IntToStringConverter.cs
Normal file
34
YandexMusic.API/Converters/IntToStringConverter.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Converters;
|
||||||
|
|
||||||
|
internal class IntToStringConverter : JsonConverter<string>
|
||||||
|
{
|
||||||
|
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Number)
|
||||||
|
{
|
||||||
|
// Пытаемся извлечь число как int или long
|
||||||
|
if (reader.TryGetInt32(out int intValue))
|
||||||
|
return intValue.ToString();
|
||||||
|
if (reader.TryGetInt64(out long longValue))
|
||||||
|
return longValue.ToString();
|
||||||
|
}
|
||||||
|
else if (reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
|
return reader.GetString();
|
||||||
|
}
|
||||||
|
else if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new JsonException($"Не удалось преобразовать {reader.TokenType} в строку.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
YandexMusic.API/Converters/StringToIntConverter.cs
Normal file
42
YandexMusic.API/Converters/StringToIntConverter.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Converters;
|
||||||
|
|
||||||
|
internal class StringToIntConverter : JsonConverter<int>
|
||||||
|
{
|
||||||
|
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
// Если текущий токен — строка
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
|
string? stringValue = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(stringValue))
|
||||||
|
{
|
||||||
|
throw new JsonException("Строка не может быть пустой или null для преобразования в int.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пробуем распарсить с учётом возможных пробелов и инвариантной культуры
|
||||||
|
if (int.TryParse(stringValue.Trim(), out int result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new JsonException($"Невозможно преобразовать строку \"{stringValue}\" в int.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если токен — число (стандартное поведение)
|
||||||
|
if (reader.TokenType == JsonTokenType.Number)
|
||||||
|
{
|
||||||
|
return reader.GetInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new JsonException($"Ожидалась строка или число, получен {reader.TokenType}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
// Записываем число как обычное JSON-число
|
||||||
|
writer.WriteNumberValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using YandexMusic.API.Models.Album;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Extensions.API
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Методы-расширения для альбома
|
|
||||||
/// </summary>
|
|
||||||
public static partial class YAlbumExtensions
|
|
||||||
{
|
|
||||||
public static YAlbum WithTracks(this YAlbum album)
|
|
||||||
{
|
|
||||||
return WithTracksAsync(album).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string AddLike(this YAlbum album)
|
|
||||||
{
|
|
||||||
return AddLikeAsync(album).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string RemoveLike(this YAlbum album)
|
|
||||||
{
|
|
||||||
return RemoveLikeAsync(album).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +1,33 @@
|
|||||||
using YandexMusic.API.Models.Album;
|
using YandexMusic.API.Models.Album;
|
||||||
|
|
||||||
namespace YandexMusic.API.Extensions.API
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Методы-расширения для альбома.
|
||||||
|
/// </summary>
|
||||||
|
public static class YAlbumExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Методы-расширения для альбома
|
/// Получает полную информацию об альбоме вместе с треками (если ещё не загружены).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class YAlbumExtensions
|
public static async Task<YAlbum> WithTracksAsync(this YAlbum album)
|
||||||
{
|
{
|
||||||
public static async Task<YAlbum> WithTracksAsync(this YAlbum album)
|
if (album.Volumes != null)
|
||||||
{
|
return album;
|
||||||
return album.Volumes != null
|
|
||||||
? album
|
|
||||||
: (await album.Context.API.Album.GetAsync(album.Context.Storage, album.Id))
|
|
||||||
.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<string> AddLikeAsync(this YAlbum album)
|
var result = await album.Context.Api.Album.GetAsync(album.Id);
|
||||||
{
|
return result ?? album;
|
||||||
return (await album.Context.API.Library.AddAlbumLikeAsync(album.Context.Storage, album))
|
|
||||||
.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<string> RemoveLikeAsync(this YAlbum album)
|
|
||||||
{
|
|
||||||
return (await album.Context.API.Library.RemoveAlbumLikeAsync(album.Context.Storage, album))
|
|
||||||
.Result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавляет альбом в список лайкнутых.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<string?> AddLikeAsync(this YAlbum album)
|
||||||
|
=> await album.Context.Api.Library.AddAlbumLikeAsync(album);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет альбом из списка лайкнутых.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<string?> RemoveLikeAsync(this YAlbum album)
|
||||||
|
=> await album.Context.Api.Library.RemoveAlbumLikeAsync(album);
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
using YandexMusic.API.Models.Artist;
|
|
||||||
using YandexMusic.API.Models.Track;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Extensions.API
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Методы-расширения для исполнителя
|
|
||||||
/// </summary>
|
|
||||||
public static partial class YArtistExtensions
|
|
||||||
{
|
|
||||||
public static YArtistBriefInfo BriefInfo(this YArtist artist)
|
|
||||||
{
|
|
||||||
return BriefInfoAsync(artist).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static YTracksPage GetTracks(this YArtist artist, int page = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return GetTracksAsync(artist, page, pageSize).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<YTrack> GetAllTracks(this YArtist artist)
|
|
||||||
{
|
|
||||||
return GetAllTracksAsync(artist).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string AddLike(this YArtist artist)
|
|
||||||
{
|
|
||||||
return AddLikeAsync(artist).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string RemoveLike(this YArtist artist)
|
|
||||||
{
|
|
||||||
return RemoveLikeAsync(artist).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +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>
|
||||||
|
public static class YArtistExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Методы-расширения для исполнителя
|
/// Получает расширенную информацию об исполнителе.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class YArtistExtensions
|
public static async Task<YArtistBriefInfo?> BriefInfoAsync(this YArtist artist)
|
||||||
{
|
=> await artist.Context.Api.Artist.GetAsync(artist.Id);
|
||||||
public static async Task<YArtistBriefInfo> BriefInfoAsync(this YArtist artist)
|
|
||||||
{
|
|
||||||
return (await artist.Context.API.Artist.GetAsync(artist.Context.Storage, artist.Id))
|
|
||||||
.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using YandexMusic.API.Models.Playlist;
|
|
||||||
using YandexMusic.API.Models.Track;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Extensions.API
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Методы-расширения для плейлиста
|
|
||||||
/// </summary>
|
|
||||||
public static partial class YPlaylistExtensions
|
|
||||||
{
|
|
||||||
private static bool CheckUser(YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return playlist.Owner.Uid == playlist.Context.Storage.User.Uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static YPlaylist WithTracks(this YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return WithTracksAsync(playlist).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string AddLike(this YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return AddLikeAsync(playlist).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string RemoveLike(this YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return RemoveLikeAsync(playlist).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static YPlaylist Rename(this YPlaylist playlist, string newName)
|
|
||||||
{
|
|
||||||
return RenameAsync(playlist, newName).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool Delete(this YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return DeleteAsync(playlist).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static YPlaylist InsertTracks(this YPlaylist playlist, params YTrack[] tracks)
|
|
||||||
{
|
|
||||||
return InsertTracksAsync(playlist, tracks).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static YPlaylist RemoveTracks(this YPlaylist playlist, params YTrack[] tracks)
|
|
||||||
{
|
|
||||||
return RemoveTracksAsync(playlist, tracks).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool UploadTracks(this YPlaylist playlist, string filePath, string fileName)
|
|
||||||
{
|
|
||||||
return UploadTracksAsync(playlist, filePath, fileName).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +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>
|
||||||
|
public static class YPlaylistExtensions
|
||||||
{
|
{
|
||||||
|
private static bool IsOwner(YPlaylist playlist)
|
||||||
|
=> playlist.Owner.Uid == playlist.Context.Storage.User.Uid;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Методы-расширения для плейлиста
|
/// Получает полную информацию о плейлисте вместе с треками.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class YPlaylistExtensions
|
public static async Task<YPlaylist?> WithTracksAsync(this YPlaylist playlist)
|
||||||
{
|
{
|
||||||
public static async Task<YPlaylist> WithTracksAsync(this YPlaylist playlist)
|
if (playlist.Tracks != null)
|
||||||
{
|
return playlist;
|
||||||
return playlist.Tracks != null
|
return await playlist.Context.Api.Playlist.GetAsync(playlist);
|
||||||
? playlist
|
|
||||||
: (await playlist.Context.API.Playlist.GetAsync(playlist.Context.Storage, playlist))
|
|
||||||
.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<string> AddLikeAsync(this YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return (await playlist.Context.API.Library.AddPlaylistLikeAsync(playlist.Context.Storage, playlist))
|
|
||||||
.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<string> RemoveLikeAsync(this YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return (await playlist.Context.API.Library.RemovePlaylistLikeAsync(playlist.Context.Storage, playlist))
|
|
||||||
.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<bool> DeleteAsync(this YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return CheckUser(playlist) && await playlist.Context.API.Playlist.DeleteAsync(playlist.Context.Storage, playlist);
|
|
||||||
}
|
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавляет плейлист в список лайкнутых.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<string?> AddLikeAsync(this YPlaylist playlist)
|
||||||
|
=> await playlist.Context.Api.Library.AddPlaylistLikeAsync(playlist);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет плейлист из списка лайкнутых.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<string?> RemoveLikeAsync(this YPlaylist playlist)
|
||||||
|
=> await playlist.Context.Api.Library.RemovePlaylistLikeAsync(playlist);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Переименовывает плейлист (только для владельца).
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<YPlaylist?> RenameAsync(this YPlaylist playlist, string newName)
|
||||||
|
=> IsOwner(playlist) ? await playlist.Context.Api.Playlist.RenameAsync(playlist, newName) : playlist;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет плейлист (только для владельца).
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<bool> DeleteAsync(this YPlaylist playlist)
|
||||||
|
=> IsOwner(playlist) && await playlist.Context.Api.Playlist.DeleteAsync(playlist);
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
if (!IsOwner(playlist)) return false;
|
||||||
|
var result = await playlist.Context.Api.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, filePath);
|
||||||
|
return result == "CREATED";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using YandexMusic.API.Models.Radio;
|
|
||||||
using YandexMusic.API.Models.Track;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Extensions.API
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Методы-расширения для радиостанции
|
|
||||||
/// </summary>
|
|
||||||
public static partial class YStationResultExtensions
|
|
||||||
{
|
|
||||||
public static List<YSequenceItem> GetTracks(this YStation station, string prevTrackId = "")
|
|
||||||
{
|
|
||||||
return GetTracksAsync(station, prevTrackId).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string SetSettings2(this YStation station, YStationSettings2 settings)
|
|
||||||
{
|
|
||||||
return SetSettings2Async(station, settings).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string SendFeedBack(this YStation station, YStationFeedbackType type, YTrack track = null, string batchId = "", double totalPlayedSeconds = 0)
|
|
||||||
{
|
|
||||||
return SendFeedBackAsync(station, type, track, batchId, totalPlayedSeconds).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +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>
|
||||||
|
public static class YStationResultExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Методы-расширения для радиостанции
|
/// Получает список треков для радиостанции.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class YStationResultExtensions
|
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<List<YSequenceItem>> GetTracksAsync(this YStation station, string prevTrackId = "")
|
|
||||||
{
|
|
||||||
return (await station.Context.API.Radio.GetStationTracksAsync(station.Context.Storage, station, prevTrackId))
|
|
||||||
.Result.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);
|
||||||
}
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
using YandexMusic.API.Models.Track;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Extensions.API
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Методы-расширения для трека
|
|
||||||
/// </summary>
|
|
||||||
public static partial class YTrackExtensions
|
|
||||||
{
|
|
||||||
public static string GetLink(this YTrack track)
|
|
||||||
{
|
|
||||||
return GetLinkAsync(track).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Save(this YTrack track, string filePath)
|
|
||||||
{
|
|
||||||
SaveAsync(track, filePath).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int AddLike(this YTrack track)
|
|
||||||
{
|
|
||||||
return AddLikeAsync(track).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int RemoveLike(this YTrack track)
|
|
||||||
{
|
|
||||||
return RemoveLikeAsync(track).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int AddDislike(this YTrack track)
|
|
||||||
{
|
|
||||||
return AddDislikeAsync(track).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int RemoveDislike(this YTrack track)
|
|
||||||
{
|
|
||||||
return RemoveDislikeAsync(track).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string SendPlayTrackInfo(this YTrack track, string from, bool fromCache = false, string playId = "", string playlistId = "", double totalPlayedSeconds = 0, double endPositionSeconds = 0)
|
|
||||||
{
|
|
||||||
return SendPlayTrackInfoAsync(track, from, fromCache, playId, playlistId, totalPlayedSeconds).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static YTrackSupplement Supplement(this YTrack track)
|
|
||||||
{
|
|
||||||
return SupplementAsync(track).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static YTrackSimilar Similar(this YTrack track)
|
|
||||||
{
|
|
||||||
return SimilarAsync(track).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +1,63 @@
|
|||||||
using YandexMusic.API.Models.Track;
|
using YandexMusic.API.Models.Track;
|
||||||
|
|
||||||
namespace YandexMusic.API.Extensions.API
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Методы-расширения для трека.
|
||||||
|
/// </summary>
|
||||||
|
public static class YTrackExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Методы-расширения для трека
|
/// Получает прямую ссылку на скачивание трека.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class YTrackExtensions
|
public static Task<string?> GetLinkAsync(this YTrack track)
|
||||||
{
|
=> track.Context.Api.Track.GetFileLinkAsync(track);
|
||||||
public static Task<string> GetLinkAsync(this YTrack track)
|
|
||||||
{
|
|
||||||
return track.Context.API.Track.GetFileLinkAsync(track.Context.Storage, track);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task SaveAsync(this YTrack track, string filePath)
|
/// <summary>
|
||||||
{
|
/// Сохраняет трек в файл.
|
||||||
return track.Context.API.Track.ExtractToFileAsync(track.Context.Storage, track, filePath);
|
/// </summary>
|
||||||
}
|
public static Task SaveAsync(this YTrack track, string filePath)
|
||||||
|
=> track.Context.Api.Track.ExtractToFileAsync(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);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
49
YandexMusic.API/Extensions/EnumHelper.cs
Normal file
49
YandexMusic.API/Extensions/EnumHelper.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Extensions;
|
||||||
|
|
||||||
|
public static class EnumHelper
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<Type, Dictionary<string, object>> _enumMaps = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пытается преобразовать строковое значение в enum с учётом атрибутов [EnumMember].
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Тип enum</typeparam>
|
||||||
|
/// <param name="memberValue">Строковое значение из JSON или другого источника</param>
|
||||||
|
/// <param name="ignoreCase">Учитывать регистр (по умолчанию true)</param>
|
||||||
|
/// <param name="result">Результат преобразования, если успешно, иначе default</param>
|
||||||
|
/// <returns>true если преобразование удалось, иначе false</returns>
|
||||||
|
public static bool TryEnumFromMemberValue<T>(string memberValue, bool ignoreCase, out T result) where T : struct, Enum
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
if (string.IsNullOrEmpty(memberValue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var type = typeof(T);
|
||||||
|
// Получаем или создаём кэш для данного enum
|
||||||
|
var map = _enumMaps.GetOrAdd(type, t =>
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<string, object>(ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
|
||||||
|
foreach (var field in t.GetFields(BindingFlags.Static | BindingFlags.Public))
|
||||||
|
{
|
||||||
|
var attr = field.GetCustomAttribute<EnumMemberAttribute>();
|
||||||
|
var key = attr?.Value ?? field.Name;
|
||||||
|
dict[key] = field.GetValue(null)!;
|
||||||
|
}
|
||||||
|
return dict;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ищем по кэшу
|
||||||
|
if (map.TryGetValue(memberValue, out var value))
|
||||||
|
{
|
||||||
|
result = (T)value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback на обычный Enum.TryParse (без учёта EnumMember)
|
||||||
|
return Enum.TryParse(memberValue, ignoreCase, out result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Extensions
|
|
||||||
{
|
|
||||||
public static class HttpRequestHeaderExtensions
|
|
||||||
{
|
|
||||||
public static string GetName(this HttpRequestHeader header)
|
|
||||||
{
|
|
||||||
return header.ToString().SplitByCapitalLetter("-");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +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[] { };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YAccessToken
|
||||||
{
|
{
|
||||||
public class YAccessToken
|
[JsonPropertyName("status")]
|
||||||
{
|
public string Status { get; set; }
|
||||||
[JsonProperty("status")]
|
|
||||||
public string Status { get; set; }
|
[JsonPropertyName("access_token")]
|
||||||
[JsonProperty("access_token")]
|
public string AccessToken { get; set; }
|
||||||
public string AccessToken { get; set; }
|
|
||||||
[JsonProperty("expires_in")]
|
[JsonPropertyName("expires_in")]
|
||||||
public string Expires { get; set; }
|
public string Expires { get; set; }
|
||||||
[JsonProperty("token_type")]
|
|
||||||
public string TokenType { get; set; }
|
[JsonPropertyName("token_type")]
|
||||||
public string Uid { get; set; }
|
public string TokenType { get; set; }
|
||||||
}
|
|
||||||
|
public string Uid { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,42 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using YandexMusic.API.Converters;
|
||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
|
|
||||||
namespace YandexMusic.API.Models.Account
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YAccount
|
||||||
{
|
{
|
||||||
public class YAccount
|
public bool Child { get; set; }
|
||||||
{
|
|
||||||
public bool Child { get; set; }
|
public string Birthday { get; set; }
|
||||||
public string Birthday { get; set; }
|
|
||||||
public string DisplayName { get; set; }
|
public string DisplayName { get; set; }
|
||||||
public string FirstName { get; set; }
|
|
||||||
public string FullName { get; set; }
|
public string FirstName { get; set; }
|
||||||
public bool HostedUser { get; set; }
|
|
||||||
public string Login { get; set; }
|
public string FullName { get; set; }
|
||||||
public bool NonOwnerFamilyMember { get; set; }
|
|
||||||
public DateTime Now { get; set; }
|
public bool HostedUser { get; set; }
|
||||||
[JsonProperty("passport-phones")]
|
|
||||||
public List<YPhone> PassportPhones { get; set; }
|
public string Login { get; set; }
|
||||||
public int Region { get; set; }
|
|
||||||
public string RegionCode { get; set; }
|
public bool NonOwnerFamilyMember { get; set; }
|
||||||
public DateTime RegisteredAt { get; set; }
|
|
||||||
public string SecondName { get; set; }
|
public DateTime Now { get; set; }
|
||||||
public bool ServiceAvailable { get; set; }
|
|
||||||
public string Uid { get; set; }
|
[JsonPropertyName("passport-phones")]
|
||||||
}
|
public List<YPhone> PassportPhones { get; set; }
|
||||||
|
|
||||||
|
public int Region { get; set; }
|
||||||
|
|
||||||
|
public string RegionCode { get; set; }
|
||||||
|
|
||||||
|
public DateTime RegisteredAt { get; set; }
|
||||||
|
|
||||||
|
public string SecondName { get; set; }
|
||||||
|
|
||||||
|
public bool ServiceAvailable { get; set; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(IntToStringConverter))]
|
||||||
|
public string Uid { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,37 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
|
|
||||||
namespace YandexMusic.API.Models.Account
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YAccountResult
|
||||||
{
|
{
|
||||||
public class YAccountResult
|
public YAccount Account { get; set; }
|
||||||
|
|
||||||
|
public string DefaultEmail { get; set; }
|
||||||
|
|
||||||
|
public List<string> HasOptions { get; set; }
|
||||||
|
|
||||||
|
public YMasterHub MasterHub { get; set; }
|
||||||
|
|
||||||
|
public YPermissions Permissions { get; set; }
|
||||||
|
|
||||||
|
public YPlus Plus { get; set; }
|
||||||
|
|
||||||
|
public bool PretrialActive { get; set; }
|
||||||
|
|
||||||
|
public bool SubEditor { get; set; }
|
||||||
|
|
||||||
|
public int SubEditorLevel { get; set; }
|
||||||
|
|
||||||
|
public YSubscription Subscription { get; set; }
|
||||||
|
|
||||||
|
public YBar BarBelow { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("bar-below")]
|
||||||
|
private YBar BarBelow2
|
||||||
{
|
{
|
||||||
public YAccount Account { get; set; }
|
set => BarBelow = value;
|
||||||
public string DefaultEmail { get; set; }
|
|
||||||
public List<string> HasOptions { get; set; }
|
|
||||||
public YMasterHub MasterHub { get; set; }
|
|
||||||
public YPermissions Permissions { get; set; }
|
|
||||||
public YPlus Plus { get; set; }
|
|
||||||
public bool PretrialActive { get; set; }
|
|
||||||
public bool SubEditor { get; set; }
|
|
||||||
public int SubEditorLevel { get; set; }
|
|
||||||
public YSubscription Subscription { get; set; }
|
|
||||||
public YBar BarBelow { get; set; }
|
|
||||||
// Повторяющееся свойство с другим названием
|
|
||||||
[JsonProperty("bar-below")]
|
|
||||||
private YBar BarBelow2
|
|
||||||
{
|
|
||||||
set => BarBelow = value;
|
|
||||||
}
|
|
||||||
public string Userhash { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string Userhash { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YAuthBase
|
||||||
{
|
{
|
||||||
public class YAuthBase
|
public YAuthStatus Status { get; set; }
|
||||||
{
|
|
||||||
public YAuthStatus Status { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("redirect_url")]
|
[JsonPropertyName("redirect_url")]
|
||||||
public string RedirectUrl { get; set; }
|
public string RedirectUrl { get; set; }
|
||||||
|
|
||||||
public List<YAuthError> Errors { get; set; }
|
public List<YAuthError> Errors { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YAuthCaptcha : YAuthBase
|
||||||
{
|
{
|
||||||
public class YAuthCaptcha : YAuthBase
|
public string Id { get; set; }
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
public string Label { get; set; }
|
public string Label { get; set; }
|
||||||
|
|
||||||
public string Mode { get; set; }
|
public string Mode { get; set; }
|
||||||
|
|
||||||
public List<YAuthCaptchaError> Error { get; set; }
|
public List<YAuthCaptchaError> Error { get; set; }
|
||||||
|
|
||||||
public bool CountryFromAudioWhiteList { get; set; }
|
public bool CountryFromAudioWhiteList { get; set; }
|
||||||
|
|
||||||
public YAuthCaptchaOptions Options { get; set; }
|
public YAuthCaptchaOptions Options { get; set; }
|
||||||
|
|
||||||
public YAuthCaptchaVoice Voice { get; set; }
|
public YAuthCaptchaVoice Voice { get; set; }
|
||||||
|
|
||||||
[JsonProperty("image_url")]
|
[JsonPropertyName("image_url")]
|
||||||
public string ImageUrl { get; set; }
|
public string ImageUrl { get; set; }
|
||||||
|
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
|
|
||||||
public string Static { get; set; }
|
public string Static { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YAuthCaptchaError
|
||||||
{
|
{
|
||||||
public class YAuthCaptchaError
|
public string Message { get; set; }
|
||||||
{
|
public YAuthCaptchaErrorCode Code { get; set; }
|
||||||
public string Message { get; set; }
|
|
||||||
public YAuthCaptchaErrorCode Code { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public enum YAuthCaptchaErrorCode
|
||||||
{
|
{
|
||||||
public enum YAuthCaptchaErrorCode
|
MissingValue,
|
||||||
{
|
CaptchaLocate,
|
||||||
MissingValue,
|
Incorrect
|
||||||
CaptchaLocate,
|
|
||||||
Incorrect
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YAuthCaptchaOptions
|
||||||
{
|
{
|
||||||
public class YAuthCaptchaOptions
|
public bool AsyncCheck { get; set; }
|
||||||
{
|
|
||||||
public bool AsyncCheck { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
using System.Text.Json.Serialization;
|
||||||
{
|
|
||||||
public class YAuthCaptchaVoice
|
|
||||||
{
|
|
||||||
public string Url { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("intro_url")]
|
namespace YandexMusic.API.Models.Account;
|
||||||
public string IntroUrl { get; set; }
|
|
||||||
}
|
public class YAuthCaptchaVoice
|
||||||
|
{
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("intro_url")]
|
||||||
|
public string IntroUrl { get; set; }
|
||||||
}
|
}
|
||||||
5
YandexMusic.API/Models/Account/YAuthEmpty.cs
Normal file
5
YandexMusic.API/Models/Account/YAuthEmpty.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YAuthEmpty
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,22 +1,27 @@
|
|||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
namespace YandexMusic.API.Models.Account
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public enum YAuthError
|
||||||
{
|
{
|
||||||
public enum YAuthError
|
[EnumMember(Value = "authorization.invalid")]
|
||||||
{
|
AuthorizationInvalid,
|
||||||
[EnumMember(Value = "authorization.invalid")]
|
|
||||||
AuthorizationInvalid,
|
[EnumMember(Value = "sessionid.invalid")]
|
||||||
[EnumMember(Value = "sessionid.invalid")]
|
SessionIdInvalid,
|
||||||
SessionIdInvalid,
|
|
||||||
[EnumMember(Value = "password.not_matched")]
|
[EnumMember(Value = "password.not_matched")]
|
||||||
PasswordNotMatched,
|
PasswordNotMatched,
|
||||||
[EnumMember(Value = "password.empty")]
|
|
||||||
PasswordEmpty,
|
[EnumMember(Value = "password.empty")]
|
||||||
[EnumMember(Value = "captcha.required")]
|
PasswordEmpty,
|
||||||
CaptchaRequired,
|
|
||||||
[EnumMember(Value = "captcha.not_matched")]
|
[EnumMember(Value = "captcha.required")]
|
||||||
CaptchaNotMatched,
|
CaptchaRequired,
|
||||||
[EnumMember(Value = "oauth_token.invalid")]
|
|
||||||
OAuthTokenInvalid
|
[EnumMember(Value = "captcha.not_matched")]
|
||||||
}
|
CaptchaNotMatched,
|
||||||
|
|
||||||
|
[EnumMember(Value = "oauth_token.invalid")]
|
||||||
|
OAuthTokenInvalid,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
namespace YandexMusic.API.Models.Account;
|
||||||
{
|
|
||||||
public class YAuthLetter : YAuthBase
|
|
||||||
{
|
|
||||||
public List<string> Code { get; set; }
|
|
||||||
|
|
||||||
public string Id { get; set; }
|
public class YAuthLetter : YAuthBase
|
||||||
}
|
{
|
||||||
|
public List<string> Code { get; set; }
|
||||||
|
|
||||||
|
public string Id { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
using System.Text.Json.Serialization;
|
||||||
{
|
|
||||||
public class YAuthLetterStatus : YAuthBase
|
|
||||||
{
|
|
||||||
[JsonProperty("magic_link_confirmed")]
|
|
||||||
public bool MagicLinkConfirmed { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("track_id")]
|
namespace YandexMusic.API.Models.Account;
|
||||||
public string TrackId { get; set; }
|
|
||||||
}
|
public class YAuthLetterStatus : YAuthBase
|
||||||
|
{
|
||||||
|
[JsonPropertyName("magic_link_confirmed")]
|
||||||
|
public bool MagicLinkConfirmed { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("track_id")]
|
||||||
|
public string TrackId { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,29 @@
|
|||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
namespace YandexMusic.API.Models.Account
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public enum YAuthMethod
|
||||||
{
|
{
|
||||||
public enum YAuthMethod
|
Password,
|
||||||
{
|
|
||||||
Password,
|
[EnumMember(Value = "magic_x_token")]
|
||||||
[EnumMember(Value = "magic_x_token")]
|
MagicToken,
|
||||||
MagicToken,
|
|
||||||
[EnumMember(Value = "magic_x_token_with_pictures")]
|
[EnumMember(Value = "magic_x_token_with_pictures")]
|
||||||
MagicTokenWithPictures,
|
MagicTokenWithPictures,
|
||||||
[EnumMember(Value = "magic_link")]
|
|
||||||
MagicLink,
|
[EnumMember(Value = "magic_link")]
|
||||||
Magic,
|
MagicLink,
|
||||||
Otp,
|
|
||||||
[EnumMember(Value = "social_gg")]
|
Magic,
|
||||||
Social,
|
|
||||||
WebAuthN,
|
Otp,
|
||||||
[EnumMember(Value = "sms_code")]
|
|
||||||
SmsCode
|
[EnumMember(Value = "social_gg")]
|
||||||
}
|
Social,
|
||||||
|
|
||||||
|
WebAuthN,
|
||||||
|
|
||||||
|
[EnumMember(Value = "sms_code")]
|
||||||
|
SmsCode,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
|
||||||
{
|
|
||||||
public class YAuthQR : YAuthBase
|
|
||||||
{
|
|
||||||
[JsonProperty("track_id")]
|
|
||||||
public string TrackId { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("csrf_token")]
|
|
||||||
public string CsrfToken { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9
YandexMusic.API/Models/Account/YAuthQrState.cs
Normal file
9
YandexMusic.API/Models/Account/YAuthQrState.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public enum YAuthQrState
|
||||||
|
{
|
||||||
|
[EnumMember(Value = "otp_auth_finished")]
|
||||||
|
OtpAuthFinished,
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
|
||||||
{
|
|
||||||
public class YAuthQRStatus : YAuthBase
|
|
||||||
{
|
|
||||||
[JsonProperty("default_uid")]
|
|
||||||
public int DefaultUid { get; set; }
|
|
||||||
|
|
||||||
public string RetPath { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("track_id")]
|
|
||||||
public string TrackId { get; set; }
|
|
||||||
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
public string State { get; set; }
|
|
||||||
|
|
||||||
public YAuthCaptcha Captcha { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public enum YAuthStatus
|
||||||
{
|
{
|
||||||
public enum YAuthStatus
|
Ok,
|
||||||
{
|
Error
|
||||||
Ok,
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
namespace YandexMusic.API.Models.Account;
|
||||||
{
|
|
||||||
public class YAuthToken
|
|
||||||
{
|
|
||||||
[JsonProperty("csfr_token")]
|
|
||||||
public string CsfrToken { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("track_id")]
|
public class YAuthToken
|
||||||
public string TrackId { get; set; }
|
{
|
||||||
}
|
public string CsfrToken { 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();
|
||||||
}
|
}
|
||||||
@@ -1,47 +1,48 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YAuthTypes : YAuthBase
|
||||||
{
|
{
|
||||||
public class YAuthTypes : YAuthBase
|
[JsonPropertyName("primary_alias_type")]
|
||||||
{
|
public string PrimaryAliasType { get; set; }
|
||||||
[JsonProperty("primary_alias_type")]
|
|
||||||
public string PrimaryAliasType { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("csrf_token")]
|
[JsonPropertyName("csrf_token")]
|
||||||
public string CsrfToken { get; set; }
|
public string CsrfToken { get; set; }
|
||||||
|
|
||||||
public string LocCsrf { get; set; }
|
public string LocCsrf { get; set; }
|
||||||
|
|
||||||
[JsonProperty("use_new_suggest_by_phone")]
|
[JsonPropertyName("use_new_suggest_by_phone")]
|
||||||
public bool UseNewSuggestByPhone { get; set; }
|
public bool UseNewSuggestByPhone { get; set; }
|
||||||
|
|
||||||
[JsonProperty("is_rfc_2fa_enabled")]
|
[JsonPropertyName("is_rfc_2fa_enabled")]
|
||||||
public bool IsRfc2faEnabled { get; set; }
|
public bool IsRfc2faEnabled { get; set; }
|
||||||
|
|
||||||
[JsonProperty("track_id")]
|
[JsonPropertyName("track_id")]
|
||||||
public string TrackId { get; set; }
|
public string TrackId { get; set; }
|
||||||
|
|
||||||
[JsonProperty("can_authorize")]
|
[JsonPropertyName("can_authorize")]
|
||||||
public string CanAuthorize { get; set; }
|
public string CanAuthorize { get; set; }
|
||||||
|
|
||||||
[JsonProperty("preferred_auth_method")]
|
[JsonPropertyName("preferred_auth_method")]
|
||||||
public YAuthMethod PreferredAuthMethod { get; set; }
|
public YAuthMethod PreferredAuthMethod { get; set; }
|
||||||
|
|
||||||
[JsonProperty("auth_methods")]
|
[JsonPropertyName("auth_methods")]
|
||||||
public List<YAuthMethod> AuthMethods { get; set; }
|
public List<YAuthMethod> AuthMethods { get; set; }
|
||||||
|
|
||||||
[JsonProperty("can_register")]
|
[JsonPropertyName("can_register")]
|
||||||
public bool CanRegister { get; set; }
|
public bool CanRegister { get; set; }
|
||||||
|
|
||||||
[JsonProperty("location_id")]
|
[JsonPropertyName("location_id")]
|
||||||
public string LocationId { get; set; }
|
public string LocationId { get; set; }
|
||||||
|
|
||||||
public string Country { get; set; }
|
public string Country { get; set; }
|
||||||
|
|
||||||
[JsonProperty("phone_number")]
|
[JsonPropertyName("phone_number")]
|
||||||
public YPhoneNumber PhoneNumberNumber { get; set; }
|
public YPhoneNumber PhoneNumberNumber { get; set; }
|
||||||
|
|
||||||
[JsonProperty("magic_link_email")]
|
[JsonPropertyName("magic_link_email")]
|
||||||
public string MagicLinkEmail { get; set; }
|
public string MagicLinkEmail { get; set; }
|
||||||
|
|
||||||
public string TractorTargetLocationHost { get; set; }
|
public string TractorTargetLocationHost { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
|
|
||||||
namespace YandexMusic.API.Models.Account
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YBar : YStyle
|
||||||
{
|
{
|
||||||
public class YBar : YStyle
|
public string AlertId { get; set; }
|
||||||
{
|
public string Text { get; set; }
|
||||||
public string AlertId { get; set; }
|
public string AlertType { get; set; }
|
||||||
public string Text { get; set; }
|
public YButton Button { get; set; }
|
||||||
public string AlertType { get; set; }
|
public bool CloseButton { get; set; }
|
||||||
public YButton Button { get; set; }
|
public YCloseButtonStyles CloseButtonStyles { get; set; }
|
||||||
public bool CloseButton { get; set; }
|
|
||||||
public YCloseButtonStyles CloseButtonStyles { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YLoginInfo
|
||||||
{
|
{
|
||||||
public class YLoginInfo
|
public string Id { get; set; }
|
||||||
{
|
public string Login { get; set; }
|
||||||
public string Id { get; set; }
|
[JsonPropertyName("client_id")]
|
||||||
public string Login { get; set; }
|
public string ClientId { get; set; }
|
||||||
[JsonProperty("client_id")]
|
[JsonPropertyName("display_name")]
|
||||||
public string ClientId { get; set; }
|
public string DisplayName { get; set; }
|
||||||
[JsonProperty("display_name")]
|
[JsonPropertyName("real_name")]
|
||||||
public string DisplayName { get; set; }
|
public string RealName { get; set; }
|
||||||
[JsonProperty("real_name")]
|
[JsonPropertyName("first_name")]
|
||||||
public string RealName { get; set; }
|
public string FirstName { get; set; }
|
||||||
[JsonProperty("first_name")]
|
[JsonPropertyName("last_name")]
|
||||||
public string FirstName { get; set; }
|
public string LastName { get; set; }
|
||||||
[JsonProperty("last_name")]
|
public string Sex { get; set; }
|
||||||
public string LastName { get; set; }
|
[JsonPropertyName("default_email")]
|
||||||
public string Sex { get; set; }
|
public string DefaultEmail { get; set; }
|
||||||
[JsonProperty("default_email")]
|
public List<string> Emails { get; set; }
|
||||||
public string DefaultEmail { get; set; }
|
public string Birthday { get; set; }
|
||||||
public List<string> Emails { get; set; }
|
[JsonPropertyName("default_avatar_id")]
|
||||||
public string Birthday { get; set; }
|
public string DefaultAvatarId { get; set; }
|
||||||
[JsonProperty("default_avatar_id")]
|
|
||||||
public string DefaultAvatarId { get; set; }
|
|
||||||
|
|
||||||
public string AvatarUrl => $"https://avatars.mds.yandex.net/get-yapic/{DefaultAvatarId}/islands-200";
|
public string AvatarUrl => $"https://avatars.mds.yandex.net/get-yapic/{DefaultAvatarId}/islands-200";
|
||||||
[JsonProperty("is_avatar_empty")]
|
[JsonPropertyName("is_avatar_empty")]
|
||||||
public bool IsAvatarEmpty { get; set; }
|
public bool IsAvatarEmpty { get; set; }
|
||||||
public string PsuId { get; set; }
|
public string PsuId { get; set; }
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YPhoneNumber
|
||||||
{
|
{
|
||||||
public class YPhoneNumber
|
[JsonPropertyName("masked_e164")]
|
||||||
{
|
public string MaskedE164 { get; set; }
|
||||||
[JsonProperty("masked_e164")]
|
|
||||||
public string MaskedE164 { get; set; }
|
|
||||||
|
|
||||||
public string E164 { get; set; }
|
public string E164 { get; set; }
|
||||||
|
|
||||||
public string International { get; set; }
|
public string International { get; set; }
|
||||||
|
|
||||||
[JsonProperty("masked_original")]
|
[JsonPropertyName("masked_original")]
|
||||||
public string MaskedOriginal { get; set; }
|
public string MaskedOriginal { get; set; }
|
||||||
|
|
||||||
public string Original { get; set; }
|
public string Original { get; set; }
|
||||||
|
|
||||||
[JsonProperty("masked_international")]
|
[JsonPropertyName("masked_international")]
|
||||||
public string MaskedInternational { get; set; }
|
public string MaskedInternational { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
namespace YandexMusic.API.Models.Account
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Account
|
||||||
{
|
{
|
||||||
public class YShortAccountInfo : YAuthBase
|
public class YShortAccountInfo : YAuthBase
|
||||||
{
|
{
|
||||||
[JsonProperty("public_id")]
|
[JsonPropertyName("public_id")]
|
||||||
public string PublicId { get; set; }
|
public string PublicId { get; set; }
|
||||||
|
|
||||||
public string Uid { get; set; }
|
public string Uid { get; set; }
|
||||||
@@ -13,69 +15,69 @@
|
|||||||
|
|
||||||
public string Birthday { get; set; }
|
public string Birthday { get; set; }
|
||||||
|
|
||||||
[JsonProperty("has_password")]
|
[JsonPropertyName("has_password")]
|
||||||
public bool HasPassword { get; set; }
|
public bool HasPassword { get; set; }
|
||||||
|
|
||||||
public List<string> Partitions { get; set; }
|
public List<string> Partitions { get; set; }
|
||||||
|
|
||||||
[JsonProperty("primary_alias_type")]
|
[JsonPropertyName("primary_alias_type")]
|
||||||
public int PrimaryAliasType { get; set; }
|
public int PrimaryAliasType { get; set; }
|
||||||
|
|
||||||
[JsonProperty("display_name")]
|
[JsonPropertyName("display_name")]
|
||||||
public string DisplayName { get; set; }
|
public string DisplayName { get; set; }
|
||||||
|
|
||||||
[JsonProperty("normalized_display_login")]
|
[JsonPropertyName("normalized_display_login")]
|
||||||
public string NormalizedDisplayLogin { get; set; }
|
public string NormalizedDisplayLogin { get; set; }
|
||||||
|
|
||||||
[JsonProperty("x_token_issued_at")]
|
[JsonPropertyName("x_token_issued_at")]
|
||||||
public int XTokenIssuedAt { get; set; }
|
public int XTokenIssuedAt { get; set; }
|
||||||
|
|
||||||
[JsonProperty("display_login")]
|
[JsonPropertyName("display_login")]
|
||||||
public string DisplayLogin { get; set; }
|
public string DisplayLogin { get; set; }
|
||||||
|
|
||||||
[JsonProperty("public_name")]
|
[JsonPropertyName("public_name")]
|
||||||
public string PublicName { get; set; }
|
public string PublicName { get; set; }
|
||||||
|
|
||||||
[JsonProperty("avatar_url")]
|
[JsonPropertyName("avatar_url")]
|
||||||
public string AvatarUrl { get; set; }
|
public string AvatarUrl { get; set; }
|
||||||
|
|
||||||
[JsonProperty("native_default_email")]
|
[JsonPropertyName("native_default_email")]
|
||||||
public string NativeDefaultEmail { get; set; }
|
public string NativeDefaultEmail { get; set; }
|
||||||
|
|
||||||
[JsonProperty("has_plus")]
|
[JsonPropertyName("has_plus")]
|
||||||
public bool HasPlus { get; set; }
|
public bool HasPlus { get; set; }
|
||||||
|
|
||||||
[JsonProperty("location_id")]
|
[JsonPropertyName("location_id")]
|
||||||
public int LocationId { get; set; }
|
public int LocationId { get; set; }
|
||||||
|
|
||||||
[JsonProperty("gender")]
|
[JsonPropertyName("gender")]
|
||||||
public string Gender { get; set; }
|
public string Gender { get; set; }
|
||||||
|
|
||||||
[JsonProperty("is_avatar_empty")]
|
[JsonPropertyName("is_avatar_empty")]
|
||||||
public bool IsAvatarEmpty { get; set; }
|
public bool IsAvatarEmpty { get; set; }
|
||||||
|
|
||||||
[JsonProperty("machine_readable_login")]
|
[JsonPropertyName("machine_readable_login")]
|
||||||
public string MachineReadableLogin { get; set; }
|
public string MachineReadableLogin { get; set; }
|
||||||
|
|
||||||
[JsonProperty("has_cards")]
|
[JsonPropertyName("has_cards")]
|
||||||
public bool HasCards { get; set; }
|
public bool HasCards { get; set; }
|
||||||
|
|
||||||
[JsonProperty("has_family")]
|
[JsonPropertyName("has_family")]
|
||||||
public bool HasFamily { get; set; }
|
public bool HasFamily { get; set; }
|
||||||
|
|
||||||
[JsonProperty("picture_login_forbidden")]
|
[JsonPropertyName("picture_login_forbidden")]
|
||||||
public bool PictureLoginForbidden { get; set; }
|
public bool PictureLoginForbidden { get; set; }
|
||||||
|
|
||||||
[JsonProperty("can_account_join_master")]
|
[JsonPropertyName("can_account_join_master")]
|
||||||
public bool CanAccountJoinMaster { get; set; }
|
public bool CanAccountJoinMaster { get; set; }
|
||||||
|
|
||||||
[JsonProperty("secure_phone_number")]
|
[JsonPropertyName("secure_phone_number")]
|
||||||
public string SecurePhoneNumber { get; set; }
|
public string SecurePhoneNumber { get; set; }
|
||||||
|
|
||||||
[JsonProperty("x_token_client_id")]
|
[JsonPropertyName("x_token_client_id")]
|
||||||
public string XTokenClientId { get; set; }
|
public string XTokenClientId { get; set; }
|
||||||
|
|
||||||
[JsonProperty("x_token_need_reset")]
|
[JsonPropertyName("x_token_need_reset")]
|
||||||
public bool XTokenNeedReset { get; set; }
|
public bool XTokenNeedReset { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,98 +1,59 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
using YandexMusic.API.Models.Artist;
|
using YandexMusic.API.Models.Artist;
|
||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
using YandexMusic.API.Models.Common.Cover;
|
using YandexMusic.API.Models.Common.Cover;
|
||||||
using YandexMusic.API.Models.Track;
|
using YandexMusic.API.Models.Track;
|
||||||
|
|
||||||
namespace YandexMusic.API.Models.Album
|
namespace YandexMusic.API.Models.Album;
|
||||||
|
|
||||||
|
/// <summary>Модель альбома.</summary>
|
||||||
|
public class YAlbum : YBaseModel
|
||||||
{
|
{
|
||||||
public sealed class YLabelConverter : JsonConverter
|
public YButton ActionButton { get; set; }
|
||||||
{
|
public List<YArtist> Artists { get; set; }
|
||||||
public override bool CanConvert(Type objectType)
|
public bool Available { get; set; }
|
||||||
{
|
public bool AvailableForMobile { get; set; }
|
||||||
throw new NotImplementedException();
|
public List<string> AvailableForOptions { get; set; }
|
||||||
}
|
public bool AvailableForPremiumUsers { get; set; }
|
||||||
|
public bool AvailablePartially { get; set; }
|
||||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
public string BackgroundImageUrl { get; set; }
|
||||||
{
|
public string BackgroundVideoUrl { get; set; }
|
||||||
if (reader.TokenType == JsonToken.Null)
|
public List<string> Bests { get; set; }
|
||||||
return null;
|
public List<string> Buy { get; set; }
|
||||||
|
public bool ChildContent { get; set; }
|
||||||
JArray jArray = JArray.Load(reader);
|
public string ContentWarning { get; set; }
|
||||||
JTokenType tokenType = jArray.FirstOrDefault()?.Type ?? JTokenType.String;
|
public string CoverUri { get; set; }
|
||||||
object label;
|
[JsonConverter(typeof(YCoverConverter))]
|
||||||
|
public YCover Cover { get; set; }
|
||||||
try
|
public YCustomWave CustomWave { get; set; }
|
||||||
{
|
public YDerivedColors DerivedColors { get; set; }
|
||||||
label = tokenType switch
|
public string Description { get; set; }
|
||||||
{
|
public List<string> Disclaimers { get; set; }
|
||||||
JTokenType.Object => jArray.ToObject<List<YLabel>>(),
|
public List<YAlbum> Duplicates { get; set; }
|
||||||
_ => jArray.ToObject<List<string>>()
|
public bool HasTrailer { get; set; }
|
||||||
};
|
public string Genre { get; set; }
|
||||||
}
|
public string Id { get; set; }
|
||||||
catch (Exception ex)
|
[JsonConverter(typeof(YLabelConverter))]
|
||||||
{
|
public dynamic Labels { get; set; }
|
||||||
throw new Exception($"Ошибка десериализации типа \"{objectType.Name}\".", ex);
|
public int LikesCount { get; set; }
|
||||||
}
|
public bool ListeningFinished { get; set; }
|
||||||
|
public string MetaTagId { get; set; }
|
||||||
return label;
|
public YMetaType MetaType { get; set; }
|
||||||
}
|
public string OgImage { get; set; }
|
||||||
|
public YPager Pager { get; set; }
|
||||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
public List<YPrerolls> Prerolls { get; set; }
|
||||||
{
|
public bool Recent { get; set; }
|
||||||
JArray array = JArray.FromObject(value);
|
public DateTime ReleaseDate { get; set; }
|
||||||
|
public string ShortDescription { get; set; }
|
||||||
array.WriteTo(writer);
|
public YSortOrder SortOrder { get; set; }
|
||||||
}
|
public string StorageDir { get; set; }
|
||||||
}
|
public string Title { get; set; }
|
||||||
|
public int TrackCount { get; set; }
|
||||||
public class YAlbum : YBaseModel
|
public YTrackPosition TrackPosition { get; set; }
|
||||||
{
|
public YTrailer Trailer { get; set; }
|
||||||
public YButton ActionButton { get; set; }
|
public string Type { get; set; }
|
||||||
public List<YArtist> Artists { get; set; }
|
public string Version { get; set; }
|
||||||
public bool Available { get; set; }
|
public bool VeryImportant { get; set; }
|
||||||
public bool AvailableForMobile { get; set; }
|
public List<List<YTrack>> Volumes { get; set; }
|
||||||
public List<string> AvailableForOptions { get; set; }
|
public int Year { get; set; }
|
||||||
public bool AvailableForPremiumUsers { get; set; }
|
|
||||||
public bool AvailablePartially { get; set; }
|
|
||||||
public string BackgroundImageUrl { get; set; }
|
|
||||||
public string BackgroundVideoUrl { get; set; }
|
|
||||||
public List<string> Bests { get; set; }
|
|
||||||
public List<string> Buy { get; set; }
|
|
||||||
public bool ChildContent { get; set; }
|
|
||||||
public string ContentWarning { get; set; }
|
|
||||||
public string CoverUri { get; set; }
|
|
||||||
[JsonConverter(typeof(YCoverConverter))]
|
|
||||||
public YCover Cover { get; set; }
|
|
||||||
public YCustomWave CustomWave { get; set; }
|
|
||||||
public YDerivedColors DerivedColors { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public List<string> Disclaimers { get; set; }
|
|
||||||
public List<YAlbum> Duplicates { get; set; }
|
|
||||||
public bool HasTrailer { get; set; }
|
|
||||||
public string Genre { get; set; }
|
|
||||||
public string Id { get; set; }
|
|
||||||
[JsonConverter(typeof(YLabelConverter))]
|
|
||||||
public dynamic Labels { get; set; }
|
|
||||||
public int LikesCount { get; set; }
|
|
||||||
public bool ListeningFinished { get; set; }
|
|
||||||
public string MetaTagId { get; set; }
|
|
||||||
public YMetaType MetaType { get; set; }
|
|
||||||
public string OgImage { get; set; }
|
|
||||||
public YPager Pager { get; set; }
|
|
||||||
public List<YPrerolls> Prerolls { get; set; }
|
|
||||||
public bool Recent { get; set; }
|
|
||||||
public DateTime ReleaseDate { get; set; }
|
|
||||||
public string ShortDescription { get; set; }
|
|
||||||
public YSortOrder SortOrder { get; set; }
|
|
||||||
public string StorageDir { get; set; }
|
|
||||||
public string Title { get; set; }
|
|
||||||
public int TrackCount { get; set; }
|
|
||||||
public YTrackPosition TrackPosition { get; set; }
|
|
||||||
public YTrailer Trailer { get; set; }
|
|
||||||
public string Type { get; set; }
|
|
||||||
public string Version { get; set; }
|
|
||||||
public bool VeryImportant { get; set; }
|
|
||||||
public List<List<YTrack>> Volumes { get; set; }
|
|
||||||
public int Year { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
32
YandexMusic.API/Models/Album/YLabelConverter.cs
Normal file
32
YandexMusic.API/Models/Album/YLabelConverter.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using YandexMusic.API.Models.Common;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Album;
|
||||||
|
|
||||||
|
/// <summary>Конвертер для поля Labels, которое может быть списком строк или объектов.</summary>
|
||||||
|
public sealed class YLabelConverter : JsonConverter<object>
|
||||||
|
{
|
||||||
|
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
|
throw new JsonException("Ожидается массив");
|
||||||
|
|
||||||
|
var list = new List<object>();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.EndArray) break;
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
|
list.Add(reader.GetString()!);
|
||||||
|
else if (reader.TokenType == JsonTokenType.StartObject)
|
||||||
|
{
|
||||||
|
var label = JsonSerializer.Deserialize<YLabel>(ref reader, options);
|
||||||
|
list.Add(label!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
|
||||||
|
=> JsonSerializer.Serialize(writer, value, options);
|
||||||
|
}
|
||||||
@@ -1,41 +1,41 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
using YandexMusic.API.Models.Common.Cover;
|
using YandexMusic.API.Models.Common.Cover;
|
||||||
|
|
||||||
namespace YandexMusic.API.Models.Artist
|
namespace YandexMusic.API.Models.Artist;
|
||||||
|
|
||||||
|
public class YArtist : YBaseModel
|
||||||
{
|
{
|
||||||
public class YArtist : YBaseModel
|
public YButton ActionButton { get; set; }
|
||||||
{
|
public bool Available { get; set; }
|
||||||
public YButton ActionButton { get; set; }
|
public bool Composer { get; set; }
|
||||||
public bool Available { get; set; }
|
public List<string> Countries { get; set; }
|
||||||
public bool Composer { get; set; }
|
public YArtistCounts Counts { get; set; }
|
||||||
public List<string> Countries { get; set; }
|
[JsonConverter(typeof(YCoverConverter))]
|
||||||
public YArtistCounts Counts { get; set; }
|
public YCover Cover { get; set; }
|
||||||
[JsonConverter(typeof(YCoverConverter))]
|
public List<string> DbAliases { get; set; }
|
||||||
public YCover Cover { get; set; }
|
|
||||||
public List<string> DbAliases { get; set; }
|
|
||||||
#warning Непонятная коллекция с содержимым разных типов
|
#warning Непонятная коллекция с содержимым разных типов
|
||||||
public List<object> Decomposed { get; set; }
|
public List<object> Decomposed { get; set; }
|
||||||
public YDerivedColors DerivedColors { get; set; }
|
public YDerivedColors DerivedColors { get; set; }
|
||||||
public YDescription Description { get; set; }
|
public YDescription Description { get; set; }
|
||||||
public YDeprecation Deprecation { get; set; }
|
public YDeprecation Deprecation { get; set; }
|
||||||
public List<string> Disclaimers { get; set; }
|
public List<string> Disclaimers { get; set; }
|
||||||
public string EndDate { get; set; }
|
public string EndDate { get; set; }
|
||||||
public string EnWikipediaLink { get; set; }
|
public string EnWikipediaLink { get; set; }
|
||||||
public List<YExtraAction> ExtraActions { get; set; }
|
public List<YExtraAction> ExtraActions { get; set; }
|
||||||
public List<string> Genres { get; set; }
|
public List<string> Genres { get; set; }
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string InitDate { get; set; }
|
public string InitDate { get; set; }
|
||||||
public int LikesCount { get; set; }
|
public int LikesCount { get; set; }
|
||||||
public List<YLink> Links { get; set; }
|
public List<YLink> Links { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public bool NoPicturesFromSearch { get; set; }
|
public bool NoPicturesFromSearch { get; set; }
|
||||||
public string OgImage { get; set; }
|
public string OgImage { get; set; }
|
||||||
public YArtistRatings Ratings { get; set; }
|
public YArtistRatings Ratings { get; set; }
|
||||||
public bool TicketsAvailable { get; set; }
|
public bool TicketsAvailable { get; set; }
|
||||||
public DateTime Timestamp { get; set; }
|
public DateTime Timestamp { get; set; }
|
||||||
public bool Various { get; set; }
|
public bool Various { get; set; }
|
||||||
public string YaMoneyId { get; set; }
|
public string YaMoneyId { get; set; }
|
||||||
public bool HasTrailer { get; set; }
|
public bool HasTrailer { get; set; }
|
||||||
public YTrailer Trailer { get; set; }
|
public YTrailer Trailer { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,33 +4,31 @@ using YandexMusic.API.Models.Common.Cover;
|
|||||||
using YandexMusic.API.Models.Playlist;
|
using YandexMusic.API.Models.Playlist;
|
||||||
using YandexMusic.API.Models.Track;
|
using YandexMusic.API.Models.Track;
|
||||||
|
|
||||||
namespace YandexMusic.API.Models.Artist
|
namespace YandexMusic.API.Models.Artist;
|
||||||
|
|
||||||
|
public class YArtistBriefInfo
|
||||||
{
|
{
|
||||||
public class YArtistBriefInfo
|
public YButton ActionButton { get; set; }
|
||||||
{
|
public List<YAlbum> Albums { get; set; }
|
||||||
public YButton ActionButton { get; set; }
|
public List<YCover> AllCovers { get; set; }
|
||||||
public List<YAlbum> Albums { get; set; }
|
public List<YAlbum> AlsoAlbums { get; set; }
|
||||||
[JsonProperty(ItemConverterType = typeof(YCoverConverter))]
|
public YArtist Artist { get; set; }
|
||||||
public List<YCover> AllCovers { get; set; }
|
public string BackgroundVideoUrl { get; set; }
|
||||||
public List<YAlbum> AlsoAlbums { get; set; }
|
public YBandlinkScannerLink BandlinkScannerLink { get; set; }
|
||||||
public YArtist Artist { get; set; }
|
public List<YClip> Clips { get; set; }
|
||||||
public string BackgroundVideoUrl { get; set; }
|
public List<YConcert> Concerts { get; set; }
|
||||||
public YBandlinkScannerLink BandlinkScannerLink { get; set; }
|
public YCustomWave CustomWave { get; set; }
|
||||||
public List<YClip> Clips { get; set; }
|
public List<YExtraAction> ExtraActions { get; set; }
|
||||||
public List<YConcert> Concerts { get; set; }
|
public bool HasPromotions { get; set; }
|
||||||
public YCustomWave CustomWave { get; set; }
|
public bool HasTrailer { get; set; }
|
||||||
public List<YExtraAction> ExtraActions { get; set; }
|
public List<string> LastReleaseIds { get; set; }
|
||||||
public bool HasPromotions { get; set; }
|
public List<YAlbum> LastReleases { get; set; }
|
||||||
public bool HasTrailer { get; set; }
|
public List<YPlaylistUidPair> PlaylistIds { get; set; }
|
||||||
public List<string> LastReleaseIds { get; set; }
|
public List<YPlaylist> Playlists { get; set; }
|
||||||
public List<YAlbum> LastReleases { get; set; }
|
public List<YTrack> PopularTracks { get; set; }
|
||||||
public List<YPlaylistUidPair> PlaylistIds { get; set; }
|
public List<YArtist> SimilarArtists { get; set; }
|
||||||
public List<YPlaylist> Playlists { get; set; }
|
public YStats Stats { get; set; }
|
||||||
public List<YTrack> PopularTracks { get; set; }
|
public List<YVideo> Videos { get; set; }
|
||||||
public List<YArtist> SimilarArtists { get; set; }
|
public List<YVinyl> Vinyls { get; set; }
|
||||||
public YStats Stats { get; set; }
|
public List<YLink> Links { get; set; }
|
||||||
public List<YVideo> Videos { get; set; }
|
|
||||||
public List<YVinyl> Vinyls { get; set; }
|
|
||||||
public List<YLink> Links { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
namespace YandexMusic.API.Models.Artist
|
namespace YandexMusic.API.Models.Artist;
|
||||||
|
|
||||||
|
public class YArtistCounts
|
||||||
{
|
{
|
||||||
public class YArtistCounts
|
public int AlsoAlbums { get; set; }
|
||||||
{
|
public int AlsoTracks { get; set; }
|
||||||
public int AlsoAlbums { get; set; }
|
public int DirectAlbums { get; set; }
|
||||||
public int AlsoTracks { get; set; }
|
public int Tracks { get; set; }
|
||||||
public int DirectAlbums { get; set; }
|
|
||||||
public int Tracks { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
namespace YandexMusic.API.Models.Artist
|
namespace YandexMusic.API.Models.Artist;
|
||||||
|
|
||||||
|
public class YArtistRatings
|
||||||
{
|
{
|
||||||
public class YArtistRatings
|
public int Day { get; set; }
|
||||||
{
|
public int Month { get; set; }
|
||||||
public int Day { get; set; }
|
public int Week { get; set; }
|
||||||
public int Month { get; set; }
|
|
||||||
public int Week { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
namespace YandexMusic.API.Models.Artist
|
namespace YandexMusic.API.Models.Artist;
|
||||||
|
|
||||||
|
public class YBandlinkScannerLink
|
||||||
{
|
{
|
||||||
public class YBandlinkScannerLink
|
public string Title { get; set; }
|
||||||
{
|
public string Subtitle { get; set; }
|
||||||
public string Title { get; set; }
|
public string Url { get; set; }
|
||||||
public string Subtitle { get; set; }
|
public string ImgUrl { get; set; }
|
||||||
public string Url { get; set; }
|
|
||||||
public string ImgUrl { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
|
|
||||||
namespace YandexMusic.API.Models.Artist
|
namespace YandexMusic.API.Models.Artist;
|
||||||
|
|
||||||
|
public class YConcert
|
||||||
{
|
{
|
||||||
public class YConcert
|
public string Address { get; set; }
|
||||||
{
|
public string AfishaUrl { get; set; }
|
||||||
public string Address { get; set; }
|
public YArtist Artist { get; set; }
|
||||||
public string AfishaUrl { get; set; }
|
public string City { get; set; }
|
||||||
public YArtist Artist { get; set; }
|
public string ConcertTitle { get; set; }
|
||||||
public string City { get; set; }
|
public string ContentRating { get; set; }
|
||||||
public string ConcertTitle { get; set; }
|
public List<decimal> Coordinates { get; set; }
|
||||||
public string ContentRating { get; set; }
|
public YCashback Cashback { get; set; }
|
||||||
public List<decimal> Coordinates { get; set; }
|
[JsonPropertyName("data-session-id")]
|
||||||
public YCashback Cashback { get; set; }
|
public string DataSessionId { get; set; }
|
||||||
[JsonProperty("data-session-id")]
|
public DateTime DateTime { get; set; }
|
||||||
public string DataSessionId { get; set; }
|
public string Hash { get; set; }
|
||||||
public DateTime DateTime { get; set; }
|
public string Id { get; set; }
|
||||||
public string Hash { get; set; }
|
public List<string> Images { get; set; }
|
||||||
public string Id { get; set; }
|
public string ImageUrl { get; set; }
|
||||||
public List<string> Images { get; set; }
|
public string Map { get; set; }
|
||||||
public string ImageUrl { get; set; }
|
public string MapUrl { get; set; }
|
||||||
public string Map { get; set; }
|
[JsonPropertyName("metro-stations")]
|
||||||
public string MapUrl { get; set; }
|
public List<YMetroStation> MetroStations { get; set; }
|
||||||
[JsonProperty("metro-stations")]
|
public string Place { get; set; }
|
||||||
public List<YMetroStation> MetroStations { get; set; }
|
public YPrice MinPrice { get; set; }
|
||||||
public string Place { get; set; }
|
|
||||||
public YPrice MinPrice { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
namespace YandexMusic.API.Models.Artist
|
namespace YandexMusic.API.Models.Artist;
|
||||||
|
|
||||||
|
public class YDeprecation
|
||||||
{
|
{
|
||||||
public class YDeprecation
|
public string TargetArtistId { get; set; }
|
||||||
{
|
|
||||||
public string TargetArtistId { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
namespace YandexMusic.API.Models.Artist
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Artist;
|
||||||
|
|
||||||
|
public class YMetroStation
|
||||||
{
|
{
|
||||||
public class YMetroStation
|
[JsonPropertyName("line-color")]
|
||||||
{
|
public string LineColor { get; set; }
|
||||||
[JsonProperty("line-color")]
|
public string Title { get; set; }
|
||||||
public string LineColor { get; set; }
|
|
||||||
public string Title { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
using YandexMusic.API.Models.Track;
|
using YandexMusic.API.Models.Track;
|
||||||
|
|
||||||
namespace YandexMusic.API.Models.Artist
|
namespace YandexMusic.API.Models.Artist;
|
||||||
{
|
|
||||||
public class YTracksPage
|
|
||||||
{
|
|
||||||
public YPager Pager { get; set; }
|
|
||||||
|
|
||||||
public List<YTrack> Tracks { get; set; }
|
public class YTracksPage
|
||||||
}
|
{
|
||||||
|
public YPager Pager { get; set; }
|
||||||
|
|
||||||
|
public List<YTrack> Tracks { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,71 +1,9 @@
|
|||||||
namespace YandexMusic.API.Models.Common.Cover
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Common.Cover;
|
||||||
|
|
||||||
|
[JsonConverter(typeof(YCoverConverter))]
|
||||||
|
public class YCover
|
||||||
{
|
{
|
||||||
public sealed class YCoverConverter : JsonConverter
|
public YCoverType Type { get; set; }
|
||||||
{
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return typeof(YCover).IsAssignableFrom(objectType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonToken.Null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
JObject jObject = JObject.Load(reader);
|
|
||||||
YCover cover;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Фиктивный тип, т.к. у такой обложки нет поля с типом
|
|
||||||
if (jObject["type"] == null)
|
|
||||||
jObject.Add("type", "color");
|
|
||||||
|
|
||||||
YCoverType type = jObject["error"] != null
|
|
||||||
? YCoverType.Error
|
|
||||||
: jObject["type"].ToObject<YCoverType>();
|
|
||||||
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case YCoverType.Error:
|
|
||||||
cover = jObject.ToObject<YCoverError>();
|
|
||||||
break;
|
|
||||||
case YCoverType.Color:
|
|
||||||
cover = jObject.ToObject<YCoverColor>();
|
|
||||||
break;
|
|
||||||
case YCoverType.FromAlbumCover:
|
|
||||||
case YCoverType.FromArtistPhotos:
|
|
||||||
cover = jObject.ToObject<YCoverImage>();
|
|
||||||
break;
|
|
||||||
case YCoverType.Pic:
|
|
||||||
cover = jObject.ToObject<YCoverPic>();
|
|
||||||
break;
|
|
||||||
case YCoverType.Mosaic:
|
|
||||||
cover = jObject.ToObject<YCoverMosaic>();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
cover = jObject.ToObject<YCover>();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new Exception($"Ошибка десериализации типа \"{objectType.Name}\".", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
JObject cover = JObject.FromObject(value, serializer);
|
|
||||||
|
|
||||||
cover.WriteTo(writer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class YCover
|
|
||||||
{
|
|
||||||
public YCoverType Type { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
namespace YandexMusic.API.Models.Common.Cover
|
namespace YandexMusic.API.Models.Common.Cover;
|
||||||
|
|
||||||
|
public class YCoverColor : YCover
|
||||||
{
|
{
|
||||||
public class YCoverColor : YCover
|
public string Uri { get; set; }
|
||||||
{
|
public string Color { get; set; }
|
||||||
public string Uri { get; set; }
|
|
||||||
public string Color { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
29
YandexMusic.API/Models/Common/Cover/YCoverConverter.cs
Normal file
29
YandexMusic.API/Models/Common/Cover/YCoverConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
namespace YandexMusic.API.Models.Common.Cover
|
namespace YandexMusic.API.Models.Common.Cover;
|
||||||
|
|
||||||
|
public class YCoverError : YCover
|
||||||
{
|
{
|
||||||
public class YCoverError : YCover
|
public string Error { get; set; }
|
||||||
{
|
|
||||||
public string Error { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user