10 Commits

Author SHA1 Message Date
FrigaT
5541d0ad27 fix Artist api
All checks were successful
Release / pack-and-publish (release) Successful in 40s
2026-04-16 18:43:07 +03:00
FrigaT
ea9f392896 Добавлен конвертер string->int
All checks were successful
Release / pack-and-publish (release) Successful in 9m39s
2026-04-14 21:25:59 +03:00
FrigaT
6dcf39de56 Добавлен readme
All checks were successful
Release / pack-and-publish (release) Successful in 30s
2026-04-14 02:54:03 +03:00
FrigaT
50da85be57 Убраны лишние классы 2026-04-14 02:47:05 +03:00
FrigaT
699d38da74 Убрал синхронность 2026-04-14 02:15:15 +03:00
FrigaT
1779ecaca9 Исправлено удаление треков из плейлиста 2026-04-14 02:08:11 +03:00
FrigaT
8abc6c5074 fix
All checks were successful
Release / pack-and-publish (release) Successful in 32s
2026-04-13 15:42:42 +03:00
FrigaT
b8f78a5856 fix execute context 2026-04-13 15:41:44 +03:00
FrigaT
21a0c5abe6 обновление json parse
All checks were successful
Release / pack-and-publish (release) Successful in 45s
2026-04-13 13:55:23 +03:00
FrigaT
a40b36ef96 Доработана конвертация json
All checks were successful
Release / pack-and-publish (release) Successful in 35s
2026-04-13 13:49:37 +03:00
29 changed files with 489 additions and 293 deletions

1
.gitignore vendored
View File

@@ -361,3 +361,4 @@ MigrationBackup/
# Fody - auto-generated XML schema # Fody - auto-generated XML schema
FodyWeavers.xsd FodyWeavers.xsd
YaMusicCli/

View File

@@ -129,6 +129,7 @@ public class YPlaylistAPI : YCommonAPI
public Task<YResponse<YPlaylist>> DeleteTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YTrack> tracks) public Task<YResponse<YPlaylist>> DeleteTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YTrack> tracks)
{ {
var distinctTracks = tracks.Distinct().ToList(); var distinctTracks = tracks.Distinct().ToList();
var changes = distinctTracks var changes = distinctTracks
.Select(t => playlist.Tracks?.FindIndex(ct => ct.Track?.GetKey() == t.GetKey()) ?? -1) .Select(t => playlist.Tracks?.FindIndex(ct => ct.Track?.GetKey() == t.GetKey()) ?? -1)
.Where(i => i != -1) .Where(i => i != -1)

View File

@@ -1,5 +1,6 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using YandexMusic.API.Converters;
using YandexMusic.API.Models.Common; using YandexMusic.API.Models.Common;
namespace YandexMusic.API.Common.Providers; namespace YandexMusic.API.Common.Providers;
@@ -10,13 +11,6 @@ public abstract class CommonRequestProvider : IRequestProvider
/// <summary>Хранилище данных авторизации.</summary> /// <summary>Хранилище данных авторизации.</summary>
protected readonly AuthStorage storage; protected readonly AuthStorage storage;
/// <summary>Настройки сериализации JSON (регистронезависимые, поддержка enum-строк).</summary>
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
/// <summary>Инициализирует новый экземпляр провайдера.</summary> /// <summary>Инициализирует новый экземпляр провайдера.</summary>
/// <param name="authStorage">Хранилище авторизации.</param> /// <param name="authStorage">Хранилище авторизации.</param>
protected CommonRequestProvider(AuthStorage authStorage) protected CommonRequestProvider(AuthStorage authStorage)
@@ -36,6 +30,17 @@ public abstract class CommonRequestProvider : IRequestProvider
{ {
var json = await response.Content.ReadAsStringAsync(); var json = await response.Content.ReadAsStringAsync();
JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
Converters = {
new JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower),
new IntToStringConverter(),
new StringToIntConverter(),
new YExecutionContextConverter(api, storage),
}
};
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
var error = JsonSerializer.Deserialize<YErrorResponse>(json, JsonOptions); var error = JsonSerializer.Deserialize<YErrorResponse>(json, JsonOptions);

View File

@@ -1,26 +0,0 @@
namespace YandexMusic.API.Common.Providers
{
/// <summary>
/// Провайдер запросов данными из файла
/// </summary>
public class MockRequestProvider : CommonRequestProvider
{
public MockRequestProvider(AuthStorage authStorage) : base(authStorage)
{
}
#region IRequestProvider
public override Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
throw new NotImplementedException();
}
#endregion IRequestProvider
}
}

View File

@@ -0,0 +1,34 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace YandexMusic.API.Converters;
public 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);
}
}

View File

@@ -0,0 +1,42 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace YandexMusic.API.Converters;
public 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);
}
}

View File

@@ -1,24 +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();
}
}

View File

@@ -1,35 +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();
}
}

View File

@@ -1,55 +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();
}
}

View File

@@ -8,6 +8,11 @@ namespace YandexMusic.API.Extensions.API;
/// </summary> /// </summary>
public static partial class YPlaylistExtensions public static partial class YPlaylistExtensions
{ {
private static bool CheckUser(YPlaylist playlist)
{
return playlist.Owner.Uid == playlist.Context.Storage.User.Uid;
}
public static async Task<YPlaylist> WithTracksAsync(this YPlaylist playlist) public static async Task<YPlaylist> WithTracksAsync(this YPlaylist playlist)
{ {
return playlist.Tracks != null return playlist.Tracks != null

View File

@@ -1,25 +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();
}
}

View File

@@ -1,54 +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();
}
}

View File

@@ -1,4 +1,5 @@
using System.Text.Json.Serialization; 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;
@@ -36,5 +37,6 @@ public class YAccount
public bool ServiceAvailable { get; set; } public bool ServiceAvailable { get; set; }
[JsonConverter(typeof(IntToStringConverter))]
public string Uid { get; set; } public string Uid { get; set; }
} }

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
using System.Runtime.Serialization;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Common.Cover; namespace YandexMusic.API.Models.Common.Cover;
@@ -8,9 +7,7 @@ public enum YCoverType
{ {
Color, Color,
Error, Error,
[EnumMember(Value = "from-artist-photos")]
FromArtistPhotos, FromArtistPhotos,
[EnumMember(Value = "from-album-cover")]
FromAlbumCover, FromAlbumCover,
Mosaic, Mosaic,
Pic, Pic,

View File

@@ -5,7 +5,7 @@ using YandexMusic.API.Common;
namespace YandexMusic.API.Models.Common; namespace YandexMusic.API.Models.Common;
/// <summary>Конвертер для внедрения контекста выполнения (API и хранилище) в модели.</summary> /// <summary>Конвертер для внедрения контекста выполнения (API и хранилище) в модели.</summary>
public class YExecutionContextConverter : JsonConverter<YBaseModel> public class YExecutionContextConverter : JsonConverter<object>
{ {
private readonly YandexMusicApi _api; private readonly YandexMusicApi _api;
private readonly AuthStorage _storage; private readonly AuthStorage _storage;
@@ -16,26 +16,27 @@ public class YExecutionContextConverter : JsonConverter<YBaseModel>
_storage = storage; _storage = storage;
} }
public override YBaseModel? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override bool CanConvert(Type typeToConvert) =>
typeof(YBaseModel).IsAssignableFrom(typeToConvert);
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
// Десериализуем объект без контекста // Убираем этот конвертер из опций, чтобы избежать рекурсии
var obj = (YBaseModel?)JsonSerializer.Deserialize(ref reader, typeToConvert, options); var innerOptions = new JsonSerializerOptions(options);
if (obj != null) innerOptions.Converters.Remove(this);
var obj = JsonSerializer.Deserialize(ref reader, typeToConvert, innerOptions);
if (obj is YBaseModel baseModel)
{ {
obj.Context = new YExecutionContext baseModel.Context = new YExecutionContext { API = _api, Storage = _storage };
{
API = _api,
Storage = _storage
};
} }
return obj; return obj;
} }
public override void Write(Utf8JsonWriter writer, YBaseModel value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{ {
// При сериализации игнорируем контекст var innerOptions = new JsonSerializerOptions(options);
var cloneOptions = new JsonSerializerOptions(options); innerOptions.Converters.Remove(this);
cloneOptions.Converters.Remove(this); JsonSerializer.Serialize(writer, value, innerOptions);
JsonSerializer.Serialize(writer, value, cloneOptions);
} }
} }

View File

@@ -1,11 +1,12 @@
using System.Runtime.Serialization; using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Common; namespace YandexMusic.API.Models.Common;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum YTrackSharingFlag public enum YTrackSharingFlag
{ {
[EnumMember(Value = "VIDEO_ALLOWED")] [JsonStringEnumMemberName("VIDEO_ALLOWED")]
VideoAllowed, VideoAllowed,
[EnumMember(Value = "COVER_ONLY")] [JsonStringEnumMemberName("COVER_ONLY")]
CoverOnly CoverOnly
} }

View File

@@ -1,4 +1,5 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using YandexMusic.API.Converters;
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;
@@ -44,6 +45,7 @@ public class YPlaylist : YBaseModel
public string Image { get; set; } public string Image { get; set; }
public bool IsBanner { get; set; } public bool IsBanner { get; set; }
public bool IsPremiere { get; set; } public bool IsPremiere { get; set; }
[JsonConverter(typeof(IntToStringConverter))]
public string Kind { get; set; } public string Kind { get; set; }
public List<YPlaylist> LastOwnerPlaylists { get; set; } public List<YPlaylist> LastOwnerPlaylists { get; set; }
public int LikesCount { get; set; } public int LikesCount { get; set; }

View File

@@ -10,6 +10,16 @@ public class YTrackAlbumPair : IEquatable<YTrackAlbumPair>
return string.Join(":", new[] { Id, AlbumId }.Where(s => !string.IsNullOrEmpty(s))); return string.Join(":", new[] { Id, AlbumId }.Where(s => !string.IsNullOrEmpty(s)));
} }
public static bool operator ==(YTrackAlbumPair left, YTrackAlbumPair right)
{
return Equals(left, right);
}
public static bool operator !=(YTrackAlbumPair left, YTrackAlbumPair right)
{
return !Equals(left, right);
}
public bool Equals(YTrackAlbumPair other) public bool Equals(YTrackAlbumPair other)
{ {
if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(null, other)) return false;

View File

@@ -227,4 +227,4 @@ var storage = new AuthStorage(settings: debug);
**Версия**: 0.0.1 **Версия**: 0.0.1
**Платформа**: .NET 10 **Платформа**: .NET 10
**Язык**: C# 12 **Язык**: C# 12
**Обновлено**: 2024 **Обновлено**: 2026

View File

@@ -5,15 +5,11 @@ namespace YandexMusic.API.Requests.Common;
internal class YRequest<T> internal class YRequest<T>
{ {
#region Поля
private HttpRequestMessage msg; private HttpRequestMessage msg;
private IRequestProvider provider; private IRequestProvider provider;
protected YandexMusicApi api; protected YandexMusicApi api;
#endregion Поля
public YRequest(HttpRequestMessage message, YandexMusicApi yandex, AuthStorage auth) public YRequest(HttpRequestMessage message, YandexMusicApi yandex, AuthStorage auth)
{ {
msg = message; msg = message;

View File

@@ -31,7 +31,7 @@ public abstract class YRequestBuilder<TResponse, TParams>
?? throw new NotImplementedException($"Отсутствует атрибут {nameof(YRequestAttribute)}"); ?? throw new NotImplementedException($"Отсутствует атрибут {nameof(YRequestAttribute)}");
api = yandex; api = yandex;
storage = auth; storage = auth;
device = $"os=CSharp; os_version=; manufacturer=K1llM@n; model=Yandex Music API; clid=; device_id={storage.DeviceId}; uuid=random"; device = $"os=CSharp; os_version=; manufacturer=FrigaT; model=Yandex Music API; clid=; device_id={storage.DeviceId}; uuid=random";
_jsonOptions = new JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
{ {

View File

@@ -35,7 +35,19 @@ public class YandexMusicApi
/// <summary>Создаёт экземпляр API с инициализацией всех подсистем.</summary> /// <summary>Создаёт экземпляр API с инициализацией всех подсистем.</summary>
public YandexMusicApi() public YandexMusicApi()
{ {
foreach (var property in GetType().GetProperties()) Album = new YAlbumAPI(this);
property.SetValue(this, Activator.CreateInstance(property.PropertyType, this)); Artist = new YArtistAPI(this);
Label = new YLabelAPI(this);
Landing = new YLandingAPI(this);
Library = new YLibraryAPI(this);
Playlist = new YPlaylistAPI(this);
Pins = new YPinsAPI(this);
Radio = new YRadioAPI(this);
Search = new YSearchAPI(this);
Track = new YTrackAPI(this);
Queue = new YQueueAPI(this);
User = new YUserAPI(this);
UserGeneratedContent = new YUgcAPI(this);
Ynison = new YYnisonAPI(this);
} }
} }

View File

@@ -0,0 +1,100 @@
param(
[string]$Version
)
Write-Host "=== YandexMusic Manual Publisher ===" -ForegroundColor Cyan
# --- Ask for version if not provided ---
if (-not $Version) {
$Version = Read-Host "Введите версию пакета (например 1.2.3). Пусто = взять из git-тега"
if (-not $Version) {
Write-Host "Читаю версию из git..." -ForegroundColor Yellow
$tag = git describe --tags --abbrev=0 2>$null
if (-not $tag) {
Write-Host "ОШИБКА: версия не указана и git-тег не найден." -ForegroundColor Red
exit 1
}
$Version = $tag.TrimStart("v")
}
}
Write-Host "Используемая версия: $Version" -ForegroundColor Green
# --- Paths ---
$csprojPath = "YandexMusic/YandexMusic.csproj"
$backupPath = "$csprojPath.bak"
$artifacts = "artifacts"
# --- Backup original csproj ---
Write-Host "Создаю резервную копию $csprojPath ..." -ForegroundColor Cyan
Copy-Item $csprojPath $backupPath -Force
# --- Replace ProjectReference with PackageReference ---
Write-Host "Патчу csproj..." -ForegroundColor Cyan
(Get-Content $csprojPath) `
-replace '<ProjectReference Include="..\/YandexMusic.API\/YandexMusic.API.csproj" \/>',
"<PackageReference Include=`"YandexMusic.API`" Version=`"$Version`" />" |
Set-Content $csprojPath
Write-Host "ProjectReference → PackageReference заменён." -ForegroundColor Green
# --- Build & Pack ---
$projects = @(
"YandexMusic.API",
"YandexMusic"
)
if (Test-Path $artifacts) {
Remove-Item $artifacts -Recurse -Force
}
New-Item -ItemType Directory -Path $artifacts | Out-Null
try {
foreach ($proj in $projects) {
Write-Host "Restoring $proj ..." -ForegroundColor Cyan
dotnet restore $proj
Write-Host "Building $proj ..." -ForegroundColor Cyan
dotnet build $proj -c Release -p:Version=$Version
Write-Host "Packing $proj ..." -ForegroundColor Cyan
dotnet pack $proj -c Release --no-build -p:PackageVersion=$Version -o $artifacts
}
Write-Host "Сборка и упаковка завершены." -ForegroundColor Green
# --- Publish ---
$apiKey = $env:NUGET_API_KEY
if (-not $apiKey) {
Write-Host "ОШИБКА: переменная окружения NUGET_API_KEY не установлена." -ForegroundColor Red
exit 1
}
Write-Host "Публикую пакеты..." -ForegroundColor Cyan
dotnet nuget push "$artifacts\*.nupkg" `
--source "https://git.frigat.duckdns.org/api/packages/FrigaT/nuget/index.json" `
--api-key $apiKey `
--skip-duplicate
Write-Host "Публикация завершена." -ForegroundColor Green
}
finally {
# --- Restore original csproj ---
Write-Host "Восстанавливаю оригинальный csproj..." -ForegroundColor Cyan
Move-Item $backupPath $csprojPath -Force
# --- Cleanup artifacts ---
if (Test-Path $artifacts) {
Write-Host "Удаляю artifacts..." -ForegroundColor Cyan
Remove-Item $artifacts -Recurse -Force
}
Write-Host "Откат завершён." -ForegroundColor Green
}
Write-Host "Готово." -ForegroundColor Cyan

View File

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

201
YandexMusic/README.md Normal file
View File

@@ -0,0 +1,201 @@
# YandexMusicClient
Асинхронный клиент для работы с API Яндекс.Музыки на C#.
Предоставляет удобные методы для авторизации, получения треков, альбомов, плейлистов, управления библиотекой, радио, очередями, а также поддержку WebSocketплеера Ynison.
---
## 📦 Возможности
- ✅ Полная поддержка API Яндекс.Музыки (треки, альбомы, исполнители, плейлисты, радио, очереди)
- ✅ Авторизация по токену, QRкоду, email, паролю приложения, капче
- ✅ Управление библиотекой (лайки/дизлайки треков, альбомов, исполнителей, плейлистов)
- ✅ Поиск по всем типам контента
- ✅ Создание и редактирование плейлистов
- ✅ Загрузка пользовательских треков (UGC)
- ✅ Работа с радиостанциями
- ✅ Управление очередями воспроизведения
- ✅ WebSocketплеер Ynison для синхронизации состояния между устройствами
---
## 🚀 Установка
### NuGet (если библиотека опубликована)
```bash
dotnet add package YandexMusic
```
### Или добавьте проект вручную
Склонируйте репозиторий и добавьте ссылку на проект `YandexMusic` в ваше решение.
---
## 🏁 Быстрый старт
### 1. Инициализация клиента и авторизация по токену
```csharp
using YandexMusic;
var client = new YandexMusicClient();
string token = аш_токен_доступа";
bool success = await client.Authorize(token);
if (success)
{
Console.WriteLine($"Авторизован как {client.Account.DisplayName}");
}
else
{
Console.WriteLine("Ошибка авторизации");
}
```
### 2. Получение трека по ID
```csharp
var track = await client.GetTrackAsync("12345678");
if (track != null)
{
Console.WriteLine($"{track.Artists[0].Name} - {track.Title}");
}
```
### 3. Получение плейлиста
```csharp
// плейлист пользователя с uid = 1234567890 и kind = 5
var playlist = await client.GetPlaylistAsync("1234567890", "5");
Console.WriteLine($"Плейлист: {playlist.Title}, треков: {playlist.TrackCount}");
```
### 4. Поиск треков
```csharp
var searchResult = await client.SearchAsync("Imagine Dragons", YSearchType.Track);
if (searchResult?.Tracks?.Results != null)
{
foreach (var track in searchResult.Tracks.Results)
Console.WriteLine($"{track.Artists[0].Name} - {track.Title}");
}
```
---
## 📚 Примеры использования
### 🔹 Работа с библиотекой (лайки)
```csharp
// Получить лайкнутые треки
var likedTracks = await client.GetLikedTracksAsync();
// Получить дизлайкнутых исполнителей
var dislikedArtists = await client.GetDislikedArtistsAsync();
```
### 🔹 Создание плейлиста
```csharp
var newPlaylist = await client.CreatePlaylistAsync("Мой новый плейлист");
Console.WriteLine($"Создан плейлист {newPlaylist.Title} (kind={newPlaylist.Kind})");
```
### 🔹 Загрузка собственного трека (UGC)
```csharp
// Сначала создайте или получите плейлист, куда загружать
var playlist = await client.GetPlaylistAsync("uid", "kind");
// Загрузка из файла
string uploadResult = await client.UploadTrackToPlaylistAsync(playlist, "my_song.mp3", @"C:\music\my_song.mp3");
// Загрузка из массива байтов
byte[] audioData = File.ReadAllBytes(@"C:\music\my_song.mp3");
string result = await client.UploadTrackToPlaylistAsync(playlist, "my_song.mp3", audioData);
```
### 🔹 Радио
```csharp
// Получить список рекомендованных станций
var stations = await client.GetRadioDashboardAsync();
var firstStation = stations.First();
// Получить треки станции
var stationTracks = await firstStation.GetTracksAsync();
foreach (var seqItem in stationTracks)
{
Console.WriteLine(seqItem.Track.Title);
}
```
### 🔹 Очереди воспроизведения
```csharp
// Получить все очереди
var queues = await client.GetQueuesAsync();
// Создать новую очередь
var newQueue = await client.CreateQueueAsync(new YQueue { ... });
```
### 🔹 Ynison WebSocket плеер (синхронизация между устройствами)
```csharp
// Подключиться к Ynison
await client.ConnectYnisonAsync();
// Подписаться на события
if (client.Ynison != null)
{
client.Ynison.OnReceive += (sender, args) =>
{
Console.WriteLine($"Новое состояние: {args.State.PlayerState.Status.Paused}");
};
}
// ... работа
// Отключиться
await client.DisconnectYnisonAsync();
```
---
## ⚠️ Обработка ошибок
Методы клиента выбрасывают исключения в случае ошибок API или сети. Рекомендуется оборачивать вызовы в try-catch:
```csharp
try
{
var track = await client.GetTrackAsync(евалидный_id");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Ошибка сети: {ex.Message}");
}
catch (YErrorResponse ex)
{
Console.WriteLine($"Ошибка API: {ex.Error.Message}");
}
```
---
## 📄 Лицензия
Данная библиотека распространяется под лицензией MIT.
Неофициальный клиент, не связан с компанией Яндекс.
---
## 🤝 Вклад
Pull Request'ы приветствуются. Сообщения об ошибках и предложения по улучшению оформляйте через Issues.
---
**Приятного использования!** 🎵

View File

@@ -1,13 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> <Authors>FrigaT</Authors>
<Description>Асинхронная библиотека для неофициального API Яндекс Музыки.</Description>
<PackageTags>yandex;music;api;async</PackageTags>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\YandexMusic.API\YandexMusic.API.csproj" /> <ProjectReference Include="..\YandexMusic.API\YandexMusic.API.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>