diff --git a/YandexMusic.API/Extensions/API/YAlbumExtensionsAsync.cs b/YandexMusic.API/Extensions/API/YAlbumExtensionsAsync.cs index 9370ffb..9823850 100644 --- a/YandexMusic.API/Extensions/API/YAlbumExtensionsAsync.cs +++ b/YandexMusic.API/Extensions/API/YAlbumExtensionsAsync.cs @@ -15,7 +15,7 @@ public static class YAlbumExtensions if (album.Volumes != null) return album; - var result = await album.Context.API.Album.GetAsync(album.Id); + var result = await album.Context.Api.Album.GetAsync(album.Id); return result ?? album; } @@ -23,11 +23,11 @@ public static class YAlbumExtensions /// Добавляет альбом в список лайкнутых. /// public static async Task AddLikeAsync(this YAlbum album) - => await album.Context.API.Library.AddAlbumLikeAsync(album); + => await album.Context.Api.Library.AddAlbumLikeAsync(album); /// /// Удаляет альбом из списка лайкнутых. /// public static async Task RemoveLikeAsync(this YAlbum album) - => await album.Context.API.Library.RemoveAlbumLikeAsync(album); + => await album.Context.Api.Library.RemoveAlbumLikeAsync(album); } \ No newline at end of file diff --git a/YandexMusic.API/Extensions/API/YArtistExtensionsAsync.cs b/YandexMusic.API/Extensions/API/YArtistExtensionsAsync.cs index 9c74542..af71313 100644 --- a/YandexMusic.API/Extensions/API/YArtistExtensionsAsync.cs +++ b/YandexMusic.API/Extensions/API/YArtistExtensionsAsync.cs @@ -12,29 +12,29 @@ public static class YArtistExtensions /// Получает расширенную информацию об исполнителе. /// public static async Task BriefInfoAsync(this YArtist artist) - => await artist.Context.API.Artist.GetAsync(artist.Id); + => await artist.Context.Api.Artist.GetAsync(artist.Id); /// /// Получает страницу треков исполнителя. /// public static async Task GetTracksAsync(this YArtist artist, int page = 0, int pageSize = 20) - => await artist.Context.API.Artist.GetTracksAsync(artist.Id, page, pageSize); + => await artist.Context.Api.Artist.GetTracksAsync(artist.Id, page, pageSize); /// /// Получает все треки исполнителя. /// public static async Task?> GetAllTracksAsync(this YArtist artist) - => (await artist.Context.API.Artist.GetAllTracksAsync(artist.Id))?.Tracks; + => (await artist.Context.Api.Artist.GetAllTracksAsync(artist.Id))?.Tracks; /// /// Добавляет исполнителя в список лайкнутых. /// public static async Task AddLikeAsync(this YArtist artist) - => await artist.Context.API.Library.AddArtistLikeAsync(artist); + => await artist.Context.Api.Library.AddArtistLikeAsync(artist); /// /// Удаляет исполнителя из списка лайкнутых. /// public static async Task RemoveLikeAsync(this YArtist artist) - => await artist.Context.API.Library.RemoveArtistLikeAsync(artist); + => await artist.Context.Api.Library.RemoveArtistLikeAsync(artist); } \ No newline at end of file diff --git a/YandexMusic.API/Extensions/API/YPlaylistExtensionsAsync.cs b/YandexMusic.API/Extensions/API/YPlaylistExtensionsAsync.cs index 43c5bff..43f158b 100644 --- a/YandexMusic.API/Extensions/API/YPlaylistExtensionsAsync.cs +++ b/YandexMusic.API/Extensions/API/YPlaylistExtensionsAsync.cs @@ -18,44 +18,44 @@ public static class YPlaylistExtensions { if (playlist.Tracks != null) return playlist; - return await playlist.Context.API.Playlist.GetAsync(playlist); + return await playlist.Context.Api.Playlist.GetAsync(playlist); } /// /// Добавляет плейлист в список лайкнутых. /// public static async Task AddLikeAsync(this YPlaylist playlist) - => await playlist.Context.API.Library.AddPlaylistLikeAsync(playlist); + => await playlist.Context.Api.Library.AddPlaylistLikeAsync(playlist); /// /// Удаляет плейлист из списка лайкнутых. /// public static async Task RemoveLikeAsync(this YPlaylist playlist) - => await playlist.Context.API.Library.RemovePlaylistLikeAsync(playlist); + => await playlist.Context.Api.Library.RemovePlaylistLikeAsync(playlist); /// /// Переименовывает плейлист (только для владельца). /// public static async Task RenameAsync(this YPlaylist playlist, string newName) - => IsOwner(playlist) ? await playlist.Context.API.Playlist.RenameAsync(playlist, newName) : playlist; + => IsOwner(playlist) ? await playlist.Context.Api.Playlist.RenameAsync(playlist, newName) : playlist; /// /// Удаляет плейлист (только для владельца). /// public static async Task DeleteAsync(this YPlaylist playlist) - => IsOwner(playlist) && await playlist.Context.API.Playlist.DeleteAsync(playlist); + => IsOwner(playlist) && await playlist.Context.Api.Playlist.DeleteAsync(playlist); /// /// Вставляет треки в начало плейлиста (только для владельца). /// public static async Task InsertTracksAsync(this YPlaylist playlist, params YTrack[] tracks) - => IsOwner(playlist) ? await playlist.Context.API.Playlist.InsertTracksAsync(playlist, tracks) : playlist; + => IsOwner(playlist) ? await playlist.Context.Api.Playlist.InsertTracksAsync(playlist, tracks) : playlist; /// /// Удаляет треки из плейлиста (только для владельца). /// public static async Task RemoveTracksAsync(this YPlaylist playlist, params YTrack[] tracks) - => IsOwner(playlist) ? await playlist.Context.API.Playlist.DeleteTracksAsync(playlist, tracks) : playlist; + => IsOwner(playlist) ? await playlist.Context.Api.Playlist.DeleteTracksAsync(playlist, tracks) : playlist; /// /// Загружает трек в плейлист (только для владельца). @@ -63,7 +63,7 @@ public static class YPlaylistExtensions public static async Task UploadTrackAsync(this YPlaylist playlist, string filePath, string fileName) { if (!IsOwner(playlist)) return false; - var result = await playlist.Context.API.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, filePath); + var result = await playlist.Context.Api.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, filePath); return result == "CREATED"; } } \ No newline at end of file diff --git a/YandexMusic.API/Extensions/API/YStationResultExtensionsAsync.cs b/YandexMusic.API/Extensions/API/YStationResultExtensionsAsync.cs index f6b9747..80fce68 100644 --- a/YandexMusic.API/Extensions/API/YStationResultExtensionsAsync.cs +++ b/YandexMusic.API/Extensions/API/YStationResultExtensionsAsync.cs @@ -12,17 +12,17 @@ public static class YStationResultExtensions /// Получает список треков для радиостанции. /// public static async Task?> GetTracksAsync(this YStation station, string prevTrackId = "") - => (await station.Context.API.Radio.GetStationTracksAsync(station, prevTrackId))?.Sequence; + => (await station.Context.Api.Radio.GetStationTracksAsync(station, prevTrackId))?.Sequence; /// /// Устанавливает настройки станции. /// public static async Task SetSettings2Async(this YStation station, YStationSettings2 settings) - => await station.Context.API.Radio.SetStationSettings2Async(station, settings); + => await station.Context.Api.Radio.SetStationSettings2Async(station, settings); /// /// Отправляет обратную связь о прослушивании. /// public static Task SendFeedbackAsync(this YStation station, YStationFeedbackType type, YTrack? track = null, string batchId = "", double totalPlayedSeconds = 0) - => station.Context.API.Radio.SendStationFeedbackAsync(station, type, track, batchId, totalPlayedSeconds); + => station.Context.Api.Radio.SendStationFeedbackAsync(station, type, track, batchId, totalPlayedSeconds); } \ No newline at end of file diff --git a/YandexMusic.API/Extensions/API/YTrackExtensionsAsync.cs b/YandexMusic.API/Extensions/API/YTrackExtensionsAsync.cs index b98c66d..d06549e 100644 --- a/YandexMusic.API/Extensions/API/YTrackExtensionsAsync.cs +++ b/YandexMusic.API/Extensions/API/YTrackExtensionsAsync.cs @@ -11,53 +11,53 @@ public static class YTrackExtensions /// Получает прямую ссылку на скачивание трека. /// public static Task GetLinkAsync(this YTrack track) - => track.Context.API.Track.GetFileLinkAsync(track); + => track.Context.Api.Track.GetFileLinkAsync(track); /// /// Сохраняет трек в файл. /// public static Task SaveAsync(this YTrack track, string filePath) - => track.Context.API.Track.ExtractToFileAsync(track, filePath); + => track.Context.Api.Track.ExtractToFileAsync(track, filePath); /// /// Добавляет трек в список лайкнутых. /// public static async Task AddLikeAsync(this YTrack track) - => await track.Context.API.Library.AddTrackLikeAsync(track); + => await track.Context.Api.Library.AddTrackLikeAsync(track); /// /// Удаляет трек из списка лайкнутых. /// public static async Task RemoveLikeAsync(this YTrack track) - => await track.Context.API.Library.RemoveTrackLikeAsync(track); + => await track.Context.Api.Library.RemoveTrackLikeAsync(track); /// /// Добавляет трек в список дизлайкнутых. /// public static async Task AddDislikeAsync(this YTrack track) - => await track.Context.API.Library.AddTrackDislikeAsync(track); + => await track.Context.Api.Library.AddTrackDislikeAsync(track); /// /// Удаляет трек из списка дизлайкнутых. /// public static async Task RemoveDislikeAsync(this YTrack track) - => await track.Context.API.Library.RemoveTrackDislikeAsync(track); + => await track.Context.Api.Library.RemoveTrackDislikeAsync(track); /// /// Отправляет информацию о воспроизведении трека. /// public static Task SendPlayTrackInfoAsync(this YTrack track, string from, bool fromCache = false, string playId = "", string playlistId = "", double totalPlayedSeconds = 0, double endPositionSeconds = 0) - => track.Context.API.Track.SendPlayTrackInfoAsync(track, from, fromCache, playId, playlistId, totalPlayedSeconds, endPositionSeconds); + => track.Context.Api.Track.SendPlayTrackInfoAsync(track, from, fromCache, playId, playlistId, totalPlayedSeconds, endPositionSeconds); /// /// Получает дополнительную информацию о треке. /// public static async Task SupplementAsync(this YTrack track) - => await track.Context.API.Track.GetSupplementAsync(track); + => await track.Context.Api.Track.GetSupplementAsync(track); /// /// Получает похожие треки. /// public static async Task SimilarAsync(this YTrack track) - => await track.Context.API.Track.GetSimilarAsync(track); + => await track.Context.Api.Track.GetSimilarAsync(track); } \ No newline at end of file diff --git a/YandexMusic.API/Models/Common/YExecutionContext.cs b/YandexMusic.API/Models/Common/YExecutionContext.cs index 32d32ed..d38fbf3 100644 --- a/YandexMusic.API/Models/Common/YExecutionContext.cs +++ b/YandexMusic.API/Models/Common/YExecutionContext.cs @@ -9,7 +9,7 @@ namespace YandexMusic.API.Models.Common; public class YExecutionContext { /// Экземпляр основного API. - public YandexMusicApi API { get; internal set; } = null!; + public YandexMusicApi Api { get; internal set; } = null!; /// Хранилище данных авторизации. public AuthStorage Storage { get; internal set; } = null!; diff --git a/YandexMusic.API/Models/Common/YExecutionContextConverter.cs b/YandexMusic.API/Models/Common/YExecutionContextConverter.cs index eddf76a..13f868b 100644 --- a/YandexMusic.API/Models/Common/YExecutionContextConverter.cs +++ b/YandexMusic.API/Models/Common/YExecutionContextConverter.cs @@ -38,7 +38,7 @@ public class YExecutionContextConverter : JsonConverter var obj = JsonSerializer.Deserialize(ref reader, typeToConvert, innerOptions); if (obj is YBaseModel baseModel) { - baseModel.Context = new YExecutionContext { API = _api, Storage = _storage }; + baseModel.Context = new YExecutionContext { Api = _api, Storage = _storage }; } return obj; } diff --git a/YandexMusic.API/Models/Common/YStorageDownloadFile.cs b/YandexMusic.API/Models/Common/YStorageDownloadFile.cs index a6981e4..7c3da85 100644 --- a/YandexMusic.API/Models/Common/YStorageDownloadFile.cs +++ b/YandexMusic.API/Models/Common/YStorageDownloadFile.cs @@ -1,9 +1,16 @@ +using System.Xml.Serialization; + namespace YandexMusic.API.Models.Common; +[XmlRoot("download-info")] public class YStorageDownloadFile { + [XmlElement("host")] public string Host { get; set; } + [XmlElement("path")] public string Path { get; set; } + [XmlElement("s")] public string S { get; set; } + [XmlElement("ts")] public string Ts { get; set; } } \ No newline at end of file diff --git a/YandexMusic.API/Requests/Common/YJsonRequestBuilder.cs b/YandexMusic.API/Requests/Common/YJsonRequestBuilder.cs index f8db7e0..acbb775 100644 --- a/YandexMusic.API/Requests/Common/YJsonRequestBuilder.cs +++ b/YandexMusic.API/Requests/Common/YJsonRequestBuilder.cs @@ -10,7 +10,17 @@ namespace YandexMusic.API.Requests.Common; /// internal abstract class YJsonRequestBuilder : YRequestBuilder { - protected YJsonRequestBuilder(YandexMusicApi api) : base(api) { } + private readonly JsonSerializerOptions _jsonOptions; + + protected YJsonRequestBuilder(YandexMusicApi api) : base(api) + { + _jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } + }; + } protected virtual async Task DeserializeAsync(HttpResponseMessage response) { @@ -43,6 +53,8 @@ internal abstract class YJsonRequestBuilder : YRequestBuilde } } + protected string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions); + /// /// Выполняет запрос и возвращает десериализованный объект типа TResponse. /// @@ -51,4 +63,4 @@ internal abstract class YJsonRequestBuilder : YRequestBuilde using var response = await ExecuteRawAsync(parameters); return await DeserializeAsync(response); } -} \ No newline at end of file +} diff --git a/YandexMusic.API/Requests/Common/YRequestBuilder.cs b/YandexMusic.API/Requests/Common/YRequestBuilder.cs index 701019f..b6f404c 100644 --- a/YandexMusic.API/Requests/Common/YRequestBuilder.cs +++ b/YandexMusic.API/Requests/Common/YRequestBuilder.cs @@ -2,8 +2,6 @@ using System.Net; using System.Net.Http.Headers; using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Web; using YandexMusic.API.Common; @@ -23,7 +21,8 @@ internal abstract class YRequestBuilder /// Шаблон пути (может содержать плейсхолдеры вида {id}). protected abstract string PathTemplate { get; } - private readonly JsonSerializerOptions _jsonOptions; + /// Определяет, нужно ли добавлять заголовок Authorization для этого запроса. + protected virtual bool ShouldAddAuthorization => true; /// Основной экземпляр API. protected YandexMusicApi Api { get; } @@ -34,12 +33,6 @@ internal abstract class YRequestBuilder protected YRequestBuilder(YandexMusicApi api) { Api = api; - _jsonOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } - }; } private string FullUrl => $"{BaseUrl.TrimEnd('/')}/{PathTemplate.TrimStart('/')}"; @@ -66,7 +59,7 @@ internal abstract class YRequestBuilder }; msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.AcceptCharset), Encoding.UTF8.WebName); msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.AcceptEncoding), "gzip"); - if (!string.IsNullOrEmpty(Storage.Token)) + if (ShouldAddAuthorization && !string.IsNullOrEmpty(Storage.Token)) msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.Authorization), $"OAuth {Storage.Token}"); SetCustomHeaders(msg.Headers); return msg; @@ -120,8 +113,6 @@ internal abstract class YRequestBuilder protected virtual HttpContent? GetContent(TParams parameters) => null; protected virtual void SetCustomHeaders(HttpRequestHeaders headers) { } - protected string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions); - /// Выполняет запрос и возвращает десериализованный ответ. public async Task ExecuteRawAsync(TParams parameters) { diff --git a/YandexMusic.API/Requests/Common/YXmlRequestBuilder.cs b/YandexMusic.API/Requests/Common/YXmlRequestBuilder.cs new file mode 100644 index 0000000..60c48a8 --- /dev/null +++ b/YandexMusic.API/Requests/Common/YXmlRequestBuilder.cs @@ -0,0 +1,47 @@ +using System.Xml; +using System.Xml.Serialization; + +namespace YandexMusic.API.Requests.Common; + +/// +/// Строитель запросов с десериализацией XML-ответа в TResponse. +/// +internal abstract class YXmlRequestBuilder : YRequestBuilder +{ + protected YXmlRequestBuilder(YandexMusicApi api) : base(api) { } + + /// + /// Десериализует XML-ответ в объект типа TResponse. + /// + protected virtual async Task DeserializeAsync(HttpResponseMessage response) + { + var xml = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + // Для XML-ошибок можно создать отдельную модель, но для простоты выбрасываем исключение + throw new Exception($"Ошибка HTTP {response.StatusCode}: {xml}"); + } + + try + { + using var stringReader = new StringReader(xml); + using var xmlReader = XmlReader.Create(stringReader, new XmlReaderSettings { Async = true }); + var serializer = new XmlSerializer(typeof(TResponse)); + return (TResponse?)serializer.Deserialize(xmlReader); + } + catch (Exception ex) + { + throw new Exception($"Ошибка десериализации XML: {ex.Message}\nXML: {xml}", ex); + } + } + + /// + /// Выполняет запрос и возвращает десериализованный объект типа TResponse. + /// + public async Task ExecuteAsync(TParams parameters) + { + using var response = await ExecuteRawAsync(parameters); + return await DeserializeAsync(response); + } +} \ No newline at end of file diff --git a/YandexMusic.API/Requests/Track/YStorageDownloadFileBuilder.cs b/YandexMusic.API/Requests/Track/YStorageDownloadFileBuilder.cs index 589c4f2..dac398f 100644 --- a/YandexMusic.API/Requests/Track/YStorageDownloadFileBuilder.cs +++ b/YandexMusic.API/Requests/Track/YStorageDownloadFileBuilder.cs @@ -6,9 +6,11 @@ using YandexMusic.API.Requests.Common; namespace YandexMusic.API.Requests.Track; /// Особый запрос – не к api.music.yandex.net, а к произвольному URL. -internal class YStorageDownloadFileBuilder : YJsonRequestBuilder +internal class YStorageDownloadFileBuilder : YXmlRequestBuilder { public YStorageDownloadFileBuilder(YandexMusicApi api) : base(api) { } + protected override bool ShouldAddAuthorization => false; + protected override string BaseUrl => "{src}"; // не используется, т.к. URL берётся из параметра protected override string Method => WebRequestMethods.Http.Get; @@ -16,10 +18,11 @@ internal class YStorageDownloadFileBuilder : YJsonRequestBuilder ""; protected override Dictionary GetSubstitutions(string src) - => new() { { "src", src.Split('?')[0] } }; + => new() { { "src", src } }; + protected override NameValueCollection GetQueryParams(string src) { - var query = new NameValueCollection { { "format", "json" } }; + var query = new NameValueCollection(); var parts = src.Split('?'); if (parts.Length > 1) {