diff --git a/.github/workflows/pytest_full.yml b/.github/workflows/pytest_full.yml index 2b052407..6d6c5237 100644 --- a/.github/workflows/pytest_full.yml +++ b/.github/workflows/pytest_full.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] - python-version: [3.7, 3.8, 3.9, "3.10"] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] steps: - name: Checkout repository. diff --git a/.readthedocs.yaml b/.readthedocs.yaml index e4531681..4094d45d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,9 +3,16 @@ # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 + +build: + os: ubuntu-20.04 + tools: + python: "3.11" + sphinx: configuration: docs/source/conf.py + python: - version: "3.7" install: - requirements: docs/requirements.txt + - requirements: requirements-dev.txt diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 00000000..7d924ea0 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,390 @@ +# Список изменений + +## Версия 2.1.0 + +**23.04.2023** + +**Переломные изменения** + +При работе над [#547](https://github.com/MarshalX/yandex-music-api/issues/547) и +[#550](https://github.com/MarshalX/yandex-music-api/issues/550) +были удалены `*args` параметры, у методов класса `Client`, которые не имели никакого эффекта. +Передать через позиционные аргументы что-то в конечный запрос не было возможно. +Удаление данной конструкции **могло** затронуть код в котором ошибочно передавались лишние аргументы. +При корректном использовании библиотеки новая версия полностью совместима со старым кодом. + +**Крупные изменения** + +- Добавлена поддержка Python 3.11. +- В модели добавлены методы `download_bytes` и `download_bytes_async`, для получения файлов в виде байтов ([#539](https://github.com/MarshalX/yandex-music-api/issues/539)). +- Добавлен новый метод получения текста и синхронного текста треков ([#568](https://github.com/MarshalX/yandex-music-api/pull/568)). +- Добавлена возможность задать `timeout` по умолчанию для `Client` ([#362](https://github.com/MarshalX/yandex-music-api/issues/362)). +- Использование настройки языка клиента во всех методах ([#554](https://github.com/MarshalX/yandex-music-api/issues/554)). +- Добавлено поле `preview_description` классу `GeneratedPlaylist`. +- Добавлены поля `pretrial_active` и `userhash` классу `Status`. +- Добавлено поле `had_any_subscription` классу `Subscription`. +- Добавлено поле `child` классу `Account`. +- Добавлены новые поля `up_title`, `rup_description`, `custom_name` классу `StationResult`. +- Добавлены новые модели: `CustomWave`, `R128`, `LyricsInfo`. +- Классу `Track` добавлены новые поля: `track_source`, `available_for_options`, `r128`, `lyrics_info`, `track_sharing_flag`. +- Классу `TrackShort` добавлены новые поля: `original_index`. +- Классу `Playlist` добавлены новые поля: `custom_wave`, `pager`. +- Классу `Album` добавлены новые поля: `available_for_options`. +- Поле `cover_white` класса `MixLink` теперь опциональное. + +**Незначительные изменения и/или исправления** + +- Добавлен генератор Camel Case псевдонимов для методов ([#542](https://github.com/MarshalX/yandex-music-api/issues/542)). +- Добавлен Makefile с сокращениями удобными при разработке библиотеки. +- Добавлено отображение модуля при нахождении неизвестного поля. +- Добавлена поддержка MD файлов для документации. +- Добавлена страница в документацию по получению токена. +- Добавлены примеры в документацию. +- Переделана структура и обновлена документации. +- Исправлен запуск генератора async клиента на Windows. +- Исправлен метод `fetch_tracks_async` у класса `Playlist`. +- Исправлены type hints у декоратора `log`. +- Исправлены type hints для `SearchResult` в классе `Search`. +- Исправлено отображение название класса в `report_unknown_fields_callback`. +- Исправлены методы-сокращения `like` и `dislike` класса `Playlist` ([#516](https://github.com/MarshalX/yandex-music-api/pull/516)). + +## Версия 2.0.0 + +**23.02.2022** + +**Поддержка asyncio и модели на dataclasses** + +**Переломные изменения** + +- Убрана поддержка `Python 3.6`. +- Удалено получение авторизационного токена по логину и паролю (метод `from_credentials` класса `Client`). +- Удалена возможность задать свой обработчик на полученные неизвестные поля от API (аргумент `report_new_fields_callback` конструктора класса `Client`. +- Удалён аргумент `fetch_account_status` из конструктора класса `Client`. Теперь необходимо вызывать метод `init` для получения ID аккаунта который будет использоваться в последующих запросах. В противном случае передача `user_id` при вызове многих методов класса `Client` становится обязательной. +- Исключение `BadRequest` переименовано в `BadRequestError`. +- Исключение `Unauthorized` переименовано в `UnauthorizedError`. +- Исключение `InvalidBitrate` переименовано в `InvalidBitrateError`. +- Исключение `TimedOut` переименовано в `TimedOutError`. +- Свойство `result` класса `Response` удалено. Вместо него добавлен метод `get_result`. +- Свойство `error` класса `Response` удалено. Вместо него добавлен метоl `get_error`. +- В JSON представлении моделей к полям, чьё имя совпадает с именем стандартных функций, больше не добавляется нижнее подчеркивание в конец (пример: `id`, а не `id_`; `max`, а не `max_`). Теперь нижнее подчеркивание добавляется только к зарезервированным словам (пример: `from` будет `from_`). + +**Крупные изменения** + +- Добавлена асинхронная версия клиента и всех методов-сокращений (класс `ClientAsync`). +- Добавлено новое исключение `NotFoundError` (наследник `NetworkError`). Будет сгенерировано при получении статус кода 404. +- Проект больше не использует `pipenv`. +- Зависимости проекта больше не требуют конкретных версий. +- Для генерации исходных файлов `Sphinx` теперь используется `sphinx-apidoc`. + +**Незначительные изменения и/или исправления** + +- Исправлена обработка серверных ошибок которые вернулись в отличном от JSON формате. +- Исправлена обработка серверных ошибок метода `search` класса `Client`. +- Предупреждения о пришедших неизвестных полях от API отключены по умолчанию. +- Используется английская локализация `Sphinx`. +- Изменена тема документации. + +## Версия 1.0.0 + +**06.02.2021** + +**Стабильная версия библиотеки** + +**Переломные изменения** + +- Поле `error` класса `Artist` теперь называется `reason`. +- Метод `users_playlists` класса `Client` теперь возвращает один объект плейлиста, когда был передан один `kind`. При передаче списка в `kind` вернётся список плейлистов ([#318](https://github.com/MarshalX/yandex-music-api/issues/318)). +- Поле `labels` класса `Album` теперь может содержать список из строк, а не только список объектов класса `Label`. + +**Крупные изменения** + +- Добавлены примеры в папку `examples`. +- **Добавлена поддержка рекомендаций для плейлистов ([#324](https://github.com/MarshalX/yandex-music-api/issues/324))**: + - Добавлен класс `PlaylistRecommendations`. + - Добавлен метод клиента для получения рекомендаций(`users_playlists_recommendations`). + - Добавлен метод `get_recommendations` классу `Playlist` для +- **Добавлено получение чартов ([#294](https://github.com/MarshalX/yandex-music-api/issues/294))**: + - Добавлены новые классы: `ChartInfo`, `ChartInfoMenu`,`ChartInfoMenuItem`. + - Добавлен метод клиента для получения чарта (`chart`). +- **Добавлена поддержка тегов/подборок ([#192](https://github.com/MarshalX/yandex-music-api/issues/192))**: + - Добавлены новые классы: `TagResult`, `Tag`. + - Добавлен новый метод клиента для получения тегов (`tags`). +- **Добавлено присоединение к коллективному плейлисту ([#317](https://github.com/MarshalX/yandex-music-api/issues/317))**: + - Добавлен новый метод клиента для присоединения(`playlists_collective_join`). +- **Добавлена поддержка очередей прослушивания ([#246](https://github.com/MarshalX/yandex-music-api/issues/246))**: + - Добавлены новые классы: `Context`, `Queue`, `QueueItem`. + - Добавлены новые методы в `Client`: `queues_list`, `queue`,`queue_update_position`, `queue_create`. + - Добавлены поля `track_id` и `from_` в класс `TrackId`. + - Добавлена возможность смены языка у клиента для ответов от API. + - Добавлена десериализация любого объекта в `JSON` пригодного для отправки в запросе на Яндекс API. +- **Добавлены следующие методы для `Client`**: + - `new_releases` – получение полного списка всех новых релизов. + - `new_playlists` – получение полного списка всех новый плейлистов. + - `podcasts` – получение подкаста с лендинга. +- **Добавлены новые сокращения в модели**: + - `download_cover_white`, `download_cover_uri` в `MixLink`. + - `download_image` в `Promotion`. + - `artists_name` в `Album` и `Track`. + - `fetch_track`, `track_full_id` в `TrackId`. + - `fetch_tracks` в `TracksList`. + - `insert_track`, `delete_tracks`, `delete` в `Playlist`. + - `playlist_id`, `fetch_playlist` в `PlaylistId`. + - `get_current_track` в `Queue`. + - `fetch_queue` в `QueueItem`. + - `next_page`, `get_page`, `prev_page` в `Search`. + - и другие... +- Добавлена поддержка новых типов поиска: подкасты, выпуски, пользователи. +- Добавлен callback для обработки новых полей. +- Добавлена информацию по поводу запуска потока по треку, плейлисту и др. +- Добавлена десериализация `decomposed` у `Artist` ([#10](https://github.com/MarshalX/yandex-music-api/issues/10)). +- Добавлен `__len__` для `TracksList` ([#380](https://github.com/MarshalX/yandex-music-api/issues/380)). +- Добавлены `__iter__`, `__len__` и `__getitem__` для классов представляющих список каких-либо объектов. +- Добавлено сокращение `fetch_tracks` классу `Playlist` для получения треков плейлиста. +- Добавлен метод `get_url` классу `Icon` для получения прямой ссылки на изображение. +- Класс `User` расширен для поддержки поля `user_info` из `Track`(поля `full_name`, `display_name`). +- **Добавлены новые классы по отчётам с Telegram бота ([#306](https://github.com/MarshalX/yandex-music-api/issues/306), [#398](https://github.com/MarshalX/yandex-music-api/issues/398))**: + - `LandingList`. + - `RenewableRemainder`. + - `Alert`. + - `AlertButton`. + - `StationData`. + - `Brand`. + - `Contest`. + - `OpenGraphData`. + - `NonAutoRenewable`. + - `Operator`. + - `Deactivation`. + - `PoetryLoverMatch`. + - `Deprecation`. +- **Добавлены новые поля классам по отчётам с Telegram бота ([#306](https://github.com/MarshalX/yandex-music-api/issues/306), [#398](https://github.com/MarshalX/yandex-music-api/issues/398))**: + - `plus` в `Product`. + - `non_auto_renewable_remainder` в `Subscription`. + - `og_image` в `Artist`. + - `meta_type` в `Album`. + - `advertisement` в `Status`. + - `best` в `Track`. + - `offer_id` и `artist_ids` в `Vinyl`. + - `playlists` в `BriefInfo`. + - `is_custom` в `Cover`. + - `play_count`, `recent`, `chart`, `track` в `TrackShort`. + - `url_part`, `og_title`, `image`, `cover_without_text`, `background_color`, `text_color`, `id_for_from`,`similar_playlists`, `last_owner_playlists` в `Playlist`. + - `bg_color` в `Chart`. + - `error` в `Artist`. + - `substituted`, `matched_track`, `can_publish`, `state`, `desired_visibility`, `filename`, `user_info`, `meta_data` в`Track`. + - `copyright_name`, `copyright_cline` в `Cover`. + - `direct` в `DownloadInfo`. + - `cheapest`, `title`, `family_sub`, `fb_image`, `fb_name`,`family`, `intro_period_duration`, `intro_price`, `start_period_duration`, `start_price`, `licence_text_parts` в `Product`. + - `storage_dir`, `duplicates` в `Album`. + - `subscribed` в `ArtistEvent`. + - `description` в `GeneratedPlaylist`. + - `genre` в `Event`. + - `show_in_regions` в `Genre`. + - `cover_uri` в `MixLink`. + - `og_description`, `top_artist` в `Playlist`. + - `full_image_url`, `mts_full_image_url` в `Station`. + - `coauthors` и `recent_tracks` в `Playlist`. + - `regions` в `User`. + - `users`, `podcasts`, `podcast_episodes`, `type_`, `page`, `per_page` в `Search`. + - `short_description`, `description`, `is_premiere`, `is_banner` в `Like`. + - `master_info` в `AutoRenewable`. + - `station_data` и `bar_below` в `Status`. + - `family_auto_renewable` в `Subscription`. + - `misspell_result` и `misspell_original` в `Search`. + - `experiment` в класс `Status`. + - `operator` и `non_auto_renewable` в `Subscription`. + - `text_color`, `short_description`, `description`, `is_premiere` и `is_banner` в `Album`. + - `hand_made_description` в `Artist`. + - `metrika_id` в `Playlist`. + - `og_image` в `Tag`. + - `url` в `Lyrics`. + - `number`, `genre` в `MetaData`. + - `poetry_lover_matches` в `Track`. + - `contest`, `dummy_description`, `dummy_page_description`, `dummy_cover`, `dummy_rollover_cover`, `og_data`, `branding` в `Playlist`. + - `available_as_rbt`, `lyrics_available`, `remember_position`, `albums`, `duration_ms`, `explicit`, `start_date`, `likes_count`, `deprecation` в `Album`. + - `lyricist`, `version`, `composer` в `MetaData`. + - `last_releases` в `BriefInfo`. + - `ya_money_id` в `Artist` ([#351](https://github.com/MarshalX/yandex-music-api/issues/351), [#370](https://github.com/MarshalX/yandex-music-api/issues/370)). + - `playlist_uuid` в `Playlist`. + - `sync_queue_enabled` в `UserSettings`. + - `background_video_uri`, `short_description`, `is_suitable_for_children` в `Track` ([#376](https://github.com/MarshalX/yandex-music-api/issues/376)). + - `meta_type`, `likes_count` в `Album` ([#386](https://github.com/MarshalX/yandex-music-api/issues/386)). + - `deprecation` в `Album`. + - `available_regions` в `Album`. + - `type`, `ready` в `Playlist`. + - `description` в `Supplement`. + +**Незначительные изменения и/или исправления** + +- **Добавлена опциональность следующим полям**: + - все поля в `MetaData`. + - `advertisement` в `Status`. + - `text_language` в `Lyrics`. + - `provider_video_id` в `VideoSupplement`. + - `title` в `VideoSupplement` ([#403](https://github.com/MarshalX/yandex-music-api/issues/403)). + - `instructions` в `Deactivation` ([#402](https://github.com/MarshalX/yandex-music-api/issues/402)). + - `id` в `Album` ([#401](https://github.com/MarshalX/yandex-music-api/issues/401)). + +- Исправлена десериализация подкастов, эпизодов подкастов и пользователей в лучшем результате поиска. +- Исправлена десериализация альбомов. В зависимости от запроса содержимое лейблов может быть списком объектом или списком строк (в поиске). +- Исправлен выбор настроек радио. +- Исправлены ошибки в документации. +- Протестирована работа на Python 3.9. + +## Версия 0.1.1 + +**25.03.2020** + +**Закончено документирование всех классов и основных методов!** + +**Переломные изменения** + +- **Классы отметок "мне нравится" для альбомов, плейлистов и исполнителей обобщены. Теперь представлены одним классом**. + - **Удаленные классы**: + - `ArtistsLikes`. + - `AlbumsLikes`. + - `PlaylistsLikes`. + - Новый класс: `Like` (поле `type` для определения содержимого). +- Изменено название пакета с `status` на `account` ([#195](https://github.com/MarshalX/yandex-music-api/issues/195)). +- **Исправлено выбрасываемое исключение при таймауте**: + - Прошлое исключение: `TimeoutError` (built-in). + - Новое исключение: `TimedOut` (`yandex_music.exceptions`). +- Удалены следующие файлы: `requirements.txt`, `requirements-dev.txt`, + `requirements-docs.txt`. + +**Крупные изменения** + +- **Добавлено обнаружение новых полей с просьбой сообщить о них ([#216](https://github.com/MarshalX/yandex-music-api/issues/216))**. + - Добавлена проверка на неизвестные поля. + - Добавлен вывод отладочной информации в виде warning'a. + - Добавлен шаблон issue для отправки логов. +- Добавлено поле `type` для класса `SearchResult` для определения типа результата поиска по объекту. +- **Добавлены настройки пользователя ([#195](https://github.com/MarshalX/yandex-music-api/issues/195))**: + - Добавлен класс `UserSettings`. + - Добавлен метод для получения своих настроек (`account_settings`). + - Добавлен метод для получения настроек другого пользователя (`users_settings`). + - Добавлен метод для изменения настроек (`account_settings_set`). +- **Добавлен возможность получить похожие треки ([#197](https://github.com/MarshalX/yandex-music-api/issues/197))**: + - Добавлен класс `TracksSimilar` с полями трека и списка похожих треков. + - Добавлен метод для получения похожих треков (`tracks_similar`). +- **Добавлены шоты от Алисы ([#185](https://github.com/MarshalX/yandex-music-api/issues/185))**: + - Добавлен метод `after_track` в класс `Client` для получения контента для воспроизведения после трека (реклама, шот). + - Добавлены методы для загрузки обложки и аудиоверсии шота. + - **Добавлены новые классы**: + - `Shot` + - `ShotData` + - `ShotEvent` + - `ShotType` +- Добавлен метод для изменения видимости плейлиста ([#179](https://github.com/MarshalX/yandex-music-api/issues/179)). +- **Добавлена поддержка Яндекс.Радио ([#20](https://github.com/MarshalX/yandex-music-api/issues/20))**: + - Исправлена отправка фидбека. + - Написана инструкция по использованию (в доке к методу). + - Добавлен аргумент для перехода по цепочке треков. + - Добавлен метод для изменения настроек станции. + +**Незначительные изменения и/или исправления** + +- Убрано дублирование информации в документации ([#247](https://github.com/MarshalX/yandex-music-api/issues/247)). +- Добавлены новые поля в класс `Track`: `version`, `remember_position` ([#238](https://github.com/MarshalX/yandex-music-api/issues/238)). +- Добавлено исключение `InvalidBitrate` при попытке загрузить недопустимый трек по критериям (кодек, битрейт). +- Исправлено получение прямой ссылки на файл с кодеком AAC ([#237](https://github.com/MarshalX/yandex-music-api/issues/237), [#25](https://github.com/MarshalX/yandex-music-api/issues/25)). +- Исправлено получение плейлиста с Алисой в лендинге ([#185](https://github.com/MarshalX/yandex-music-api/issues/185)). +- Исправлено название поля с ссылкой на источник в классе `Description` (с `url` на `uri`). +- Исправлена десериализация несуществующего исполнителя. +- Добавлено поле `version` в класс `Album` ([#178](https://github.com/MarshalX/yandex-music-api/issues/178)). +- Поле `picture` класса `Vinyl` теперь опциональное. +- Поле `week` класса `Ratings` теперь опциональное. +- Поле `product_id` класса `AutoRenewable` теперь опциональное ([#182](https://github.com/MarshalX/yandex-music-api/issues/182)). +- Правки замечаний по codacy. + +## Версия 0.0.16 + +**29.12.2019** + +**Переломные изменения** + +- Поле `account` переименовано в `me` и теперь содержит объект `Status`, вместо `Account` ([#162](https://github.com/MarshalX/yandex-music-api/issues/162)). +- Убрано использование зарезервированных имён в аргументах конструкторов (теперь они с `_` на конце). Имена с нижними подчёркиваниями есть как при сериализации так и при десериализации ([#168](https://github.com/MarshalX/yandex-music-api/issues/168)). + +**Крупные изменения** + +- **Добавлены аннотации типов во всей библиотеке!** + +**Незначительные изменения и/или исправления** + +- Добавлен аргумент `fetch_account_status` для опциональности получения информации об аккаунте при инициализации клиента ([#162](https://github.com/MarshalX/yandex-music-api/issues/162)). +- Добавлены тесты c передачей пустого словаря в `de_json` и `de_list` ([#174](https://github.com/MarshalX/yandex-music-api/issues/174)). +- Использование `ujson` при наличии, обновлены зависимости ([#161](https://github.com/MarshalX/yandex-music-api/issues/161)). +- Добавлен в зависимости для разработки `importlib_metadata` для поддержки старых версий (в новой версии `pytest` его больше не используют, в угоду `importlib.metadata` [#pytest-5537](https://github.com/pytest-dev/pytest/issues/5537))) ([#161](https://github.com/MarshalX/yandex-music-api/issues/161)). +- Добавлен в зависимости для разработки `atomicwrites`, который используется `pytest` теперь только на `Windows` - [#pytest-6148](https://github.com/pytest-dev/pytest/pull/6148) ([#161](https://github.com/MarshalX/yandex-music-api/issues/161)). +- Исправлен баг с передачей `timeout` аргумента в аргумент `params` в следующих методах: `artists`, `albums`, `playlists_list` ([#120](https://github.com/MarshalX/yandex-music-api/issues/120)). +- Исправлена инициализация клиента при помощи логина и пароля с использованием прокси ([#159](https://github.com/MarshalX/yandex-music-api/issues/159)). +- Исправлен баг в загрузке обложки альбома. + +## Версия 0.0.15 + +**01.12.2019** + +**Переломные изменения** + +- У классов `Artist`, `Track` и `Playlist` изменился перечень полей для генерации хеша. + +**Крупные изменения** + +- **Добавлена возможность выполнять запросы через прокси-сервер для использовании библиотеки на зарубежных серверах ([#139](https://github.com/MarshalX/yandex-music-api/issues/139))**. + - Добавлен пример использования в `README`. +- **Добавлена обработка капчи при авторизации с возможностью использования callback-функции для её обработки ([#140](https://github.com/MarshalX/yandex-music-api/issues/140))**: + - **Новые исключения**: + - **Captcha**: + - CaptchaRequired. + - CaptchaWrong. + - **Новые классы**: + - CaptchaResponse. + - **Новые примеры в `README`**: + - Пример обработки с использованием callback-функции. + - Пример полностью своей обработки капчи. +- Добавлена документация для класса `Search` ([#83](https://github.com/MarshalX/yandex-music-api/issues/83)). +- **Добавлена возможность получения всех альбомов исполнителя ([#141](https://github.com/MarshalX/yandex-music-api/issues/141))**: + - **Новые классы**: + - ArtistAlbums. + - **Новые методы**: + - `artists_direct_albums` у `Client`. + - `get_albums` у `Artist`. +- **Добавлена обработка несуществующего плейлиста ([#147](https://github.com/MarshalX/yandex-music-api/issues/147))**: + - **Новые классы**: + - `PlaylistAbsence`. + +**Незначительные изменения и/или исправления** + +- Исправлен баг с загрузкой файлов ([#149](https://github.com/MarshalX/yandex-music-api/issues/149)). +- Исправлен баг некорректной десериализации плейлиста при отсутствии прав на него ([#147](https://github.com/MarshalX/yandex-music-api/issues/147)). +- Исправлен баг неправильной десериализации треков и артистов у собственных загруженных файлов ([#154](https://github.com/MarshalX/yandex-music-api/issues/154)). + +## Версия 0.0.14 + +**10.11.2019** + +**Переломные изменения** + +- Практически у всех классов был обновлён список полей участвующих при сравнении объектов. +- Если в атрибутах для сравнения объектов присутствуют списки, то они будут преобразованы к frozenset. +- Убрано конвертирование даты из строки в объект. Теперь все даты представлены строками в ISO формате. +- Классы `AlbumSearchResult`, `ArtistSearchResult`, `PlaylistSearchResult`, `TrackSearchResult`, `VideoSearchResult` были объединены в один – `SearchResult`. + +**Крупные изменения** + +- Добавлен метод получения треков исполнителя ([#123](https://github.com/MarshalX/yandex-music-api/pull/123)). +- Добавлены классы-обёртки над пагинацией (`Pager`) и списка треков артиста (`ArtistsTracks`). +- Добавлено **554** unit-теста для всех классов-обёрток над объектами API. +- Добавлен codecov и workflows для GitHub Actions. + +**Незначительные изменения и/или исправления** + +- Поле `cover_uri` класса `Album` теперь опциональное. +- Поле `region` у класса `Account` теперь не обязательное. +- Исправлен баг в `.to_dict()` методе, связанный с десериализацией объектов списков и словарей. +- Исправлен баг в `.to_dict()` методе, связанный с не рекурсивной десериализацией. +- Исправлена десериализация `similar_artists` в `BriefInfo`. +- Исправлен баг с десериализацией `artist` в классе `ArtistEvent`. +- Исправлен баг десериализации списка альбомов и артистов у класса `Track` ([#122](https://github.com/MarshalX/yandex-music-api/pull/122)). +- Исправлена загрузка обложки у трека. +- Исправлены сравнения объектов. diff --git a/CHANGES.rst b/CHANGES.rst deleted file mode 100644 index c6f07019..00000000 --- a/CHANGES.rst +++ /dev/null @@ -1,400 +0,0 @@ -================ -Список изменений -================ - -Версия 2.0.0 -============ - -**23.02.2022** - -**Поддержка asyncio и модели на dataclasses** - -**Переломные изменения** - -- Убрана поддержка ``Python 3.6``. -- Удалено получение авторизационного токена по логину и паролю (метод ``from_credentials`` класса ``Client``). -- Удалена возможность задать свой обработчик на полученные неизвестные поля от API (аргумент ``report_new_fields_callback`` конструктора класса ``Client``. -- Удалён аргумент ``fetch_account_status`` из конструктора класса ``Client``. Теперь необходимо вызывать метод ``init`` для получения ID аккаунта который будет использоваться в последующих запросах. В противном случае, передача ``user_id`` при вызове многих методов класса ``Client`` становится обязательной. -- Исключение ``BadRequest`` переименовано в ``BadRequestError``. -- Исключение ``Unauthorized`` переименовано в ``UnauthorizedError``. -- Исключение ``InvalidBitrate`` переименовано в ``InvalidBitrateError``. -- Исключение ``TimedOut`` переименовано в ``TimedOutError``. -- Свойство ``result`` класса ``Response`` удалено. Вместо него добавлен метод ``get_result``. -- Свойство ``error`` класса ``Response`` удалено. Вместо него добавлен метод ``get_error``. -- В JSON представлении моделей к полям, чьё имя совпадает с именем стандартных функций, больше не добавляется нижнее подчеркивание в конец (пример: ``id``, а не ``id_``; ``max``, а не ``max_``). Теперь нижнее подчеркивание добавляется только к зарезервированным словам (пример: ``from`` будет ``from_``). - -**Крупные изменения** - -- Добавлена асинхронная версия клиента и всех методов-сокращений (класс ``ClientAsync``). -- Добавлено новое исключение ``NotFoundError`` (наследник ``NetworkError``). Будет сгенерировано при получении статус кода 404. -- Проект больше не использует ``pipenv``. -- Зависимости проекта больше не требуют конкретных версий. -- Для генерации исходных файлов ``Sphinx`` теперь используется ``sphinx-apidoc``. - -**Незначительные изменения и/или исправления** - -- Исправлена обработка серверных ошибок которые вернулись в отличном от JSON формате. -- Исправлена обработка серверных ошибок метода ``search`` класса ``Client``. -- Предупреждения о пришедших неизвестных полях от API отключены по умолчанию. -- Используется английская локализация ``Sphinx``. -- Изменена тема документации. - -Версия 1.0.0 -============ - -**06.02.2021** - -**Стабильная версия библиотеки** - -**Переломные изменения** - -- Поле ``error`` класса ``Artist`` теперь называется ``reason``. -- Метод ``users_playlists`` класса ``Client`` теперь возвращает один объект плейлиста, когда был передан один ``kind``. При передаче списка в ``kind`` вернётся список плейлистов (`#318`_). -- Поле ``labels`` класса ``Album`` теперь может содержать список из строк, а не только список объектов класса ``Label``. - -**Крупные изменения** - -- Добавлены примеры в папку ``examples``. -- Добавлена поддержка рекомендаций для плейлистов (`#324`_): - - Добавлен класс ``PlaylistRecommendations``. - - Добавлен метод клиента для получения рекомендаций (``users_playlists_recommendations``). - - Добавлен метод ``get_recommendations`` классу ``Playlist`` для получения рекомендаций. -- Добавлено получение чартов (`#294`_): - - Добавлены новые классы: ``ChartInfo``, ``ChartInfoMenu``, ``ChartInfoMenuItem``. - - Добавлен метод клиента для получения чарта (``chart``). -- Добавлена поддержка тегов/подборок (`#192`_): - - Добавлены новые классы: ``TagResult``, ``Tag``. - - Добавлен новый метод клиента для получения тегов (``tags``). -- Добавлено присоединение к коллективному плейлисту (`#317`_): - - Добавлен новый метод клиента для присоединения (``playlists_collective_join``). -- Добавлена поддержка очередей прослушивания (`#246`_): - - Добавлены новые классы: ``Context``, ``Queue``, ``QueueItem``. - - Добавлены новые методы в ``Client``: ``queues_list``, ``queue``, ``queue_update_position``, ``queue_create``. - - Добавлены поля ``track_id`` и ``from_`` в класс ``TrackId``. - - Добавлена возможность смены языка у клиента для ответов от API. - - Добавлена десериализация любого объекта в ``JSON`` пригодного для отправки в запросе на Яндекс API. -- Добавлены следующие методы для ``Client``: - - ``new_releases`` – получение полного списка всех новых релизов. - - ``new_playlists`` – получение полного списка всех новый плейлистов. - - ``podcasts`` – получение подкаста с лендинга. -- Добавлены новые сокращения в модели: - - ``download_cover_white``, ``download_cover_uri`` в ``MixLink``. - - ``download_image`` в ``Promotion``. - - ``artists_name`` в ``Album`` и ``Track``. - - ``fetch_track``, ``track_full_id`` в ``TrackId``. - - ``fetch_tracks`` в ``TracksList``. - - ``insert_track``, ``delete_tracks``, ``delete`` в ``Playlist``. - - ``playlist_id``, ``fetch_playlist`` в ``PlaylistId``. - - ``get_current_track`` в ``Queue``. - - ``fetch_queue`` в ``QueueItem``. - - ``next_page``, ``get_page``, ``prev_page`` в ``Search``. - - и другие... -- Добавлена поддержка новых типов поиска: подкасты, выпуски, пользователи. -- Добавлен коллбек для обработки новых полей. -- Добавлена информацию по поводу запуска потока по треку, плейлисту и др. -- Добавлена десериализация ``decomposed`` у ``Artist`` (`#10`_). -- Добавлен ``__len__`` для ``TracksList`` (`#380`_). -- Добавлены ``__iter__``, ``__len__`` и ``__getitem__`` для классов представляющих список каких-либо объектов. -- Добавлено сокращение ``fetch_tracks`` классу ``Playlist`` для получения треков плейлиста. -- Добавлен метод ``get_url`` классу ``Icon`` для получения прямой ссылки на изображение. -- Класс ``User`` расширен для поддержки поля ``user_info`` из ``Track`` (поля ``full_name``, ``display_name``). -- Добавлены новые классы по отчётам с Telegram бота (`#306`_, `#398`_): - - ``LandingList``. - - ``RenewableRemainder``. - - ``Alert``. - - ``AlertButton``. - - ``StationData``. - - ``Brand``. - - ``Contest``. - - ``OpenGraphData``. - - ``NonAutoRenewable``. - - ``Operator``. - - ``Deactivation``. - - ``PoetryLoverMatch``. - - ``Deprecation``. -- Добавлены новые поля классам по отчётам с Telegram бота (`#306`_, `#398`_): - - ``plus`` в ``Product``. - - ``non_auto_renewable_remainder`` в ``Subscription``. - - ``og_image`` в ``Artist``. - - ``meta_type`` в ``Album``. - - ``advertisement`` в ``Status``. - - ``best`` в ``Track``. - - ``offer_id`` и ``artist_ids`` в ``Vinyl``. - - ``playlists`` в ``BriefInfo``. - - ``is_custom`` в ``Cover``. - - ``play_count``, ``recent``, ``chart``, ``track`` в ``TrackShort``. - - ``url_part``, ``og_title``, ``image``, ``cover_without_text``, ``background_color``, ``text_color``, ``id_for_from``, ``similar_playlists``, ``last_owner_playlists`` в ``Playlist``. - - ``bg_color`` в ``Chart``. - - ``error`` в ``Artist``. - - ``substituted``, ``matched_track``, ``can_publish``, ``state``, ``desired_visibility``, ``filename``, ``user_info``, ``meta_data`` в ``Track``. - - ``copyright_name``, ``copyright_cline`` в ``Cover``. - - ``direct`` в ``DownloadInfo``. - - ``cheapest``, ``title``, ``family_sub``, ``fb_image``, ``fb_name``, ``family``, ``intro_period_duration``, ``intro_price``, ``start_period_duration``, ``start_price``, ``licence_text_parts`` в ``Product``. - - ``storage_dir``, ``duplicates`` в ``Album``. - - ``subscribed`` в ``ArtistEvent``. - - ``description`` в ``GeneratedPlaylist``. - - ``genre`` в ``Event``. - - ``show_in_regions`` в ``Genre``. - - ``cover_uri`` в ``MixLink``. - - ``og_description``, ``top_artist`` в ``Playlist``. - - ``full_image_url``, ``mts_full_image_url`` в ``Station``. - - ``coauthors`` и ``recent_tracks`` в ``Playlist``. - - ``regions`` в ``User``. - - ``users``, ``podcasts``, ``podcast_episodes``, ``type_``, ``page``, ``per_page`` в ``Search``. - - ``short_description``, ``description``, ``is_premiere``, ``is_banner`` в ``Like``. - - ``master_info`` в ``AutoRenewable``. - - ``station_data`` и ``bar_below`` в ``Status``. - - ``family_auto_renewable`` в ``Subscription``. - - ``misspell_result`` и ``misspell_original`` в ``Search``. - - ``experiment`` в класс ``Status``. - - ``operator`` и ``non_auto_renewable`` в ``Subscription``. - - ``text_color``, ``short_description``, ``description``, ``is_premiere`` и ``is_banner`` в ``Album``. - - ``hand_made_description`` в ``Artist``. - - ``metrika_id`` в ``Playlist``. - - ``og_image`` в ``Tag``. - - ``url`` в ``Lyrics``. - - ``number``, ``genre`` в ``MetaData``. - - ``poetry_lover_matches`` в ``Track``. - - ``contest``, ``dummy_description``, ``dummy_page_description``, ``dummy_cover``, ``dummy_rollover_cover``, ``og_data``, ``branding`` в ``Playlist``. - - ``available_as_rbt``, ``lyrics_available``, ``remember_position``, ``albums``, ``duration_ms``, ``explicit``, ``start_date``, ``likes_count``, ``deprecation`` в ``Album``. - - ``lyricist``, ``version``, ``composer`` в ``MetaData``. - - ``last_releases`` в ``BriefInfo``. - - ``ya_money_id`` в ``Artist`` (`#351`_, `#370`_). - - ``playlist_uuid`` в ``Playlist``. - - ``sync_queue_enabled`` в ``UserSettings``. - - ``background_video_uri``, ``short_description``, ``is_suitable_for_children`` в ``Track`` (`#376`_). - - ``meta_type``, ``likes_count`` в ``Album`` (`#386`_). - - ``deprecation`` в ``Album``. - - ``available_regions`` в ``Album``. - - ``type``, ``ready`` в ``Playlist``. - - ``description`` в ``Supplement``. - -**Незначительные изменения и/или исправления** - -- Добавлена опциональность следующим полям: - - все поля в ``MetaData``. - - ``advertisement`` в ``Status``. - - ``text_language`` в ``Lyrics``. - - ``provider_video_id`` в ``VideoSupplement``. - - ``title`` в ``VideoSupplement`` (`#403`_). - - ``instructions`` в ``Deactivation`` (`#402`_). - - ``id`` в ``Album`` (`#401`_). -- Исправлена десериализация подкастов, эпизодов подкастов и пользователей в лучшем результате поиска. -- Исправлена десериализация альбомов. В зависимости от запроса содержимое лейблов может быть списком объектом или списком строк (в поиске). -- Исправлен выбор настроек радио. -- Исправлены ошибки в документации. -- Протестирована работа на Python 3.9. - -.. _`#318`: https://github.com/MarshalX/yandex-music-api/issues/318 -.. _`#306`: https://github.com/MarshalX/yandex-music-api/issues/306 -.. _`#324`: https://github.com/MarshalX/yandex-music-api/issues/324 -.. _`#294`: https://github.com/MarshalX/yandex-music-api/issues/294 -.. _`#192`: https://github.com/MarshalX/yandex-music-api/issues/192 -.. _`#317`: https://github.com/MarshalX/yandex-music-api/issues/317 -.. _`#10`: https://github.com/MarshalX/yandex-music-api/issues/10 -.. _`#386`: https://github.com/MarshalX/yandex-music-api/issues/386 -.. _`#246`: https://github.com/MarshalX/yandex-music-api/issues/246 -.. _`#376`: https://github.com/MarshalX/yandex-music-api/issues/376 -.. _`#351`: https://github.com/MarshalX/yandex-music-api/issues/351 -.. _`#370`: https://github.com/MarshalX/yandex-music-api/issues/370 -.. _`#380`: https://github.com/MarshalX/yandex-music-api/issues/380 -.. _`#398`: https://github.com/MarshalX/yandex-music-api/issues/398 -.. _`#401`: https://github.com/MarshalX/yandex-music-api/issues/401 -.. _`#402`: https://github.com/MarshalX/yandex-music-api/issues/402 -.. _`#403`: https://github.com/MarshalX/yandex-music-api/issues/403 - -Версия 0.1.1 -============ - -**25.03.2020** - -**Закончено документирование всех классов и основных методов!** - -**Переломные изменения** - -- Классы отметок "мне нравится" для альбомов, плейлистов и исполнителей обобщены. Теперь представлены одним классом. - - Удаленные классы: - - ``ArtistsLikes``. - - ``AlbumsLikes``. - - ``PlaylistsLikes``. - - Новый класс: ``Like`` (поле ``type`` для определения содержимого). -- Изменено название пакета с ``status`` на ``account`` (`#195`_). -- Исправлено выбрасываемое исключение при таймауте: - - Прошлое исключение: ``TimeoutError`` (built-in). - - Новое исключение: ``TimedOut`` (``yandex_music.exceptions``). -- Удалены следующие файлы: ``requirements.txt``, ``requirements-dev.txt``, ``requirements-docs.txt``. - -**Крупные изменения** - -- Добавлено обнаружение новых полей с просьбой сообщить о них (`#216`_). - - Добавлена проверка на неизвестные поля. - - Добавлен вывод отладочной информации в виде warning'a. - - Добавлен шаблон issue для отправки логов. -- Добавлено поле ``type`` для класса ``SearchResult`` для определения типа результата поиска по объекту. -- Добавлены настройки пользователя (`#195`_): - - Добавлен класс ``UserSettings``. - - Добавлен метод для получения своих настроек (``account_settings``). - - Добавлен метод для получения настроек другого пользователя (``users_settings``). - - Добавлен метод для изменения настроек (``account_settings_set``). -- Добавлен возможность получить похожие треки (`#197`_): - - Добавлен класс ``TracksSimilar`` с полями трека и списка похожих треков. - - Добавлен метод для получения похожих треков (``tracks_similar``). -- Добавлены шоты от Алисы (`#185`_): - - Добавлен метод ``after_track`` в класс ``Client`` для получения контента для воспроизведения после трека (реклама, шот). - - Добавлены методы для загрузки обложки и аудиоверсии шота. - - Добавлены новые классы: - - ``Shot`` - - ``ShotData`` - - ``ShotEvent`` - - ``ShotType`` -- Добавлен метод для изменения видимости плейлиста (`#179`_). -- Добавлена поддержка Яндекс.Радио (`#20`_): - - Исправлена отправка фидбека. - - Написана инструкция по использованию (в доке к методу). - - Добавлен аргумент для перехода по цепочке треков. - - Добавлен метод для изменения настроек станции. - -**Незначительные изменения и/или исправления** - -- Убрано дублирование информации в документации (`#247`_). -- Добавлены новые поля в класс ``Track``: ``version``, ``remember_position`` (`#238`_). -- Добавлено исключение ``InvalidBitrate`` при попытке загрузить недопустимый трек по критериям (кодек, битрейт). -- Исправлено получение прямой ссылки на файл с кодеком AAC (`#237`_, `#25`_). -- Исправлено получение плейлиста с Алисой в лендинге (`#185`_). -- Исправлено название поля с ссылкой на источник в классе ``Description`` (с ``url`` на ``uri``). -- Исправлена десериализация несуществующего исполнителя. -- Добавлено поле ``version`` в класс ``Album`` (`#178`_). -- Поле ``picture`` класса ``Vinyl`` теперь опциональное. -- Поле ``week`` класса ``Ratings`` теперь опциональное. -- Поле ``product_id`` класса ``AutoRenewable`` теперь опциональное (`#182`_). -- Правки замечаний по codacy. - -.. _`#216`: https://github.com/MarshalX/yandex-music-api/issues/216 -.. _`#247`: https://github.com/MarshalX/yandex-music-api/issues/247 -.. _`#237`: https://github.com/MarshalX/yandex-music-api/issues/237 -.. _`#25`: https://github.com/MarshalX/yandex-music-api/issues/25 -.. _`#238`: https://github.com/MarshalX/yandex-music-api/issues/238 -.. _`#182`: https://github.com/MarshalX/yandex-music-api/issues/182 -.. _`#195`: https://github.com/MarshalX/yandex-music-api/issues/195 -.. _`#197`: https://github.com/MarshalX/yandex-music-api/issues/197 -.. _`#20`: https://github.com/MarshalX/yandex-music-api/issues/20 -.. _`#185`: https://github.com/MarshalX/yandex-music-api/issues/185 -.. _`#179`: https://github.com/MarshalX/yandex-music-api/issues/179 -.. _`#178`: https://github.com/MarshalX/yandex-music-api/issues/178 - -Версия 0.0.16 -============= - -**29.12.2019** - -**Переломные изменения** - -- Поле ``account`` переименовано в ``me`` и теперь содержит объект ``Status``, вместо ``Account`` (`#162`_). -- Убрано использование зарезервированных имён в аргументах конструкторов (теперь они с ``_`` на конце). Имена с нижними подчёркиваниями есть как при сериализации так и при десериализации (`#168`_). - -**Крупные изменения** - -- **Добавлены аннотации типов во всей библиотеке!** - -**Незначительные изменения и/или исправления** - -- Добавлен аргумент ``fetch_account_status`` для опциональности получения информации об аккаунте при инициализации клиента (`#162`_). -- Добавлены тесты c передачей пустого словаря в ``de_json`` и ``de_list`` (`#174`_). -- Использование ``ujson`` при наличии, обновлены зависимости (`#161`_). -- Добавлен в зависимости для разработки ``importlib_metadata`` для поддержки старых версий (в новой версии ``pytest`` его больше не используют, в угоду ``importlib.metadata`` `#pytest-5537`_)) (`#161`_). -- Добавлен в зависимости для разработки ``atomicwrites``, который используется ``pytest`` теперь только на ``Windows`` - `#pytest-6148`_ (`#161`_). -- Исправлен баг с передачей ``timeout`` аргумента в аргумент ``params`` в следующих методах: ``artists``, ``albums``, ``playlists_list`` (`#120`_). -- Исправлена инициализация клиента при помощи логина и пароля с использованием прокси (`#159`_). -- Исправлен баг в загрузке обложки альбома. - -.. _`#162`: https://github.com/MarshalX/yandex-music-api/issues/162 -.. _`#161`: https://github.com/MarshalX/yandex-music-api/issues/161 -.. _`#159`: https://github.com/MarshalX/yandex-music-api/issues/159 -.. _`#168`: https://github.com/MarshalX/yandex-music-api/issues/168 -.. _`#120`: https://github.com/MarshalX/yandex-music-api/issues/120 -.. _`#174`: https://github.com/MarshalX/yandex-music-api/issues/174 -.. _`#pytest-5537`: https://github.com/pytest-dev/pytest/issues/5537 -.. _`#pytest-6148`: https://github.com/pytest-dev/pytest/pull/6148 - -Версия 0.0.15 -============= - -**01.12.2019** - -**Переломные изменения** - -- У классов ``Artist``, ``Track`` и ``Playlist`` изменился перечень полей для генерации хеша. - -**Крупные изменения** - -- Добавлена возможность выполнять запросы через прокси-сервер для использовании библиотеки на зарубежных серверах (`#139`_). - - Добавлен пример использования в ``README``. -- Добавлена обработка капчи при авторизации с возможностью использования callback-функции для её обработки (`#140`_): - - Новые исключения: - - Captcha: - - CaptchaRequired. - - CaptchaWrong. - - Новые классы: - - CaptchaResponse. - - Новые примеры в ``README``: - - Пример обработки с использованием callback-функции. - - Пример полностью своей обработки капчи. -- Добавлена документация для класса ``Search`` (`#83`_). -- Добавлена возможность получения всех альбомов исполнителя (`#141`_): - - Новые классы: - - ArtistAlbums. - - Новые методы: - - ``artists_direct_albums`` у ``Client``. - - ``get_albums`` у ``Artist``. -- Добавлена обработка несуществующего плейлиста (`#147`_): - - Новые классы: - - ``PlaylistAbsence``. - -**Незначительные изменения и/или исправления** - -- Исправлен баг с загрузкой файлов (`#149`_). -- Исправлен баг некорректной десериализации плейлиста при отсутствии прав на него (`#147`_). -- Исправлен баг неправильной десериализации треков и артистов у собственных загруженных файлов (`#154`_). - -.. _`#139`: https://github.com/MarshalX/yandex-music-api/issues/139 -.. _`#140`: https://github.com/MarshalX/yandex-music-api/issues/140 -.. _`#83`: https://github.com/MarshalX/yandex-music-api/issues/83 -.. _`#141`: https://github.com/MarshalX/yandex-music-api/issues/141 -.. _`#149`: https://github.com/MarshalX/yandex-music-api/issues/149 -.. _`#147`: https://github.com/MarshalX/yandex-music-api/issues/147 -.. _`#154`: https://github.com/MarshalX/yandex-music-api/issues/154 - -Версия 0.0.14 -============= - -**10.11.2019** - -**Переломные изменения** - -- Практически у всех классов был обновлён список полей участвующих при сравнении объектов. -- Если в атрибутах для сравнения объектов присутствуют списки, то они будут преобразованы к frozenset. -- Убрано конвертирование даты из строки в объект. Теперь все даты представлены строками в ISO формате. -- Классы ``AlbumSearchResult``, ``ArtistSearchResult``, ``PlaylistSearchResult``, ``TrackSearchResult``, ``VideoSearchResult`` были объединены в один - ``SearchResult``. - -**Крупные изменения** - -- Добавлен метод получения треков исполнителя (`#123`_). -- Добавлены классы-обёртки над пагинацией (``Pager``) и списка треков артиста (``ArtistsTracks``). -- Добавлено **554** unit-теста для всех классов-обёрток над объектами API. -- Добавлен codecov и workflows для GitHub Actions. - -.. _`#123`: https://github.com/MarshalX/yandex-music-api/pull/123 - -**Незначительные изменения и/или исправления** - -- Поле ``cover_uri`` класса ``Album`` теперь опциональное. -- Поле ``region`` у класса ``Account`` теперь не обязательное. -- Исправлен баг в ``.to_dict()`` методе, связанный с десериализцией объектов списков и словарей. -- Исправлен баг в ``.to_dict()`` методе, связанный с не рекурсивной десериализацией. -- Исправлена десериализация ``similar_artists`` в ``BriefInfo``. -- Исправлен баг с десериализацией ``artist`` в классе ``ArtistEvent``. -- Исправлен баг десериализации списка альбомов и артистов у класса ``Track`` (`#122`_). -- Исправлена загрузка обложки у трека. -- Исправлены сравнения объектов. - -.. _`#122`: https://github.com/MarshalX/yandex-music-api/pull/122 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c143deb5..bda3b6be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,73 +1,97 @@ # Как внести свой вклад -Внесение своего вклада в этот проект ничем не отличается внесения в других -open source проекты на GitHub'e, но есть несколько ключевых моментов, о которых -стоит знать и помнить. +Внесение своего вклада в этот проект ничем не отличается внесения в других open source проекты на GitHub'e, но есть несколько ключевых моментов, о которых стоит знать и помнить. -Все необходимые пакеты для разработки есть в `requirements-dev.txt`. -Установить их можно с помощью следующей команды: -``` +Все необходимые пакеты для разработки есть в `requirements-dev.txt`. Установить их можно с помощью следующей команды: +```shell pip install -r requirements-dev.txt ``` Основные типы вклада: -- добавление новых полей к существующим классам; -- добавление новых классов; +- добавление новых полей к существующим моделям; +- добавление новых моделей; - установка опциональности какого-то поля; - добавление нового метода в `Client` (новый запрос на API); +- добавление нового метода-сокращения в модель; - добавление примера использование в папку [examples](examples). -Ваш вклад может быть более грандиозным. Так, например, можно написать -интеграционные тесты для класса `Client` и всех методов-сокращений в моделях. -Написать тесты для класса запросов. Написать генератор кода для быстрого добавления -новых полей. +Ваш вклад может быть более грандиозным. Так, например, можно написать интеграционные тесты для класса `Client` и всех методов-сокращений в моделях. Написать тесты для класса запросов. Написать генератор кода для быстрого добавления новых полей. ## Pull Requests -PR'ы должны быть сделаны в `dev` ветку. Определённого шаблона оформления -нет. Если это закрывает какое-то issue, то стоит сослаться на него с ключевым -словом "close". Например, "close #123". Обращайте внимание прошёл ли Ваш PR все -проверки GitHub Actions (тесты, проверка стиля кода). +PR'ы должны быть сделаны в `dev` ветку. Определённого шаблона оформления нет. Если это закрывает какое-то issue, то стоит сослаться на него с ключевым словом "close". Например, "close #123". Обращайте внимание прошёл ли Ваш PR все проверки GitHub Actions (тесты, проверка стиля кода). ## Тесты -Для тестирования используется `pytest`. На данный момент отсутствуют -интеграционные тесты. Поэтому всё что сейчас -требутеся — это юнит тесты классов-обёрток и их дополнительных методов -(если имеются), которые не являются сокращениями для методов клиента. +Для тестирования используется `pytest`. На данный момент отсутствуют интеграционные тесты. Поэтому всё что сейчас требуется — это модульные тесты классов-обёрток и их дополнительных методов (если имеются), которые не являются сокращениями для методов клиента. -Тесты модели должны покрывать 5 основных вещей: конструктор, десериализацию -только обязательных полей, десериализацию всех полей, сравнение -объектов модели, десериализацию пустого словаря. По необходимости десериализацию -списка объектов. +Тесты модели должны покрывать 5 основных вещей: конструктор, десериализацию только обязательных полей, десериализацию всех полей, сравнение объектов модели, десериализацию пустого словаря. По необходимости десериализацию списка объектов. Примеры доступны в папке [tests](tests). ## Документация -Для документации используется `Sphinx`. Документация ведется в [google style](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). -К каждому классу и методу должна быть написана документация на русском языке. -Типы каждого значения. По возможности описано предназначение поля. Если -невозможно логически понять из названия — ставим `TODO`. Обязательно отмечаем -какие поля являются опциональными. Пишем заметки по известным значениям полей, -чтобы из контекста догадываться о предназначении. +Для документации используется `Sphinx`. Документация ведется в [google style](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). К каждому классу и методу должна быть написана документация на русском языке. Указаны типы каждого значения. По возможности описано предназначение поля. Если невозможно логически понять из названия или данных — ставим `TODO`. Обязательно отмечаем какие поля являются опциональными. Пишем заметки по известным значениям полей, чтобы из контекста догадываться о предназначении. -Если добавлен новый файл, то не забудьте сгенерировать `.rst` файл -выполнив команду `make gen` в папке `docs` и добавить его в GIT. +Если добавлен новый класс или функция, то не забудьте сгенерировать `.rst` файл выполнив команду `make gen` в папке `docs` и добавить его в GIT. -Чтобы собрать документацию выполните `make html` в папке `docs`. +Чтобы собрать документацию выполните `make html` в папке `docs`. Для отчистки используйте `make clean`. ## Форматирование кода (стиль написания) -В проекте используется `black`. Не забывайте перед публикацией -отформатировать код и проверить его на работоспособность. +В проекте используется `black`. Не забывайте перед публикацией отформатировать код и проверить его на работоспособность. Используются одинарные кавычки. Запускайте `black` с конфигом из основной директории: + +```shell +black --config=black.toml yandex_music +``` + +или + +```shell +make black +``` ## Создание новых моделей -Исходите из логики при помещении модели в определённый пакет. -Добавьте свою модель для короткого импорта в одну из секций `__init__.py` файла -и пропишите название в `__all__`. Обязательно следите за тем, какие поля -присутствуют всегда, а какие нет. По возможности минимизируйте количество -опциональных полей. Не забывайте перечислить поля, по которым можно отличить -один объект от другого в `_id_attrs` (метод `__post_init__`). Экземпляр класса -`Client` передаётся в каждую модель для возможности написания методов-сокращений. +Исходите из логики при помещении модели в определённый пакет. Добавьте свою модель для короткого импорта в одну из секций `__init__.py` файла и пропишите название в `__all__`. + +Обязательно следите за тем, какие поля присутствуют всегда, а какие нет. По возможности минимизируйте количество опциональных полей. + +Не забывайте перечислить поля, по которым можно отличить один объект от другого в `_id_attrs` (метод `__post_init__`). + +Экземпляр класса `Client` передаётся в каждую модель для возможности написания методов-сокращений. При наличии дополнительных методов у модели не забудьте создать асинхронную версию метода добавив в название суффикс `_async`. + +Кроме этого, если у вашей модели есть метод, например, `download_async()`, то такому методу следует создать camel case псевдоним (`downloadAsync()`). Для создания псевдонимов существует генератор. Всё что вам надо сделать это поместить в конце файла с моделью маркер: + +``` +# camelCase псевдонимы +``` +([пример](https://github.com/MarshalX/yandex-music-api/blob/a30082f4929e56381c870cb03103777ae29bcc6b/yandex_music/tracks_list.py#L80)) + +После чего запустить генератор: +```shell +python generate_camel_case_aliases.py +``` + +### Создание новых методов клиента + +Если ваша задача включает добавление нового API метода, то не забудьте сгенерировать асинхронную версию клиента. Сделать это можно следующей командой: + +```shell +python generate_async_version.py +``` + +Ни в коем случае не редактируйте файл `client_async.py` и `request_async.py` руками! + +## Makefile + +_Используйте WSL если вы на Windows._ + +Для упрощения жизни в корне проекта существует [Makefile](Makefile). + +Команда +```shell +make all +``` + +Выполнит за вас black для основного модуля и тестов, сгенерирует асинхронную версию клиента, сгенерирует camel case псевдонимы. diff --git a/MANIFEST.in b/MANIFEST.in index 843b1e27..26b0dd07 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include LICENSE CHANGES.rst \ No newline at end of file +include LICENSE CHANGES.md \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..22fc1f80 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +# makefile for Yandex Music project + +black: + black --config=black.toml yandex_music + +black_test: + black --config=black.toml tests + +gen_async: + python generate_async_version.py + +gen_alias: + python generate_camel_case_aliases.py + +gen: + make gen_async && make gen_alias + +b: + make black + +bt: + make black_test + +g: + make gen + +all: + make g && make b && make bt + +a: + make all diff --git a/README.md b/README.md new file mode 100644 index 00000000..17869730 --- /dev/null +++ b/README.md @@ -0,0 +1,301 @@ +## Yandex Music API + +> Делаю то, что по определённым причинам не сделала компания Yandex. + +⚠️ Это неофициальная библиотека. + +Сообщество разработчиков общаются и помогают друг другу в [Telegram чате](https://t.me/yandex_music_api), присоединяйтесь! + +[![Поддерживаемые Python версии](https://img.shields.io/badge/python-3.7+-blue.svg)](https://pypi.org/project/yandex-music/) +[![Покрытие кода тестами](https://codecov.io/gh/MarshalX/yandex-music-api/branch/main/graph/badge.svg)](https://codecov.io/gh/MarshalX/yandex-music-api) +[![Качество кода](https://api.codacy.com/project/badge/Grade/27011a5a8d9f4b278d1bfe2fe8725fed)](https://app.codacy.com/gh/MarshalX/yandex-music-api) +[![Статус тестов](https://github.com/MarshalX/yandex-music-api/actions/workflows/pytest_full.yml/badge.svg)](https://github.com/MarshalX/yandex-music-api/actions/workflows/pytest_full.yml) +[![Статус документации](https://readthedocs.org/projects/yandex-music/badge/?version=latest)](https://yandex-music.readthedocs.io/en/latest/?badge=latest) +[![Лицензия LGPLv3](https://img.shields.io/badge/license-LGPLv3-lightgrey.svg)](https://www.gnu.org/licenses/lgpl-3.0.html) + +### Содержание + - [Введение](#введение) + 1. [Доступ к вашим данным Яндекс.Музыка](#доступ-к-вашим-данным-яндексмузыка) + - [Установка](#установка) + - [Начало работы](#начало-работы) + 1. [Изучение по примерам](#изучение-по-примерам) + 2. [Особенности использования асинхронного клиента](#особенности-использования-асинхронного-клиента) + 3. [Логирование](#логирование) + 4. [Документация](#документация) + - [Получение помощи](#получение-помощи) + - [Список изменений](#список-изменений) + - [Реализации на других языках](#реализации-на-других-языках) + 1. [C#](#c) + 2. [PHP](#php) + 3. [JavaScript](#javascript) + - [Разработанные проекты](#разработанные-проекты) + 1. [Плагин для Kodi](#плагин-для-kodi) + 2. [Telegram бот-клиент](#telegram-бот-клиент) + - [Благодарность](#благодарность) + - [Внесение своего вклада в проект](#внесение-своего-вклада-в-проект) + - [Спонсоры](#спонсоры) + - [Лицензия](#лицензия) + +### Введение + +Эта библиотека предоставляется Python интерфейс для никем незадокументированного и сделанного только для себя API Яндекс Музыки. + +Она совместима с версиями Python 3.7+ и поддерживает работу как с синхронном, так и асинхронным (asyncio) кодом. + +В дополнение к реализации чистого API данная библиотека имеет ряд классов-обёрток объектов высокого уровня дабы сделать разработку клиентов и скриптов простой и понятной. Вся документация была написана с нуля исходя из логического анализа в ходе обратной разработки(reverse engineering) API. + +#### Доступ к вашим данным Яндекс.Музыка + +Начиная с версии [2.0.0](https://github.com/MarshalX/yandex-music-api/blob/a30082f4929e56381c870cb03103777ae29bcc6b/CHANGES.rst#%D0%B2%D0%B5%D1%80%D1%81%D0%B8%D1%8F-200) библиотека больше не предоставляет интерфейсы для работы с OAuth Яндекс и Яндекс.Паспорт. Задача по получению токена для доступа к данным на плечах разработчиков использующих данную библиотеку. О том как получить токен читайте в документации. + +### Установка + +Вы можете установить или обновить Yandex Music API при помощи: + +``` shell +pip install -U yandex-music +``` + +Или Вы можете установить из исходного кода с помощью: + +``` shell +git clone https://github.com/MarshalX/yandex-music-api +cd yandex-music-api +python setup.py install +``` + +### Начало работы + +Приступив к работе первым делом необходимо создать экземпляр клиента. + +Инициализация синхронного клиента: + +``` python +from yandex_music import Client + +client = Client() +client.init() + +# или + +client = Client().init() +``` + +Инициализация асинхронного клиента: + +``` python +from yandex_music import ClientAsync + +client = ClientAsync() +await client.init() + +# или + +client = await Client().init() +``` + +Вызов `init()` необходим для получение информации для упрощения будущих запросов. + +Работа без авторизации ограничена. Так, например, для загрузки будут доступны только первые 30 секунд аудиофайла. Для понимания всех ограничений зайдите на сайт Яндекс.Музыка под инкогнито и воспользуйтесь сервисом. + +Для доступа к своим личным данным следует авторизоваться. Это осуществляется через токен аккаунта Яндекс.Музыка. + +Авторизация: + +``` python +from yandex_music import Client + +client = Client('token').init() +``` + +После успешного создания клиента Вы вольны в выборе необходимого метода из API. Все они доступны у объекта класса `Client`. Подробнее в методах клиента в [документации](https://yandex-music.readthedocs.io/en/latest/yandex_music.client.html). + +Пример получения первого трека из плейлиста "Мне нравится" и его загрузка: + +``` python +from yandex_music import Client + +client = Client('token').init() +client.users_likes_tracks()[0].fetch_track().download('example.mp3') +``` + +В примере выше клиент получает список треков которые были отмечены как понравившиеся. API возвращает объект [TracksList](https://yandex-music.readthedocs.io/en/latest/yandex_music.tracks_list.html) в котором содержится список с треками класса [TrackShort](https://yandex-music.readthedocs.io/en/latest/yandex_music.track_short.html). Данный класс содержит наиважнейшую информацию о треке и никаких подробностей, поэтому для получения полной версии трека со всей информацией необходимо обратиться к методу `fetch_track()`. Затем можно скачать трек методом `download()`. + +Пример получения треков по ID: + +``` python +from yandex_music import Client + +client = Client().init() +client.tracks(['10994777:1193829', '40133452:5206873', '48966383:6693286', '51385674:7163467']) +``` + +В качестве ID трека выступает его уникальный номер и номер альбома. Первым треком из примера является следующий трек:music.yandex.ru/album/**1193829**/track/**10994777** + +Выполнение запросов с использование прокси в синхронной версии: + +``` python +from yandex_music.utils.request import Request +from yandex_music import Client + +request = Request(proxy_url='socks5://user:password@host:port') +client = Client(request=request).init() +``` + +Примеры proxy url: + - socks5://user::port + - + - + - + +Больше примеров тут: [proxies - advanced usage - requests](https://2.python-requests.org/en/master/user/advanced/#proxies) + +Выполнение запросов с использование прокси в асинхронной версии: + +``` python +from yandex_music.utils.request_async import Request +from yandex_music import ClientAsync + +request = Request(proxy_url='http://user:pass@some.proxy.com') +client = await ClientAsync(request=request).init() +``` + +Socks прокси не поддерживаются в асинхронной версии. + +Про поддерживаемые прокси тут: [proxy support - advanced usage - aiohttp](https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support) + +#### Изучение по примерам + +Вот несколько примеров для обзора. Даже если это не Ваш подход к обучению, пожалуйста, возьмите и бегло просмотрите их. + +Код примеров опубликован в открытом доступе, поэтому Вы можете взять его и начать писать вокруг своё. + +Посетите [эту страницу](https://github.com/MarshalX/yandex-music-api/blob/main/examples/), чтобы изучить официальные примеры. + +#### Особенности использования асинхронного клиента + +При работе с асинхронной версией библиотеке стоит всегда помнить +следующие особенности: + - Клиент следует импортировать с названием `ClientAsync`, а не просто `Client`. + - При использовании методов-сокращений нужно выбирать метод с суффиксом `_async`. + +Пояснение ко второму пункту: + +``` python +from yandex_music import ClientAsync + +client = await ClientAsync('token').init() +liked_short_track = (await client.users_likes_tracks())[0] + +# правильно +full_track = await liked_short_track.fetch_track_async() +await full_track.download_async() + +# НЕПРАВИЛЬНО +full_track = await liked_short_track.fetch_track() +await full_track.download() +``` + +#### Логирование + +Данная библиотека использует `logging` модуль. Чтобы настроить логирование на стандартный вывод, поместите + +``` python +import logging +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +``` + +в начало вашего скрипта. + +Вы также можете использовать логирование в вашем приложении, вызвав `logging.getLogger()` и установить уровень какой Вы хотите: + +``` python +logger = logging.getLogger() +logger.setLevel(logging.INFO) +``` + +Если Вы хотите `DEBUG` логирование: + +``` python +logger.setLevel(logging.DEBUG) +``` + +### Документация + +Документация `yandex-music-api` расположена на [readthedocs.io](https://yandex-music.readthedocs.io/). Вашей отправной точкой должен быть класс `Client`, а точнее его методы. Именно они выполняют все запросы на API и возвращают Вам готовые объекты. [Класс Client на readthedocs.io](https://yandex-music.readthedocs.io/en/latest/yandex_music.client.html). + +### Получение помощи + +Получить помощь можно несколькими путями: + - Задать вопрос в [Telegram чате](https://t.me/yandex_music_api), где мы помогаем друг другу, присоединяйтесь\! + - Сообщить о баге можно [создав Bug Report](https://github.com/MarshalX/yandex-music-api/issues/new?assignees=MarshalX&labels=bug&template=bug-report.md&title=). + - Предложить новую фичу или задать вопрос можно [создав discussion](https://github.com/MarshalX/yandex-music-api/discussions/new). + - Найти ответ на вопрос в [документации библиотеки](https://yandex-music.readthedocs.io/en/latest/). + +### Список изменений + +Весь список изменений ведётся в файле [CHANGES.md](https://github.com/MarshalX/yandex-music-api/blob/main/CHANGES.md). + +### Реализации на других языках + +#### C# + +Реализация с совершенно другим подходом, так как используется API для frontend'a, а не мобильных и десктопных приложений: [Winster332/Yandex.Music.Api](https://github.com/Winster332/Yandex.Music.Api). + +[@Winster332](https://github.com/Winster332) не сильно проявляет активность, но существует форк, который продолжил начатое. Эндпоинты изменены с фронтовых на мобильные: [K1llMan/Yandex.Music.Api](https://github.com/K1llMan/Yandex.Music.Api). + +#### PHP + +Частично переписанная текущая библиотека на PHP: [LuckyWins/yandex-music-api](https://github.com/LuckyWins/yandex-music-api). + +#### JavaScript + +API wrapper на Node.JS. Не обновлялся больше двух лет: [itsmepetrov/yandex-music-api](https://github.com/itsmepetrov/yandex-music-api). Продолжение разработки заброшенной библиотеки: [kontsevoye/ym-api](https://github.com/kontsevoye/ym-api). + +### Разработанные проекты + +#### Плагин для Kodi + +Плагин может проигрывать пользовательские плейлисты и плейлисты Яндекса, поиск по Яндекс Музыке, радио. + +Сайт проекта: [ymkodi.ru](https://ymkodi.ru/). Исходный код: [kodi.plugin.yandex-music](https://github.com/Angel777d/kodi.plugin.yandex-music). +Автор: [@Angel777d](https://github.com/Angel777d). + +[![Плагин для Kodi](https://raw.githubusercontent.com/Angel777d/kodi.plugin.yandex-music/master/assets/img/kody_yandex_music_plugin.png)](https://ymkodi.ru/) + +#### Telegram бот-клиент + +Неофициальный бот. Умные и ваши плейлисты, понравившиеся треки. Лайки, дизлайки, текста песен, поиск, распознавание песен, похожие треки! Полноценный клиент на базе мессенджера. + +Сайт проекта: [music-yandex-bot.ru](https://music-yandex-bot.ru/). Бот в Telegram: [@music\_yandex\_bot](https://t.me/music_yandex_bot). Автор: [@MarshalX](https://github.com/MarshalX). + +Статья на habr.com с описанием реализации: [Под капотом бота-клиента Яндекс.Музыки](https://habr.com/ru/post/487428/). + +[![Telegram бот-клиент](https://hsto.org/webt/uv/4s/a3/uv4sa3pslohuzlmuzrjzteju2dk.png)](https://music-yandex-bot.ru/) + +### Благодарность + +Спасибо разработчикам `python-telegram-bot`. Выбрал Вас в качестве примера. + +### Внесение своего вклада в проект + +Внесение своего вклада максимально приветствуется! Есть перечень пунктов, который стоит соблюдать. Каждый пункт перечня расписан в [CONTRIBUTING.md](https://github.com/MarshalX/yandex-music-api/blob/main/CONTRIBUTING.md). + +Вы можете помочь и сообщив о [баге](https://github.com/MarshalX/yandex-music-api/issues/new?assignees=MarshalX&labels=bug&template=bug-report.md&title=) или о [новом поле пришедшем от API](https://github.com/MarshalX/yandex-music-api/issues/new?assignees=&labels=feature&template=found-unknown-fields.md&title=%D0%9D%D0%BE%D0%B2%D0%BE%D0%B5+%D0%BD%D0%B5%D0%B8%D0%B7%D0%B2%D0%B5%D1%81%D1%82%D0%BD%D0%BE%D0%B5+%D0%BF%D0%BE%D0%BB%D0%B5+%D0%BE%D1%82+API). + +### Спонсоры + +#### JetBrains + +JetBrains Logo (Main) logo. + +> JetBrains предоставляет бесплатный набор инструментов для разработки активным контрибьюторам некоммерческих проектов с открытым исходным кодом. + +[Лицензии для проектов с открытым исходным кодом — Программы поддержки](https://jb.gg/OpenSourceSupport) + +### Лицензия + +Вы можете копировать, распространять и модифицировать программное обеспечение при условии, что модификации описаны и лицензированы бесплатно в соответствии с [LGPL-3](https://www.gnu.org/licenses/lgpl-3.0.html). Произведения производных (включая модификации или что-либо статически связанное с библиотекой) могут распространяться только в соответствии с LGPL-3, но приложения, которые используют библиотеку, необязательно. diff --git a/README.rst b/README.rst deleted file mode 100644 index e8a42a2b..00000000 --- a/README.rst +++ /dev/null @@ -1,429 +0,0 @@ -================ -Yandex Music API -================ - - Делаю то, что по определённым причинам не сделала компания Yandex. - -⚠️ Это неофициальная библиотека. - -Сообщество разработчиков общаются и помогают друг другу -в `Telegram чате `_, присоединяйтесь! - -.. image:: https://img.shields.io/badge/python-3.7+-blue.svg - :target: https://pypi.org/project/yandex-music/ - :alt: Поддерживаемые Python версии - -.. image:: https://codecov.io/gh/MarshalX/yandex-music-api/branch/main/graph/badge.svg - :target: https://codecov.io/gh/MarshalX/yandex-music-api - :alt: Покрытие кода тестами - -.. image:: https://api.codacy.com/project/badge/Grade/27011a5a8d9f4b278d1bfe2fe8725fed - :target: https://www.codacy.com/manual/MarshalX/yandex-music-api - :alt: Качество кода - -.. image:: https://github.com/MarshalX/yandex-music-api/actions/workflows/pytest_full.yml/badge.svg - :target: https://github.com/MarshalX/yandex-music-api/actions/workflows/pytest_full.yml - :alt: Статус тестов - -.. image:: https://readthedocs.org/projects/yandex-music/badge/?version=latest - :target: https://yandex-music.readthedocs.io/en/latest/?badge=latest - :alt: Статус документации - -.. image:: https://img.shields.io/badge/license-LGPLv3-lightgrey.svg - :target: https://www.gnu.org/licenses/lgpl-3.0.html - :alt: Лицензия LGPLv3 - - -========== -Содержание -========== - -- `Введение`_ - - #. `Доступ к вашим данным Яндекс.Музыка`_ - -- `Установка`_ - -- `Начало работы`_ - - #. `Изучение по примерам`_ - - #. `Особенности использования асинхронного клиента`_ - - #. `Логирование`_ - - #. `Документация`_ - -- `Получение помощи`_ - -- `Список изменений`_ - -- `Реализации на других языках`_ - - #. `C#`_ - - #. `PHP`_ - - #. `JavaScript`_ - -- `Разработанные проекты`_ - - #. `Плагин для Kodi`_ - - #. `Telegram бот-клиент`_ - -- `Благодарность`_ - -- `Внесение своего вклада в проект`_ - -- `Лицензия`_ - -======== -Введение -======== - -Эта библиотека предоставляется Python интерфейс для никем -незадокументированного и сделанного только для себя API Яндекс Музыки. - -Она совместима с версиями Python 3.7+ и поддерживает работу как с синхронном, -так и асинхронным (asyncio) кодом. - -В дополнение к реализации чистого API данная библиотека имеет ряд -классов-обёрток объектов высокого уровня дабы сделать разработку клиентов -и скриптов простой и понятной. Вся документация была написана с нуля исходя -из логического анализа в ходе обратной разработки (reverse engineering) API. - ------------------------------------ -Доступ к вашим данным Яндекс.Музыка ------------------------------------ - -Начиная с версии `2.0.0 `_ библиотека больше не предоставляет интерфейсы для работы -с OAuth Яндекс и Яндекс.Паспорт. Задача по получению токена для доступа к данным -на плечах разработчиков использующих данную библиотеку. - -========= -Установка -========= - -Вы можете установить или обновить Yandex Music API при помощи: - -.. code:: shell - - pip install yandex-music --upgrade - -Или Вы можете установить из исходного кода с помощью: - -.. code:: shell - - git clone https://github.com/MarshalX/yandex-music-api - cd yandex-music-api - python setup.py install - -============= -Начало работы -============= - -Приступив к работе первым делом необходимо создать экземпляр клиента. - -Инициализация синхронного клиента: - -.. code:: python - - from yandex_music import Client - - client = Client() - client.init() - - # или - - client = Client().init() - -Инициализация асинхронного клиента: - -.. code:: python - - from yandex_music import ClientAsync - - client = ClientAsync() - await client.init() - - # или - - client = await Client().init() - -Вызов ``init()`` необходим для получение информации для упрощения будущих запросов. - -Работа без авторизации ограничена. Так, например, для загрузки будут доступны -только первые 30 секунд аудиофайла. Для понимания всех ограничений зайдите на -сайт Яндекс.Музыка под инкогнито и воспользуйтесь сервисом. - -Для доступа к своим личным данным следует авторизоваться. -Это осуществляется через токен аккаунта Яндекс.Музыка. - -Авторизация: - -.. code:: python - - from yandex_music import Client - - client = Client('token').init() - -После успешного создания клиента Вы вольны в выборе необходимого метода -из API. Все они доступны у объекта класса ``Client``. Подробнее в методах клиента -в `документации `_. - -Пример получения первого трека из плейлиста "Мне нравится" и его загрузка: - -.. code:: python - - from yandex_music import Client - - client = Client('token').init() - client.users_likes_tracks()[0].fetch_track().download('example.mp3') - -В примере выше клиент получает список треков которые были отмечены как -понравившиеся. API возвращает объект -`TracksList `_ -в котором содержится список с треками класса -`TrackShort `_. -Данный класс содержит наиважнейшую информацию о треке и никаких подробностей, -поэтому для получения полной версии трека со всей информацией необходимо -обратиться к методу ``fetch_track()``. Затем можно скачать трек методом ``download()``. - -Пример получения треков по ID: - -.. code:: python - - from yandex_music import Client - - client = Client().init() - client.tracks(['10994777:1193829', '40133452:5206873', '48966383:6693286', '51385674:7163467']) - -В качестве ID трека выступает его уникальный номер и номер альбома. -Первым треком из примера является следующий трек: -music.yandex.ru/album/**1193829**/track/**10994777** - -Выполнение запросов с использование прокси в синхронной версии: - -.. code:: python - - from yandex_music.utils.request import Request - from yandex_music import Client - - request = Request(proxy_url='socks5://user:password@host:port') - client = Client(request=request).init() - -Примеры proxy url: - -- socks5://user:password@host:port -- http://host:port -- https://host:port -- http://user:password@host - -Больше примеров тут: `proxies - advanced usage - requests `_ - -Выполнение запросов с использование прокси в асинхронной версии: - -.. code:: python - - from yandex_music.utils.request_async import Request - from yandex_music import ClientAsync - - request = Request(proxy_url='http://user:pass@some.proxy.com') - client = await ClientAsync(request=request).init() - -Socks прокси не поддерживаются в асинхронной версии. - -Про поддерживаемые прокси тут: `proxy support - advanced usage - aiohttp `_ - --------------------- -Изучение по примерам --------------------- - -Вот несколько примеров для обзора. Даже если это не Ваш подход к -обучению, пожалуйста, возьмите и бегло просмотрите их. - -Код примеров опубликован в открытом доступе, поэтому -Вы можете взять его и начать писать вокруг своё. - -Посетите `эту страницу `_ -чтобы изучить официальные примеры. - ----------------------------------------------- -Особенности использования асинхронного клиента ----------------------------------------------- - -При работе с асинхронной версией библиотеке стоит всегда помнить -следующие особенности: - -- Клиент следует импортировать с названием ``ClientAsync``, а не просто ``Client``. -- При использовании методов-сокращений нужно выбирать метод с суффиксом ``_async``. - -Пояснение ко второму пункту: - -.. code:: python - - from yandex_music import ClientAsync - - client = await ClientAsync('token').init() - liked_short_track = (await client.users_likes_tracks())[0] - - # правильно - full_track = await liked_short_track.fetch_track_async() - await full_track.download_async() - - # НЕПРАВИЛЬНО - full_track = await liked_short_track.fetch_track() - await full_track.download() - ------------ -Логирование ------------ - -Данная библиотека использует ``logging`` модуль. Чтобы настроить логирование на -стандартный вывод, поместите - -.. code:: python - - import logging - logging.basicConfig(level=logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - -в начало вашего скрипта. - -Вы также можете использовать логирование в вашем приложении, вызвав -``logging.getLogger()`` и установить уровень какой Вы хотите: - -.. code:: python - - logger = logging.getLogger() - logger.setLevel(logging.INFO) - -Если Вы хотите ``DEBUG`` логирование: - -.. code:: python - - logger.setLevel(logging.DEBUG) - -============ -Документация -============ - -Документация ``yandex-music-api`` расположена на -`readthedocs.io `_. -Вашей отправной точкой должен быть класс ``Client``, а точнее его методы. -Именно они выполняют все -запросы на API и возвращают Вам готовые объекты. -`Класс Client на readthedocs.io `_. - -================ -Получение помощи -================ - -Получить помощь можно несколькими путями: - -- Задать вопрос в `Telegram чате `_, где мы помогаем друг другу, присоединяйтесь! -- Сообщить о баге можно `создав Bug Report `_. -- Предложить новую фичу или задать вопрос можно `создав discussion `_. -- Найти ответ на вопрос в `документации библиотеки `_. - -================ -Список изменений -================ - -Весь список изменений ведётся в файле `CHANGES.rst `_. - - -=========================== -Реализации на других языках -=========================== - --- -C# --- - -Реализация с совершенно другим подходом, так как используется API для frontend'a, -а не мобильных и десктопных приложений: -`Winster332/Yandex.Music.Api `_. - -`@Winster332 `_ не сильно проявляет активность, -но существует форк, который продолжил начатое. Эндпоинты изменены с фронтовых на -мобильные: `K1llMan/Yandex.Music.Api `_. - ---- -PHP ---- - -Частично переписанная текущая библиотека на PHP: -`LuckyWins/yandex-music-api `_. - ----------- -JavaScript ----------- - -API wrapper на Node.JS. Не обновлялся больше двух лет: -`itsmepetrov/yandex-music-api `_. -Продолжение разработки заброшенной библиотеки: `kontsevoye/ym-api `_. - -===================== -Разработанные проекты -===================== - ---------------- -Плагин для Kodi ---------------- - -Плагин может проигрывать пользовательские плейлисты и плейлисты Яндекса, поиск -по Яндекс Музыке, радио. - -Сайт проекта: `ymkodi.ru `_. -Исходный код: `kodi.plugin.yandex-music `_. -Автор: `@Angel777d `_. - -.. image:: https://raw.githubusercontent.com/Angel777d/kodi.plugin.yandex-music/master/assets/img/kody_yandex_music_plugin.png - :target: https://ymkodi.ru/ - :alt: Плагин для Kodi - -------------------- -Telegram бот-клиент -------------------- - -Неофициальный бот. Умные и ваши плейлисты, понравившиеся треки. Лайки, дизлайки, текста песен, -поиск, распознавание песен, похожие треки! Полноценный клиент на базе мессенджера. - -Сайт проекта: `music-yandex-bot.ru `_. -Бот в Telegram: `@music_yandex_bot `_. -Автор: `@MarshalX `_. - -Статья на habr.com с описанием реализации: `Под капотом бота-клиента Яндекс.Музыки `_. - -.. image:: https://hsto.org/webt/uv/4s/a3/uv4sa3pslohuzlmuzrjzteju2dk.png - :target: https://music-yandex-bot.ru/ - :alt: Telegram бот-клиент - -============= -Благодарность -============= - -Спасибо разработчикам ``python-telegram-bot``. Выбрал Вас в качестве примера. - -=============================== -Внесение своего вклада в проект -=============================== - -Внесение своего вклада максимально приветствуется! Есть перечень пунктов, -который стоит соблюдать. Каждый пункт перечня расписан в `CONTRIBUTING.md `_. - -Вы можете помочь и сообщив о `баге `_ -или о `новом поле пришедшем от API `_. - -======== -Лицензия -======== - -Вы можете копировать, распространять и модифицировать программное обеспечение -при условии, что модификации описаны и лицензированы бесплатно в соответствии -с `LGPL-3 `_. Произведения -производных (включая модификации или что-либо статически связанное с библиотекой) -могут распространяться только в соответствии с LGPL-3, но приложения, которые -используют библиотеку, необязательно. diff --git a/SECURITY.md b/SECURITY.md index 224c3dbd..96d48576 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,10 +2,10 @@ ## Поддерживаемые версии -| Версия | Поддержка | -| ------- | ------------------ | -| 2.0.0 | :white_check_mark: | -| < 2.0.0 | :x: | +| Версия | Поддержка | +|---------|-----------| +| 2.1.1 | ✅ | +| < 2.1.1 | ❌ | ## Сообщение об уязвимости diff --git a/docs/requirements.txt b/docs/requirements.txt index df572648..4f7236f2 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ sphinx sphinx-copybutton -furo \ No newline at end of file +furo +myst-parser \ No newline at end of file diff --git a/docs/source/changes.md b/docs/source/changes.md new file mode 100644 index 00000000..110dedc7 --- /dev/null +++ b/docs/source/changes.md @@ -0,0 +1,2 @@ +```{include} ../../CHANGES.md +``` \ No newline at end of file diff --git a/docs/source/changes.rst b/docs/source/changes.rst deleted file mode 100644 index 1cb32f6b..00000000 --- a/docs/source/changes.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../CHANGES.rst \ No newline at end of file diff --git a/docs/source/client.md b/docs/source/client.md new file mode 100644 index 00000000..bcc8ef66 --- /dev/null +++ b/docs/source/client.md @@ -0,0 +1,22 @@ +# Клиент + +Приступив к работе первым делом необходимо создать экземпляр клиента. + +Инициализация синхронного клиента: + +``` python +from yandex_music import Client + +client = Client() +client.init() + +# или + +client = Client().init() +``` + +После успешного создания клиента вы вольны в выборе необходимого метода из API. Все они доступны у объекта класса `Client` и описаны ниже. Используйте навигацию из меню справа для быстрого доступа. + +```{eval-rst} +.. include:: yandex_music.client.rst +``` diff --git a/docs/source/client_async.md b/docs/source/client_async.md new file mode 100644 index 00000000..090d20fc --- /dev/null +++ b/docs/source/client_async.md @@ -0,0 +1,46 @@ +# Асинхронный клиент + +Приступив к работе первым делом необходимо создать экземпляр клиента. + +Инициализация асинхронного клиента: + +``` python +from yandex_music import ClientAsync + +client = ClientAsync() +await client.init() + +# или + +client = await Client().init() +``` + +После успешного создания клиента вы вольны в выборе необходимого метода из API. Все они доступны у объекта класса `ClientAsync` и описаны ниже. Используйте навигацию из меню справа для быстрого доступа. + +**Особенности использования асинхронного клиента** + +При работе с асинхронной версией библиотеке стоит всегда помнить +следующие особенности: +- Клиент следует импортировать с названием `ClientAsync`, а не просто `Client`. +- При использовании методов-сокращений нужно выбирать метод с суффиксом `_async`. + +Пояснение ко второму пункту: + +``` python +from yandex_music import ClientAsync + +client = await ClientAsync('token').init() +liked_short_track = (await client.users_likes_tracks())[0] + +# правильно +full_track = await liked_short_track.fetch_track_async() +await full_track.download_async() + +# НЕПРАВИЛЬНО +full_track = await liked_short_track.fetch_track() +await full_track.download() +``` + +```{eval-rst} +.. include:: yandex_music.client_async.rst +``` diff --git a/docs/source/code_of_conduct.md b/docs/source/code_of_conduct.md new file mode 100644 index 00000000..cc6912b9 --- /dev/null +++ b/docs/source/code_of_conduct.md @@ -0,0 +1,2 @@ +```{include} ../../CODE_OF_CONDUCT.md +``` diff --git a/docs/source/conf.py b/docs/source/conf.py index 7e61b2ff..53a4fa4d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,11 +17,16 @@ master_doc = 'index' +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown', +} + # -- Project information ----------------------------------------------------- project = 'Yandex Music API' -copyright = '2019-2022 Il`ya (Marshal) ' -author = 'Il`ya Semyonov' +copyright = '2019-2023 Ilya (Marshal) ' +author = 'Ilya (Marshal)' language = 'en' @@ -30,7 +35,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx_copybutton'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx_copybutton', 'myst_parser'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -40,6 +45,12 @@ # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] +# myst + +myst_heading_anchors = 4 +# https://myst-parser.readthedocs.io/en/latest/syntax/optional.html?highlight=header-anchors#code-fences-using-colons +myst_enable_extensions = ["colon_fence"] +# TODO add substitution https://myst-parser.readthedocs.io/en/latest/syntax/optional.html?highlight=header-anchors#substitutions-with-jinja2 # -- Options for HTML output ------------------------------------------------- diff --git a/docs/source/contributing.md b/docs/source/contributing.md new file mode 100644 index 00000000..ef6daa82 --- /dev/null +++ b/docs/source/contributing.md @@ -0,0 +1,2 @@ +```{include} ../../CONTRIBUTING.md +``` diff --git a/docs/source/examples.chart.md b/docs/source/examples.chart.md new file mode 100644 index 00000000..9509da14 --- /dev/null +++ b/docs/source/examples.chart.md @@ -0,0 +1,40 @@ +# Получение чарта + +Пример работы с чартом ЯМ. Получение треков, отображение позиций и их изменения с использованием эмодзи. + +```python +import os + +from yandex_music import Client + + +CHART_ID = 'world' +TOKEN = os.environ.get('TOKEN') + +client = Client(TOKEN).init() +chart = client.chart(CHART_ID).chart + +text = [f'🏆 {chart.title}', chart.description, '', 'Треки:'] + +for track_short in chart.tracks: + track, chart = track_short.track, track_short.chart + artists = '' + if track.artists: + artists = ' - ' + ', '.join(artist.name for artist in track.artists) + + track_text = f'{track.title}{artists}' + + if chart.progress == 'down': + track_text = '🔻 ' + track_text + elif chart.progress == 'up': + track_text = '🔺 ' + track_text + elif chart.progress == 'new': + track_text = '🆕 ' + track_text + elif chart.position == 1: + track_text = '👑 ' + track_text + + track_text = f'{chart.position} {track_text}' + text.append(track_text) + +print('\n'.join(text)) +``` diff --git a/docs/source/examples.daily_playlist_updater.md b/docs/source/examples.daily_playlist_updater.md new file mode 100644 index 00000000..9c38daa5 --- /dev/null +++ b/docs/source/examples.daily_playlist_updater.md @@ -0,0 +1,43 @@ +# Обновление стрика дейлика + +Отмечает "Плейлист дня" как прослушанный сегодня (добавляет +1 к счетчику). + +```python +import sys +import datetime +from yandex_music.client import Client + +# Help text +if len(sys.argv) == 1 or len(sys.argv) > 3: + print('Usage: DailyPlaylistUpdater.py token') + print('token - Authentication token') + quit() +# Authorization +elif len(sys.argv) == 2: + client = Client(sys.argv[1]).init() + +# Current daily playlist +PersonalPlaylistBlocks = client.landing(blocks=['personalplaylists']).blocks[0] +DailyPlaylist = next( + x.data.data for x in PersonalPlaylistBlocks.entities if x.data.data.generated_playlist_type == 'playlistOfTheDay' +) + +# Check if we don't need to update it +if DailyPlaylist.play_counter.updated: + modifiedDate = datetime.datetime.strptime(DailyPlaylist.modified, "%Y-%m-%dT%H:%M:%S%z").date() + if datetime.datetime.now().date() == modifiedDate: + print('\x1b[6;30;43m' + 'Looks like it has been already updated today' + '\x1b[0m') + quit() + +# Updated playlist +updatedPlaylist = client.users_playlists(user_id=DailyPlaylist.uid, kind=DailyPlaylist.kind)[0] + +if updatedPlaylist.play_counter.updated and not DailyPlaylist.play_counter.updated: + print('\x1b[6;30;42m' + 'Success!' + '\x1b[0m') +else: + print('\x1b[6;30;41m' + 'Something has gone wrong and nothing updated' + '\x1b[0m') + + # Debug information + print('Before:\n modified: %s\n PlayCounter: %s' % (DailyPlaylist.modified, DailyPlaylist.play_counter)) + print('After:\n modified: %s\n PlayCounter: %s' % (updatedPlaylist.modified, updatedPlaylist.play_counter)) +``` diff --git a/docs/source/examples.get_album_with_tracks.md b/docs/source/examples.get_album_with_tracks.md new file mode 100644 index 00000000..1aed554f --- /dev/null +++ b/docs/source/examples.get_album_with_tracks.md @@ -0,0 +1,44 @@ +# Получение альбома с треками + +Пример получения информации об альбоме. Пример отображения треков вместе с исполнителями и названием. + +```python +import os + +from yandex_music import Client + +# без авторизации недоступен список треков альбома +TOKEN = os.environ.get('TOKEN') +ALBUM_ID = 2832563 + +client = Client(TOKEN).init() + +album = client.albums_with_tracks(ALBUM_ID) +tracks = [] +for i, volume in enumerate(album.volumes): + if len(album.volumes) > 1: + tracks.append(f'💿 Диск {i + 1}') + tracks += volume + +text = 'АЛЬБОМ\n\n' +text += f'{album.title}\n' +text += f"Исполнитель: {', '.join([artist.name for artist in album.artists])}\n" +text += f'{album.year} · {album.genre}\n' + +cover = album.cover_uri +if cover: + text += f'Обложка: {cover.replace("%%", "400x400")}\n\n' + +text += 'Список треков:' + +print(text) + +for track in tracks: + if isinstance(track, str): + print(track) + else: + artists = '' + if track.artists: + artists = ' - ' + ', '.join(artist.name for artist in track.artists) + print(track.title + artists) +``` diff --git a/docs/source/examples.like_and_dislike.md b/docs/source/examples.like_and_dislike.md new file mode 100644 index 00000000..11d22e0a --- /dev/null +++ b/docs/source/examples.like_and_dislike.md @@ -0,0 +1,32 @@ +# Лайки и дизлайки сущностей + +Пример установки отметок "Мне нравится" и "Мне не нравится" на альбомы, треки, плейлисты и исполнителей. + +```python +import os + +from yandex_music import Client + + +TOKEN = os.environ.get('TOKEN') +ALBUM_ID = 2832563 + +client = Client(TOKEN).init() + +success = client.users_likes_albums_add(ALBUM_ID) +answer = 'Лайкнут' if success else 'Произошла ошибка' + +print(answer) + +success = client.users_likes_albums_remove(ALBUM_ID) +answer = 'Дизлайкнут' if success else 'Произошла ошибка' + +print(answer) + +# Тоже самое и в другими сущностями (плейлист, трек, исполнитель) +# client.users_likes_playlists_add(f'{user_id}:{playlist_id}') +# client.users_likes_playlists_remove(f'{user_id}:{playlist_id}') +# client.users_likes_tracks_add(track_id) +# client.users_likes_tracks_remove(track_id) +# и т.д. Читайте документацию. +``` diff --git a/docs/source/examples.lyrics_playing_track.md b/docs/source/examples.lyrics_playing_track.md new file mode 100644 index 00000000..1780151f --- /dev/null +++ b/docs/source/examples.lyrics_playing_track.md @@ -0,0 +1,34 @@ +# Текст текущего играющего трека + +Пример работы с очередями и получением текста. Выводит текущий проигрываемый трек и его текст. + +```python +import os + +from yandex_music import Client +from yandex_music.exceptions import NotFoundError + + +TOKEN = os.environ.get('TOKEN') + +client = Client(TOKEN).init() + +queues = client.queues_list() +# Последняя проигрываемая очередь всегда в начале списка +last_queue = client.queue(queues[0].id) + +last_track_id = last_queue.get_current_track() +last_track = last_track_id.fetch_track() + +artists = ', '.join(last_track.artists_name()) +title = last_track.title +print(f'Сейчас играет: {artists} - {title}') + +try: + lyrics = last_track.get_lyrics('LRC') + print(lyrics.fetch_lyrics()) + + print(f'\nИсточник: {lyrics.major.pretty_name}') +except NotFoundError: + print('Текст песни отсутствует') +``` diff --git a/docs/source/examples.md b/docs/source/examples.md new file mode 100644 index 00000000..bcf70c0e --- /dev/null +++ b/docs/source/examples.md @@ -0,0 +1,25 @@ +# Примеры + +В этом разделе есть небольшие примеры, чтобы показать, как выглядят скрипты, +написанные с помощью `yandex-music-api`. + +Перед просмотром примеров обязательно прочитайте секцию "[Начало работы](https://github.com/MarshalX/yandex-music-api#%D0%B8%D0%B7%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BF%D0%BE-%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80%D0%B0%D0%BC)" +в основном README файле. Там есть сниппеты, которые помогут разобраться. + +Все примеры лицензированы в соответствии с +[Лицензией CC0](https://github.com/MarshalX/yandex-music-api/blob/master/examples/LICENSE.txt) +и поэтому полностью предназначены для общественного достояния. +Вы можете использовать их в качестве базы для своих собственных скриптов, +не беспокоясь об авторских правах. + +```{toctree} +examples.chart.md +examples.daily_playlist_updater.md +examples.get_album_with_tracks.md +examples.like_and_dislike.md +examples.lyrics_playing_track.md +examples.proxy.md +examples.search.md +``` + +Больше примеров доступно в папке [examples](https://github.com/MarshalX/yandex-music-api/tree/main/examples)! diff --git a/docs/source/examples.proxy.md b/docs/source/examples.proxy.md new file mode 100644 index 00000000..b6448b53 --- /dev/null +++ b/docs/source/examples.proxy.md @@ -0,0 +1,32 @@ +# Использование прокси + +Пример использования прокси, когда у пользователя нет подписки +(так как Яндекс.Музыка недоступна за пределами СНГ. Актуально для расположения +скрипта на зарубежном сервере). При проблемах с авторизацией или токеном +использование клиента без авторизации. + +```python +import os + +from yandex_music import Client +from yandex_music.exceptions import YandexMusicError +from yandex_music.utils.request import Request + +yandex_music_token = os.environ.get('YANDEX_MUSIC_TOKEN') +proxied_request = Request(proxy_url=os.environ.get('PROXY_URL')) + +try: + if not yandex_music_token: + raise YandexMusicError() + + # подключаемся без прокси для получения информации об аккаунте (доступно из других стран) + client = Client(yandex_music_token, request=Request()).init() + # проверяем отсутствие подписки у пользователя + if client.me and client.me.plus and not client.me.plus.has_plus: + # если подписки нет - пересоздаем клиент с использованием прокси + client = Client(yandex_music_token, request=proxied_request).init() +except YandexMusicError: + # если есть проблемы с авторизацией, токеном или чем-либо еще, то инициализируем клиент без авторизации + # так как сервисом можно пользоваться будучи гостем, но со своими ограничениями + client = Client(request=proxied_request) +``` diff --git a/docs/source/examples.search.md b/docs/source/examples.search.md new file mode 100644 index 00000000..825b6321 --- /dev/null +++ b/docs/source/examples.search.md @@ -0,0 +1,70 @@ +# Работа с поиском + +Пример работы с поиском. Осуществление поисковых запросов, обработка лучшего результата и отображение статистики по найденным данным. + +```python +from yandex_music import Client + + +client = Client().init() + +type_to_name = { + 'track': 'трек', + 'artist': 'исполнитель', + 'album': 'альбом', + 'playlist': 'плейлист', + 'video': 'видео', + 'user': 'пользователь', + 'podcast': 'подкаст', + 'podcast_episode': 'эпизод подкаста', +} + + +def send_search_request_and_print_result(query): + search_result = client.search(query) + + text = [f'Результаты по запросу "{query}":', ''] + + best_result_text = '' + if search_result.best: + type_ = search_result.best.type + best = search_result.best.result + + text.append(f'❗️Лучший результат: {type_to_name.get(type_)}') + + if type_ in ['track', 'podcast_episode']: + artists = '' + if best.artists: + artists = ' - ' + ', '.join(artist.name for artist in best.artists) + best_result_text = best.title + artists + elif type_ == 'artist': + best_result_text = best.name + elif type_ in ['album', 'podcast']: + best_result_text = best.title + elif type_ == 'playlist': + best_result_text = best.title + elif type_ == 'video': + best_result_text = f'{best.title} {best.text}' + + text.append(f'Содержимое лучшего результата: {best_result_text}\n') + + if search_result.artists: + text.append(f'Исполнителей: {search_result.artists.total}') + if search_result.albums: + text.append(f'Альбомов: {search_result.albums.total}') + if search_result.tracks: + text.append(f'Треков: {search_result.tracks.total}') + if search_result.playlists: + text.append(f'Плейлистов: {search_result.playlists.total}') + if search_result.videos: + text.append(f'Видео: {search_result.videos.total}') + + text.append('') + print('\n'.join(text)) + + +if __name__ == '__main__': + while True: + input_query = input('Введите поисковой запрос: ') + send_search_request_and_print_result(input_query) +``` diff --git a/docs/source/index.rst b/docs/source/index.rst index 130009d1..7c5e4950 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,10 +6,20 @@ Документация библиотеки ======================= -.. include:: ../../README.rst +.. include:: ../../README.md + :parser: myst_parser.sphinx_ .. toctree:: + :maxdepth: 2 readme - yandex_music + token + client + client_async + examples + module changes + contributing + code_of_conduct + security + licence diff --git a/docs/source/licence.md b/docs/source/licence.md new file mode 100644 index 00000000..3b3a2484 --- /dev/null +++ b/docs/source/licence.md @@ -0,0 +1,173 @@ +# Лицензия + +Вы можете копировать, распространять и модифицировать программное обеспечение при условии, что модификации описаны и лицензированы бесплатно в соответствии с [LGPL-3](https://www.gnu.org/licenses/lgpl-3.0.html). Произведения производных (включая модификации или что-либо статически связанное с библиотекой) могут распространяться только в соответствии с LGPL-3, но приложения, которые используют библиотеку, необязательно. + +Лицензия в репозитории: [LICENSE](https://github.com/MarshalX/yandex-music-api/blob/main/LICENSE) + +```text + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. +``` diff --git a/docs/source/module.md b/docs/source/module.md new file mode 100644 index 00000000..e82341c7 --- /dev/null +++ b/docs/source/module.md @@ -0,0 +1,5 @@ +# Модуль + +```{eval-rst} +.. include:: yandex_music.rst +``` diff --git a/docs/source/readme.content.md b/docs/source/readme.content.md new file mode 100644 index 00000000..07e29a4c --- /dev/null +++ b/docs/source/readme.content.md @@ -0,0 +1,247 @@ +# Введение + +Эта библиотека предоставляется Python интерфейс для никем незадокументированного и сделанного только для себя API Яндекс Музыки. + +Она совместима с версиями Python 3.7+ и поддерживает работу как с синхронном, так и асинхронным (asyncio) кодом. + +В дополнение к реализации чистого API данная библиотека имеет ряд классов-обёрток объектов высокого уровня дабы сделать разработку клиентов и скриптов простой и понятной. Вся документация была написана с нуля исходя из логического анализа в ходе обратной разработки(reverse engineering) API. + +## Доступ к вашим данным Яндекс.Музыка + +Начиная с версии [2.0.0](https://github.com/MarshalX/yandex-music-api/blob/a30082f4929e56381c870cb03103777ae29bcc6b/CHANGES.rst#%D0%B2%D0%B5%D1%80%D1%81%D0%B8%D1%8F-200) библиотека больше не предоставляет интерфейсы для работы с OAuth Яндекс и Яндекс.Паспорт. Задача по получению токена для доступа к данным на плечах разработчиков использующих данную библиотеку. О том как получить токен читайте в следующем разделе. + +# Установка + +Вы можете установить или обновить Yandex Music API при помощи: + +``` shell +pip install -U yandex-music +``` + +Или Вы можете установить из исходного кода с помощью: + +``` shell +git clone https://github.com/MarshalX/yandex-music-api +cd yandex-music-api +python setup.py install +``` + +# Начало работы + +Приступив к работе первым делом необходимо создать экземпляр клиента. + +Инициализация синхронного клиента: + +``` python +from yandex_music import Client + +client = Client() +client.init() + +# или + +client = Client().init() +``` + +Инициализация асинхронного клиента: + +``` python +from yandex_music import ClientAsync + +client = ClientAsync() +await client.init() + +# или + +client = await Client().init() +``` + +Вызов `init()` необходим для получение информации для упрощения будущих запросов. + +Работа без авторизации ограничена. Так, например, для загрузки будут доступны только первые 30 секунд аудиофайла. Для понимания всех ограничений зайдите на сайт Яндекс.Музыка под инкогнито и воспользуйтесь сервисом. + +Для доступа к своим личным данным следует авторизоваться. Это осуществляется через токен аккаунта Яндекс.Музыка. + +Авторизация: + +``` python +from yandex_music import Client + +client = Client('token').init() +``` + +После успешного создания клиента Вы вольны в выборе необходимого метода из API. Все они доступны у объекта класса `Client`. Подробнее в методах клиента в [документации](https://yandex-music.readthedocs.io/en/latest/yandex_music.client.html). + +Пример получения первого трека из плейлиста "Мне нравится" и его загрузка: + +``` python +from yandex_music import Client + +client = Client('token').init() +client.users_likes_tracks()[0].fetch_track().download('example.mp3') +``` + +В примере выше клиент получает список треков которые были отмечены как понравившиеся. API возвращает объект [TracksList](https://yandex-music.readthedocs.io/en/latest/yandex_music.tracks_list.html) в котором содержится список с треками класса [TrackShort](https://yandex-music.readthedocs.io/en/latest/yandex_music.track_short.html). Данный класс содержит наиважнейшую информацию о треке и никаких подробностей, поэтому для получения полной версии трека со всей информацией необходимо обратиться к методу `fetch_track()`. Затем можно скачать трек методом `download()`. + +Пример получения треков по ID: + +``` python +from yandex_music import Client + +client = Client().init() +client.tracks(['10994777:1193829', '40133452:5206873', '48966383:6693286', '51385674:7163467']) +``` + +В качестве ID трека выступает его уникальный номер и номер альбома. Первым треком из примера является следующий трек:music.yandex.ru/album/**1193829**/track/**10994777** + +Выполнение запросов с использование прокси в синхронной версии: + +``` python +from yandex_music.utils.request import Request +from yandex_music import Client + +request = Request(proxy_url='socks5://user:password@host:port') +client = Client(request=request).init() +``` + +Примеры proxy url: +- socks5://user::port +- +- +- + +Больше примеров тут: [proxies - advanced usage - requests](https://2.python-requests.org/en/master/user/advanced/#proxies) + +Выполнение запросов с использование прокси в асинхронной версии: + +``` python +from yandex_music.utils.request_async import Request +from yandex_music import ClientAsync + +request = Request(proxy_url='http://user:pass@some.proxy.com') +client = await ClientAsync(request=request).init() +``` + +Socks прокси не поддерживаются в асинхронной версии. + +Про поддерживаемые прокси тут: [proxy support - advanced usage - aiohttp](https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support) + +## Изучение по примерам + +Вот несколько примеров для обзора. Даже если это не Ваш подход к обучению, пожалуйста, возьмите и бегло просмотрите их. + +Код примеров опубликован в открытом доступе, поэтому Вы можете взять его и начать писать вокруг своё. + +Посетите [эту страницу](https://github.com/MarshalX/yandex-music-api/blob/main/examples/), чтобы изучить официальные примеры. + +## Особенности использования асинхронного клиента + +При работе с асинхронной версией библиотеке стоит всегда помнить +следующие особенности: +- Клиент следует импортировать с названием `ClientAsync`, а не просто `Client`. +- При использовании методов-сокращений нужно выбирать метод с суффиксом `_async`. + +Пояснение ко второму пункту: + +``` python +from yandex_music import ClientAsync + +client = await ClientAsync('token').init() +liked_short_track = (await client.users_likes_tracks())[0] + +# правильно +full_track = await liked_short_track.fetch_track_async() +await full_track.download_async() + +# НЕПРАВИЛЬНО +full_track = await liked_short_track.fetch_track() +await full_track.download() +``` + +## Логирование + +Данная библиотека использует `logging` модуль. Чтобы настроить логирование на стандартный вывод, поместите + +``` python +import logging +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +``` + +в начало вашего скрипта. + +Вы также можете использовать логирование в вашем приложении, вызвав `logging.getLogger()` и установить уровень какой Вы хотите: + +``` python +logger = logging.getLogger() +logger.setLevel(logging.INFO) +``` + +Если Вы хотите `DEBUG` логирование: + +``` python +logger.setLevel(logging.DEBUG) +``` + +# Получение помощи + +Получить помощь можно несколькими путями: +- Задать вопрос в [Telegram чате](https://t.me/yandex_music_api), где мы помогаем друг другу, присоединяйтесь\! +- Сообщить о баге можно [создав Bug Report](https://github.com/MarshalX/yandex-music-api/issues/new?assignees=MarshalX&labels=bug&template=bug-report.md&title=). +- Предложить новую фичу или задать вопрос можно [создав discussion](https://github.com/MarshalX/yandex-music-api/discussions/new). +- Найти ответ на вопрос в [документации библиотеки](https://yandex-music.readthedocs.io/en/latest/). + +# Список изменений + +Весь список изменений ведётся в файле [CHANGES.md](https://github.com/MarshalX/yandex-music-api/blob/main/CHANGES.md). + +# Реализации на других языках + +## C# + +Реализация с совершенно другим подходом, так как используется API для frontend'a, а не мобильных и десктопных приложений: [Winster332/Yandex.Music.Api](https://github.com/Winster332/Yandex.Music.Api). + +[@Winster332](https://github.com/Winster332) не сильно проявляет активность, но существует форк, который продолжил начатое. Эндпоинты изменены с фронтовых на мобильные: [K1llMan/Yandex.Music.Api](https://github.com/K1llMan/Yandex.Music.Api). + +## PHP + +Частично переписанная текущая библиотека на PHP: [LuckyWins/yandex-music-api](https://github.com/LuckyWins/yandex-music-api). + +## JavaScript + +API wrapper на Node.JS. Не обновлялся больше двух лет: [itsmepetrov/yandex-music-api](https://github.com/itsmepetrov/yandex-music-api). Продолжение разработки заброшенной библиотеки: [kontsevoye/ym-api](https://github.com/kontsevoye/ym-api). + +# Разработанные проекты + +## Плагин для Kodi + +Плагин может проигрывать пользовательские плейлисты и плейлисты Яндекса, поиск по Яндекс Музыке, радио. + +Сайт проекта: [ymkodi.ru](https://ymkodi.ru/). Исходный код: [kodi.plugin.yandex-music](https://github.com/Angel777d/kodi.plugin.yandex-music). +Автор: [@Angel777d](https://github.com/Angel777d). + +[![Плагин для Kodi](https://raw.githubusercontent.com/Angel777d/kodi.plugin.yandex-music/master/assets/img/kody_yandex_music_plugin.png)](https://ymkodi.ru/) + +## Telegram бот-клиент + +Неофициальный бот. Умные и ваши плейлисты, понравившиеся треки. Лайки, дизлайки, текста песен, поиск, распознавание песен, похожие треки! Полноценный клиент на базе мессенджера. + +Сайт проекта: [music-yandex-bot.ru](https://music-yandex-bot.ru/). Бот в Telegram: [@music\_yandex\_bot](https://t.me/music_yandex_bot). Автор: [@MarshalX](https://github.com/MarshalX). + +Статья на habr.com с описанием реализации: [Под капотом бота-клиента Яндекс.Музыки](https://habr.com/ru/post/487428/). + +[![Telegram бот-клиент](https://hsto.org/webt/uv/4s/a3/uv4sa3pslohuzlmuzrjzteju2dk.png)](https://music-yandex-bot.ru/) + +# Благодарность и спонсоры + +Спасибо разработчикам `python-telegram-bot`. Выбрал Вас в качестве примера. + +## JetBrains + +JetBrains Logo (Main) logo. + +> JetBrains предоставляет бесплатный набор инструментов для разработки активным контрибьюторам некоммерческих проектов с открытым исходным кодом. + +[Лицензии для проектов с открытым исходным кодом — Программы поддержки](https://jb.gg/OpenSourceSupport) diff --git a/docs/source/readme.md b/docs/source/readme.md new file mode 100644 index 00000000..aa8866c3 --- /dev/null +++ b/docs/source/readme.md @@ -0,0 +1,20 @@ +# Беглый обзор + +## Yandex Music API + +> Делаю то, что по определённым причинам не сделала компания Yandex. + +⚠️ Это неофициальная библиотека. + +Сообщество разработчиков общаются и помогают друг другу в [Telegram чате](https://t.me/yandex_music_api), присоединяйтесь! + +[![Поддерживаемые Python версии](https://img.shields.io/badge/python-3.7+-blue.svg)](https://pypi.org/project/yandex-music/) +[![Покрытие кода тестами](https://codecov.io/gh/MarshalX/yandex-music-api/branch/main/graph/badge.svg)](https://codecov.io/gh/MarshalX/yandex-music-api) +[![Качество кода](https://api.codacy.com/project/badge/Grade/27011a5a8d9f4b278d1bfe2fe8725fed)](https://app.codacy.com/gh/MarshalX/yandex-music-api) +[![Статус тестов](https://github.com/MarshalX/yandex-music-api/actions/workflows/pytest_full.yml/badge.svg)](https://github.com/MarshalX/yandex-music-api/actions/workflows/pytest_full.yml) +[![Статус документации](https://readthedocs.org/projects/yandex-music/badge/?version=latest)](https://yandex-music.readthedocs.io/en/latest/?badge=latest) +[![Лицензия LGPLv3](https://img.shields.io/badge/license-LGPLv3-lightgrey.svg)](https://www.gnu.org/licenses/lgpl-3.0.html) + +```{toctree} +readme.content.md +``` diff --git a/docs/source/readme.rst b/docs/source/readme.rst deleted file mode 100644 index 38ba8043..00000000 --- a/docs/source/readme.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../README.rst \ No newline at end of file diff --git a/docs/source/security.md b/docs/source/security.md new file mode 100644 index 00000000..ff2e9449 --- /dev/null +++ b/docs/source/security.md @@ -0,0 +1,2 @@ +```{include} ../../SECURITY.md +``` diff --git a/docs/source/token.md b/docs/source/token.md new file mode 100644 index 00000000..8764e625 --- /dev/null +++ b/docs/source/token.md @@ -0,0 +1,17 @@ +# Получение токена + +**Своё OAuth приложение создать нельзя.** Единственный вариант это использовать приложения официальных клиентов Яндекс.Музыка. + +**Существует основные варианты получения токена:** +- [Вебсайт](https://music-yandex-bot.ru/) (работает не для всех аккаунтов) +- Android приложение: [APK файл](https://github.com/MarshalX/yandex-music-token/releases) +- Расширение для [Google Chrome](https://chrome.google.com/webstore/detail/yandex-music-token/lcbjeookjibfhjjopieifgjnhlegmkib) +- Расширение для [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/yandex-music-token/) + +Каждый вариант выше позволяет скопировать токен. Код каждого варианта [открыт](https://github.com/MarshalX/yandex-music-token). + +**Полезные ссылки:** +- [Способ вместо расширения для продвинутых](https://github.com/MarshalX/yandex-music-api/discussions/513#discussioncomment-2729781) +- [Скрипт получения токена из другого проекта для Яндекс Станции](https://github.com/AlexxIT/YandexStation/blob/master/custom_components/yandex_station/core/yandex_session.py) + +Полученный токен можно передавать в конструктор классов `yandex_music.Client` и `yandex_client.ClientAsync`. diff --git a/docs/source/yandex_music.playlist.custom_wave.rst b/docs/source/yandex_music.playlist.custom_wave.rst new file mode 100644 index 00000000..8834cee9 --- /dev/null +++ b/docs/source/yandex_music.playlist.custom_wave.rst @@ -0,0 +1,7 @@ +yandex\_music.playlist.custom\_wave +=================================== + +.. automodule:: yandex_music.playlist.custom_wave + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/yandex_music.playlist.rst b/docs/source/yandex_music.playlist.rst index 24070435..f49ae339 100644 --- a/docs/source/yandex_music.playlist.rst +++ b/docs/source/yandex_music.playlist.rst @@ -15,6 +15,7 @@ Submodules yandex_music.playlist.brand yandex_music.playlist.case_forms yandex_music.playlist.contest + yandex_music.playlist.custom_wave yandex_music.playlist.made_for yandex_music.playlist.open_graph_data yandex_music.playlist.play_counter diff --git a/docs/source/yandex_music.track.lyrics_info.rst b/docs/source/yandex_music.track.lyrics_info.rst new file mode 100644 index 00000000..af853e61 --- /dev/null +++ b/docs/source/yandex_music.track.lyrics_info.rst @@ -0,0 +1,7 @@ +yandex\_music.track.lyrics\_info +================================ + +.. automodule:: yandex_music.track.lyrics_info + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/yandex_music.track.lyrics_major.rst b/docs/source/yandex_music.track.lyrics_major.rst new file mode 100644 index 00000000..1c717ab5 --- /dev/null +++ b/docs/source/yandex_music.track.lyrics_major.rst @@ -0,0 +1,7 @@ +yandex\_music.track.lyrics\_major +================================= + +.. automodule:: yandex_music.track.lyrics_major + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/yandex_music.track.r128.rst b/docs/source/yandex_music.track.r128.rst new file mode 100644 index 00000000..5dc5dbfb --- /dev/null +++ b/docs/source/yandex_music.track.r128.rst @@ -0,0 +1,7 @@ +yandex\_music.track.r128 +======================== + +.. automodule:: yandex_music.track.r128 + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/yandex_music.track.rst b/docs/source/yandex_music.track.rst index b5f395dc..07a4bac1 100644 --- a/docs/source/yandex_music.track.rst +++ b/docs/source/yandex_music.track.rst @@ -13,9 +13,13 @@ Submodules :maxdepth: 4 yandex_music.track.licence_text_part + yandex_music.track.lyrics_info + yandex_music.track.lyrics_major yandex_music.track.major yandex_music.track.meta_data yandex_music.track.normalization yandex_music.track.poetry_lover_match + yandex_music.track.r128 yandex_music.track.track + yandex_music.track.track_lyrics yandex_music.track.tracks_similar diff --git a/docs/source/yandex_music.track.track_lyrics.rst b/docs/source/yandex_music.track.track_lyrics.rst new file mode 100644 index 00000000..49108cdd --- /dev/null +++ b/docs/source/yandex_music.track.track_lyrics.rst @@ -0,0 +1,7 @@ +yandex\_music.track.track\_lyrics +================================= + +.. automodule:: yandex_music.track.track_lyrics + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/yandex_music.utils.convert_track_id.rst b/docs/source/yandex_music.utils.convert_track_id.rst new file mode 100644 index 00000000..a3c3e2be --- /dev/null +++ b/docs/source/yandex_music.utils.convert_track_id.rst @@ -0,0 +1,7 @@ +yandex\_music.utils.convert\_track\_id +====================================== + +.. automodule:: yandex_music.utils.convert_track_id + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/yandex_music.utils.rst b/docs/source/yandex_music.utils.rst index 35e7cc10..9a6bdfbb 100644 --- a/docs/source/yandex_music.utils.rst +++ b/docs/source/yandex_music.utils.rst @@ -12,7 +12,9 @@ Submodules .. toctree:: :maxdepth: 4 + yandex_music.utils.convert_track_id yandex_music.utils.difference yandex_music.utils.request yandex_music.utils.request_async yandex_music.utils.response + yandex_music.utils.sign_request diff --git a/docs/source/yandex_music.utils.sign_request.rst b/docs/source/yandex_music.utils.sign_request.rst new file mode 100644 index 00000000..2fa57bc4 --- /dev/null +++ b/docs/source/yandex_music.utils.sign_request.rst @@ -0,0 +1,7 @@ +yandex\_music.utils.sign\_request +================================= + +.. automodule:: yandex_music.utils.sign_request + :members: + :undoc-members: + :show-inheritance: diff --git a/examples/lyrics_playing_track.py b/examples/lyrics_playing_track.py index baaa3496..256f8b87 100644 --- a/examples/lyrics_playing_track.py +++ b/examples/lyrics_playing_track.py @@ -1,6 +1,7 @@ import os from yandex_music import Client +from yandex_music.exceptions import NotFoundError TOKEN = os.environ.get('TOKEN') @@ -18,8 +19,10 @@ title = last_track.title print(f'Сейчас играет: {artists} - {title}') -supplement = last_track.get_supplement() -if supplement.lyrics: - print(supplement.lyrics.full_lyrics) -else: +try: + lyrics = last_track.get_lyrics('LRC') + print(lyrics.fetch_lyrics()) + + print(f'\nИсточник: {lyrics.major.pretty_name}') +except NotFoundError: print('Текст песни отсутствует') diff --git a/examples/player.py b/examples/player.py index 7b8d9240..a32fcb2b 100644 --- a/examples/player.py +++ b/examples/player.py @@ -43,7 +43,7 @@ print(args) sys.exit() -if type(args.token) is str and re.match(r'^[A-z0-9]{39}$', args.token): +if type(args.token) is str and re.match(r'^[A-Za-z0-9]{39}$', args.token): if not args.no_save_token: parser.get_default('token').write_text(args.token) else: diff --git a/examples/radio_example/README.md b/examples/radio_example/README.md index c4732e5b..83b753f2 100644 --- a/examples/radio_example/README.md +++ b/examples/radio_example/README.md @@ -1,7 +1,7 @@ # Пример работы с радио Документация: -- [rotor_station_tracks](https://yandex-music.readthedocs.io/ru/latest/yandex_music.client.html#yandex_music.Client.rotor_station_tracks) – +- [rotor_station_tracks](https://yandex-music.readthedocs.io/en/latest/yandex_music.client.html#yandex_music.client.Client.rotor_station_tracks) – Получение цепочки треков определённой станции. Читайте примечание. Примеры: diff --git a/generate_async_version.py b/generate_async_version.py old mode 100644 new mode 100755 index a3f19ee7..3b4aad96 --- a/generate_async_version.py +++ b/generate_async_version.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 import subprocess @@ -8,7 +9,7 @@ def gen_request(output_request_filename): - with open('yandex_music/utils/request.py', 'r') as f: + with open('yandex_music/utils/request.py', 'r', encoding='UTF-8') as f: code = f.read() code = code.replace('import requests', 'import asyncio\nimport aiohttp\nimport aiofiles') @@ -29,10 +30,9 @@ def gen_request(output_request_filename): code = code.replace(f'self.{method}(', f'await self.{method}(') code = code.replace('proxies=self.proxies', 'proxy=self.proxy_url') - code = code.replace('timeout=timeout', 'timeout=aiohttp.ClientTimeout(total=timeout)') - # undo one specific case code = code.replace( - 'self.retrieve(url, timeout=aiohttp.ClientTimeout(total=timeout)', 'self.retrieve(url, timeout=timeout' + "kwargs['timeout'] = self._timeout", + f"kwargs['timeout'] = aiohttp.ClientTimeout(total=self._timeout)\n{' ' * 8}else:\n{' ' * 12}kwargs['timeout'] = aiohttp.ClientTimeout(total=kwargs['timeout'])", ) # download method @@ -44,12 +44,12 @@ def gen_request(output_request_filename): code = code.replace('requests.request', 'aiohttp.request') code = DISCLAIMER + code - with open(output_request_filename, 'w') as f: + with open(output_request_filename, 'w', encoding='UTF-8') as f: f.write(code) def gen_client(output_client_filename): - with open('yandex_music/client.py', 'r') as f: + with open('yandex_music/client.py', 'r', encoding='UTF-8') as f: code = f.read() code = code.replace('Client', 'ClientAsync') @@ -69,18 +69,12 @@ def gen_client(output_client_filename): code = code.replace(f'self.{method}(', f'await self.{method}(') # specific cases - code = code.replace( - 'self.users_playlists_change(', - 'await self.users_playlists_change(' - ) - code = code.replace( - 'self.rotor_station_feedback(', - 'await self.rotor_station_feedback(' - ) + code = code.replace('self.users_playlists_change(', 'await self.users_playlists_change(') + code = code.replace('self.rotor_station_feedback(', 'await self.rotor_station_feedback(') code = code.replace('return DownloadInfo.de_list', 'return await DownloadInfo.de_list_async') code = DISCLAIMER + code - with open(output_client_filename, 'w') as f: + with open(output_client_filename, 'w', encoding='UTF-8') as f: f.write(code) diff --git a/generate_camel_case_aliases.py b/generate_camel_case_aliases.py new file mode 100755 index 00000000..ef425415 --- /dev/null +++ b/generate_camel_case_aliases.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +import os +import ast + +SOURCE_FOLDER = 'yandex_music' +EXCLUDED_FUNCTIONS = {'de_dict', 'de_json', 'de_list', 'de_json_async', 'de_list_async'} + +ALIAS_TEMPLATE = ''' +#: Псевдоним для :attr:`{name}` +{camel_case_name} = {name} +''' + +ALIAS_SECTION_MARKER = ' # camelCase псевдонимы' + + +def _validate_function_name(function_name: str) -> bool: + if function_name.startswith('_'): + return False + + if function_name in EXCLUDED_FUNCTIONS: + return False + + # camel case will be the same + if '_' not in function_name: + return False + + return True + + +def convert_snake_case_to_camel_case(string: str) -> str: + camel_case = ''.join(word.title() for word in string.split('_')) + return camel_case[0].lower() + camel_case[1:] + + +def _generate_code(function_name: str, intent=0) -> str: + camel_case_name = convert_snake_case_to_camel_case(function_name) + code = ALIAS_TEMPLATE.format(name=function_name, camel_case_name=camel_case_name) + + code_lines = [line for line in code.split('\n') if line] + code_lines = [f'{" " * intent}{line}' for line in code_lines] + code = '\n'.join(code_lines) + + return code + + +def _process_file(file: str) -> None: + with open(file, 'r', encoding='UTF-8') as f: + count_of_class_def = 0 + file_aliases_code_fragments = [] + tree = ast.parse(f.read()) + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + count_of_class_def += 1 + + if isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef): + if _validate_function_name(node.name): + alias_code = _generate_code(node.name, node.col_offset) + file_aliases_code_fragments.append(alias_code) + + # there are no such cases in data models yet + # only in yandex_music/exceptions.py and yandex_music/utils/difference.py + if count_of_class_def != 1: + return + + f.seek(0) + file_code_lines = f.read().splitlines() + + marker_lineno = None + for lineno, code_line in enumerate(file_code_lines): + if code_line == ALIAS_SECTION_MARKER: + marker_lineno = lineno + break + + # we can't process files without markers now + if marker_lineno is None: + return + + # remove prev aliases + file_code_lines = file_code_lines[:marker_lineno + 1] + file_code_lines.append('') + file_code_lines.extend(file_aliases_code_fragments) + file_code_lines.append('') + + new_file_code = '\n'.join(file_code_lines) + + with open(file, 'w', encoding='UTF-8') as f: + f.write(new_file_code) + + +def main(): + for root, _, files in os.walk(SOURCE_FOLDER): + for file in files: + if file.endswith('.py') and file != '__init__.py': + filepath = os.path.join(root, file) + _process_file(filepath) + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 360565b0..4a728e1f 100644 --- a/setup.py +++ b/setup.py @@ -15,13 +15,13 @@ def run_tests(self): with open('yandex_music/__init__.py', encoding='utf-8') as f: version = re.findall(r"__version__ = '(.+)'", f.read())[0] -with open('README.rst', 'r', encoding='utf-8') as f: +with open('README.md', 'r', encoding='utf-8') as f: readme = f.read() setup( name='yandex-music', version=version, - author='Il`ya Semyonov', + author='Ilya (Marshal)', author_email='ilya@marshal.dev', license='LGPLv3', url='https://github.com/MarshalX/yandex-music-api/', @@ -29,6 +29,7 @@ def run_tests(self): 'яндекс музыка апи обёртка библиотека клиент', description='Неофициальная Python библиотека для работы с API сервиса Яндекс.Музыка.', long_description=readme, + long_description_content_type='text/markdown', packages=find_packages(), install_requires=['requests[socks]', 'aiohttp', 'aiofiles'], include_package_data=True, @@ -48,6 +49,7 @@ def run_tests(self): 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', "Programming Language :: Python :: Implementation", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", @@ -56,10 +58,9 @@ def run_tests(self): cmdclass={'test': PyTest}, tests_require=['pytest'], project_urls={ - 'Code': 'https://github.com/MarshalX/yandex-music-api', - 'Documentation': 'https://yandex-music.readthedocs.io', - 'Chat': 'https://t.me/yandex_music_api', + 'Documentation': 'https://yandex-music.rtfd.io', + 'Telegram chat': 'https://t.me/yandex_music_api', 'Codecov': 'https://codecov.io/gh/MarshalX/yandex-music-api', - 'Codacy': 'https://www.codacy.com/manual/MarshalX/yandex-music-api', + 'Codacy': 'https://app.codacy.com/gh/MarshalX/yandex-music-api', }, ) diff --git a/tests/__init__.py b/tests/__init__.py index 9120e2e2..b7f60fd7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -93,3 +93,8 @@ from .test_context import TestContext from .test_queue_item import TestQueueItem from .test_deprecation import TestDeprecation +from .test_lyrics_major import TestLyricsMajor +from .test_track_lyrics import TestTrackLyrics +from .test_custom_wave import TestCustomWave +from .test_r128 import TestR128 +from .test_lyrics_info import TestLyricsInfo diff --git a/tests/conftest.py b/tests/conftest.py index 84a46506..a7301719 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ Client, Counts, Cover, + CustomWave, Day, Description, DiscreteScale, @@ -92,6 +93,10 @@ Brand, Context, Deprecation, + TrackLyrics, + LyricsMajor, + R128, + LyricsInfo, ) from . import ( TestAccount, @@ -108,6 +113,7 @@ TestChartInfoMenuItem, TestCounts, TestCover, + TestCustomWave, TestDay, TestDescription, TestDiscreteScale, @@ -178,6 +184,10 @@ TestBrand, TestContext, TestDeprecation, + TestLyricsMajor, + TestTrackLyrics, + TestR128, + TestLyricsInfo, ) @@ -242,7 +252,7 @@ def artist_decomposed(artist_without_nested_artist): @pytest.fixture(scope='session') -def track_factory(major, normalization, user, meta_data, poetry_lover_match): +def track_factory(major, normalization, user, meta_data, poetry_lover_match, r_128, lyrics_info): class TrackFactory: def get(self, artists, albums, track_without_nested_tracks=None): return Track( @@ -284,6 +294,11 @@ def get(self, artists, albums, track_without_nested_tracks=None): TestTrack.background_video_uri, TestTrack.short_description, TestTrack.is_suitable_for_children, + TestTrack.track_source, + TestTrack.available_for_options, + r_128, + lyrics_info, + TestTrack.track_sharing_flag, ) return TrackFactory() @@ -314,6 +329,26 @@ def track_without_nested_tracks(artist, album, track_factory): return track_factory.get([artist], [album]) +@pytest.fixture(scope='session') +def lyrics_major(): + return LyricsMajor( + TestLyricsMajor.id, + TestLyricsMajor.name, + TestLyricsMajor.pretty_name, + ) + + +@pytest.fixture(scope='session') +def track_lyrics(lyrics_major): + return TrackLyrics( + TestTrackLyrics.download_url, + TestTrackLyrics.lyric_id, + TestTrackLyrics.external_lyric_id, + TestTrackLyrics.writer, + lyrics_major, + ) + + @pytest.fixture(scope='session') def album_factory(label, track_position): class AlbumFactory: @@ -364,6 +399,7 @@ def get(self, artists, volumes, albums=None, deprecation=None): TestAlbum.likes_count, deprecation, TestAlbum.available_regions, + TestAlbum.available_for_options, ) return AlbumFactory() @@ -399,6 +435,8 @@ def playlist_factory( contest, open_graph_data, brand, + custom_wave, + pager, ): class PlaylistFactory: def get(self, similar_playlists, last_owner_playlists): @@ -458,6 +496,8 @@ def get(self, similar_playlists, last_owner_playlists): TestPlaylist.ready, TestPlaylist.is_for_from, TestPlaylist.regions, + custom_wave, + pager, ) return PlaylistFactory() @@ -481,6 +521,7 @@ def generated_playlist(playlist): TestGeneratedPlaylist.notify, playlist, TestGeneratedPlaylist.description, + TestGeneratedPlaylist.preview_description, ) @@ -860,6 +901,7 @@ def account(passport_phone): [passport_phone], TestAccount.registered_at, TestAccount.has_info_for_app_metrica, + TestAccount.child, ) @@ -879,6 +921,7 @@ def subscription(renewable_remainder, auto_renewable, operator, non_auto_renewab renewable_remainder, [auto_renewable], [auto_renewable], + TestSubscription.had_any_subscription, [operator], non_auto_renewable, TestSubscription.can_start_trial, @@ -998,6 +1041,8 @@ def status(account, permissions, subscription, plus, station_data, alert): alert, TestStatus.premium_region, TestStatus.experiment, + TestStatus.pretrial_active, + TestStatus.userhash, ) @@ -1131,7 +1176,15 @@ def chart_item(track, chart): @pytest.fixture(scope='session') def station_result(station, rotor_settings, ad_params): return StationResult( - station, rotor_settings, rotor_settings, ad_params, TestStationResult.explanation, TestStationResult.prerolls + station, + rotor_settings, + rotor_settings, + ad_params, + TestStationResult.explanation, + TestStationResult.prerolls, + TestStationResult.rup_title, + TestStationResult.rup_description, + TestStationResult.custom_name, ) @@ -1262,3 +1315,18 @@ def search_result_with_results_and_type(request, types, results): [results[request.param]], types[request.param], ) + + +@pytest.fixture(scope='session') +def custom_wave(): + return CustomWave(TestCustomWave.title, TestCustomWave.animation_url, TestCustomWave.position) + + +@pytest.fixture(scope='session') +def r_128(): + return R128(TestR128.i, TestR128.tp) + + +@pytest.fixture(scope='session') +def lyrics_info(): + return LyricsInfo(TestLyricsInfo.has_available_sync_lyrics, TestLyricsInfo.has_available_text_lyrics) diff --git a/tests/test_account.py b/tests/test_account.py index 22e9854a..eabade83 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -15,6 +15,7 @@ class TestAccount: birthday = '1999-08-10' registered_at = '2018-06-10T09:34:22+00:00' has_info_for_app_metrica = False + child = False def test_expected_values(self, account, passport_phone): assert account.now == self.now @@ -31,16 +32,18 @@ def test_expected_values(self, account, passport_phone): assert account.passport_phones == [passport_phone] assert account.registered_at == self.registered_at assert account.has_info_for_app_metrica == self.has_info_for_app_metrica + assert account.child == self.child def test_de_json_none(self, client): assert Account.de_json({}, client) is None def test_de_json_required(self, client): - json_dict = {'now': self.now, 'service_available': self.service_available} + json_dict = {'now': self.now, 'service_available': self.service_available, 'child': self.child} account = Account.de_json(json_dict, client) assert account.now == self.now assert account.service_available == self.service_available + assert account.child == self.child def test_de_json_all(self, client, passport_phone): json_dict = { @@ -58,6 +61,7 @@ def test_de_json_all(self, client, passport_phone): 'passport_phones': [passport_phone.to_dict()], 'registered_at': self.registered_at, 'has_info_for_app_metrica': self.has_info_for_app_metrica, + 'child': self.child, } account = Account.de_json(json_dict, client) @@ -75,9 +79,10 @@ def test_de_json_all(self, client, passport_phone): assert account.passport_phones == [passport_phone] assert account.registered_at == self.registered_at assert account.has_info_for_app_metrica == self.has_info_for_app_metrica + assert account.child == self.child def test_equality(self, user): - a = Account(self.now, self.service_available) + a = Account(self.now, self.service_available, self.child) assert a != user assert hash(a) != hash(user) diff --git a/tests/test_album.py b/tests/test_album.py index 715e303d..d150f59b 100644 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -46,6 +46,7 @@ class TestAlbum: start_date = '2020-06-30' likes_count = 2 available_regions = ['kg', 'tm', 'by', 'kz', 'md', 'ru', 'am', 'ge', 'uz', 'tj', 'il', 'az', 'ua'] + available_for_options = ['bookmate'] def test_expected_values( self, @@ -101,6 +102,7 @@ def test_expected_values( assert album.likes_count == self.likes_count assert album.deprecation == deprecation assert album.available_regions == self.available_regions + assert album.available_for_options == self.available_for_options def test_de_json_none(self, client): assert Album.de_json({}, client) is None @@ -160,6 +162,7 @@ def test_de_json_all(self, client, artist, label, track_position, track, album_w 'likes_count': self.likes_count, 'deprecation': deprecation.to_dict(), 'available_regions': self.available_regions, + 'available_for_options': self.available_for_options, } album = Album.de_json(json_dict, client) @@ -207,6 +210,7 @@ def test_de_json_all(self, client, artist, label, track_position, track, album_w assert album.likes_count == self.likes_count assert album.deprecation == deprecation assert album.available_regions == self.available_regions + assert album.available_for_options == self.available_for_options def test_equality(self, artist, label): a = Album(self.id) diff --git a/tests/test_convert_track_id.py b/tests/test_convert_track_id.py new file mode 100644 index 00000000..3a3d59d8 --- /dev/null +++ b/tests/test_convert_track_id.py @@ -0,0 +1,14 @@ +from yandex_music.utils.convert_track_id import convert_track_id_to_number + + +class TestConvertTrackId: + track_id = 37696396 + album_id = 4784420 + + def test_convert_from_str(self): + assert convert_track_id_to_number(f'{self.track_id}:{self.album_id}') == self.track_id + assert convert_track_id_to_number(f'{self.track_id}:') == self.track_id + assert convert_track_id_to_number(f'{self.track_id}') == self.track_id + + def test_convert_from_int(self): + assert convert_track_id_to_number(self.track_id) == self.track_id diff --git a/tests/test_custom_wave.py b/tests/test_custom_wave.py new file mode 100644 index 00000000..c52258ce --- /dev/null +++ b/tests/test_custom_wave.py @@ -0,0 +1,52 @@ +import pytest + +from yandex_music import CustomWave + + +class TestCustomWave: + title = 'В стиле: Трибунал' + animation_url = 'https://music-custom-wave-media.s3.yandex.net/base.json' + position = 'default' + + def test_expected_values(self, custom_wave): + assert custom_wave.title == self.title + assert custom_wave.animation_url == self.animation_url + assert custom_wave.position == self.position + + def test_de_json_none(self, client): + assert CustomWave.de_json({}, client) is None + + def test_de_json_required(self, client): + json_dict = { + 'title': self.title, + 'animation_url': self.animation_url, + 'position': self.position, + } + customwave = CustomWave.de_json(json_dict, client) + + assert customwave.title == self.title + assert customwave.animation_url == self.animation_url + assert customwave.position == self.position + + def test_de_json_all(self, client): + json_dict = { + 'title': self.title, + 'animation_url': self.animation_url, + 'position': self.position, + } + customwave = CustomWave.de_json(json_dict, client) + + assert customwave.title == self.title + assert customwave.animation_url == self.animation_url + assert customwave.position == self.position + + def test_equality(self): + a = CustomWave(self.title, self.animation_url, self.position) + b = CustomWave('', self.animation_url, self.position) + c = CustomWave(self.title, self.animation_url, self.position) + + assert a != b + assert hash(a) != hash(b) + assert a is not b + + assert a == c diff --git a/tests/test_generated_playlist.py b/tests/test_generated_playlist.py index 36c9b9f5..25af9053 100644 --- a/tests/test_generated_playlist.py +++ b/tests/test_generated_playlist.py @@ -6,6 +6,7 @@ class TestGeneratedPlaylist: ready = True notify = False description = [] + preview_description = 'Звучит по-вашему каждый день' def test_expected_values(self, generated_playlist, playlist): assert generated_playlist.type == self.type @@ -13,6 +14,7 @@ def test_expected_values(self, generated_playlist, playlist): assert generated_playlist.notify == self.notify assert generated_playlist.data == playlist assert generated_playlist.description == self.description + assert generated_playlist.preview_description == self.preview_description def test_de_json_none(self, client): assert GeneratedPlaylist.de_json({}, client) is None @@ -36,6 +38,7 @@ def test_de_json_all(self, client, playlist): 'notify': self.notify, 'data': playlist.to_dict(), 'description': self.description, + 'preview_description': self.preview_description, } generated_playlist = GeneratedPlaylist.de_json(json_dict, client) @@ -44,6 +47,7 @@ def test_de_json_all(self, client, playlist): assert generated_playlist.notify == self.notify assert generated_playlist.data == playlist assert generated_playlist.description == self.description + assert generated_playlist.preview_description == self.preview_description def test_equality(self, playlist): a = GeneratedPlaylist(self.type, self.ready, self.notify, playlist) diff --git a/tests/test_lyrics_info.py b/tests/test_lyrics_info.py new file mode 100644 index 00000000..465ef5a6 --- /dev/null +++ b/tests/test_lyrics_info.py @@ -0,0 +1,34 @@ +from yandex_music import LyricsInfo + + +class TestLyricsInfo: + has_available_sync_lyrics = False + has_available_text_lyrics = True + + def test_expected_values(self, lyrics_info): + assert lyrics_info.has_available_sync_lyrics == self.has_available_sync_lyrics + assert lyrics_info.has_available_text_lyrics == self.has_available_text_lyrics + + def test_de_json_none(self, client): + assert LyricsInfo.de_json({}, client) is None + + def test_de_json_required(self, client): + json_dict = { + 'has_available_sync_lyrics': self.has_available_sync_lyrics, + 'has_available_text_lyrics': self.has_available_text_lyrics, + } + lyrics_info = LyricsInfo.de_json(json_dict, client) + + assert lyrics_info.has_available_sync_lyrics == self.has_available_sync_lyrics + assert lyrics_info.has_available_text_lyrics == self.has_available_text_lyrics + + def test_equality(self): + a = LyricsInfo(self.has_available_sync_lyrics, self.has_available_text_lyrics) + b = LyricsInfo(True, self.has_available_text_lyrics) + c = LyricsInfo(self.has_available_sync_lyrics, self.has_available_text_lyrics) + + assert a != b + assert hash(a) != hash(b) + assert a is not b + + assert a == c diff --git a/tests/test_lyrics_major.py b/tests/test_lyrics_major.py new file mode 100644 index 00000000..a74878c1 --- /dev/null +++ b/tests/test_lyrics_major.py @@ -0,0 +1,42 @@ +from yandex_music import LyricsMajor + + +class TestLyricsMajor: + id = 560 + name = 'MUSIXMATCH' + pretty_name = 'Musixmatch' + + def test_expected_values(self, lyrics_major): + assert lyrics_major.id == self.id + assert lyrics_major.name == self.name + assert lyrics_major.pretty_name == self.pretty_name + + def test_de_json_none(self, client): + assert LyricsMajor.de_json({}, client) is None + + def test_de_json_required(self, client): + json_dict = {'id': self.id, 'name': self.name, 'pretty_name': self.pretty_name} + + lyrics_major = LyricsMajor.de_json(json_dict, client) + assert lyrics_major.id == self.id + assert lyrics_major.name == self.name + assert lyrics_major.pretty_name == self.pretty_name + + def test_de_json_all(self, client): + json_dict = {'id': self.id, 'name': self.name, 'pretty_name': self.pretty_name} + + lyrics_major = LyricsMajor.de_json(json_dict, client) + assert lyrics_major.id == self.id + assert lyrics_major.name == self.name + assert lyrics_major.pretty_name == self.pretty_name + + def test_equality(self): + a = LyricsMajor(self.id, self.name, self.pretty_name) + b = LyricsMajor(10, self.name, self.pretty_name) + c = LyricsMajor(self.id, self.name, self.pretty_name) + + assert a != b + assert hash(a) != hash(b) + assert a is not b + + assert a == c diff --git a/tests/test_mix_link.py b/tests/test_mix_link.py index 94754cb9..6fcd8218 100644 --- a/tests/test_mix_link.py +++ b/tests/test_mix_link.py @@ -37,7 +37,6 @@ def test_de_json_required(self, client): 'text_color': self.text_color, 'background_color': self.background_color, 'background_image_uri': self.background_image_uri, - 'cover_white': self.cover_white, } mix_link = MixLink.de_json(json_dict, client) @@ -47,7 +46,6 @@ def test_de_json_required(self, client): assert mix_link.text_color == self.text_color assert mix_link.background_color == self.background_color assert mix_link.background_image_uri == self.background_image_uri - assert mix_link.cover_white == self.cover_white def test_de_json_all(self, client): json_dict = { @@ -79,7 +77,6 @@ def test_equality(self): self.text_color, self.background_color, self.background_image_uri, - self.cover_white, ) b = MixLink( self.title, @@ -88,7 +85,6 @@ def test_equality(self): self.text_color, self.background_color, self.background_image_uri, - self.cover_white, ) c = MixLink( self.title, @@ -97,7 +93,6 @@ def test_equality(self): '#000000', self.background_color, self.background_image_uri, - self.cover_white, ) d = MixLink( self.title, @@ -106,7 +101,6 @@ def test_equality(self): self.text_color, self.background_color, self.background_image_uri, - self.cover_white, ) assert a != b != c diff --git a/tests/test_playlist.py b/tests/test_playlist.py index c899e7bb..88927d1c 100644 --- a/tests/test_playlist.py +++ b/tests/test_playlist.py @@ -59,6 +59,8 @@ def test_expected_values( contest, open_graph_data, brand, + custom_wave, + pager, ): assert playlist.owner == user assert playlist.uid == self.uid @@ -115,6 +117,8 @@ def test_expected_values( assert playlist.ready == self.ready assert playlist.is_for_from == self.is_for_from assert playlist.regions == self.regions + assert playlist.custom_wave == custom_wave + assert playlist.pager == pager def test_de_json_none(self, client): assert Playlist.de_json({}, client) is None @@ -153,6 +157,8 @@ def test_de_json_all( contest, open_graph_data, brand, + custom_wave, + pager, ): json_dict = { 'owner': user.to_dict(), @@ -210,6 +216,8 @@ def test_de_json_all( 'playlist_uuid': self.playlist_uuid, 'type': self.type, 'ready': self.ready, + 'custom_wave': custom_wave.to_dict(), + 'pager': pager.to_dict(), } playlist = Playlist.de_json(json_dict, client) @@ -268,6 +276,8 @@ def test_de_json_all( assert playlist.ready == self.ready assert playlist.is_for_from == self.is_for_from assert playlist.regions == self.regions + assert playlist.custom_wave == custom_wave + assert playlist.pager == pager def test_equality(self, user, cover, made_for, play_counter, playlist_absence): a = Playlist(user, cover, made_for, play_counter, playlist_absence) diff --git a/tests/test_r128.py b/tests/test_r128.py new file mode 100644 index 00000000..f117148d --- /dev/null +++ b/tests/test_r128.py @@ -0,0 +1,33 @@ +import pytest + +from yandex_music import R128 + + +class TestR128: + i = -13.12 + tp = 0.63 + + def test_expected_values(self, r_128): + assert r_128.i == self.i + assert r_128.tp == self.tp + + def test_de_json_none(self, client): + assert R128.de_json({}, client) is None + + def test_de_json_required(self, client): + json_dict = {'i': self.i, 'tp': self.tp} + r128 = R128.de_json(json_dict, client) + + assert r128.i == self.i + assert r128.tp == self.tp + + def test_equality(self): + a = R128(self.i, self.tp) + b = R128(-8.98, self.tp) + c = R128(self.i, self.tp) + + assert a != b + assert hash(a) != hash(b) + assert a is not b + + assert a == c diff --git a/tests/test_sign_request.py b/tests/test_sign_request.py new file mode 100644 index 00000000..862df7f1 --- /dev/null +++ b/tests/test_sign_request.py @@ -0,0 +1,23 @@ +import datetime + +from yandex_music.utils.sign_request import get_sign_request + + +class TestSignRequest: + timestamp = 1668687184 + track_id = 4784420 + key = 'SUPER_SECRET_KEY' + + sign_value = 'vssEEweZhgv2Aud0rdH9maOXUC03ZkZ/hlo6bSRN8Qg=' + + def test_sign_request(self, monkeypatch): + class FakeDatetime(datetime.datetime): + @classmethod + def now(cls): + return datetime.datetime.fromtimestamp(self.timestamp) + + monkeypatch.setattr('datetime.datetime', FakeDatetime) + sign = get_sign_request(self.track_id, self.key) + + assert sign.timestamp == self.timestamp + assert sign.value == self.sign_value diff --git a/tests/test_station_result.py b/tests/test_station_result.py index a9b7a0dd..e4d1e914 100644 --- a/tests/test_station_result.py +++ b/tests/test_station_result.py @@ -4,6 +4,9 @@ class TestStationResult: explanation = '' prerolls = [] + rup_title = 'Моя волна' + rup_description = 'Волна подстраивается под жанр и\xa0вас. Слушайте только то, что\xa0нравится!' + custom_name = "R'n'B" def test_expected_values(self, station_result, station, rotor_settings, ad_params): assert station_result.station == station @@ -12,6 +15,9 @@ def test_expected_values(self, station_result, station, rotor_settings, ad_param assert station_result.ad_params == ad_params assert station_result.explanation == self.explanation assert station_result.prerolls == self.prerolls + assert station_result.rup_title == self.rup_title + assert station_result.rup_description == self.rup_description + assert station_result.custom_name == self.custom_name def test_de_json_none(self, client): assert StationResult.de_json({}, client) is None @@ -25,6 +31,8 @@ def test_de_json_required(self, client, station, rotor_settings, ad_params): 'settings': rotor_settings.to_dict(), 'settings2': rotor_settings.to_dict(), 'ad_params': ad_params.to_dict(), + 'rup_title': self.rup_title, + 'rup_description': self.rup_description, } station_result = StationResult.de_json(json_dict, client) @@ -32,6 +40,8 @@ def test_de_json_required(self, client, station, rotor_settings, ad_params): assert station_result.settings == rotor_settings assert station_result.settings2 == rotor_settings assert station_result.ad_params == ad_params + assert station_result.rup_title == self.rup_title + assert station_result.rup_description == self.rup_description def test_de_json_all(self, client, station, rotor_settings, ad_params): json_dict = { @@ -41,6 +51,9 @@ def test_de_json_all(self, client, station, rotor_settings, ad_params): 'ad_params': ad_params.to_dict(), 'explanation': self.explanation, 'prerolls': self.prerolls, + 'rup_title': self.rup_title, + 'rup_description': self.rup_description, + 'custom_name': self.custom_name, } station_result = StationResult.de_json(json_dict, client) @@ -50,6 +63,9 @@ def test_de_json_all(self, client, station, rotor_settings, ad_params): assert station_result.ad_params == ad_params assert station_result.explanation == self.explanation assert station_result.prerolls == self.prerolls + assert station_result.rup_title == self.rup_title + assert station_result.rup_description == self.rup_description + assert station_result.custom_name == self.custom_name def test_equality(self, station, rotor_settings, ad_params): a = StationResult(station, rotor_settings, rotor_settings, ad_params) diff --git a/tests/test_status.py b/tests/test_status.py index 74829832..dcd848c0 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -6,11 +6,13 @@ class TestStatus: cache_limit = 99 subeditor = False subeditor_level = 0 - default_email = 'Ilya@marshal.by' + default_email = 'yandex_music@yandex.com' skips_per_hour = None station_exists = None premium_region = None experiment = 109 + pretrial_active = False + userhash = '2a1d970ce4dadc3333280aa8727d1c41a380a7622521ecef67928cd4213adb8f' def test_expected_values(self, status, account, permissions, subscription, plus, alert): assert status.account == account @@ -27,6 +29,8 @@ def test_expected_values(self, status, account, permissions, subscription, plus, assert status.bar_below == alert assert status.premium_region == self.premium_region assert status.experiment == self.experiment + assert status.pretrial_active == self.pretrial_active + assert status.userhash == self.userhash def test_de_json_none(self, client): assert Status.de_json({}, client) is None @@ -54,6 +58,8 @@ def test_de_json_all(self, client, account, permissions, subscription, plus, ale 'advertisement': self.advertisement, 'bar_below': alert.to_dict(), 'experiment': self.experiment, + 'pretrial_active': self.pretrial_active, + 'userhash': self.userhash, } status = Status.de_json(json_dict, client) @@ -71,6 +77,8 @@ def test_de_json_all(self, client, account, permissions, subscription, plus, ale assert status.bar_below == alert assert status.premium_region == self.premium_region assert status.experiment == self.experiment + assert status.pretrial_active == self.pretrial_active + assert status.userhash == self.userhash def test_equality(self, account, permissions, subscription): a = Status(account, permissions) diff --git a/tests/test_subscription.py b/tests/test_subscription.py index c16a86fe..733358bd 100644 --- a/tests/test_subscription.py +++ b/tests/test_subscription.py @@ -5,6 +5,7 @@ class TestSubscription: can_start_trial = False mcdonalds = False end = None + had_any_subscription = False def test_expected_values(self, subscription, renewable_remainder, auto_renewable, non_auto_renewable, operator): assert subscription.non_auto_renewable_remainder == renewable_remainder @@ -15,6 +16,7 @@ def test_expected_values(self, subscription, renewable_remainder, auto_renewable assert subscription.can_start_trial == self.can_start_trial assert subscription.mcdonalds == self.mcdonalds assert subscription.end == self.end + assert subscription.had_any_subscription == self.had_any_subscription def test_de_json_none(self, client): assert Subscription.de_json({}, client) is None @@ -24,12 +26,14 @@ def test_de_json_required(self, client, renewable_remainder, auto_renewable): 'non_auto_renewable_remainder': renewable_remainder.to_dict(), 'auto_renewable': [auto_renewable.to_dict()], 'family_auto_renewable': [auto_renewable.to_dict()], + 'had_any_subscription': self.had_any_subscription, } subscription = Subscription.de_json(json_dict, client) assert subscription.non_auto_renewable_remainder == renewable_remainder assert subscription.auto_renewable == [auto_renewable] assert subscription.family_auto_renewable == [auto_renewable] + assert subscription.had_any_subscription == self.had_any_subscription def test_de_json_all(self, client, renewable_remainder, auto_renewable, non_auto_renewable, operator): json_dict = { @@ -41,6 +45,7 @@ def test_de_json_all(self, client, renewable_remainder, auto_renewable, non_auto 'family_auto_renewable': [auto_renewable.to_dict()], 'non_auto_renewable': non_auto_renewable.to_dict(), 'operator': [operator.to_dict()], + 'had_any_subscription': self.had_any_subscription, } subscription = Subscription.de_json(json_dict, client) @@ -52,10 +57,11 @@ def test_de_json_all(self, client, renewable_remainder, auto_renewable, non_auto assert subscription.can_start_trial == self.can_start_trial assert subscription.mcdonalds == self.mcdonalds assert subscription.end == self.end + assert subscription.had_any_subscription == self.had_any_subscription def test_equality(self, renewable_remainder, auto_renewable): - a = Subscription(renewable_remainder, [auto_renewable], [auto_renewable]) - b = Subscription(renewable_remainder, [], [auto_renewable]) + a = Subscription(renewable_remainder, [auto_renewable], [auto_renewable], self.had_any_subscription) + b = Subscription(renewable_remainder, [], [auto_renewable], self.had_any_subscription) assert a != b != auto_renewable assert hash(a) != hash(b) != hash(auto_renewable) diff --git a/tests/test_track.py b/tests/test_track.py index 79eb34a0..49940b55 100644 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -36,6 +36,9 @@ class TestTrack: ' желания познакомиться с девушкой, достаточно много. Обычно они сводятся к опасениям,' ) is_suitable_for_children = True + track_source = 'OWN' + available_for_options = ['bookmate'] + track_sharing_flag = 'VIDEO_ALLOWED' def test_expected_values( self, @@ -48,6 +51,8 @@ def test_expected_values( user, meta_data, poetry_lover_match, + r_128, + lyrics_info, ): assert track.id == self.id assert track.title == self.title @@ -87,6 +92,11 @@ def test_expected_values( assert track.background_video_uri == self.background_video_uri assert track.short_description == self.short_description assert track.is_suitable_for_children == self.is_suitable_for_children + assert track.track_source == self.track_source + assert track.available_for_options == self.available_for_options + assert track.r128 == r_128 + assert track.lyrics_info == lyrics_info + assert track.track_sharing_flag == self.track_sharing_flag def test_de_json_none(self, client): assert Track.de_json({}, client) is None @@ -111,6 +121,8 @@ def test_de_json_all( user, meta_data, poetry_lover_match, + r_128, + lyrics_info, ): json_dict = { 'id': self.id, @@ -151,6 +163,11 @@ def test_de_json_all( 'background_video_uri': self.background_video_uri, 'short_description': self.short_description, 'is_suitable_for_children': self.is_suitable_for_children, + 'track_source': self.track_source, + 'available_for_options': self.available_for_options, + 'r128': r_128.to_dict(), + 'lyrics_info': lyrics_info.to_dict(), + 'track_sharing_flag': self.track_sharing_flag, } track = Track.de_json(json_dict, client) @@ -192,6 +209,11 @@ def test_de_json_all( assert track.background_video_uri == self.background_video_uri assert track.short_description == self.short_description assert track.is_suitable_for_children == self.is_suitable_for_children + assert track.track_source == self.track_source + assert track.available_for_options == self.available_for_options + assert track.r128 == r_128 + assert track.lyrics_info == lyrics_info + assert track.track_sharing_flag == self.track_sharing_flag def test_equality(self): a = Track(self.id) diff --git a/tests/test_track_lyrics.py b/tests/test_track_lyrics.py new file mode 100644 index 00000000..0e9d0f11 --- /dev/null +++ b/tests/test_track_lyrics.py @@ -0,0 +1,61 @@ +from yandex_music import TrackLyrics + + +class TestTrackLyrics: + download_url = 'https://music-lyrics.s3-private.mds.yandex.net/8145339.f0f2e9e0/37320085?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20221113T085654Z&X-Amz-SignedHeaders=host&X-Amz-Expires=86400&X-Amz-Credential=B8LQDON9RSp6Pcbw1Hxz%2F20221113%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=148de126a9ca9a91cb157e6032d178c39bf604d2e4acec6155d81e51090533ac' + lyric_id = 8145339 + external_lyric_id = '8638863' + writer = ['Mother Mother'] + + def test_expected_values(self, track_lyrics, lyrics_major): + assert track_lyrics.download_url == self.download_url + assert track_lyrics.lyric_id == self.lyric_id + assert track_lyrics.external_lyric_id == self.external_lyric_id + assert track_lyrics.writers == self.writer + assert track_lyrics.major == lyrics_major + + def test_de_json_none(self, client): + assert TrackLyrics.de_json({}, client) is None + + def test_de_json_required(self, client, lyrics_major): + json_dict = { + 'download_url': self.download_url, + 'lyric_id': self.lyric_id, + 'external_lyric_id': self.external_lyric_id, + 'writers': self.writer, + 'major': lyrics_major.to_dict(), + } + + track_lyrics = TrackLyrics.de_json(json_dict, client) + assert track_lyrics.download_url == self.download_url + assert track_lyrics.lyric_id == self.lyric_id + assert track_lyrics.external_lyric_id == self.external_lyric_id + assert track_lyrics.writers == self.writer + assert track_lyrics.major == lyrics_major + + def test_de_json_all(self, client, lyrics_major): + json_dict = { + 'download_url': self.download_url, + 'lyric_id': self.lyric_id, + 'external_lyric_id': self.external_lyric_id, + 'writers': self.writer, + 'major': lyrics_major.to_dict(), + } + + track_lyrics = TrackLyrics.de_json(json_dict, client) + assert track_lyrics.download_url == self.download_url + assert track_lyrics.lyric_id == self.lyric_id + assert track_lyrics.external_lyric_id == self.external_lyric_id + assert track_lyrics.writers == self.writer + assert track_lyrics.major == lyrics_major + + def test_equality(self, lyrics_major): + a = TrackLyrics(self.download_url, self.lyric_id, self.external_lyric_id, self.writer, lyrics_major) + b = TrackLyrics(self.download_url, 50, self.external_lyric_id, self.writer, lyrics_major) + c = TrackLyrics(self.download_url, self.lyric_id, self.external_lyric_id, self.writer, lyrics_major) + + assert a != b + assert hash(a) != hash(b) + assert a is not b + + assert a == c diff --git a/tests/test_track_short.py b/tests/test_track_short.py index 0cf2208a..aec00bf8 100644 --- a/tests/test_track_short.py +++ b/tests/test_track_short.py @@ -13,6 +13,7 @@ def track_short(track, chart): TestTrackShort.recent, chart, track, + TestTrackShort.original_index, ) @@ -22,6 +23,7 @@ class TestTrackShort: album_id = None play_count = 0 recent = False + original_index = 23 def test_expected_values(self, track_short, track, chart): assert track_short.id == self.id @@ -31,6 +33,7 @@ def test_expected_values(self, track_short, track, chart): assert track_short.recent == self.recent assert track_short.track == track assert track_short.chart == chart + assert track_short.original_index == self.original_index def test_de_json_none(self, client): assert TrackShort.de_json({}, client) is None @@ -54,6 +57,7 @@ def test_de_json_all(self, client, track, chart): 'recent': self.recent, 'track': track.to_dict(), 'chart': chart.to_dict(), + 'original_index': self.original_index, } track_short = TrackShort.de_json(json_dict, client) @@ -64,6 +68,7 @@ def test_de_json_all(self, client, track, chart): assert track_short.recent == self.recent assert track_short.track == track assert track_short.chart == chart + assert track_short.original_index == self.original_index def test_equality(self): a = TrackShort(self.id, self.timestamp, self.album_id) diff --git a/yandex_music/__init__.py b/yandex_music/__init__.py index 5b92d71e..1311e5b4 100644 --- a/yandex_music/__init__.py +++ b/yandex_music/__init__.py @@ -1,6 +1,6 @@ -__version__ = '2.0.1' +__version__ = '2.1.1' __license__ = 'GNU Lesser General Public License v3 (LGPLv3)' -__copyright__ = 'Copyright (C) 2019-2022 Il`ya (Marshal) ' +__copyright__ = 'Copyright (C) 2019-2023 Ilya (Marshal) ' from .base import YandexMusicObject @@ -44,6 +44,7 @@ from .playlist.made_for import MadeFor from .playlist.user import User from .playlist.contest import Contest +from .playlist.custom_wave import CustomWave from .playlist.open_graph_data import OpenGraphData from .playlist.brand import Brand from .playlist.play_counter import PlayCounter @@ -62,11 +63,15 @@ from .tracks_list import TracksList from .track.major import Major from .track.licence_text_part import LicenceTextPart +from .track.track_lyrics import TrackLyrics +from .track.lyrics_major import LyricsMajor from .track.poetry_lover_match import PoetryLoverMatch from .track.meta_data import MetaData from .track.normalization import Normalization from .track.track import Track from .track.tracks_similar import SimilarTracks +from .track.r128 import R128 +from .track.lyrics_info import LyricsInfo from .feed.generated_playlist import GeneratedPlaylist from .feed.album_event import AlbumEvent @@ -260,4 +265,8 @@ 'Queue', 'QueueItem', 'Deprecation', + 'TrackLyrics', + 'CustomWave', + 'R128', + 'LyricsInfo', ] diff --git a/yandex_music/account/account.py b/yandex_music/account/account.py index 8e363999..5d9e1a50 100644 --- a/yandex_music/account/account.py +++ b/yandex_music/account/account.py @@ -26,6 +26,7 @@ class Account(YandexMusicObject): passport_phones (:obj:`list` из :obj:`yandex_music.PassportPhone`): Мобильные номера. registered_at (:obj:`str`, optional): Дата создания учётной записи. has_info_for_app_metrica (:obj:`bool`, optional): Наличие информации для App Metrica. + child (:obj:`bool`): Дочерний / детский аккаунт (скорее детский, позволяет ограничить доступный контент ребенку на Кинопоиске). client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. **kwargs: Произвольные ключевые аргументы полученные от API. """ @@ -43,7 +44,8 @@ class Account(YandexMusicObject): birthday: Optional[str] = None passport_phones: List['PassportPhone'] = None registered_at: Optional[str] = None - has_info_for_app_metrica: bool = False + has_info_for_app_metrica: bool = None + child: bool = None client: Optional['Client'] = None def __post_init__(self): diff --git a/yandex_music/account/status.py b/yandex_music/account/status.py index 7eddcead..7e501ca7 100644 --- a/yandex_music/account/status.py +++ b/yandex_music/account/status.py @@ -27,6 +27,8 @@ class Status(YandexMusicObject): bar_below (:obj:`yandex_music.Alert`, optional): Блок с предупреждениями о конце подписке и подарках. premium_region (:obj:`int`, optional): Регион TODO. experiment (:obj:`int`, optional): Включенная новая фича на аккаунте (её ID) TODO. + pretrial_active (:obj:`bool`, optional): TODO. + userhash (:obj:`str`, optional): Хэш-код идентификатора пользователя. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. """ @@ -45,6 +47,8 @@ class Status(YandexMusicObject): bar_below: Optional['Alert'] = None premium_region: Optional[int] = None experiment: Optional[int] = None + pretrial_active: Optional[bool] = None + userhash: Optional[str] = None client: Optional['Client'] = None def __post_init__(self): diff --git a/yandex_music/account/subscription.py b/yandex_music/account/subscription.py index a217e2c1..1fa0dd33 100644 --- a/yandex_music/account/subscription.py +++ b/yandex_music/account/subscription.py @@ -20,12 +20,14 @@ class Subscription(YandexMusicObject): can_start_trial (:obj:`bool`, optional): Есть ли возможность начать пробный период. mcdonalds (:obj:`bool`, optional): mcdonalds TODO. end (:obj:`str`, optional): Дата окончания. + had_any_subscription (:obj:'bool'): Наличие какой-либо подписки в прошлом. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. """ non_auto_renewable_remainder: 'RenewableRemainder' auto_renewable: List['AutoRenewable'] family_auto_renewable: List['AutoRenewable'] + had_any_subscription: bool operator: List['Operator'] = None non_auto_renewable: Optional['NonAutoRenewable'] = None can_start_trial: Optional[bool] = None diff --git a/yandex_music/account/user_settings.py b/yandex_music/account/user_settings.py index 4b9e958f..a26076b7 100644 --- a/yandex_music/account/user_settings.py +++ b/yandex_music/account/user_settings.py @@ -16,7 +16,7 @@ class UserSettings(YandexMusicObject): Доступные значения для полей `user_music_visibility` и `user_social_visibility`: `private`, `public`. - Notes: + Note: `promos_disabled`, `ads_disabled`, `rbt_disabled` устарели и не работают. `last_fm_scrobbling_enabled`, `facebook_scrobbling_enabled` выглядят устаревшими. diff --git a/yandex_music/album/album.py b/yandex_music/album/album.py index c9a4bfe4..a9b7af4c 100644 --- a/yandex_music/album/album.py +++ b/yandex_music/album/album.py @@ -20,8 +20,10 @@ class Album(YandexMusicObject): Известные значения поля `meta_type`: `music`. + Известные значения поля `available_for_options`: `bookmate`. + Attributes: - id_(:obj:`int`, optional): Идентификатор альбома. + id (:obj:`int`, optional): Идентификатор альбома. error (:obj:`str`, optional): Ошибка получения альбома. title (:obj:`str`, optional): Название альбома. track_count (:obj:`int`, optional): Количество треков. @@ -65,7 +67,8 @@ class Album(YandexMusicObject): start_date (:obj:`str`, optional): Дата начала в формате ISO 8601 TODO. likes_count (:obj:`int`, optional): Количество лайков TODO. deprecation (:obj:`yandex_music.Deprecation`, optional): TODO. - available_regions (:obj:`list` из :obj:`str`, optional): Регионы, где доступн альбом. + available_regions (:obj:`list` из :obj:`str`, optional): Регионы, где доступен альбом. + available_for_options (:obj:`list` из :obj:`str`, optional): Возможные опции для альбома. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. """ @@ -114,6 +117,7 @@ class Album(YandexMusicObject): likes_count: Optional[int] = None deprecation: Optional['Deprecation'] = None available_regions: Optional[List[str]] = None + available_for_options: Optional[List[str]] = None client: Optional['Client'] = None def __post_init__(self): @@ -133,6 +137,28 @@ async def with_tracks_async(self, *args, **kwargs) -> Optional['Album']: """ return await self.client.albums_with_tracks(self.id, *args, **kwargs) + def get_cover_url(self, size: str = '200x200') -> str: + """Возвращает URL обложки. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`str`: URL обложки. + """ + return f'https://{self.cover_uri.replace("%%", size)}' + + def get_og_image_url(self, size: str = '200x200') -> str: + """Возвращает URL OG обложки. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`str`: URL обложки. + """ + return f'https://{self.og_image.replace("%%", size)}' + def download_cover(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -140,7 +166,7 @@ def download_cover(self, filename: str, size: str = '200x200') -> None: filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + self.client.request.download(self.get_cover_url(size), filename) async def download_cover_async(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -149,7 +175,7 @@ async def download_cover_async(self, filename: str, size: str = '200x200') -> No filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - await self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + await self.client.request.download(self.get_cover_url(size), filename) def download_og_image(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -160,7 +186,7 @@ def download_og_image(self, filename: str, size: str = '200x200') -> None: filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + self.client.request.download(self.get_og_image_url(size), filename) async def download_og_image_async(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -171,7 +197,55 @@ async def download_og_image_async(self, filename: str, size: str = '200x200') -> filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - await self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + await self.client.request.download(self.get_og_image_url(size), filename) + + def download_cover_bytes(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return self.client.request.retrieve(self.get_cover_url(size)) + + async def download_cover_bytes_async(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return await self.client.request.retrieve(self.get_cover_url(size)) + + def download_og_image_bytes(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Предпочтительнее использовать `self.download_cover()`. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return self.client.request.retrieve(self.get_og_image_url(size)) + + async def download_og_image_bytes_async(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Предпочтительнее использовать `self.download_cover_async()`. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return await self.client.request.retrieve(self.get_og_image_url(size)) def like(self, *args, **kwargs) -> bool: """Сокращение для:: @@ -260,6 +334,10 @@ def de_list(cls, data: dict, client: 'Client') -> List['Album']: withTracks = with_tracks #: Псевдоним для :attr:`with_tracks_async` withTracksAsync = with_tracks_async + #: Псевдоним для :attr:`get_cover_url` + getCoverUrl = get_cover_url + #: Псевдоним для :attr:`get_og_image_url` + getOgImageUrl = get_og_image_url #: Псевдоним для :attr:`download_cover` downloadCover = download_cover #: Псевдоним для :attr:`download_cover_async` @@ -268,5 +346,17 @@ def de_list(cls, data: dict, client: 'Client') -> List['Album']: downloadOgImage = download_og_image #: Псевдоним для :attr:`download_og_image_async` downloadOgImageAsync = download_og_image_async + #: Псевдоним для :attr:`download_cover_bytes` + downloadCoverBytes = download_cover_bytes + #: Псевдоним для :attr:`download_cover_bytes_async` + downloadCoverBytesAsync = download_cover_bytes_async + #: Псевдоним для :attr:`download_og_image_bytes` + downloadOgImageBytes = download_og_image_bytes + #: Псевдоним для :attr:`download_og_image_bytes_async` + downloadOgImageBytesAsync = download_og_image_bytes_async + #: Псевдоним для :attr:`like_async` + likeAsync = like_async + #: Псевдоним для :attr:`dislike_async` + dislikeAsync = dislike_async #: Псевдоним для :attr:`artists_name` artistsName = artists_name diff --git a/yandex_music/album/track_position.py b/yandex_music/album/track_position.py index d64825f3..74fb6cfc 100644 --- a/yandex_music/album/track_position.py +++ b/yandex_music/album/track_position.py @@ -11,7 +11,7 @@ class TrackPosition(YandexMusicObject): """Класс, представляющий позицию трека. - None: + Note: Позиция трека в альбоме, который возвращается при получении самого трека. Volume на фронте именуется как "Диск". diff --git a/yandex_music/artist/artist.py b/yandex_music/artist/artist.py index 02eab5e8..920eddc8 100644 --- a/yandex_music/artist/artist.py +++ b/yandex_music/artist/artist.py @@ -29,7 +29,7 @@ class Artist(YandexMusicObject): links (:obj:`list` из :obj:`yandex_music.Link`, optional): Ссылки на ресурсы исполнителя. tickets_available (:obj:`bool`, optional): Имеются ли в продаже билеты на концерт. likes_count (:obj:`int`, optional): Количество лайков. - popular_tracks (:obj:`list` :obj:`yandex_music.Track`, optional): Популярные треки. + popular_tracks (:obj:`list` из :obj:`yandex_music.Track`, optional): Популярные треки. regions (:obj:`list` из :obj:`str`, optional): Регион TODO. decomposed (:obj:`list` из :obj:`str` и :obj:`yandex_music.Artist`, optional): Декомпозиция всех исполнителей. Лист, где чередуется разделитель и артист. Фиты и прочее. @@ -81,6 +81,28 @@ class Artist(YandexMusicObject): def __post_init__(self): self._id_attrs = (self.id, self.name, self.cover) + def get_op_image_url(self, size: str = '200x200') -> str: + """Возвращает URL OP обложки. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`str`: URL обложки. + """ + return f'https://{self.op_image.replace("%%", size)}' + + def get_og_image_url(self, size: str = '200x200') -> str: + """Возвращает URL OG обложки. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`str`: URL обложки. + """ + return f'https://{self.og_image.replace("%%", size)}' + def download_og_image(self, filename: str, size: str = '200x200') -> None: """Загрузка изображения для Open Graph. @@ -88,7 +110,7 @@ def download_og_image(self, filename: str, size: str = '200x200') -> None: filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + self.client.request.download(self.get_og_image_url(size), filename) async def download_og_image_async(self, filename: str, size: str = '200x200') -> None: """Загрузка изображения для Open Graph. @@ -97,7 +119,7 @@ async def download_og_image_async(self, filename: str, size: str = '200x200') -> filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - await self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + await self.client.request.download(self.get_og_image_url(size), filename) def download_op_image(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -109,7 +131,7 @@ def download_op_image(self, filename: str, size: str = '200x200') -> None: filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - self.client.request.download(f'https://{self.op_image.replace("%%", size)}', filename) + self.client.request.download(self.get_op_image_url(size), filename) async def download_op_image_async(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -121,7 +143,57 @@ async def download_op_image_async(self, filename: str, size: str = '200x200') -> filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - await self.client.request.download(f'https://{self.op_image.replace("%%", size)}', filename) + await self.client.request.download(self.get_op_image_url(size), filename) + + def download_og_image_bytes(self, size: str = '200x200') -> bytes: + """Загрузка изображения для Open Graph и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Изображение в виде байтов. + """ + return self.client.request.retrieve(self.get_og_image_url(size)) + + async def download_og_image_bytes_async(self, size: str = '200x200') -> bytes: + """Загрузка изображения для Open Graph и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Изображение в виде байтов. + """ + return await self.client.request.retrieve(self.get_og_image_url(size)) + + def download_op_image_bytes(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Notes: + Используйте это только когда нет self.cover! + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return self.client.request.retrieve(self.get_op_image_url(size)) + + async def download_op_image_bytes_async(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Notes: + Используйте это только когда нет self.cover! + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return await self.client.request.retrieve(self.get_op_image_url(size)) def like(self, *args, **kwargs) -> bool: """Сокращение для:: @@ -233,6 +305,10 @@ def de_list(cls, data: dict, client: 'Client') -> List['Artist']: # camelCase псевдонимы + #: Псевдоним для :attr:`get_op_image_url` + getOpImageUrl = get_op_image_url + #: Псевдоним для :attr:`get_og_image_url` + getOgImageUrl = get_og_image_url #: Псевдоним для :attr:`download_og_image` downloadOgImage = download_og_image #: Псевдоним для :attr:`download_og_image_async` @@ -241,6 +317,18 @@ def de_list(cls, data: dict, client: 'Client') -> List['Artist']: downloadOpImage = download_op_image #: Псевдоним для :attr:`download_op_image_async` downloadOpImageAsync = download_op_image_async + #: Псевдоним для :attr:`download_og_image_bytes` + downloadOgImageBytes = download_og_image_bytes + #: Псевдоним для :attr:`download_og_image_bytes_async` + downloadOgImageBytesAsync = download_og_image_bytes_async + #: Псевдоним для :attr:`download_op_image_bytes` + downloadOpImageBytes = download_op_image_bytes + #: Псевдоним для :attr:`download_op_image_bytes_async` + downloadOpImageBytesAsync = download_op_image_bytes_async + #: Псевдоним для :attr:`like_async` + likeAsync = like_async + #: Псевдоним для :attr:`dislike_async` + dislikeAsync = dislike_async #: Псевдоним для :attr:`get_tracks` getTracks = get_tracks #: Псевдоним для :attr:`get_tracks_async` @@ -249,7 +337,3 @@ def de_list(cls, data: dict, client: 'Client') -> List['Artist']: getAlbums = get_albums #: Псевдоним для :attr:`get_albums_async` getAlbumsAsync = get_albums_async - #: Псевдоним для :attr:`like_async` - likeAsync = like_async - #: Псевдоним для :attr:`dislike_async` - dislikeAsync = dislike_async diff --git a/yandex_music/base.py b/yandex_music/base.py index b72a2f4d..8f95e98a 100644 --- a/yandex_music/base.py +++ b/yandex_music/base.py @@ -35,12 +35,12 @@ def __getitem__(self, item): return self.__dict__[item] @staticmethod - def report_unknown_fields_callback(obj, unknown_fields): + def report_unknown_fields_callback(cls, unknown_fields): logger.warning( f'Found unknown fields received from API! Please copy warn message ' f'and send to {new_issue_by_template_url} (github issue), thank you!' ) - logger.warning(f'Type: {type(obj)}; fields: {unknown_fields}') + logger.warning(f'Type: {cls.__module__}.{cls.__name__}; fields: {unknown_fields}') @classmethod def de_json(cls, data: dict, client: Optional['Client']) -> Optional[dict]: @@ -70,7 +70,7 @@ def de_json(cls, data: dict, client: Optional['Client']) -> Optional[dict]: unknown_data[k] = v if client.report_unknown_fields and unknown_data: - YandexMusicObject.report_unknown_fields_callback(cls, unknown_data) + cls.report_unknown_fields_callback(cls, unknown_data) return cleaned_data diff --git a/yandex_music/client.py b/yandex_music/client.py index c8df0526..a036e7bc 100644 --- a/yandex_music/client.py +++ b/yandex_music/client.py @@ -1,7 +1,7 @@ import functools import logging from datetime import datetime -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Union, TypeVar, Callable, Any from yandex_music import ( Album, @@ -28,6 +28,7 @@ Status, Suggestions, SimilarTracks, + TrackLyrics, Track, TracksList, UserSettings, @@ -45,6 +46,7 @@ from yandex_music.exceptions import BadRequestError from yandex_music.utils.difference import Difference from yandex_music.utils.request import Request +from yandex_music.utils.sign_request import get_sign_request de_list = { 'artist': Artist.de_list, @@ -55,12 +57,14 @@ logging.getLogger(__name__).addHandler(logging.NullHandler()) +F = TypeVar('F', bound=Callable[..., Any]) -def log(method): + +def log(method: F) -> F: logger = logging.getLogger(method.__module__) @functools.wraps(method) - def wrapper(*args, **kwargs): + def wrapper(*args, **kwargs) -> Any: logger.debug(f'Entering: {method.__name__}') result = method(*args, **kwargs) @@ -95,12 +99,12 @@ class Client(YandexMusicObject): base_url (:obj:`str`, optional): Ссылка на API Yandex Music. request (:obj:`yandex_music.utils.request.Request`, optional): Пре-инициализация :class:`yandex_music.utils.request.Request`. - language (:obj:`str`, optional): Язык, на котором будут приходить ответы от API. + language (:obj:`str`, optional): Язык, на котором будут приходить ответы от API. По умолчанию русский. report_unknown_fields (:obj:`bool`, optional): Включить предупреждения о неизвестных полях от API, которых нет в библиотеке. """ - notice_displayed = False + __notice_displayed = False def __init__( self, @@ -110,10 +114,10 @@ def __init__( language: str = 'ru', report_unknown_fields=False, ) -> None: - if not Client.notice_displayed: + if not Client.__notice_displayed: print(f'Yandex Music API v{__version__}, {__copyright__}') print(f'Licensed under the terms of the {__license__}', end='\n\n') - Client.notice_displayed = True + Client.__notice_displayed = True self.logger = logging.getLogger(__name__) self.token = token @@ -153,12 +157,10 @@ def init(self): return self @log - def account_status(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Status]: + def account_status(self, *args, **kwargs) -> Optional[Status]: """Получение статуса аккаунта. Нет обязательных параметров. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -170,17 +172,15 @@ def account_status(self, timeout: Union[int, float] = None, *args, **kwargs) -> url = f'{self.base_url}/account/status' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return Status.de_json(result, self) @log - def account_settings(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[UserSettings]: + def account_settings(self, *args, **kwargs) -> Optional[UserSettings]: """Получение настроек текущего пользователя. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -193,7 +193,7 @@ def account_settings(self, timeout: Union[int, float] = None, *args, **kwargs) - url = f'{self.base_url}/account/settings' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return UserSettings.de_json(result, self) @@ -203,7 +203,6 @@ def account_settings_set( param: str = None, value: Union[str, int, bool] = None, data: Dict[str, Union[str, int, bool]] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[UserSettings]: @@ -216,8 +215,6 @@ def account_settings_set( param (:obj:`str`): Название параметра для изменения. value (:obj:`str` | :obj:`int` | :obj:`bool`): Значение параметра. data (:obj:`dict`): Словарь параметров и значений для множественного изменения. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -232,19 +229,15 @@ def account_settings_set( if not data: data = {param: str(value)} - # TODO (MarshalX) значения в data типа bool должны быть приведены к str при работе с async клиентом. - - result = self._request.post(url, data=data, timeout=timeout, *args, **kwargs) + result = self._request.post(url, data, *args, **kwargs) return UserSettings.de_json(result, self) @log - def settings(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Settings]: + def settings(self, *args, **kwargs) -> Optional[Settings]: """Получение предложений по покупке. Нет обязательных параметров. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -257,17 +250,15 @@ def settings(self, timeout: Union[int, float] = None, *args, **kwargs) -> Option url = f'{self.base_url}/settings' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return Settings.de_json(result, self) @log - def permission_alerts(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[PermissionAlerts]: + def permission_alerts(self, *args, **kwargs) -> Optional[PermissionAlerts]: """Получение оповещений. Нет обязательных параметров. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -279,17 +270,15 @@ def permission_alerts(self, timeout: Union[int, float] = None, *args, **kwargs) url = f'{self.base_url}/permission-alerts' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return PermissionAlerts.de_json(result, self) @log - def account_experiments(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Experiments]: + def account_experiments(self, *args, **kwargs) -> Optional[Experiments]: """Получение значений экспериментальных функций аккаунта. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -301,21 +290,22 @@ def account_experiments(self, timeout: Union[int, float] = None, *args, **kwargs url = f'{self.base_url}/account/experiments' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return Experiments.de_json(result, self) @log def consume_promo_code( - self, code: str, language: str = 'en', timeout: Union[int, float] = None, *args, **kwargs + self, code: str, language: Optional[str] = None, *args, **kwargs ) -> Optional[PromoCodeStatus]: """Активация промо-кода. + Note: + Доступные языки: en, uz, uk, us, ru, kk, hy. + Args: code (:obj:`str`): Промо-код. - language (:obj:`str`, optional): Язык ответа API в ISO 639-1. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. + language (:obj:`str`, optional): Язык ответа API в ISO 639-1. По умолчанию язык клиента. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -327,17 +317,18 @@ def consume_promo_code( url = f'{self.base_url}/account/consume-promo-code' - result = self._request.post(url, {'code': code, 'language': language}, timeout=timeout, *args, **kwargs) + if not language: + language = self.language + + result = self._request.post(url, {'code': code, 'language': language}, *args, **kwargs) return PromoCodeStatus.de_json(result, self) @log - def feed(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Feed]: + def feed(self, *args, **kwargs) -> Optional[Feed]: """Получение потока информации (фида) подобранного под пользователя. Содержит умные плейлисты. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -349,22 +340,20 @@ def feed(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[F url = f'{self.base_url}/feed' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return Feed.de_json(result, self) @log - def feed_wizard_is_passed(self, timeout: Union[int, float] = None, *args, **kwargs) -> bool: + def feed_wizard_is_passed(self, *args, **kwargs) -> bool: url = f'{self.base_url}/feed/wizard/is-passed' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return result.get('is_wizard_passed') or False @log - def landing( - self, blocks: Union[str, List[str]], timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[Landing]: + def landing(self, blocks: Union[str, List[str]], *args, **kwargs) -> Optional[Landing]: """Получение лендинг-страницы содержащий блоки с новыми релизами, чартами, плейлистами с новинками и т.д. Note: @@ -373,8 +362,6 @@ def landing( Args: blocks (:obj:`str` | :obj:`list` из :obj:`str`): Блок или список блоков необходимых для выдачи. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -386,14 +373,14 @@ def landing( url = f'{self.base_url}/landing3' - result = self._request.get( - url, {'blocks': blocks, 'eitherUserId': '10254713668400548221'}, timeout=timeout, *args, **kwargs - ) + result = self._request.get(url, {'blocks': blocks, 'eitherUserId': '10254713668400548221'}, *args, **kwargs) + # TODO (MarshalX) что тут делает константа с чьим-то User ID + # https://github.com/MarshalX/yandex-music-api/issues/553 return Landing.de_json(result, self) @log - def chart(self, chart_option: str = '', timeout: Union[int, float] = None, *args, **kwargs) -> Optional[ChartInfo]: + def chart(self, chart_option: str = '', *args, **kwargs) -> Optional[ChartInfo]: """Получение чарта. Note: @@ -402,8 +389,6 @@ def chart(self, chart_option: str = '', timeout: Union[int, float] = None, *args Args: chart_option (:obj:`str` optional): Параметры чарта. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -418,17 +403,15 @@ def chart(self, chart_option: str = '', timeout: Union[int, float] = None, *args if chart_option: url = f'{url}/{chart_option}' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return ChartInfo.de_json(result, self) @log - def new_releases(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[LandingList]: + def new_releases(self, *args, **kwargs) -> Optional[LandingList]: """Получение полного списка всех новых релизов (альбомов). Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -440,17 +423,15 @@ def new_releases(self, timeout: Union[int, float] = None, *args, **kwargs) -> Op url = f'{self.base_url}/landing3/new-releases' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return LandingList.de_json(result, self) @log - def new_playlists(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[LandingList]: + def new_playlists(self, *args, **kwargs) -> Optional[LandingList]: """Получение полного списка всех новых плейлистов. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -462,21 +443,19 @@ def new_playlists(self, timeout: Union[int, float] = None, *args, **kwargs) -> O url = f'{self.base_url}/landing3/new-playlists' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return LandingList.de_json(result, self) @log - def podcasts(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[LandingList]: + def podcasts(self, *args, **kwargs) -> Optional[LandingList]: """Получение подкастов с лендинга. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: - :obj:`yandex_music.LandingList`: Список подскастов. + :obj:`yandex_music.LandingList`: Список подкастов. Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. @@ -484,17 +463,15 @@ def podcasts(self, timeout: Union[int, float] = None, *args, **kwargs) -> Option url = f'{self.base_url}/landing3/podcasts' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return LandingList.de_json(result, self) @log - def genres(self, timeout: Union[int, float] = None, *args, **kwargs) -> List[Genre]: + def genres(self, *args, **kwargs) -> List[Genre]: """Получение жанров музыки. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -506,12 +483,12 @@ def genres(self, timeout: Union[int, float] = None, *args, **kwargs) -> List[Gen url = f'{self.base_url}/genres' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return Genre.de_list(result, self) @log - def tags(self, tag_id: str, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[TagResult]: + def tags(self, tag_id: str, *args, **kwargs) -> Optional[TagResult]: """Получение тега (подборки). Note: @@ -522,8 +499,6 @@ def tags(self, tag_id: str, timeout: Union[int, float] = None, *args, **kwargs) Args: tag_id (:obj:`str`): Уникальный идентификатор тега. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -535,7 +510,7 @@ def tags(self, tag_id: str, timeout: Union[int, float] = None, *args, **kwargs) url = f'{self.base_url}/tags/{tag_id}/playlist-ids' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return TagResult.de_json(result, self) @@ -544,7 +519,6 @@ def tracks_download_info( self, track_id: Union[str, int], get_direct_links: bool = False, - timeout: Union[int, float] = None, *args, **kwargs, ) -> List[DownloadInfo]: @@ -553,8 +527,6 @@ def tracks_download_info( Args: track_id (:obj:`str` | :obj:`list` из :obj:`str`): Уникальный идентификатор трека или треков. get_direct_links (:obj:`bool`, optional): Получить ли при вызове метода прямую ссылку на загрузку. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -566,20 +538,20 @@ def tracks_download_info( url = f'{self.base_url}/tracks/{track_id}/download-info' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return DownloadInfo.de_list(result, self, get_direct_links) @log - def track_supplement( - self, track_id: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[Supplement]: + def track_supplement(self, track_id: Union[str, int], *args, **kwargs) -> Optional[Supplement]: """Получение дополнительной информации о треке. + Warning: + Получение текста из дополнительной информации устарело. Используйте + :func:`yandex_music.Client.tracks_lyrics`. + Args: - track_id (:obj:`str`): Уникальный идентификатор трека. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. + track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор трека. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -591,20 +563,59 @@ def track_supplement( url = f'{self.base_url}/tracks/{track_id}/supplement' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return Supplement.de_json(result, self) @log - def tracks_similar( - self, track_id: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[SimilarTracks]: + def tracks_lyrics( + self, + track_id: Union[str, int], + format: str = 'TEXT', + **kwargs, + ) -> Optional[TrackLyrics]: + """Получение текста трека. + + Note: + Для работы с методом необходима авторизация. + + Известные значения для аргумента format: + - `LRC` - формат с временными метками. + - `TEXT` - простой текст. + + Args: + track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор трека. + format (:obj:`str`): Формат текста. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.TrackLyrics` | :obj:`None`: Информация о тексте трека. + + Raises: + :class:`yandex_music.exceptions.UnauthorizedError`: Метод вызван без авторизации. + :class:`yandex_music.exceptions.NotFoundError`: Текст у трека отсутствует. + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/tracks/{track_id}/lyrics' + + sign = get_sign_request(track_id) + params = { + 'format': format, + 'timeStamp': sign.timestamp, + 'sign': sign.value, + } + + result = self._request.get(url, params=params, **kwargs) + + return TrackLyrics.de_json(result, self) + + @log + def tracks_similar(self, track_id: Union[str, int], *args, **kwargs) -> Optional[SimilarTracks]: """Получение похожих треков. Args: - track_id (:obj:`str`): Уникальный идентификатор трека. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. + track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор трека. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -616,7 +627,7 @@ def tracks_similar( url = f'{self.base_url}/tracks/{track_id}/similar' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return SimilarTracks.de_json(result, self) @@ -635,7 +646,6 @@ def play_audio( total_played_seconds: int = 0, end_position_seconds: int = 0, client_now: str = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> bool: @@ -654,8 +664,6 @@ def play_audio( total_played_seconds (:obj:`int`, optional): Сколько было всего воспроизведено трека в секундах. end_position_seconds (:obj:`int`, optional): Окончательное значение воспроизведенных секунд. client_now (:obj:`str`, optional): Текущая дата и время клиента в ISO. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -685,20 +693,16 @@ def play_audio( 'client-now': client_now or f'{datetime.now().isoformat()}Z', } - result = self._request.post(url, data, timeout=timeout, *args, **kwargs) + result = self._request.post(url, data, *args, **kwargs) return result == 'ok' @log - def albums_with_tracks( - self, album_id: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[Album]: + def albums_with_tracks(self, album_id: Union[str, int], *args, **kwargs) -> Optional[Album]: """Получение альбома по его уникальному идентификатору вместе с треками. Args: album_id (:obj:`str` | :obj:`int`): Уникальный идентификатор альбома. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -710,7 +714,7 @@ def albums_with_tracks( url = f'{self.base_url}/albums/{album_id}/with-tracks' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return Album.de_json(result, self) @@ -722,7 +726,6 @@ def search( type_: str = 'all', page: int = 0, playlist_in_best: bool = True, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Search]: @@ -741,8 +744,6 @@ def search( type_ (:obj:`str`): Среди какого типа искать (трек, плейлист, альбом, исполнитель, пользователь, подкаст). page (:obj:`int`): Номер страницы. playlist_in_best (:obj:`bool`): Выдавать ли плейлисты лучшим вариантом поиска. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -762,7 +763,7 @@ def search( 'playlist-in-best': str(playlist_in_best), } - result = self._request.get(url, params, timeout=timeout, *args, **kwargs) + result = self._request.get(url, params, *args, **kwargs) if isinstance(result, str): raise BadRequestError(result) @@ -770,13 +771,11 @@ def search( return Search.de_json(result, self) @log - def search_suggest(self, part: str, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Suggestions]: + def search_suggest(self, part: str, *args, **kwargs) -> Optional[Suggestions]: """Получение подсказок по введенной части поискового запроса. Args: part (:obj:`str`): Часть поискового запроса. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -788,14 +787,12 @@ def search_suggest(self, part: str, timeout: Union[int, float] = None, *args, ** url = f'{self.base_url}/search/suggest' - result = self._request.get(url, {'part': part}, timeout=timeout, *args, **kwargs) + result = self._request.get(url, {'part': part}, *args, **kwargs) return Suggestions.de_json(result, self) @log - def users_settings( - self, user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[UserSettings]: + def users_settings(self, user_id: Union[str, int] = None, *args, **kwargs) -> Optional[UserSettings]: """Получение настроек пользователя. Note: @@ -804,8 +801,6 @@ def users_settings( Args: user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя чьи настройки хотим получить. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -820,7 +815,7 @@ def users_settings( url = f'{self.base_url}/users/{user_id}/settings' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return UserSettings.de_json(result.get('user_settings'), self) @@ -829,7 +824,6 @@ def users_playlists( self, kind: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Union[Playlist, List[Playlist]]: @@ -842,8 +836,6 @@ def users_playlists( kind (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста или их список. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -862,26 +854,22 @@ def users_playlists( data = {'kinds': kind} - result = self._request.post(url, data, timeout=timeout, *args, **kwargs) + result = self._request.post(url, data, *args, **kwargs) return Playlist.de_list(result, self) else: url = f'{self.base_url}/users/{user_id}/playlists/{kind}' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return Playlist.de_json(result, self) @log - def users_playlists_recommendations( - self, kind: Union[str, int], user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs - ): + def users_playlists_recommendations(self, kind: Union[str, int], user_id: Union[str, int] = None, *args, **kwargs): """Получение рекомендаций для плейлиста. Args: kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. user_id (:obj:`str` | :obj:`int`): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -895,7 +883,7 @@ def users_playlists_recommendations( url = f'{self.base_url}/users/{user_id}/playlists/{kind}/recommendations' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return PlaylistRecommendations.de_json(result, self) @@ -905,7 +893,6 @@ def users_playlists_create( title: str, visibility: str = 'public', user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Playlist]: @@ -915,8 +902,6 @@ def users_playlists_create( title (:obj:`str`): Название. visibility (:obj:`str`, optional): Модификатор доступа. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -933,21 +918,17 @@ def users_playlists_create( data = {'title': title, 'visibility': visibility} - result = self._request.post(url, data, timeout=timeout, *args, **kwargs) + result = self._request.post(url, data, *args, **kwargs) return Playlist.de_json(result, self) @log - def users_playlists_delete( - self, kind: Union[str, int], user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs - ) -> bool: + def users_playlists_delete(self, kind: Union[str, int], user_id: Union[str, int] = None, *args, **kwargs) -> bool: """Удаление плейлиста. Args: kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -962,7 +943,7 @@ def users_playlists_delete( url = f'{self.base_url}/users/{user_id}/playlists/{kind}/delete' - result = self._request.post(url, timeout=timeout, *args, **kwargs) + result = self._request.post(url, *args, **kwargs) return result == 'ok' @@ -972,7 +953,6 @@ def users_playlists_name( kind: Union[str, int], name: str, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Playlist]: @@ -982,8 +962,6 @@ def users_playlists_name( kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. name (:obj:`str`): Новое название. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -998,7 +976,7 @@ def users_playlists_name( url = f'{self.base_url}/users/{user_id}/playlists/{kind}/name' - result = self._request.post(url, {'value': name}, timeout=timeout, *args, **kwargs) + result = self._request.post(url, {'value': name}, *args, **kwargs) return Playlist.de_json(result, self) @@ -1008,7 +986,6 @@ def users_playlists_visibility( kind: Union[str, int], visibility: str, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Playlist]: @@ -1021,8 +998,6 @@ def users_playlists_visibility( kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. visibility (:obj:`str`): Новое название. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1037,7 +1012,7 @@ def users_playlists_visibility( url = f'{self.base_url}/users/{user_id}/playlists/{kind}/visibility' - result = self._request.post(url, {'value': visibility}, timeout=timeout, *args, **kwargs) + result = self._request.post(url, {'value': visibility}, *args, **kwargs) return Playlist.de_json(result, self) @@ -1048,7 +1023,6 @@ def users_playlists_change( diff: str, revision: int = 1, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Playlist]: @@ -1064,8 +1038,6 @@ def users_playlists_change( revision (:obj:`int`): TODO. diff (:obj:`str`): JSON представления отличий старого и нового плейлиста. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1082,7 +1054,7 @@ def users_playlists_change( data = {'kind': kind, 'revision': revision, 'diff': diff} - result = self._request.post(url, data, timeout=timeout, *args, **kwargs) + result = self._request.post(url, data, *args, **kwargs) return Playlist.de_json(result, self) @@ -1095,7 +1067,6 @@ def users_playlists_insert_track( at: int = 0, revision: int = 1, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Playlist]: @@ -1111,8 +1082,6 @@ def users_playlists_insert_track( at (:obj:`int`): Индекс для вставки. revision (:obj:`int`): TODO. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1127,7 +1096,7 @@ def users_playlists_insert_track( diff = Difference().add_insert(at, {'id': track_id, 'album_id': album_id}) - return self.users_playlists_change(kind, diff.to_json(), revision, user_id, timeout, *args, **kwargs) + return self.users_playlists_change(kind, diff.to_json(), revision, user_id, *args, **kwargs) @log def users_playlists_delete_track( @@ -1137,7 +1106,6 @@ def users_playlists_delete_track( to: int, revision: int = 1, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Playlist]: @@ -1152,8 +1120,6 @@ def users_playlists_delete_track( to (:obj:`int`): По какой индекс. revision (:obj:`int`): TODO. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1168,18 +1134,16 @@ def users_playlists_delete_track( diff = Difference().add_delete(from_, to) - return self.users_playlists_change(kind, diff.to_json(), revision, user_id, timeout, *args, **kwargs) + return self.users_playlists_change(kind, diff.to_json(), revision, user_id, *args, **kwargs) @log - def rotor_account_status(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Status]: - """Получение статуса пользователя с дополнителньыми полями. + def rotor_account_status(self, *args, **kwargs) -> Optional[Status]: + """Получение статуса пользователя с дополнительными полями. Note: Данный статус отличается от обычного наличием дополнительных полей, например, `skips_per_hour`. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1192,17 +1156,15 @@ def rotor_account_status(self, timeout: Union[int, float] = None, *args, **kwarg url = f'{self.base_url}/rotor/account/status' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return Status.de_json(result, self) @log - def rotor_stations_dashboard(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Dashboard]: + def rotor_stations_dashboard(self, *args, **kwargs) -> Optional[Dashboard]: """Получение рекомендованных станций текущего пользователя. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1214,28 +1176,26 @@ def rotor_stations_dashboard(self, timeout: Union[int, float] = None, *args, **k url = f'{self.base_url}/rotor/stations/dashboard' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return Dashboard.de_json(result, self) @log - def rotor_stations_list( - self, language: str = 'ru', timeout: Union[int, float] = None, *args, **kwargs - ) -> List[StationResult]: + def rotor_stations_list(self, language: Optional[str] = None, *args, **kwargs) -> List[StationResult]: """Получение всех радиостанций с настройками пользователя. Note: + Доступные языки: en, uz, uk, us, ru, kk, hy. + Чтобы определить что за тип станции (жанры, настроения, занятие и т.д.) необходимо смотреть в поле `id_for_from`. Args: - language (:obj:`str`): Язык, на котором будет информация о станциях. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. + language (:obj:`str`, optional): Язык, на котором будет информация о станциях. По умолчанию язык клиента. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: - :obj:`list` из :obj:`yandex_music.StationResult` | :obj:`None`: Станции или :obj:`None`. + :obj:`list` из :obj:`yandex_music.StationResult`: Список станций. Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. @@ -1243,7 +1203,10 @@ def rotor_stations_list( url = f'{self.base_url}/rotor/stations/list' - result = self._request.get(url, {'language': language}, timeout=timeout, *args, **kwargs) + if not language: + language = self.language + + result = self._request.get(url, {'language': language}, *args, **kwargs) return StationResult.de_list(result, self) @@ -1257,16 +1220,14 @@ def rotor_station_feedback( batch_id: str = None, total_played_seconds: Union[int, float] = None, track_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: - """Отправка ответной реакции на происходящее при прослушивании радио. + """Отправка обратной связи на действия при прослушивании радио. Note: Сообщения о начале прослушивания радио, начале и конце трека, его пропуска. - Известные типы фидбека: `radioStarted`, `trackStarted`, `trackFinished`, `skip`. + Известные типы обратной связи: `radioStarted`, `trackStarted`, `trackFinished`, `skip`. Пример `station`: `user:onyourwave`, `genre:allrock`. @@ -1274,15 +1235,13 @@ def rotor_station_feedback( Args: station (:obj:`str`): Станция. - type_ (:obj:`str`): Тип отправляемого фидбека. + type_ (:obj:`str`): Тип отправляемого отзыва. timestamp (:obj:`str` | :obj:`float` | :obj:`int`, optional): Текущее время и дата. from_ (:obj:`str`, optional): Откуда начато воспроизведение радио. batch_id (:obj:`str`, optional): Уникальный идентификатор партии треков. Возвращается при получении треков. total_played_seconds (:obj:`int` |:obj:`float`, optional): Сколько было проиграно секунд трека перед действием. track_id (:obj:`int` | :obj:`str`, optional): Уникальной идентификатор трека. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1312,7 +1271,7 @@ def rotor_station_feedback( if total_played_seconds: data.update({'totalPlayedSeconds': total_played_seconds}) - result = self._request.post(url, params=params, json=data, timeout=timeout, *args, **kwargs) + result = self._request.post(url, params=params, json=data, **kwargs) return result == 'ok' @@ -1323,13 +1282,11 @@ def rotor_station_feedback_radio_started( from_: str, batch_id: str = None, timestamp: Union[str, float, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Сокращение для:: - client.rotor_station_feedback(station, 'radioStarted', timestamp, from, *args, **kwargs) + client.rotor_station_feedback(station, 'radioStarted', timestamp, from, batch_id, **kwargs) Returns: :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. @@ -1337,9 +1294,7 @@ def rotor_station_feedback_radio_started( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self.rotor_station_feedback( - station, 'radioStarted', timestamp, from_=from_, batch_id=batch_id, timeout=timeout, *args, **kwargs - ) + return self.rotor_station_feedback(station, 'radioStarted', timestamp, from_=from_, batch_id=batch_id, **kwargs) @log def rotor_station_feedback_track_started( @@ -1348,13 +1303,12 @@ def rotor_station_feedback_track_started( track_id: Union[str, int], batch_id: str = None, timestamp: Union[str, float, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Сокращение для:: - client.rotor_station_feedback(station, 'trackStarted', timestamp, track_id, *args, **kwargs) + client.rotor_station_feedback(station, 'trackStarted', timestamp, track_id=track_id, + batch_id=batch_id, **kwargs) Returns: :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. @@ -1363,7 +1317,7 @@ def rotor_station_feedback_track_started( :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ return self.rotor_station_feedback( - station, 'trackStarted', timestamp, track_id=track_id, batch_id=batch_id, timeout=timeout, *args, **kwargs + station, 'trackStarted', timestamp, track_id=track_id, batch_id=batch_id, **kwargs ) @log @@ -1374,14 +1328,12 @@ def rotor_station_feedback_track_finished( total_played_seconds: float, batch_id: str = None, timestamp: Union[str, float, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Сокращение для:: - client.rotor_station_feedback(station, 'trackFinished', timestamp, track_id, total_played_seconds, - *args, **kwargs) + client.rotor_station_feedback(station, 'trackFinished', timestamp, + track_id=track_id, total_played_seconds=total_played_seconds, batch_id=batch_id, **kwargs) Returns: :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. @@ -1396,8 +1348,6 @@ def rotor_station_feedback_track_finished( track_id=track_id, total_played_seconds=total_played_seconds, batch_id=batch_id, - timeout=timeout, - *args, **kwargs, ) @@ -1409,14 +1359,12 @@ def rotor_station_feedback_skip( total_played_seconds: float, batch_id: str = None, timestamp: Union[str, float, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Сокращение для:: - client.rotor_station_feedback(station, 'skip', timestamp, track_id, total_played_seconds, - *args, **kwargs) + client.rotor_station_feedback(station, 'skip', timestamp, track_id=track_id, + total_played_seconds=total_played_seconds, batch_id=batch_id, **kwargs) Returns: :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. @@ -1431,21 +1379,15 @@ def rotor_station_feedback_skip( track_id=track_id, total_played_seconds=total_played_seconds, batch_id=batch_id, - timeout=timeout, - *args, **kwargs, ) @log - def rotor_station_info( - self, station: str, timeout: Union[int, float] = None, *args, **kwargs - ) -> List[StationResult]: + def rotor_station_info(self, station: str, *args, **kwargs) -> List[StationResult]: """Получение информации о станции и пользовательских настроек на неё. Args: station (:obj:`str`): Станция. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1457,7 +1399,7 @@ def rotor_station_info( url = f'{self.base_url}/rotor/station/{station}/info' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return StationResult.de_list(result, self) @@ -1467,10 +1409,8 @@ def rotor_station_settings2( station: str, mood_energy: str, diversity: str, - language: str = 'not-russian', + language: str = 'not-russian', # TODO(#555): заменить на any type_: str = 'rotor', - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Изменение настроек определённой станции. @@ -1492,8 +1432,6 @@ def rotor_station_settings2( diversity (:obj:`str`): Треки. language (:obj:`str`): Язык. type_ (:obj:`str`): Тип. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1510,7 +1448,7 @@ def rotor_station_settings2( if language: data.update({'language': language}) - result = self._request.post(url, json=data, timeout=timeout, *args, **kwargs) + result = self._request.post(url, json=data, **kwargs) return result == 'ok' @@ -1520,7 +1458,6 @@ def rotor_station_tracks( station: str, settings2: bool = True, queue: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[StationTracksResult]: @@ -1533,11 +1470,11 @@ def rotor_station_tracks( Для продолжения цепочки треков необходимо: 1. Передавать `ID` трека, что был до этого (первый в цепочки). - 2. Отправить фидбек о конче или скипе трека, что был передан в `queue`. + 2. Отправить фидбек о конце или скипе трека, что был передан в `queue`. 3. Отправить фидбек о начале следующего трека (второй в цепочки). 4. Выполнить запрос получения треков. В ответе придёт новые треки или произойдёт сдвиг цепочки на 1 элемент. - Проход по цепочке до коцна не изучен. Часто встречаются дубликаты. + Проход по цепочке до конца не изучен. Часто встречаются дубликаты. Все официальные клиенты выполняют запросы с `settings2 = True`. @@ -1545,8 +1482,6 @@ def rotor_station_tracks( station (:obj:`str`): Станция. settings2 (:obj:`bool`, optional): Использовать ли второй набор настроек. queue (:obj:`str` | :obj:`int` , optional): Уникальной идентификатор трека, который только что был. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1565,20 +1500,16 @@ def rotor_station_tracks( if queue: params = {'queue': queue} - result = self._request.get(url, params=params, timeout=timeout, *args, **kwargs) + result = self._request.get(url, params, *args, **kwargs) return StationTracksResult.de_json(result, self) @log - def artists_brief_info( - self, artist_id: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[BriefInfo]: + def artists_brief_info(self, artist_id: Union[str, int], *args, **kwargs) -> Optional[BriefInfo]: """Получение информации об артисте. Args: artist_id (:obj:`str` | :obj:`int`): Уникальный идентификатор исполнителя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1589,7 +1520,7 @@ def artists_brief_info( """ url = f'{self.base_url}/artists/{artist_id}/brief-info' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return BriefInfo.de_json(result, self) @@ -1599,7 +1530,6 @@ def artists_tracks( artist_id: Union[str, int], page: int = 0, page_size: int = 20, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[ArtistTracks]: @@ -1609,8 +1539,6 @@ def artists_tracks( artist_id (:obj:`str` | :obj:`int`): Уникальный идентификатор артиста. page (:obj:`int`, optional): Номер страницы. page_size (:obj:`int`, optional): Количество треков на странице. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1624,7 +1552,7 @@ def artists_tracks( params = {'page': page, 'page-size': page_size} - result = self._request.get(url, params, timeout=timeout, *args, **kwargs) + result = self._request.get(url, params, *args, **kwargs) return ArtistTracks.de_json(result, self) @@ -1635,7 +1563,6 @@ def artists_direct_albums( page: int = 0, page_size: int = 20, sort_by: str = 'year', - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[ArtistAlbums]: @@ -1649,8 +1576,6 @@ def artists_direct_albums( page (:obj:`int`, optional): Номер страницы. page_size (:obj:`int`, optional): Количество альбомов на странице. sort_by (:obj:`str`, optional): Параметр для сортировки. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1664,7 +1589,7 @@ def artists_direct_albums( params = {'sort-by': sort_by, 'page': page, 'page-size': page_size} - result = self._request.get(url, params, timeout=timeout, *args, **kwargs) + result = self._request.get(url, params, *args, **kwargs) return ArtistAlbums.de_json(result, self) @@ -1674,7 +1599,6 @@ def _like_action( ids: Union[List[Union[str, int]], str, int], remove: bool = False, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> bool: @@ -1693,8 +1617,6 @@ def _like_action( remove (:obj:`bool`, optional): Если :obj:`True` то снимает отметку, иначе ставит. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1709,7 +1631,7 @@ def _like_action( action = 'remove' if remove else 'add-multiple' url = f'{self.base_url}/users/{user_id}/likes/{object_type}s/{action}' - result = self._request.post(url, {f'{object_type}-ids': ids}, timeout=timeout, *args, **kwargs) + result = self._request.post(url, {f'{object_type}-ids': ids}, *args, **kwargs) if object_type == 'track': return 'revision' in result @@ -1721,8 +1643,6 @@ def users_likes_tracks_add( self, track_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Поставить отметку "Мне нравится" треку/трекам. @@ -1735,8 +1655,6 @@ def users_likes_tracks_add( идентификатор трека или треков. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1745,15 +1663,13 @@ def users_likes_tracks_add( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._like_action('track', track_ids, False, user_id, timeout, *args, **kwargs) + return self._like_action('track', track_ids, remove=False, user_id=user_id, **kwargs) @log def users_likes_tracks_remove( self, track_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Снять отметку "Мне нравится" у трека/треков. @@ -1763,8 +1679,6 @@ def users_likes_tracks_remove( идентификатор трека или треков. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1773,15 +1687,13 @@ def users_likes_tracks_remove( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._like_action('track', track_ids, True, user_id, timeout, *args, **kwargs) + return self._like_action('track', track_ids, remove=True, user_id=user_id, **kwargs) @log def users_likes_artists_add( self, artist_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Поставить отметку "Мне нравится" исполнителю/исполнителям. @@ -1791,8 +1703,6 @@ def users_likes_artists_add( идентификатор артиста или артистов. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1801,15 +1711,13 @@ def users_likes_artists_add( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._like_action('artist', artist_ids, False, user_id, timeout, *args, **kwargs) + return self._like_action('artist', artist_ids, remove=False, user_id=user_id, **kwargs) @log def users_likes_artists_remove( self, artist_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Снять отметку "Мне нравится" у исполнителя/исполнителей. @@ -1819,8 +1727,6 @@ def users_likes_artists_remove( идентификатор артиста или артистов. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1829,15 +1735,13 @@ def users_likes_artists_remove( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._like_action('artist', artist_ids, True, user_id, timeout, *args, **kwargs) + return self._like_action('artist', artist_ids, remove=True, user_id=user_id, **kwargs) @log def users_likes_playlists_add( self, playlist_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Поставить отметку "Мне нравится" плейлисту/плейлистам. @@ -1851,8 +1755,6 @@ def users_likes_playlists_add( идентификатор плейлиста или плейлистов. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1861,15 +1763,13 @@ def users_likes_playlists_add( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._like_action('playlist', playlist_ids, False, user_id, timeout, *args, **kwargs) + return self._like_action('playlist', playlist_ids, remove=False, user_id=user_id, **kwargs) @log def users_likes_playlists_remove( self, playlist_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Снять отметку "Мне нравится" у плейлиста/плейлистов. @@ -1883,8 +1783,6 @@ def users_likes_playlists_remove( идентификатор плейлиста или плейлистов. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1893,15 +1791,13 @@ def users_likes_playlists_remove( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._like_action('playlist', playlist_ids, True, user_id, timeout, *args, **kwargs) + return self._like_action('playlist', playlist_ids, remove=True, user_id=user_id, **kwargs) @log def users_likes_albums_add( self, album_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Поставить отметку "Мне нравится" альбому/альбомам. @@ -1911,8 +1807,6 @@ def users_likes_albums_add( идентификатор артиста или артистов. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1921,15 +1815,13 @@ def users_likes_albums_add( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._like_action('album', album_ids, False, user_id, timeout, *args, **kwargs) + return self._like_action('album', album_ids, remove=False, user_id=user_id, **kwargs) @log def users_likes_albums_remove( self, album_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Снять отметку "Мне нравится" у альбома/альбомов. @@ -1939,8 +1831,6 @@ def users_likes_albums_remove( идентификатор артиста или артистов. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1949,14 +1839,13 @@ def users_likes_albums_remove( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._like_action('album', album_ids, True, user_id, timeout, *args, **kwargs) + return self._like_action('album', album_ids, remove=True, user_id=user_id, **kwargs) def _get_list( self, object_type: str, ids: Union[List[Union[str, int]], int, str], params: dict = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> List[Union[Artist, Album, Track, Playlist]]: @@ -1967,8 +1856,6 @@ def _get_list( ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный идентификатор объекта или объектов. params (:obj:`dict`, optional): Параметры, которые будут переданы в запрос. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1985,21 +1872,17 @@ def _get_list( url = f'{self.base_url}/{object_type}s' + ('/list' if object_type == 'playlist' else '') - result = self._request.post(url, params, timeout=timeout, *args, **kwargs) + result = self._request.post(url, params, *args, **kwargs) return de_list[object_type](result, self) @log - def artists( - self, artist_ids: Union[List[Union[str, int]], int, str], timeout: Union[int, float] = None, *args, **kwargs - ) -> List[Artist]: + def artists(self, artist_ids: Union[List[Union[str, int]], int, str], *args, **kwargs) -> List[Artist]: """Получение исполнителя/исполнителей. Args: artist_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный идентификатор исполнителя или исполнителей. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2008,19 +1891,15 @@ def artists( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._get_list('artist', artist_ids, timeout=timeout, *args, **kwargs) + return self._get_list('artist', artist_ids, *args, **kwargs) @log - def albums( - self, album_ids: Union[List[Union[str, int]], int, str], timeout: Union[int, float] = None, *args, **kwargs - ) -> List[Album]: + def albums(self, album_ids: Union[List[Union[str, int]], int, str], *args, **kwargs) -> List[Album]: """Получение альбома/альбомов. Args: album_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный идентификатор альбома или альбомов. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2029,14 +1908,13 @@ def albums( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._get_list('album', album_ids, timeout=timeout, *args, **kwargs) + return self._get_list('album', album_ids, *args, **kwargs) @log def tracks( self, track_ids: Union[List[Union[str, int]], int, str], with_positions: bool = True, - timeout: Union[int, float] = None, *args, **kwargs, ) -> List[Track]: @@ -2046,8 +1924,6 @@ def tracks( track_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный идентификатор трека или треков. with_positions (:obj:`bool`, optional): С позициями TODO. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2056,23 +1932,26 @@ def tracks( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._get_list('track', track_ids, {'with-positions': str(with_positions)}, timeout, *args, **kwargs) + return self._get_list('track', track_ids, {'with-positions': str(with_positions)}, *args, **kwargs) @log - def playlists_list( - self, playlist_ids: Union[List[Union[str, int]], int, str], timeout: Union[int, float] = None, *args, **kwargs - ) -> List[Playlist]: + def playlists_list(self, playlist_ids: Union[List[Union[str, int]], int, str], *args, **kwargs) -> List[Playlist]: """Получение плейлиста/плейлистов. Note: Идентификатор плейлиста указывается в формате `owner_id:playlist_id`. Где `playlist_id` - идентификатор плейлиста, `owner_id` - уникальный идентификатор владельца плейлиста. + Данный метод возвращает сокращенную модель плейлиста для отображения больших список. + + Warning: + Данный метод не возвращает список треков у плейлиста! Для получения объекта :obj:`yandex_music.Playlist` c + заполненным полем `tracks` используйте метод :func:`yandex_music.Client.users_playlists` или + метод :func:`yandex_music.Playlist.fetch_tracks`. + Args: playlist_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный идентификатор плейлиста или плейлистов. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2081,12 +1960,10 @@ def playlists_list( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._get_list('playlist', playlist_ids, timeout=timeout, *args, **kwargs) + return self._get_list('playlist', playlist_ids, *args, **kwargs) @log - def playlists_collective_join( - self, user_id: int, token: str, timeout: Union[int, float] = None, *args, **kwargs - ) -> bool: + def playlists_collective_join(self, user_id: int, token: str, **kwargs) -> bool: """Присоединение к плейлисту как соавтор. Note: @@ -2098,8 +1975,6 @@ def playlists_collective_join( Args: user_id (:obj:`int`): Владелец плейлиста. token (:obj:`str`): Токен для присоединения. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs: Произвольные аргументы (будут переданы в запрос). Returns: @@ -2112,21 +1987,17 @@ def playlists_collective_join( params = {'uid': user_id, 'token': token} - result = self._request.post(url, params=params, timeout=timeout, *args, **kwargs) + result = self._request.post(url, params=params, **kwargs) return result == 'ok' @log - def users_playlists_list( - self, user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs - ) -> List[Playlist]: + def users_playlists_list(self, user_id: Union[str, int] = None, *args, **kwargs) -> List[Playlist]: """Получение списка плейлистов пользователя. Args: user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2140,7 +2011,7 @@ def users_playlists_list( url = f'{self.base_url}/users/{user_id}/playlists/list' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return Playlist.de_list(result, self) @@ -2149,7 +2020,6 @@ def _get_likes( object_type: str, user_id: Union[str, int] = None, params: dict = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Union[List[Like], Optional[TracksList]]: @@ -2160,8 +2030,6 @@ def _get_likes( user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. params (:obj:`dict`, optional): Параметры, которые будут переданы в запрос. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2175,7 +2043,7 @@ def _get_likes( url = f'{self.base_url}/users/{user_id}/likes/{object_type}s' - result = self._request.get(url, params, timeout=timeout, *args, **kwargs) + result = self._request.get(url, params, *args, **kwargs) if object_type == 'track': return TracksList.de_json(result.get('library'), self) @@ -2187,7 +2055,6 @@ def users_likes_tracks( self, user_id: Union[str, int] = None, if_modified_since_revision: int = 0, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[TracksList]: @@ -2197,8 +2064,6 @@ def users_likes_tracks( user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. if_modified_since_revision (:obj:`int`, optional): TODO. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2208,21 +2073,17 @@ def users_likes_tracks( :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ return self._get_likes( - 'track', user_id, {'if-modified-since-revision': if_modified_since_revision}, timeout, *args, **kwargs + 'track', user_id, {'if-modified-since-revision': if_modified_since_revision}, *args, **kwargs ) @log - def users_likes_albums( - self, user_id: Union[str, int] = None, rich: bool = True, timeout: Union[int, float] = None, *args, **kwargs - ) -> List[Like]: + def users_likes_albums(self, user_id: Union[str, int] = None, rich: bool = True, *args, **kwargs) -> List[Like]: """Получение альбомов с отметкой "Мне нравится". Args: user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. rich (:obj:`bool`, optional): Если False, то приходит укороченная версия. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2231,14 +2092,13 @@ def users_likes_albums( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._get_likes('album', user_id, {'rich': str(rich)}, timeout, *args, **kwargs) + return self._get_likes('album', user_id, {'rich': str(rich)}, *args, **kwargs) @log def users_likes_artists( self, user_id: Union[str, int] = None, with_timestamps: bool = True, - timeout: Union[int, float] = None, *args, **kwargs, ) -> List[Like]: @@ -2248,8 +2108,6 @@ def users_likes_artists( user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. with_timestamps (:obj:`bool`, optional): С временными метками TODO. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2258,19 +2116,15 @@ def users_likes_artists( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._get_likes('artist', user_id, {'with-timestamps': str(with_timestamps)}, timeout, *args, **kwargs) + return self._get_likes('artist', user_id, {'with-timestamps': str(with_timestamps)}, *args, **kwargs) @log - def users_likes_playlists( - self, user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs - ) -> List[Like]: + def users_likes_playlists(self, user_id: Union[str, int] = None, *args, **kwargs) -> List[Like]: """Получение плейлистов с отметкой "Мне нравится". Args: user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2279,14 +2133,13 @@ def users_likes_playlists( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._get_likes('playlist', user_id, timeout=timeout, *args, **kwargs) + return self._get_likes('playlist', user_id, *args, **kwargs) @log def users_dislikes_tracks( self, user_id: Union[str, int] = None, if_modified_since_revision: int = 0, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[TracksList]: @@ -2296,8 +2149,6 @@ def users_dislikes_tracks( user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. if_modified_since_revision (:obj:`bool`, optional): TODO. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2311,9 +2162,7 @@ def users_dislikes_tracks( url = f'{self.base_url}/users/{user_id}/dislikes/tracks' - result = self._request.get( - url, {'if_modified_since_revision': if_modified_since_revision}, timeout=timeout, *args, **kwargs - ) + result = self._request.get(url, {'if_modified_since_revision': if_modified_since_revision}, *args, **kwargs) return TracksList.de_json(result.get('library'), self) @@ -2322,7 +2171,6 @@ def _dislike_action( ids: Union[List[Union[str, int]], str, int], remove: bool = False, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> bool: @@ -2334,8 +2182,6 @@ def _dislike_action( remove (:obj:`bool`, optional): Если :obj:`True`, то снимает отметку, иначе ставит. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2350,7 +2196,7 @@ def _dislike_action( action = 'remove' if remove else 'add-multiple' url = f'{self.base_url}/users/{user_id}/dislikes/tracks/{action}' - result = self._request.post(url, {f'track-ids': ids}, timeout=timeout, *args, **kwargs) + result = self._request.post(url, {f'track-ids': ids}, *args, **kwargs) return 'revision' in result @@ -2359,8 +2205,6 @@ def users_dislikes_tracks_add( self, track_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Поставить отметку "Не рекомендовать" треку/трекам. @@ -2373,8 +2217,6 @@ def users_dislikes_tracks_add( идентификатор трека или треков. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2383,15 +2225,13 @@ def users_dislikes_tracks_add( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._dislike_action(track_ids, False, user_id, timeout, *args, **kwargs) + return self._dislike_action(track_ids, remove=False, user_id=user_id, **kwargs) @log def users_dislikes_tracks_remove( self, track_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Снять отметку "Не рекомендовать" у трека/треков. @@ -2401,8 +2241,6 @@ def users_dislikes_tracks_remove( идентификатор трека или треков. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2411,7 +2249,7 @@ def users_dislikes_tracks_remove( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return self._dislike_action(track_ids, True, user_id, timeout, *args, **kwargs) + return self._dislike_action(track_ids, remove=True, user_id=user_id, **kwargs) @log def after_track( @@ -2422,7 +2260,6 @@ def after_track( context: str = 'playlist', types: str = 'shot', from_: str = 'mobile-landing-origin-default', - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[ShotEvent]: @@ -2447,8 +2284,6 @@ def after_track( context (:obj:`str`, optional): Место, откуда было вызвано получение. types (:obj:`str`, optional): Тип того, что вернуть после трека. from_ (:obj:`str`, optional): Место, с которого попали в контекст. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2469,13 +2304,14 @@ def after_track( 'types': types, } - result = self._request.get(url, params=params, timeout=timeout, *args, **kwargs) + result = self._request.get(url, params, *args, **kwargs) - # TODO судя по всему эндпоинт ещё возвращает рекламу после треков для пользователей без подписки. + # TODO (MarshalX) судя по всему ручка ещё возвращает рекламу после треков для пользователей без подписки. + # https://github.com/MarshalX/yandex-music-api/issues/557 return ShotEvent.de_json(result.get('shot_event'), self) @log - def queues_list(self, device: str = None, timeout: Union[int, float] = None, *args, **kwargs) -> List[QueueItem]: + def queues_list(self, device: str = None, *args, **kwargs) -> List[QueueItem]: """Получение всех очередей треков с разных устройств для синхронизации между ними. Note: @@ -2486,8 +2322,6 @@ def queues_list(self, device: str = None, timeout: Union[int, float] = None, *ar Args: device (:obj:`str`, optional): Содержит информацию об устройстве с которого выполняется запрос. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2502,18 +2336,16 @@ def queues_list(self, device: str = None, timeout: Union[int, float] = None, *ar url = f'{self.base_url}/queues' self._request.headers['X-Yandex-Music-Device'] = device - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return QueueItem.de_list(result.get('queues'), self) @log - def queue(self, queue_id: str, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Queue]: + def queue(self, queue_id: str, *args, **kwargs) -> Optional[Queue]: """Получение информации об очереди треков и самих треков в ней. Args: queue_id (:obj:`str`): Уникальный идентификатор очереди. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2524,14 +2356,12 @@ def queue(self, queue_id: str, timeout: Union[int, float] = None, *args, **kwarg """ url = f'{self.base_url}/queues/{queue_id}' - result = self._request.get(url, timeout=timeout, *args, **kwargs) + result = self._request.get(url, *args, **kwargs) return Queue.de_json(result, self) @log - def queue_update_position( - self, queue_id: str, current_index: int, device: str = None, timeout: Union[int, float] = None, *args, **kwargs - ) -> bool: + def queue_update_position(self, queue_id: str, current_index: int, device: str = None, **kwargs) -> bool: """Установка текущего индекса проигрываемого трека в очереди треков. Note: @@ -2541,8 +2371,6 @@ def queue_update_position( queue_id (:obj:`str`): Уникальный идентификатор очереди. current_index (:obj:`int`): Текущий индекс. device (:obj:`str`, optional): Содержит информацию об устройстве с которого выполняется запрос. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2557,23 +2385,17 @@ def queue_update_position( url = f'{self.base_url}/queues/{queue_id}/update-position' self._request.headers['X-Yandex-Music-Device'] = device - result = self._request.post( - url, {'isInteractive': False}, params={'currentIndex': current_index}, timeout=timeout, *args, **kwargs - ) + result = self._request.post(url, {'isInteractive': False}, params={'currentIndex': current_index}, **kwargs) return result.get('status') == 'ok' @log - def queue_create( - self, queue: Union[Queue, str], device: str = None, timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[str]: + def queue_create(self, queue: Union[Queue, str], device: str = None, *args, **kwargs) -> Optional[str]: """Создание новой очереди треков. Args: queue (:obj:`yandex_music.Queue` | :obj:`str`): Объект очереди или JSON строка с этим объектом. device (:obj:`str`, optional): Содержит информацию об устройстве с которого выполняется запрос. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2591,9 +2413,9 @@ def queue_create( url = f'{self.base_url}/queues' self._request.headers['X-Yandex-Music-Device'] = device - result = self._request.post(url, queue, timeout=timeout, *args, **kwargs) + result = self._request.post(url, queue, *args, **kwargs) - return result.get('id_') + return result.get('id') # camelCase псевдонимы @@ -2619,6 +2441,8 @@ def queue_create( tracksDownloadInfo = tracks_download_info #: Псевдоним для :attr:`track_supplement` trackSupplement = track_supplement + #: Псевдоним для :attr:`tracks_lyrics` + tracksLyrics = tracks_lyrics #: Псевдоним для :attr:`tracks_similar` tracksSimilar = tracks_similar #: Псевдоним для :attr:`play_audio` diff --git a/yandex_music/client_async.py b/yandex_music/client_async.py index 5b3320d6..88b0b448 100644 --- a/yandex_music/client_async.py +++ b/yandex_music/client_async.py @@ -5,7 +5,7 @@ import functools import logging from datetime import datetime -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Union, TypeVar, Callable, Any from yandex_music import ( Album, @@ -32,6 +32,7 @@ Status, Suggestions, SimilarTracks, + TrackLyrics, Track, TracksList, UserSettings, @@ -49,6 +50,7 @@ from yandex_music.exceptions import BadRequestError from yandex_music.utils.difference import Difference from yandex_music.utils.request_async import Request +from yandex_music.utils.sign_request import get_sign_request de_list = { 'artist': Artist.de_list, @@ -59,12 +61,14 @@ logging.getLogger(__name__).addHandler(logging.NullHandler()) +F = TypeVar('F', bound=Callable[..., Any]) -def log(method): + +def log(method: F) -> F: logger = logging.getLogger(method.__module__) @functools.wraps(method) - async def wrapper(*args, **kwargs): + async def wrapper(*args, **kwargs) -> Any: logger.debug(f'Entering: {method.__name__}') result = await method(*args, **kwargs) @@ -99,12 +103,12 @@ class ClientAsync(YandexMusicObject): base_url (:obj:`str`, optional): Ссылка на API Yandex Music. request (:obj:`yandex_music.utils.request.Request`, optional): Пре-инициализация :class:`yandex_music.utils.request.Request`. - language (:obj:`str`, optional): Язык, на котором будут приходить ответы от API. + language (:obj:`str`, optional): Язык, на котором будут приходить ответы от API. По умолчанию русский. report_unknown_fields (:obj:`bool`, optional): Включить предупреждения о неизвестных полях от API, которых нет в библиотеке. """ - notice_displayed = False + __notice_displayed = False def __init__( self, @@ -114,10 +118,10 @@ def __init__( language: str = 'ru', report_unknown_fields=False, ) -> None: - if not ClientAsync.notice_displayed: + if not ClientAsync.__notice_displayed: print(f'Yandex Music API v{__version__}, {__copyright__}') print(f'Licensed under the terms of the {__license__}', end='\n\n') - ClientAsync.notice_displayed = True + ClientAsync.__notice_displayed = True self.logger = logging.getLogger(__name__) self.token = token @@ -157,12 +161,10 @@ async def init(self): return self @log - async def account_status(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Status]: + async def account_status(self, *args, **kwargs) -> Optional[Status]: """Получение статуса аккаунта. Нет обязательных параметров. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -174,17 +176,15 @@ async def account_status(self, timeout: Union[int, float] = None, *args, **kwarg url = f'{self.base_url}/account/status' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return Status.de_json(result, self) @log - async def account_settings(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[UserSettings]: + async def account_settings(self, *args, **kwargs) -> Optional[UserSettings]: """Получение настроек текущего пользователя. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -197,7 +197,7 @@ async def account_settings(self, timeout: Union[int, float] = None, *args, **kwa url = f'{self.base_url}/account/settings' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return UserSettings.de_json(result, self) @@ -207,7 +207,6 @@ async def account_settings_set( param: str = None, value: Union[str, int, bool] = None, data: Dict[str, Union[str, int, bool]] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[UserSettings]: @@ -220,8 +219,6 @@ async def account_settings_set( param (:obj:`str`): Название параметра для изменения. value (:obj:`str` | :obj:`int` | :obj:`bool`): Значение параметра. data (:obj:`dict`): Словарь параметров и значений для множественного изменения. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -236,19 +233,15 @@ async def account_settings_set( if not data: data = {param: str(value)} - # TODO (MarshalX) значения в data типа bool должны быть приведены к str при работе с async клиентом. - - result = await self._request.post(url, data=data, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, data, *args, **kwargs) return UserSettings.de_json(result, self) @log - async def settings(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Settings]: + async def settings(self, *args, **kwargs) -> Optional[Settings]: """Получение предложений по покупке. Нет обязательных параметров. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -261,17 +254,15 @@ async def settings(self, timeout: Union[int, float] = None, *args, **kwargs) -> url = f'{self.base_url}/settings' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return Settings.de_json(result, self) @log - async def permission_alerts(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[PermissionAlerts]: + async def permission_alerts(self, *args, **kwargs) -> Optional[PermissionAlerts]: """Получение оповещений. Нет обязательных параметров. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -283,17 +274,15 @@ async def permission_alerts(self, timeout: Union[int, float] = None, *args, **kw url = f'{self.base_url}/permission-alerts' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return PermissionAlerts.de_json(result, self) @log - async def account_experiments(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Experiments]: + async def account_experiments(self, *args, **kwargs) -> Optional[Experiments]: """Получение значений экспериментальных функций аккаунта. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -305,21 +294,22 @@ async def account_experiments(self, timeout: Union[int, float] = None, *args, ** url = f'{self.base_url}/account/experiments' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return Experiments.de_json(result, self) @log async def consume_promo_code( - self, code: str, language: str = 'en', timeout: Union[int, float] = None, *args, **kwargs + self, code: str, language: Optional[str] = None, *args, **kwargs ) -> Optional[PromoCodeStatus]: """Активация промо-кода. + Note: + Доступные языки: en, uz, uk, us, ru, kk, hy. + Args: code (:obj:`str`): Промо-код. - language (:obj:`str`, optional): Язык ответа API в ISO 639-1. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. + language (:obj:`str`, optional): Язык ответа API в ISO 639-1. По умолчанию язык клиента. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -331,17 +321,18 @@ async def consume_promo_code( url = f'{self.base_url}/account/consume-promo-code' - result = await self._request.post(url, {'code': code, 'language': language}, timeout=timeout, *args, **kwargs) + if not language: + language = self.language + + result = await self._request.post(url, {'code': code, 'language': language}, *args, **kwargs) return PromoCodeStatus.de_json(result, self) @log - async def feed(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Feed]: + async def feed(self, *args, **kwargs) -> Optional[Feed]: """Получение потока информации (фида) подобранного под пользователя. Содержит умные плейлисты. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -353,22 +344,20 @@ async def feed(self, timeout: Union[int, float] = None, *args, **kwargs) -> Opti url = f'{self.base_url}/feed' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return Feed.de_json(result, self) @log - async def feed_wizard_is_passed(self, timeout: Union[int, float] = None, *args, **kwargs) -> bool: + async def feed_wizard_is_passed(self, *args, **kwargs) -> bool: url = f'{self.base_url}/feed/wizard/is-passed' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return result.get('is_wizard_passed') or False @log - async def landing( - self, blocks: Union[str, List[str]], timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[Landing]: + async def landing(self, blocks: Union[str, List[str]], *args, **kwargs) -> Optional[Landing]: """Получение лендинг-страницы содержащий блоки с новыми релизами, чартами, плейлистами с новинками и т.д. Note: @@ -377,8 +366,6 @@ async def landing( Args: blocks (:obj:`str` | :obj:`list` из :obj:`str`): Блок или список блоков необходимых для выдачи. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -391,15 +378,15 @@ async def landing( url = f'{self.base_url}/landing3' result = await self._request.get( - url, {'blocks': blocks, 'eitherUserId': '10254713668400548221'}, timeout=timeout, *args, **kwargs + url, {'blocks': blocks, 'eitherUserId': '10254713668400548221'}, *args, **kwargs ) + # TODO (MarshalX) что тут делает константа с чьим-то User ID + # https://github.com/MarshalX/yandex-music-api/issues/553 return Landing.de_json(result, self) @log - async def chart( - self, chart_option: str = '', timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[ChartInfo]: + async def chart(self, chart_option: str = '', *args, **kwargs) -> Optional[ChartInfo]: """Получение чарта. Note: @@ -408,8 +395,6 @@ async def chart( Args: chart_option (:obj:`str` optional): Параметры чарта. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -424,17 +409,15 @@ async def chart( if chart_option: url = f'{url}/{chart_option}' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return ChartInfo.de_json(result, self) @log - async def new_releases(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[LandingList]: + async def new_releases(self, *args, **kwargs) -> Optional[LandingList]: """Получение полного списка всех новых релизов (альбомов). Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -446,17 +429,15 @@ async def new_releases(self, timeout: Union[int, float] = None, *args, **kwargs) url = f'{self.base_url}/landing3/new-releases' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return LandingList.de_json(result, self) @log - async def new_playlists(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[LandingList]: + async def new_playlists(self, *args, **kwargs) -> Optional[LandingList]: """Получение полного списка всех новых плейлистов. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -468,21 +449,19 @@ async def new_playlists(self, timeout: Union[int, float] = None, *args, **kwargs url = f'{self.base_url}/landing3/new-playlists' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return LandingList.de_json(result, self) @log - async def podcasts(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[LandingList]: + async def podcasts(self, *args, **kwargs) -> Optional[LandingList]: """Получение подкастов с лендинга. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: - :obj:`yandex_music.LandingList`: Список подскастов. + :obj:`yandex_music.LandingList`: Список подкастов. Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. @@ -490,17 +469,15 @@ async def podcasts(self, timeout: Union[int, float] = None, *args, **kwargs) -> url = f'{self.base_url}/landing3/podcasts' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return LandingList.de_json(result, self) @log - async def genres(self, timeout: Union[int, float] = None, *args, **kwargs) -> List[Genre]: + async def genres(self, *args, **kwargs) -> List[Genre]: """Получение жанров музыки. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -512,12 +489,12 @@ async def genres(self, timeout: Union[int, float] = None, *args, **kwargs) -> Li url = f'{self.base_url}/genres' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return Genre.de_list(result, self) @log - async def tags(self, tag_id: str, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[TagResult]: + async def tags(self, tag_id: str, *args, **kwargs) -> Optional[TagResult]: """Получение тега (подборки). Note: @@ -528,8 +505,6 @@ async def tags(self, tag_id: str, timeout: Union[int, float] = None, *args, **kw Args: tag_id (:obj:`str`): Уникальный идентификатор тега. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -541,7 +516,7 @@ async def tags(self, tag_id: str, timeout: Union[int, float] = None, *args, **kw url = f'{self.base_url}/tags/{tag_id}/playlist-ids' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return TagResult.de_json(result, self) @@ -550,7 +525,6 @@ async def tracks_download_info( self, track_id: Union[str, int], get_direct_links: bool = False, - timeout: Union[int, float] = None, *args, **kwargs, ) -> List[DownloadInfo]: @@ -559,8 +533,6 @@ async def tracks_download_info( Args: track_id (:obj:`str` | :obj:`list` из :obj:`str`): Уникальный идентификатор трека или треков. get_direct_links (:obj:`bool`, optional): Получить ли при вызове метода прямую ссылку на загрузку. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -572,20 +544,20 @@ async def tracks_download_info( url = f'{self.base_url}/tracks/{track_id}/download-info' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return await DownloadInfo.de_list_async(result, self, get_direct_links) @log - async def track_supplement( - self, track_id: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[Supplement]: + async def track_supplement(self, track_id: Union[str, int], *args, **kwargs) -> Optional[Supplement]: """Получение дополнительной информации о треке. + Warning: + Получение текста из дополнительной информации устарело. Используйте + :func:`yandex_music.ClientAsync.tracks_lyrics`. + Args: - track_id (:obj:`str`): Уникальный идентификатор трека. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. + track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор трека. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -597,20 +569,59 @@ async def track_supplement( url = f'{self.base_url}/tracks/{track_id}/supplement' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return Supplement.de_json(result, self) @log - async def tracks_similar( - self, track_id: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[SimilarTracks]: + async def tracks_lyrics( + self, + track_id: Union[str, int], + format: str = 'TEXT', + **kwargs, + ) -> Optional[TrackLyrics]: + """Получение текста трека. + + Note: + Для работы с методом необходима авторизация. + + Известные значения для аргумента format: + - `LRC` - формат с временными метками. + - `TEXT` - простой текст. + + Args: + track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор трека. + format (:obj:`str`): Формат текста. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.TrackLyrics` | :obj:`None`: Информация о тексте трека. + + Raises: + :class:`yandex_music.exceptions.UnauthorizedError`: Метод вызван без авторизации. + :class:`yandex_music.exceptions.NotFoundError`: Текст у трека отсутствует. + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/tracks/{track_id}/lyrics' + + sign = get_sign_request(track_id) + params = { + 'format': format, + 'timeStamp': sign.timestamp, + 'sign': sign.value, + } + + result = await self._request.get(url, params=params, **kwargs) + + return TrackLyrics.de_json(result, self) + + @log + async def tracks_similar(self, track_id: Union[str, int], *args, **kwargs) -> Optional[SimilarTracks]: """Получение похожих треков. Args: - track_id (:obj:`str`): Уникальный идентификатор трека. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. + track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор трека. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -622,7 +633,7 @@ async def tracks_similar( url = f'{self.base_url}/tracks/{track_id}/similar' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return SimilarTracks.de_json(result, self) @@ -641,7 +652,6 @@ async def play_audio( total_played_seconds: int = 0, end_position_seconds: int = 0, client_now: str = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> bool: @@ -660,8 +670,6 @@ async def play_audio( total_played_seconds (:obj:`int`, optional): Сколько было всего воспроизведено трека в секундах. end_position_seconds (:obj:`int`, optional): Окончательное значение воспроизведенных секунд. client_now (:obj:`str`, optional): Текущая дата и время клиента в ISO. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -691,20 +699,16 @@ async def play_audio( 'client-now': client_now or f'{datetime.now().isoformat()}Z', } - result = await self._request.post(url, data, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, data, *args, **kwargs) return result == 'ok' @log - async def albums_with_tracks( - self, album_id: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[Album]: + async def albums_with_tracks(self, album_id: Union[str, int], *args, **kwargs) -> Optional[Album]: """Получение альбома по его уникальному идентификатору вместе с треками. Args: album_id (:obj:`str` | :obj:`int`): Уникальный идентификатор альбома. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -716,7 +720,7 @@ async def albums_with_tracks( url = f'{self.base_url}/albums/{album_id}/with-tracks' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return Album.de_json(result, self) @@ -728,7 +732,6 @@ async def search( type_: str = 'all', page: int = 0, playlist_in_best: bool = True, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Search]: @@ -747,8 +750,6 @@ async def search( type_ (:obj:`str`): Среди какого типа искать (трек, плейлист, альбом, исполнитель, пользователь, подкаст). page (:obj:`int`): Номер страницы. playlist_in_best (:obj:`bool`): Выдавать ли плейлисты лучшим вариантом поиска. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -768,7 +769,7 @@ async def search( 'playlist-in-best': str(playlist_in_best), } - result = await self._request.get(url, params, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, params, *args, **kwargs) if isinstance(result, str): raise BadRequestError(result) @@ -776,15 +777,11 @@ async def search( return Search.de_json(result, self) @log - async def search_suggest( - self, part: str, timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[Suggestions]: + async def search_suggest(self, part: str, *args, **kwargs) -> Optional[Suggestions]: """Получение подсказок по введенной части поискового запроса. Args: part (:obj:`str`): Часть поискового запроса. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -796,14 +793,12 @@ async def search_suggest( url = f'{self.base_url}/search/suggest' - result = await self._request.get(url, {'part': part}, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, {'part': part}, *args, **kwargs) return Suggestions.de_json(result, self) @log - async def users_settings( - self, user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[UserSettings]: + async def users_settings(self, user_id: Union[str, int] = None, *args, **kwargs) -> Optional[UserSettings]: """Получение настроек пользователя. Note: @@ -812,8 +807,6 @@ async def users_settings( Args: user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя чьи настройки хотим получить. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -828,7 +821,7 @@ async def users_settings( url = f'{self.base_url}/users/{user_id}/settings' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return UserSettings.de_json(result.get('user_settings'), self) @@ -837,7 +830,6 @@ async def users_playlists( self, kind: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Union[Playlist, List[Playlist]]: @@ -850,8 +842,6 @@ async def users_playlists( kind (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста или их список. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -870,26 +860,24 @@ async def users_playlists( data = {'kinds': kind} - result = await self._request.post(url, data, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, data, *args, **kwargs) return Playlist.de_list(result, self) else: url = f'{self.base_url}/users/{user_id}/playlists/{kind}' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return Playlist.de_json(result, self) @log async def users_playlists_recommendations( - self, kind: Union[str, int], user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs + self, kind: Union[str, int], user_id: Union[str, int] = None, *args, **kwargs ): """Получение рекомендаций для плейлиста. Args: kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. user_id (:obj:`str` | :obj:`int`): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -903,7 +891,7 @@ async def users_playlists_recommendations( url = f'{self.base_url}/users/{user_id}/playlists/{kind}/recommendations' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return PlaylistRecommendations.de_json(result, self) @@ -913,7 +901,6 @@ async def users_playlists_create( title: str, visibility: str = 'public', user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Playlist]: @@ -923,8 +910,6 @@ async def users_playlists_create( title (:obj:`str`): Название. visibility (:obj:`str`, optional): Модификатор доступа. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -941,21 +926,19 @@ async def users_playlists_create( data = {'title': title, 'visibility': visibility} - result = await self._request.post(url, data, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, data, *args, **kwargs) return Playlist.de_json(result, self) @log async def users_playlists_delete( - self, kind: Union[str, int], user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs + self, kind: Union[str, int], user_id: Union[str, int] = None, *args, **kwargs ) -> bool: """Удаление плейлиста. Args: kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -970,7 +953,7 @@ async def users_playlists_delete( url = f'{self.base_url}/users/{user_id}/playlists/{kind}/delete' - result = await self._request.post(url, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, *args, **kwargs) return result == 'ok' @@ -980,7 +963,6 @@ async def users_playlists_name( kind: Union[str, int], name: str, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Playlist]: @@ -990,8 +972,6 @@ async def users_playlists_name( kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. name (:obj:`str`): Новое название. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1006,7 +986,7 @@ async def users_playlists_name( url = f'{self.base_url}/users/{user_id}/playlists/{kind}/name' - result = await self._request.post(url, {'value': name}, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, {'value': name}, *args, **kwargs) return Playlist.de_json(result, self) @@ -1016,7 +996,6 @@ async def users_playlists_visibility( kind: Union[str, int], visibility: str, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Playlist]: @@ -1029,8 +1008,6 @@ async def users_playlists_visibility( kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. visibility (:obj:`str`): Новое название. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1045,7 +1022,7 @@ async def users_playlists_visibility( url = f'{self.base_url}/users/{user_id}/playlists/{kind}/visibility' - result = await self._request.post(url, {'value': visibility}, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, {'value': visibility}, *args, **kwargs) return Playlist.de_json(result, self) @@ -1056,7 +1033,6 @@ async def users_playlists_change( diff: str, revision: int = 1, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Playlist]: @@ -1072,8 +1048,6 @@ async def users_playlists_change( revision (:obj:`int`): TODO. diff (:obj:`str`): JSON представления отличий старого и нового плейлиста. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1090,7 +1064,7 @@ async def users_playlists_change( data = {'kind': kind, 'revision': revision, 'diff': diff} - result = await self._request.post(url, data, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, data, *args, **kwargs) return Playlist.de_json(result, self) @@ -1103,7 +1077,6 @@ async def users_playlists_insert_track( at: int = 0, revision: int = 1, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Playlist]: @@ -1119,8 +1092,6 @@ async def users_playlists_insert_track( at (:obj:`int`): Индекс для вставки. revision (:obj:`int`): TODO. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1135,7 +1106,7 @@ async def users_playlists_insert_track( diff = Difference().add_insert(at, {'id': track_id, 'album_id': album_id}) - return await self.users_playlists_change(kind, diff.to_json(), revision, user_id, timeout, *args, **kwargs) + return await self.users_playlists_change(kind, diff.to_json(), revision, user_id, *args, **kwargs) @log async def users_playlists_delete_track( @@ -1145,7 +1116,6 @@ async def users_playlists_delete_track( to: int, revision: int = 1, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[Playlist]: @@ -1160,8 +1130,6 @@ async def users_playlists_delete_track( to (:obj:`int`): По какой индекс. revision (:obj:`int`): TODO. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1176,18 +1144,16 @@ async def users_playlists_delete_track( diff = Difference().add_delete(from_, to) - return await self.users_playlists_change(kind, diff.to_json(), revision, user_id, timeout, *args, **kwargs) + return await self.users_playlists_change(kind, diff.to_json(), revision, user_id, *args, **kwargs) @log - async def rotor_account_status(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Status]: - """Получение статуса пользователя с дополнителньыми полями. + async def rotor_account_status(self, *args, **kwargs) -> Optional[Status]: + """Получение статуса пользователя с дополнительными полями. Note: Данный статус отличается от обычного наличием дополнительных полей, например, `skips_per_hour`. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1200,17 +1166,15 @@ async def rotor_account_status(self, timeout: Union[int, float] = None, *args, * url = f'{self.base_url}/rotor/account/status' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return Status.de_json(result, self) @log - async def rotor_stations_dashboard(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Dashboard]: + async def rotor_stations_dashboard(self, *args, **kwargs) -> Optional[Dashboard]: """Получение рекомендованных станций текущего пользователя. Args: - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1222,28 +1186,26 @@ async def rotor_stations_dashboard(self, timeout: Union[int, float] = None, *arg url = f'{self.base_url}/rotor/stations/dashboard' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return Dashboard.de_json(result, self) @log - async def rotor_stations_list( - self, language: str = 'ru', timeout: Union[int, float] = None, *args, **kwargs - ) -> List[StationResult]: + async def rotor_stations_list(self, language: Optional[str] = None, *args, **kwargs) -> List[StationResult]: """Получение всех радиостанций с настройками пользователя. Note: + Доступные языки: en, uz, uk, us, ru, kk, hy. + Чтобы определить что за тип станции (жанры, настроения, занятие и т.д.) необходимо смотреть в поле `id_for_from`. Args: - language (:obj:`str`): Язык, на котором будет информация о станциях. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. + language (:obj:`str`, optional): Язык, на котором будет информация о станциях. По умолчанию язык клиента. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: - :obj:`list` из :obj:`yandex_music.StationResult` | :obj:`None`: Станции или :obj:`None`. + :obj:`list` из :obj:`yandex_music.StationResult`: Список станций. Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. @@ -1251,7 +1213,10 @@ async def rotor_stations_list( url = f'{self.base_url}/rotor/stations/list' - result = await self._request.get(url, {'language': language}, timeout=timeout, *args, **kwargs) + if not language: + language = self.language + + result = await self._request.get(url, {'language': language}, *args, **kwargs) return StationResult.de_list(result, self) @@ -1265,16 +1230,14 @@ async def rotor_station_feedback( batch_id: str = None, total_played_seconds: Union[int, float] = None, track_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: - """Отправка ответной реакции на происходящее при прослушивании радио. + """Отправка обратной связи на действия при прослушивании радио. Note: Сообщения о начале прослушивания радио, начале и конце трека, его пропуска. - Известные типы фидбека: `radioStarted`, `trackStarted`, `trackFinished`, `skip`. + Известные типы обратной связи: `radioStarted`, `trackStarted`, `trackFinished`, `skip`. Пример `station`: `user:onyourwave`, `genre:allrock`. @@ -1282,15 +1245,13 @@ async def rotor_station_feedback( Args: station (:obj:`str`): Станция. - type_ (:obj:`str`): Тип отправляемого фидбека. + type_ (:obj:`str`): Тип отправляемого отзыва. timestamp (:obj:`str` | :obj:`float` | :obj:`int`, optional): Текущее время и дата. from_ (:obj:`str`, optional): Откуда начато воспроизведение радио. batch_id (:obj:`str`, optional): Уникальный идентификатор партии треков. Возвращается при получении треков. total_played_seconds (:obj:`int` |:obj:`float`, optional): Сколько было проиграно секунд трека перед действием. track_id (:obj:`int` | :obj:`str`, optional): Уникальной идентификатор трека. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1320,7 +1281,7 @@ async def rotor_station_feedback( if total_played_seconds: data.update({'totalPlayedSeconds': total_played_seconds}) - result = await self._request.post(url, params=params, json=data, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, params=params, json=data, **kwargs) return result == 'ok' @@ -1331,13 +1292,11 @@ async def rotor_station_feedback_radio_started( from_: str, batch_id: str = None, timestamp: Union[str, float, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Сокращение для:: - client.rotor_station_feedback(station, 'radioStarted', timestamp, from, *args, **kwargs) + client.rotor_station_feedback(station, 'radioStarted', timestamp, from, batch_id, **kwargs) Returns: :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. @@ -1346,7 +1305,7 @@ async def rotor_station_feedback_radio_started( :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ return await self.rotor_station_feedback( - station, 'radioStarted', timestamp, from_=from_, batch_id=batch_id, timeout=timeout, *args, **kwargs + station, 'radioStarted', timestamp, from_=from_, batch_id=batch_id, **kwargs ) @log @@ -1356,13 +1315,12 @@ async def rotor_station_feedback_track_started( track_id: Union[str, int], batch_id: str = None, timestamp: Union[str, float, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Сокращение для:: - client.rotor_station_feedback(station, 'trackStarted', timestamp, track_id, *args, **kwargs) + client.rotor_station_feedback(station, 'trackStarted', timestamp, track_id=track_id, + batch_id=batch_id, **kwargs) Returns: :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. @@ -1371,7 +1329,7 @@ async def rotor_station_feedback_track_started( :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ return await self.rotor_station_feedback( - station, 'trackStarted', timestamp, track_id=track_id, batch_id=batch_id, timeout=timeout, *args, **kwargs + station, 'trackStarted', timestamp, track_id=track_id, batch_id=batch_id, **kwargs ) @log @@ -1382,14 +1340,12 @@ async def rotor_station_feedback_track_finished( total_played_seconds: float, batch_id: str = None, timestamp: Union[str, float, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Сокращение для:: - client.rotor_station_feedback(station, 'trackFinished', timestamp, track_id, total_played_seconds, - *args, **kwargs) + client.rotor_station_feedback(station, 'trackFinished', timestamp, + track_id=track_id, total_played_seconds=total_played_seconds, batch_id=batch_id, **kwargs) Returns: :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. @@ -1404,8 +1360,6 @@ async def rotor_station_feedback_track_finished( track_id=track_id, total_played_seconds=total_played_seconds, batch_id=batch_id, - timeout=timeout, - *args, **kwargs, ) @@ -1417,14 +1371,12 @@ async def rotor_station_feedback_skip( total_played_seconds: float, batch_id: str = None, timestamp: Union[str, float, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Сокращение для:: - client.rotor_station_feedback(station, 'skip', timestamp, track_id, total_played_seconds, - *args, **kwargs) + client.rotor_station_feedback(station, 'skip', timestamp, track_id=track_id, + total_played_seconds=total_played_seconds, batch_id=batch_id, **kwargs) Returns: :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. @@ -1439,21 +1391,15 @@ async def rotor_station_feedback_skip( track_id=track_id, total_played_seconds=total_played_seconds, batch_id=batch_id, - timeout=timeout, - *args, **kwargs, ) @log - async def rotor_station_info( - self, station: str, timeout: Union[int, float] = None, *args, **kwargs - ) -> List[StationResult]: + async def rotor_station_info(self, station: str, *args, **kwargs) -> List[StationResult]: """Получение информации о станции и пользовательских настроек на неё. Args: station (:obj:`str`): Станция. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1465,7 +1411,7 @@ async def rotor_station_info( url = f'{self.base_url}/rotor/station/{station}/info' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return StationResult.de_list(result, self) @@ -1475,10 +1421,8 @@ async def rotor_station_settings2( station: str, mood_energy: str, diversity: str, - language: str = 'not-russian', + language: str = 'not-russian', # TODO(#555): заменить на any type_: str = 'rotor', - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Изменение настроек определённой станции. @@ -1500,8 +1444,6 @@ async def rotor_station_settings2( diversity (:obj:`str`): Треки. language (:obj:`str`): Язык. type_ (:obj:`str`): Тип. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1518,7 +1460,7 @@ async def rotor_station_settings2( if language: data.update({'language': language}) - result = await self._request.post(url, json=data, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, json=data, **kwargs) return result == 'ok' @@ -1528,7 +1470,6 @@ async def rotor_station_tracks( station: str, settings2: bool = True, queue: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[StationTracksResult]: @@ -1541,11 +1482,11 @@ async def rotor_station_tracks( Для продолжения цепочки треков необходимо: 1. Передавать `ID` трека, что был до этого (первый в цепочки). - 2. Отправить фидбек о конче или скипе трека, что был передан в `queue`. + 2. Отправить фидбек о конце или скипе трека, что был передан в `queue`. 3. Отправить фидбек о начале следующего трека (второй в цепочки). 4. Выполнить запрос получения треков. В ответе придёт новые треки или произойдёт сдвиг цепочки на 1 элемент. - Проход по цепочке до коцна не изучен. Часто встречаются дубликаты. + Проход по цепочке до конца не изучен. Часто встречаются дубликаты. Все официальные клиенты выполняют запросы с `settings2 = True`. @@ -1553,8 +1494,6 @@ async def rotor_station_tracks( station (:obj:`str`): Станция. settings2 (:obj:`bool`, optional): Использовать ли второй набор настроек. queue (:obj:`str` | :obj:`int` , optional): Уникальной идентификатор трека, который только что был. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1573,20 +1512,16 @@ async def rotor_station_tracks( if queue: params = {'queue': queue} - result = await self._request.get(url, params=params, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, params, *args, **kwargs) return StationTracksResult.de_json(result, self) @log - async def artists_brief_info( - self, artist_id: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[BriefInfo]: + async def artists_brief_info(self, artist_id: Union[str, int], *args, **kwargs) -> Optional[BriefInfo]: """Получение информации об артисте. Args: artist_id (:obj:`str` | :obj:`int`): Уникальный идентификатор исполнителя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1597,7 +1532,7 @@ async def artists_brief_info( """ url = f'{self.base_url}/artists/{artist_id}/brief-info' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return BriefInfo.de_json(result, self) @@ -1607,7 +1542,6 @@ async def artists_tracks( artist_id: Union[str, int], page: int = 0, page_size: int = 20, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[ArtistTracks]: @@ -1617,8 +1551,6 @@ async def artists_tracks( artist_id (:obj:`str` | :obj:`int`): Уникальный идентификатор артиста. page (:obj:`int`, optional): Номер страницы. page_size (:obj:`int`, optional): Количество треков на странице. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1632,7 +1564,7 @@ async def artists_tracks( params = {'page': page, 'page-size': page_size} - result = await self._request.get(url, params, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, params, *args, **kwargs) return ArtistTracks.de_json(result, self) @@ -1643,7 +1575,6 @@ async def artists_direct_albums( page: int = 0, page_size: int = 20, sort_by: str = 'year', - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[ArtistAlbums]: @@ -1657,8 +1588,6 @@ async def artists_direct_albums( page (:obj:`int`, optional): Номер страницы. page_size (:obj:`int`, optional): Количество альбомов на странице. sort_by (:obj:`str`, optional): Параметр для сортировки. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1672,7 +1601,7 @@ async def artists_direct_albums( params = {'sort-by': sort_by, 'page': page, 'page-size': page_size} - result = await self._request.get(url, params, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, params, *args, **kwargs) return ArtistAlbums.de_json(result, self) @@ -1682,7 +1611,6 @@ async def _like_action( ids: Union[List[Union[str, int]], str, int], remove: bool = False, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> bool: @@ -1701,8 +1629,6 @@ async def _like_action( remove (:obj:`bool`, optional): Если :obj:`True` то снимает отметку, иначе ставит. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1717,7 +1643,7 @@ async def _like_action( action = 'remove' if remove else 'add-multiple' url = f'{self.base_url}/users/{user_id}/likes/{object_type}s/{action}' - result = await self._request.post(url, {f'{object_type}-ids': ids}, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, {f'{object_type}-ids': ids}, *args, **kwargs) if object_type == 'track': return 'revision' in result @@ -1729,8 +1655,6 @@ async def users_likes_tracks_add( self, track_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Поставить отметку "Мне нравится" треку/трекам. @@ -1743,8 +1667,6 @@ async def users_likes_tracks_add( идентификатор трека или треков. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1753,15 +1675,13 @@ async def users_likes_tracks_add( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._like_action('track', track_ids, False, user_id, timeout, *args, **kwargs) + return await self._like_action('track', track_ids, remove=False, user_id=user_id, **kwargs) @log async def users_likes_tracks_remove( self, track_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Снять отметку "Мне нравится" у трека/треков. @@ -1771,8 +1691,6 @@ async def users_likes_tracks_remove( идентификатор трека или треков. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1781,15 +1699,13 @@ async def users_likes_tracks_remove( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._like_action('track', track_ids, True, user_id, timeout, *args, **kwargs) + return await self._like_action('track', track_ids, remove=True, user_id=user_id, **kwargs) @log async def users_likes_artists_add( self, artist_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Поставить отметку "Мне нравится" исполнителю/исполнителям. @@ -1799,8 +1715,6 @@ async def users_likes_artists_add( идентификатор артиста или артистов. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1809,15 +1723,13 @@ async def users_likes_artists_add( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._like_action('artist', artist_ids, False, user_id, timeout, *args, **kwargs) + return await self._like_action('artist', artist_ids, remove=False, user_id=user_id, **kwargs) @log async def users_likes_artists_remove( self, artist_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Снять отметку "Мне нравится" у исполнителя/исполнителей. @@ -1827,8 +1739,6 @@ async def users_likes_artists_remove( идентификатор артиста или артистов. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1837,15 +1747,13 @@ async def users_likes_artists_remove( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._like_action('artist', artist_ids, True, user_id, timeout, *args, **kwargs) + return await self._like_action('artist', artist_ids, remove=True, user_id=user_id, **kwargs) @log async def users_likes_playlists_add( self, playlist_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Поставить отметку "Мне нравится" плейлисту/плейлистам. @@ -1859,8 +1767,6 @@ async def users_likes_playlists_add( идентификатор плейлиста или плейлистов. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1869,15 +1775,13 @@ async def users_likes_playlists_add( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._like_action('playlist', playlist_ids, False, user_id, timeout, *args, **kwargs) + return await self._like_action('playlist', playlist_ids, remove=False, user_id=user_id, **kwargs) @log async def users_likes_playlists_remove( self, playlist_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Снять отметку "Мне нравится" у плейлиста/плейлистов. @@ -1891,8 +1795,6 @@ async def users_likes_playlists_remove( идентификатор плейлиста или плейлистов. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1901,15 +1803,13 @@ async def users_likes_playlists_remove( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._like_action('playlist', playlist_ids, True, user_id, timeout, *args, **kwargs) + return await self._like_action('playlist', playlist_ids, remove=True, user_id=user_id, **kwargs) @log async def users_likes_albums_add( self, album_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Поставить отметку "Мне нравится" альбому/альбомам. @@ -1919,8 +1819,6 @@ async def users_likes_albums_add( идентификатор артиста или артистов. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1929,15 +1827,13 @@ async def users_likes_albums_add( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._like_action('album', album_ids, False, user_id, timeout, *args, **kwargs) + return await self._like_action('album', album_ids, remove=False, user_id=user_id, **kwargs) @log async def users_likes_albums_remove( self, album_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Снять отметку "Мне нравится" у альбома/альбомов. @@ -1947,8 +1843,6 @@ async def users_likes_albums_remove( идентификатор артиста или артистов. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1957,14 +1851,13 @@ async def users_likes_albums_remove( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._like_action('album', album_ids, True, user_id, timeout, *args, **kwargs) + return await self._like_action('album', album_ids, remove=True, user_id=user_id, **kwargs) async def _get_list( self, object_type: str, ids: Union[List[Union[str, int]], int, str], params: dict = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> List[Union[Artist, Album, Track, Playlist]]: @@ -1975,8 +1868,6 @@ async def _get_list( ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный идентификатор объекта или объектов. params (:obj:`dict`, optional): Параметры, которые будут переданы в запрос. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -1993,21 +1884,17 @@ async def _get_list( url = f'{self.base_url}/{object_type}s' + ('/list' if object_type == 'playlist' else '') - result = await self._request.post(url, params, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, params, *args, **kwargs) return de_list[object_type](result, self) @log - async def artists( - self, artist_ids: Union[List[Union[str, int]], int, str], timeout: Union[int, float] = None, *args, **kwargs - ) -> List[Artist]: + async def artists(self, artist_ids: Union[List[Union[str, int]], int, str], *args, **kwargs) -> List[Artist]: """Получение исполнителя/исполнителей. Args: artist_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный идентификатор исполнителя или исполнителей. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2016,19 +1903,15 @@ async def artists( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._get_list('artist', artist_ids, timeout=timeout, *args, **kwargs) + return await self._get_list('artist', artist_ids, *args, **kwargs) @log - async def albums( - self, album_ids: Union[List[Union[str, int]], int, str], timeout: Union[int, float] = None, *args, **kwargs - ) -> List[Album]: + async def albums(self, album_ids: Union[List[Union[str, int]], int, str], *args, **kwargs) -> List[Album]: """Получение альбома/альбомов. Args: album_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный идентификатор альбома или альбомов. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2037,14 +1920,13 @@ async def albums( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._get_list('album', album_ids, timeout=timeout, *args, **kwargs) + return await self._get_list('album', album_ids, *args, **kwargs) @log async def tracks( self, track_ids: Union[List[Union[str, int]], int, str], with_positions: bool = True, - timeout: Union[int, float] = None, *args, **kwargs, ) -> List[Track]: @@ -2054,8 +1936,6 @@ async def tracks( track_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный идентификатор трека или треков. with_positions (:obj:`bool`, optional): С позициями TODO. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2064,13 +1944,11 @@ async def tracks( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._get_list( - 'track', track_ids, {'with-positions': str(with_positions)}, timeout, *args, **kwargs - ) + return await self._get_list('track', track_ids, {'with-positions': str(with_positions)}, *args, **kwargs) @log async def playlists_list( - self, playlist_ids: Union[List[Union[str, int]], int, str], timeout: Union[int, float] = None, *args, **kwargs + self, playlist_ids: Union[List[Union[str, int]], int, str], *args, **kwargs ) -> List[Playlist]: """Получение плейлиста/плейлистов. @@ -2078,11 +1956,16 @@ async def playlists_list( Идентификатор плейлиста указывается в формате `owner_id:playlist_id`. Где `playlist_id` - идентификатор плейлиста, `owner_id` - уникальный идентификатор владельца плейлиста. + Данный метод возвращает сокращенную модель плейлиста для отображения больших список. + + Warning: + Данный метод не возвращает список треков у плейлиста! Для получения объекта :obj:`yandex_music.Playlist` c + заполненным полем `tracks` используйте метод :func:`yandex_music.ClientAsync.users_playlists` или + метод :func:`yandex_music.Playlist.fetch_tracks`. + Args: playlist_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный идентификатор плейлиста или плейлистов. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2091,12 +1974,10 @@ async def playlists_list( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._get_list('playlist', playlist_ids, timeout=timeout, *args, **kwargs) + return await self._get_list('playlist', playlist_ids, *args, **kwargs) @log - async def playlists_collective_join( - self, user_id: int, token: str, timeout: Union[int, float] = None, *args, **kwargs - ) -> bool: + async def playlists_collective_join(self, user_id: int, token: str, **kwargs) -> bool: """Присоединение к плейлисту как соавтор. Note: @@ -2108,8 +1989,6 @@ async def playlists_collective_join( Args: user_id (:obj:`int`): Владелец плейлиста. token (:obj:`str`): Токен для присоединения. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs: Произвольные аргументы (будут переданы в запрос). Returns: @@ -2122,21 +2001,17 @@ async def playlists_collective_join( params = {'uid': user_id, 'token': token} - result = await self._request.post(url, params=params, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, params=params, **kwargs) return result == 'ok' @log - async def users_playlists_list( - self, user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs - ) -> List[Playlist]: + async def users_playlists_list(self, user_id: Union[str, int] = None, *args, **kwargs) -> List[Playlist]: """Получение списка плейлистов пользователя. Args: user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2150,7 +2025,7 @@ async def users_playlists_list( url = f'{self.base_url}/users/{user_id}/playlists/list' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return Playlist.de_list(result, self) @@ -2159,7 +2034,6 @@ async def _get_likes( object_type: str, user_id: Union[str, int] = None, params: dict = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Union[List[Like], Optional[TracksList]]: @@ -2170,8 +2044,6 @@ async def _get_likes( user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. params (:obj:`dict`, optional): Параметры, которые будут переданы в запрос. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2185,7 +2057,7 @@ async def _get_likes( url = f'{self.base_url}/users/{user_id}/likes/{object_type}s' - result = await self._request.get(url, params, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, params, *args, **kwargs) if object_type == 'track': return TracksList.de_json(result.get('library'), self) @@ -2197,7 +2069,6 @@ async def users_likes_tracks( self, user_id: Union[str, int] = None, if_modified_since_revision: int = 0, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[TracksList]: @@ -2207,8 +2078,6 @@ async def users_likes_tracks( user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. if_modified_since_revision (:obj:`int`, optional): TODO. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2218,12 +2087,12 @@ async def users_likes_tracks( :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ return await self._get_likes( - 'track', user_id, {'if-modified-since-revision': if_modified_since_revision}, timeout, *args, **kwargs + 'track', user_id, {'if-modified-since-revision': if_modified_since_revision}, *args, **kwargs ) @log async def users_likes_albums( - self, user_id: Union[str, int] = None, rich: bool = True, timeout: Union[int, float] = None, *args, **kwargs + self, user_id: Union[str, int] = None, rich: bool = True, *args, **kwargs ) -> List[Like]: """Получение альбомов с отметкой "Мне нравится". @@ -2231,8 +2100,6 @@ async def users_likes_albums( user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. rich (:obj:`bool`, optional): Если False, то приходит укороченная версия. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2241,14 +2108,13 @@ async def users_likes_albums( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._get_likes('album', user_id, {'rich': str(rich)}, timeout, *args, **kwargs) + return await self._get_likes('album', user_id, {'rich': str(rich)}, *args, **kwargs) @log async def users_likes_artists( self, user_id: Union[str, int] = None, with_timestamps: bool = True, - timeout: Union[int, float] = None, *args, **kwargs, ) -> List[Like]: @@ -2258,8 +2124,6 @@ async def users_likes_artists( user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. with_timestamps (:obj:`bool`, optional): С временными метками TODO. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2268,21 +2132,15 @@ async def users_likes_artists( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._get_likes( - 'artist', user_id, {'with-timestamps': str(with_timestamps)}, timeout, *args, **kwargs - ) + return await self._get_likes('artist', user_id, {'with-timestamps': str(with_timestamps)}, *args, **kwargs) @log - async def users_likes_playlists( - self, user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs - ) -> List[Like]: + async def users_likes_playlists(self, user_id: Union[str, int] = None, *args, **kwargs) -> List[Like]: """Получение плейлистов с отметкой "Мне нравится". Args: user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2291,14 +2149,13 @@ async def users_likes_playlists( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._get_likes('playlist', user_id, timeout=timeout, *args, **kwargs) + return await self._get_likes('playlist', user_id, *args, **kwargs) @log async def users_dislikes_tracks( self, user_id: Union[str, int] = None, if_modified_since_revision: int = 0, - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[TracksList]: @@ -2308,8 +2165,6 @@ async def users_dislikes_tracks( user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. if_modified_since_revision (:obj:`bool`, optional): TODO. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2324,7 +2179,7 @@ async def users_dislikes_tracks( url = f'{self.base_url}/users/{user_id}/dislikes/tracks' result = await self._request.get( - url, {'if_modified_since_revision': if_modified_since_revision}, timeout=timeout, *args, **kwargs + url, {'if_modified_since_revision': if_modified_since_revision}, *args, **kwargs ) return TracksList.de_json(result.get('library'), self) @@ -2334,7 +2189,6 @@ async def _dislike_action( ids: Union[List[Union[str, int]], str, int], remove: bool = False, user_id: Union[str, int] = None, - timeout: Union[int, float] = None, *args, **kwargs, ) -> bool: @@ -2346,8 +2200,6 @@ async def _dislike_action( remove (:obj:`bool`, optional): Если :obj:`True`, то снимает отметку, иначе ставит. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2362,7 +2214,7 @@ async def _dislike_action( action = 'remove' if remove else 'add-multiple' url = f'{self.base_url}/users/{user_id}/dislikes/tracks/{action}' - result = await self._request.post(url, {f'track-ids': ids}, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, {f'track-ids': ids}, *args, **kwargs) return 'revision' in result @@ -2371,8 +2223,6 @@ async def users_dislikes_tracks_add( self, track_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Поставить отметку "Не рекомендовать" треку/трекам. @@ -2385,8 +2235,6 @@ async def users_dislikes_tracks_add( идентификатор трека или треков. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2395,15 +2243,13 @@ async def users_dislikes_tracks_add( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._dislike_action(track_ids, False, user_id, timeout, *args, **kwargs) + return await self._dislike_action(track_ids, remove=False, user_id=user_id, **kwargs) @log async def users_dislikes_tracks_remove( self, track_ids: Union[List[Union[str, int]], str, int], user_id: Union[str, int] = None, - timeout: Union[int, float] = None, - *args, **kwargs, ) -> bool: """Снять отметку "Не рекомендовать" у трека/треков. @@ -2413,8 +2259,6 @@ async def users_dislikes_tracks_remove( идентификатор трека или треков. user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан используется ID текущего пользователя. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2423,7 +2267,7 @@ async def users_dislikes_tracks_remove( Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._dislike_action(track_ids, True, user_id, timeout, *args, **kwargs) + return await self._dislike_action(track_ids, remove=True, user_id=user_id, **kwargs) @log async def after_track( @@ -2434,7 +2278,6 @@ async def after_track( context: str = 'playlist', types: str = 'shot', from_: str = 'mobile-landing-origin-default', - timeout: Union[int, float] = None, *args, **kwargs, ) -> Optional[ShotEvent]: @@ -2459,8 +2302,6 @@ async def after_track( context (:obj:`str`, optional): Место, откуда было вызвано получение. types (:obj:`str`, optional): Тип того, что вернуть после трека. from_ (:obj:`str`, optional): Место, с которого попали в контекст. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2481,15 +2322,14 @@ async def after_track( 'types': types, } - result = await self._request.get(url, params=params, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, params, *args, **kwargs) - # TODO судя по всему эндпоинт ещё возвращает рекламу после треков для пользователей без подписки. + # TODO (MarshalX) судя по всему ручка ещё возвращает рекламу после треков для пользователей без подписки. + # https://github.com/MarshalX/yandex-music-api/issues/557 return ShotEvent.de_json(result.get('shot_event'), self) @log - async def queues_list( - self, device: str = None, timeout: Union[int, float] = None, *args, **kwargs - ) -> List[QueueItem]: + async def queues_list(self, device: str = None, *args, **kwargs) -> List[QueueItem]: """Получение всех очередей треков с разных устройств для синхронизации между ними. Note: @@ -2500,8 +2340,6 @@ async def queues_list( Args: device (:obj:`str`, optional): Содержит информацию об устройстве с которого выполняется запрос. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2516,18 +2354,16 @@ async def queues_list( url = f'{self.base_url}/queues' self._request.headers['X-Yandex-Music-Device'] = device - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return QueueItem.de_list(result.get('queues'), self) @log - async def queue(self, queue_id: str, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Queue]: + async def queue(self, queue_id: str, *args, **kwargs) -> Optional[Queue]: """Получение информации об очереди треков и самих треков в ней. Args: queue_id (:obj:`str`): Уникальный идентификатор очереди. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2538,14 +2374,12 @@ async def queue(self, queue_id: str, timeout: Union[int, float] = None, *args, * """ url = f'{self.base_url}/queues/{queue_id}' - result = await self._request.get(url, timeout=timeout, *args, **kwargs) + result = await self._request.get(url, *args, **kwargs) return Queue.de_json(result, self) @log - async def queue_update_position( - self, queue_id: str, current_index: int, device: str = None, timeout: Union[int, float] = None, *args, **kwargs - ) -> bool: + async def queue_update_position(self, queue_id: str, current_index: int, device: str = None, **kwargs) -> bool: """Установка текущего индекса проигрываемого трека в очереди треков. Note: @@ -2555,8 +2389,6 @@ async def queue_update_position( queue_id (:obj:`str`): Уникальный идентификатор очереди. current_index (:obj:`int`): Текущий индекс. device (:obj:`str`, optional): Содержит информацию об устройстве с которого выполняется запрос. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2572,22 +2404,18 @@ async def queue_update_position( self._request.headers['X-Yandex-Music-Device'] = device result = await self._request.post( - url, {'isInteractive': False}, params={'currentIndex': current_index}, timeout=timeout, *args, **kwargs + url, {'isInteractive': False}, params={'currentIndex': current_index}, **kwargs ) return result.get('status') == 'ok' @log - async def queue_create( - self, queue: Union[Queue, str], device: str = None, timeout: Union[int, float] = None, *args, **kwargs - ) -> Optional[str]: + async def queue_create(self, queue: Union[Queue, str], device: str = None, *args, **kwargs) -> Optional[str]: """Создание новой очереди треков. Args: queue (:obj:`yandex_music.Queue` | :obj:`str`): Объект очереди или JSON строка с этим объектом. device (:obj:`str`, optional): Содержит информацию об устройстве с которого выполняется запрос. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). Returns: @@ -2605,9 +2433,9 @@ async def queue_create( url = f'{self.base_url}/queues' self._request.headers['X-Yandex-Music-Device'] = device - result = await self._request.post(url, queue, timeout=timeout, *args, **kwargs) + result = await self._request.post(url, queue, *args, **kwargs) - return result.get('id_') + return result.get('id') # camelCase псевдонимы @@ -2633,6 +2461,8 @@ async def queue_create( tracksDownloadInfo = tracks_download_info #: Псевдоним для :attr:`track_supplement` trackSupplement = track_supplement + #: Псевдоним для :attr:`tracks_lyrics` + tracksLyrics = tracks_lyrics #: Псевдоним для :attr:`tracks_similar` tracksSimilar = tracks_similar #: Псевдоним для :attr:`play_audio` diff --git a/yandex_music/cover.py b/yandex_music/cover.py index 6677d890..6239817d 100644 --- a/yandex_music/cover.py +++ b/yandex_music/cover.py @@ -42,6 +42,20 @@ class Cover(YandexMusicObject): def __post_init__(self): self._id_attrs = (self.prefix, self.version, self.uri, self.items_uri) + def get_url(self, index: int = 0, size: str = '200x200') -> str: + """Возвращает URL обложки. + + Args: + index (:obj:`int`, optional): Индекс элемента в списке ссылок на обложки если нет `self.uri`. + size (:obj:`str`, optional): Размер изображения. + + Returns: + :obj:`str`: URL адрес. + """ + uri = self.uri or self.items_uri[index] + + return f'https://{uri.replace("%%", size)}' + def download(self, filename: str, index: int = 0, size: str = '200x200') -> None: """Загрузка обложки. @@ -50,9 +64,7 @@ def download(self, filename: str, index: int = 0, size: str = '200x200') -> None index (:obj:`int`, optional): Индекс элемента в списке ссылок на обложки если нет `self.uri`. size (:obj:`str`, optional): Размер изображения. """ - uri = self.uri or self.items_uri[index] - - self.client.request.download(f'https://{uri.replace("%%", size)}', filename) + self.client.request.download(self.get_url(index, size), filename) async def download_async(self, filename: str, index: int = 0, size: str = '200x200') -> None: """Загрузка обложки. @@ -62,9 +74,31 @@ async def download_async(self, filename: str, index: int = 0, size: str = '200x2 index (:obj:`int`, optional): Индекс элемента в списке ссылок на обложки если нет `self.uri`. size (:obj:`str`, optional): Размер изображения. """ - uri = self.uri or self.items_uri[index] + await self.client.request.download(self.get_url(index, size), filename) + + def download_bytes(self, index: int = 0, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. - await self.client.request.download(f'https://{uri.replace("%%", size)}', filename) + Args: + index (:obj:`int`, optional): Индекс элемента в списке ссылок на обложки если нет `self.uri`. + size (:obj:`str`, optional): Размер изображения. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return self.client.request.retrieve(self.get_url(index, size)) + + async def download_bytes_async(self, index: int = 0, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Args: + index (:obj:`int`, optional): Индекс элемента в списке ссылок на обложки если нет `self.uri`. + size (:obj:`str`, optional): Размер изображения. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return await self.client.request.retrieve(self.get_url(index, size)) @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['Cover']: @@ -106,5 +140,11 @@ def de_list(cls, data: dict, client: 'Client') -> List['Cover']: # camelCase псевдонимы + #: Псевдоним для :attr:`get_url` + getUrl = get_url #: Псевдоним для :attr:`download_async` downloadAsync = download_async + #: Псевдоним для :attr:`download_bytes` + downloadBytes = download_bytes + #: Псевдоним для :attr:`download_bytes_async` + downloadBytesAsync = download_bytes_async diff --git a/yandex_music/download_info.py b/yandex_music/download_info.py index 46622a67..17c8e5cf 100644 --- a/yandex_music/download_info.py +++ b/yandex_music/download_info.py @@ -10,6 +10,8 @@ from yandex_music import Client from xml.dom.minicompat import NodeList +SIGN_SALT = 'XGRlBW9FXlekgbPrRHuSiA' + @model class DownloadInfo(YandexMusicObject): @@ -52,7 +54,7 @@ def __build_direct_link(self, xml: str) -> str: path = self._get_text_node_data(doc.getElementsByTagName('path')) ts = self._get_text_node_data(doc.getElementsByTagName('ts')) s = self._get_text_node_data(doc.getElementsByTagName('s')) - sign = md5(('XGRlBW9FXlekgbPrRHuSiA' + path[1::] + s).encode('utf-8')).hexdigest() + sign = md5((SIGN_SALT + path[1::] + s).encode('utf-8')).hexdigest() return f'https://{host}/get-mp3/{sign}/{ts}{path}' @@ -108,6 +110,28 @@ async def download_async(self, filename: str) -> None: await self.client.request.download(self.direct_link, filename) + def download_bytes(self) -> bytes: + """Загрузка трека и возврат в виде байтов. + + Returns: + :obj:`bytes`: Трек в виде байтов. + """ + if self.direct_link is None: + self.get_direct_link() + + return self.client.request.retrieve(self.direct_link) + + async def download_bytes_async(self) -> bytes: + """Загрузка трека и возврат в виде байтов. + + Returns: + :obj:`bytes`: Трек в виде байтов. + """ + if self.direct_link is None: + await self.get_direct_link_async() + + return await self.client.request.retrieve(self.direct_link) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['DownloadInfo']: """Десериализация объекта. @@ -184,3 +208,7 @@ async def de_list_async(cls, data: dict, client: 'Client', get_direct_links: boo getDirectLinkAsync = get_direct_link_async #: Псевдоним для :attr:`download_async` downloadAsync = download_async + #: Псевдоним для :attr:`download_bytes` + downloadBytes = download_bytes + #: Псевдоним для :attr:`download_bytes_async` + downloadBytesAsync = download_bytes_async diff --git a/yandex_music/exceptions.py b/yandex_music/exceptions.py index ef892f44..1162210e 100644 --- a/yandex_music/exceptions.py +++ b/yandex_music/exceptions.py @@ -8,6 +8,8 @@ class UnauthorizedError(YandexMusicError): """ +# TODO (MarshalX) На самом деле поиск еще происходит по кодеку +# https://github.com/MarshalX/yandex-music-api/issues/552 class InvalidBitrateError(YandexMusicError): """Класс исключения, вызываемого при попытке загрузки трека с недоступным битрейтом. @@ -28,7 +30,7 @@ class NotFoundError(NetworkError): """Класс исключения, вызываемый в случае ответа от сервера со статус кодом 404.""" -# TimeoutError builtin. И не знаю хотим ли использовать его для синк и asyncio.TimeoutError для асинк +# TimeoutError builtin. Пока не знаю хотим ли использовать его для синхронной и asyncio.TimeoutError для асинхронной class TimedOutError(NetworkError): """Класс исключения, вызываемого для случаев истечения времени ожидания.""" diff --git a/yandex_music/experiments.py b/yandex_music/experiments.py index 5e4e81eb..c2b55b6e 100644 --- a/yandex_music/experiments.py +++ b/yandex_music/experiments.py @@ -10,7 +10,7 @@ @model class Experiments(YandexMusicObject): - """Класс, представляющий какие-то свистелки-перделки, флажки, режимы экспериментальных функций. + """Класс, представляющий какие-то свистелки и перделки, флажки, режимы экспериментальных функций. Attributes: client (:obj:`yandex_music.Client`): Клиент Yandex Music. @@ -35,7 +35,7 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['Experiments']: client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. Returns: - :obj:`yandex_music.Experiments`: Какие-то свистелки-перделки, флажки, режимы экспериментальных функций. + :obj:`yandex_music.Experiments`: Какие-то свистелки и перделки, флажки, режимы экспериментальных функций. """ if not data: return None diff --git a/yandex_music/feed/album_event.py b/yandex_music/feed/album_event.py index 92e5fbd4..4c3a7df8 100644 --- a/yandex_music/feed/album_event.py +++ b/yandex_music/feed/album_event.py @@ -12,7 +12,7 @@ class AlbumEvent(YandexMusicObject): """Класс, представляющий альбом в событии фида. Attributes: - album (:obj:`yandex_music.Album` | :obj:`None`): Альбом. + album (:obj:`yandex_music.Album`, optional): Альбом. tracks (:obj:`list` из :obj:`yandex_music.Track`): Треки. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. """ diff --git a/yandex_music/feed/artist_event.py b/yandex_music/feed/artist_event.py index 29ccb7e0..71b607ef 100644 --- a/yandex_music/feed/artist_event.py +++ b/yandex_music/feed/artist_event.py @@ -12,9 +12,9 @@ class ArtistEvent(YandexMusicObject): """Класс, представляющий артиста в событии фида. Attributes: - artist (:obj:`yandex_music.Artist` | :obj:`None`): Артист. - tracks (:obj:`list` :obj:`yandex_music.Track`): Треки. - similar_to_artists_from_history (:obj:`list` :obj:`yandex_music.Artist`): Похожие артисты из истории. + artist (:obj:`yandex_music.Artist`, optional): Артист. + tracks (:obj:`list` из :obj:`yandex_music.Track`): Треки. + similar_to_artists_from_history (:obj:`list` из :obj:`yandex_music.Artist`): Похожие артисты из истории. subscribed (:obj:`bool`): Подписан ли на событие. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. """ diff --git a/yandex_music/feed/generated_playlist.py b/yandex_music/feed/generated_playlist.py index 3f1957b4..4df42957 100644 --- a/yandex_music/feed/generated_playlist.py +++ b/yandex_music/feed/generated_playlist.py @@ -21,6 +21,7 @@ class GeneratedPlaylist(YandexMusicObject): notify (:obj:`bool`): Уведомлён ли пользователь об обновлении содержания. data (:obj:`yandex_music.Playlist`, optional): Сгенерированный плейлист. description (:obj:`list`, optional): Описание TODO. + preview_description (:obj:`str`, optional): Короткое описание под блоком лендинга. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. """ @@ -29,6 +30,7 @@ class GeneratedPlaylist(YandexMusicObject): notify: bool data: Optional['Playlist'] description: Optional[list] = None + preview_description: Optional[str] = None client: Optional['Client'] = None def __post_init__(self): diff --git a/yandex_music/genre/images.py b/yandex_music/genre/images.py index e7b4ee54..4655c0a9 100644 --- a/yandex_music/genre/images.py +++ b/yandex_music/genre/images.py @@ -40,6 +40,22 @@ def download_300x300(self, filename: str) -> None: """ self.client.request.download(self._300x300, filename) + def download_208x208_bytes(self) -> bytes: + """Загрузка изображения 208x208 и возврат в виде байтов. + + Returns: + :obj:`bytes`: Изображение в виде байтов. + """ + return self.client.request.retrieve(self._208x208) + + def download_300x300_bytes(self) -> bytes: + """Загрузка изображения 300x300 и возврат в виде байтов. + + Returns: + :obj:`bytes`: Изображение в виде байтов. + """ + return self.client.request.retrieve(self._300x300) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['Images']: """Десериализация объекта. @@ -57,3 +73,14 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['Images']: data = super(Images, cls).de_json(data, client) return cls(client=client, **data) + + # camelCase псевдонимы + + #: Псевдоним для :attr:`download_208x208` + download208X208 = download_208x208 + #: Псевдоним для :attr:`download_300x300` + download300X300 = download_300x300 + #: Псевдоним для :attr:`download_208x208_bytes` + download208X208Bytes = download_208x208_bytes + #: Псевдоним для :attr:`download_300x300_bytes` + download300X300Bytes = download_300x300_bytes diff --git a/yandex_music/icon.py b/yandex_music/icon.py index a815a846..8a36b8b2 100644 --- a/yandex_music/icon.py +++ b/yandex_music/icon.py @@ -42,6 +42,28 @@ async def download_async(self, filename: str, size: str = '200x200') -> None: """ await self.client.request.download(self.get_url(size), filename) + def download_bytes(self, size: str = '200x200') -> bytes: + """Загрузка иконки и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер иконки. + + Returns: + :obj:`bytes`: Иконка в виде байтов. + """ + return self.client.request.retrieve(self.get_url(size)) + + async def download_bytes_async(self, size: str = '200x200') -> bytes: + """Загрузка иконки и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер иконки. + + Returns: + :obj:`bytes`: Иконка в виде байтов. + """ + return await self.client.request.retrieve(self.get_url(size)) + def get_url(self, size: str = '200x200'): """Получение URL иконки. @@ -72,3 +94,9 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['Icon']: #: Псевдоним для :attr:`download_async` downloadAsync = download_async + #: Псевдоним для :attr:`download_bytes` + downloadBytes = download_bytes + #: Псевдоним для :attr:`download_bytes_async` + downloadBytesAsync = download_bytes_async + #: Псевдоним для :attr:`get_url` + getUrl = get_url diff --git a/yandex_music/landing/mix_link.py b/yandex_music/landing/mix_link.py index ea0a8d3b..a86e3a02 100644 --- a/yandex_music/landing/mix_link.py +++ b/yandex_music/landing/mix_link.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING, Optional, List from yandex_music import YandexMusicObject +from yandex_music.exceptions import YandexMusicError from yandex_music.utils import model if TYPE_CHECKING: @@ -23,7 +24,7 @@ class MixLink(YandexMusicObject): text_color (:obj:`str`): Цвет текста (HEX). background_color (:obj:`str`): Цвет заднего фона. background_image_uri (:obj:`str`): Ссылка на изображение заднего фона. - cover_white (:obj:`str`): Ссылка на изображение с обложкой TODO. + cover_white (:obj:`str`, optional): Ссылка на изображение с обложкой TODO. cover_uri (:obj:`str`, optional): Ссылка на изображение с обложкой. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. """ @@ -34,7 +35,7 @@ class MixLink(YandexMusicObject): text_color: str background_color: str background_image_uri: str - cover_white: str + cover_white: Optional[str] = None cover_uri: Optional[str] = None client: Optional['Client'] = None @@ -46,9 +47,45 @@ def __post_init__(self): self.text_color, self.background_color, self.background_image_uri, - self.cover_white, ) + def get_cover_url(self, size: str = '200x200') -> str: + """Возвращает URL обложки. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`str`: URL обложки. + """ + return f'https://{self.cover_uri.replace("%%", size)}' + + def get_cover_white_url(self, size: str = '200x200') -> str: + """Возвращает URL обложки white. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`str`: URL обложки. + """ + + if not self.cover_white: + raise YandexMusicError('You can\'t get cover white because it\'s None.') + + return f'https://{self.cover_white.replace("%%", size)}' + + def get_background_url(self, size: str = '200x200') -> str: + """Возвращает URL заднего фона. + + Args: + size (:obj:`str`, optional): Размер заднего фона. + + Returns: + :obj:`str`: URL заднего фона. + """ + return f'https://{self.background_image_uri.replace("%%", size)}' + def download_background_image(self, filename: str, size: str = '200x200') -> None: """Загрузка заднего фона. @@ -56,7 +93,7 @@ def download_background_image(self, filename: str, size: str = '200x200') -> Non filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер заднего фона. """ - self.client.request.download(f'https://{self.background_image_uri.replace("%%", size)}', filename) + self.client.request.download(self.get_background_url(size), filename) async def download_background_image_async(self, filename: str, size: str = '200x200') -> None: """Загрузка заднего фона. @@ -65,7 +102,7 @@ async def download_background_image_async(self, filename: str, size: str = '200x filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер заднего фона. """ - await self.client.request.download(f'https://{self.background_image_uri.replace("%%", size)}', filename) + await self.client.request.download(self.get_background_url(size), filename) def download_cover_white(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки TODO. @@ -74,7 +111,7 @@ def download_cover_white(self, filename: str, size: str = '200x200') -> None: filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - self.client.request.download(f'https://{self.cover_white.replace("%%", size)}', filename) + self.client.request.download(self.get_cover_white_url(size), filename) async def download_cover_white_async(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки TODO. @@ -83,7 +120,7 @@ async def download_cover_white_async(self, filename: str, size: str = '200x200') filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - await self.client.request.download(f'https://{self.cover_white.replace("%%", size)}', filename) + await self.client.request.download(self.get_cover_white_url(size), filename) def download_cover_uri(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -92,7 +129,7 @@ def download_cover_uri(self, filename: str, size: str = '200x200') -> None: filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + self.client.request.download(self.get_cover_url(size), filename) async def download_cover_uri_async(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -101,7 +138,73 @@ async def download_cover_uri_async(self, filename: str, size: str = '200x200') - filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - await self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + await self.client.request.download(self.get_cover_url(size), filename) + + def download_background_image_bytes(self, size: str = '200x200') -> bytes: + """Загрузка заднего фона и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер заднего фона. + + Returns: + :obj:`bytes`: Задний фон в виде байтов. + """ + return self.client.request.retrieve(self.get_background_url(size)) + + async def download_background_image_bytes_async(self, size: str = '200x200') -> bytes: + """Загрузка заднего фона и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер заднего фона. + + Returns: + :obj:`bytes`: Задний фон в виде байтов. + """ + return await self.client.request.retrieve(self.get_background_url(size)) + + def download_cover_white_bytes(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов TODO. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return self.client.request.retrieve(self.get_cover_white_url(size)) + + async def download_cover_white_bytes_async(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов TODO. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return await self.client.request.retrieve(self.get_cover_white_url(size)) + + def download_cover_uri_bytes(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return self.client.request.retrieve(self.get_cover_url(size)) + + async def download_cover_uri_bytes_async(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return await self.client.request.retrieve(self.get_cover_url(size)) @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['MixLink']: @@ -143,6 +246,12 @@ def de_list(cls, data: dict, client: 'Client') -> List['MixLink']: # camelCase псевдонимы + #: Псевдоним для :attr:`get_cover_url` + getCoverUrl = get_cover_url + #: Псевдоним для :attr:`get_cover_white_url` + getCoverWhiteUrl = get_cover_white_url + #: Псевдоним для :attr:`get_background_url` + getBackgroundUrl = get_background_url #: Псевдоним для :attr:`download_background_image` downloadBackgroundImage = download_background_image #: Псевдоним для :attr:`download_background_image_async` @@ -155,3 +264,15 @@ def de_list(cls, data: dict, client: 'Client') -> List['MixLink']: downloadCoverUri = download_cover_uri #: Псевдоним для :attr:`download_cover_uri_async` downloadCoverUriAsync = download_cover_uri_async + #: Псевдоним для :attr:`download_background_image_bytes` + downloadBackgroundImageBytes = download_background_image_bytes + #: Псевдоним для :attr:`download_background_image_bytes_async` + downloadBackgroundImageBytesAsync = download_background_image_bytes_async + #: Псевдоним для :attr:`download_cover_white_bytes` + downloadCoverWhiteBytes = download_cover_white_bytes + #: Псевдоним для :attr:`download_cover_white_bytes_async` + downloadCoverWhiteBytesAsync = download_cover_white_bytes_async + #: Псевдоним для :attr:`download_cover_uri_bytes` + downloadCoverUriBytes = download_cover_uri_bytes + #: Псевдоним для :attr:`download_cover_uri_bytes_async` + downloadCoverUriBytesAsync = download_cover_uri_bytes_async diff --git a/yandex_music/landing/promotion.py b/yandex_music/landing/promotion.py index 3a9df76c..828707aa 100644 --- a/yandex_music/landing/promotion.py +++ b/yandex_music/landing/promotion.py @@ -53,6 +53,17 @@ def __post_init__(self): self.image, ) + def get_image_url(self, size: str = '300x300') -> str: + """Возвращает URL изображения. + + Args: + size (:obj:`str`, optional): Размер изображения. + + Returns: + :obj:`str`: URL изображения. + """ + return f'https://{self.image.replace("%%", size)}' + def download_image(self, filename: str, size: str = '300x300') -> None: """Загрузка рекламного изображения. @@ -60,7 +71,7 @@ def download_image(self, filename: str, size: str = '300x300') -> None: filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер изображения. """ - self.client.request.download(f'https://{self.image.replace("%%", size)}', filename) + self.client.request.download(self.get_image_url(size), filename) async def download_image_async(self, filename: str, size: str = '300x300') -> None: """Загрузка рекламного изображения. @@ -69,7 +80,29 @@ async def download_image_async(self, filename: str, size: str = '300x300') -> No filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер изображения. """ - await self.client.request.download(f'https://{self.image.replace("%%", size)}', filename) + await self.client.request.download(self.get_image_url(size), filename) + + def download_image_bytes(self, size: str = '300x300') -> bytes: + """Загрузка рекламного изображения и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер изображения. + + Returns: + :obj:`bytes`: Рекламное изображение в виде байтов. + """ + return self.client.request.retrieve(self.get_image_url(size)) + + async def download_image_bytes_async(self, size: str = '300x300') -> bytes: + """Загрузка рекламного изображения и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер изображения. + + Returns: + :obj:`bytes`: Рекламное изображение в виде байтов. + """ + return await self.client.request.retrieve(self.get_image_url(size)) @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['Promotion']: @@ -111,7 +144,13 @@ def de_list(cls, data: dict, client: 'Client') -> List['Promotion']: # camelCase псевдонимы + #: Псевдоним для :attr:`get_image_url` + getImageUrl = get_image_url #: Псевдоним для :attr:`download_image` downloadImage = download_image #: Псевдоним для :attr:`download_image_async` downloadImageAsync = download_image_async + #: Псевдоним для :attr:`download_image_bytes` + downloadImageBytes = download_image_bytes + #: Псевдоним для :attr:`download_image_bytes_async` + downloadImageBytesAsync = download_image_bytes_async diff --git a/yandex_music/landing/track_id.py b/yandex_music/landing/track_id.py index 386eac27..8d5171e3 100644 --- a/yandex_music/landing/track_id.py +++ b/yandex_music/landing/track_id.py @@ -95,9 +95,9 @@ def de_list(cls, data: dict, client: 'Client') -> List['TrackId']: # camelCase псевдонимы + #: Псевдоним для :attr:`track_full_id` + trackFullId = track_full_id #: Псевдоним для :attr:`fetch_track` fetchTrack = fetch_track #: Псевдоним для :attr:`fetch_track_async` fetchTrackAsync = fetch_track_async - #: Псевдоним для :attr:`track_full_id` - trackFullId = track_full_id diff --git a/yandex_music/pager.py b/yandex_music/pager.py index 5c3b57cc..c5d488aa 100644 --- a/yandex_music/pager.py +++ b/yandex_music/pager.py @@ -9,7 +9,7 @@ @model class Pager(YandexMusicObject): - """Класс, представляющий пагинатор. + """Класс, представляющий пагинацию. Attributes: total (:obj:`int`): Всего треков. @@ -35,7 +35,7 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['Pager']: client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. Returns: - :obj:`yandex_music.Pager`: Пагинатор. + :obj:`yandex_music.Pager`: Пагинация. """ if not data: return None diff --git a/yandex_music/playlist/case_forms.py b/yandex_music/playlist/case_forms.py index eba2bc66..21a658ae 100644 --- a/yandex_music/playlist/case_forms.py +++ b/yandex_music/playlist/case_forms.py @@ -48,7 +48,7 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['CaseForms']: client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. Returns: - :obj:`yandex_music.CaseForms`: TODO. + :obj:`yandex_music.CaseForms`: Склонение имени. """ if not data: return None diff --git a/yandex_music/playlist/custom_wave.py b/yandex_music/playlist/custom_wave.py new file mode 100644 index 00000000..41886d3b --- /dev/null +++ b/yandex_music/playlist/custom_wave.py @@ -0,0 +1,48 @@ +from typing import Any, TYPE_CHECKING, Optional, List + +from yandex_music import YandexMusicObject +from yandex_music.utils import model + +if TYPE_CHECKING: + from yandex_music import Client + + +@model +class CustomWave(YandexMusicObject): + """Класс, представляющий дополнительное описание плейлиста. + + Note: + Известные значения `position`: `default`. + + Attributes: + title (:obj:`str`): Название плейлиста. + animation_url (:obj:`str`): JSON анимация Lottie. + position (:obj:`str`): Позиция TODO. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + """ + + title: str + animation_url: str + position: str + client: Optional['Client'] = None + + def __post_init__(self): + self._id_attrs = (self.title, self.animation_url, self.position) + + @classmethod + def de_json(cls, data: dict, client: 'Client') -> Optional['CustomWave']: + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + + Returns: + :obj:`yandex_music.CustomWave`: Описание плейлиста. + """ + if not data: + return None + + data = super(CustomWave, cls).de_json(data, client) + + return cls(client=client, **data) diff --git a/yandex_music/playlist/playlist.py b/yandex_music/playlist/playlist.py index b64b2c6c..8cbc43f4 100644 --- a/yandex_music/playlist/playlist.py +++ b/yandex_music/playlist/playlist.py @@ -18,6 +18,8 @@ Contest, OpenGraphData, Brand, + Pager, + CustomWave, ) @@ -94,6 +96,8 @@ class Playlist(YandexMusicObject): ready (:obj:`bool`, optional): Готовность TODO. is_for_from: TODO. regions: Регион TODO. + custom_wave (:obj:'yandex_music.CustomWave`, optional): Описание плейлиста. TODO. + pager (:obj:`yandex_music.Pager`, optional): Пагинатор. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. """ @@ -152,6 +156,8 @@ class Playlist(YandexMusicObject): ready: Optional[bool] = None is_for_from: Any = None regions: Any = None + custom_wave: Optional['CustomWave'] = None + pager: Optional['Pager'] = None client: Optional['Client'] = None def __post_init__(self): @@ -179,6 +185,28 @@ async def get_recommendations_async(self, *args, **kwargs) -> Optional['Playlist """ return await self.client.users_playlists_recommendations(self.kind, self.owner.uid, *args, **kwargs) + def get_animated_cover_url(self, size: str = '300x300') -> str: + """Возвращает URL анимированной обложки. + + Args: + size (:obj:`str`, optional): Размер анимированной обложки. + + Returns: + :obj:`str`: URL анимированной обложки. + """ + return f'https://{self.animated_cover_uri.replace("%%", size)}' + + def get_og_image_url(self, size: str = '300x300') -> str: + """Возвращает URL обложки. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`str`: URL обложки. + """ + return f'https://{self.og_image.replace("%%", size)}' + def download_animated_cover(self, filename: str, size: str = '200x200') -> None: """Загрузка анимированной обложки. @@ -186,7 +214,7 @@ def download_animated_cover(self, filename: str, size: str = '200x200') -> None: filename (:obj:`str`): Путь для сохранения файла с названием и расширением (GIF). size (:obj:`str`, optional): Размер анимированной обложки. """ - self.client.request.download(f'https://{self.animated_cover_uri.replace("%%", size)}', filename) + self.client.request.download(self.get_animated_cover_url(size), filename) async def download_animated_cover_async(self, filename: str, size: str = '200x200') -> None: """Загрузка анимированной обложки. @@ -195,7 +223,7 @@ async def download_animated_cover_async(self, filename: str, size: str = '200x20 filename (:obj:`str`): Путь для сохранения файла с названием и расширением (GIF). size (:obj:`str`, optional): Размер анимированной обложки. """ - await self.client.request.download(f'https://{self.animated_cover_uri.replace("%%", size)}', filename) + await self.client.request.download(self.get_animated_cover_url(size), filename) def download_og_image(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -206,7 +234,7 @@ def download_og_image(self, filename: str, size: str = '200x200') -> None: filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + self.client.request.download(self.get_og_image_url(size), filename) async def download_og_image_async(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -217,7 +245,55 @@ async def download_og_image_async(self, filename: str, size: str = '200x200') -> filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - await self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + await self.client.request.download(self.get_og_image_url(size), filename) + + def download_animated_cover_bytes(self, size: str = '200x200') -> bytes: + """Загрузка анимированной обложки и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер анимированной обложки. + + Returns: + :obj:`bytes`: Анимированная обложка в виде байтов. + """ + return self.client.request.retrieve(self.get_animated_cover_url(size)) + + async def download_animated_cover_bytes_async(self, size: str = '200x200') -> bytes: + """Загрузка анимированной обложки и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер анимированной обложки. + + Returns: + :obj:`bytes`: Анимированная обложка в виде байтов. + """ + return await self.client.request.retrieve(self.get_animated_cover_url(size)) + + def download_og_image_bytes(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Используйте это только когда нет self.cover! + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return self.client.request.retrieve(self.get_og_image_url(size)) + + async def download_og_image_bytes_async(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Используйте это только когда нет self.cover! + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return await self.client.request.retrieve(self.get_og_image_url(size)) def rename(self, name: str) -> None: client, kind = self.client, self.kind @@ -275,7 +351,7 @@ async def fetch_tracks_async(self, *args, **kwargs) -> List['TrackShort']: await client.users_playlists(playlist.kind, playlist.owner.id, *args, **kwargs).tracks """ - return await self.client.users_playlists(self.kind, self.owner.uid, *args, **kwargs).tracks + return (await self.client.users_playlists(self.kind, self.owner.uid, *args, **kwargs)).tracks def insert_track(self, track_id: int, album_id: int, *args, **kwargs) -> Optional['Playlist']: """Сокращение для:: @@ -356,6 +432,8 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['Playlist']: Contest, OpenGraphData, Brand, + CustomWave, + Pager, ) data['owner'] = User.de_json(data.get('owner'), client) @@ -380,6 +458,9 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['Playlist']: data['playlist_absence'] = PlaylistAbsence.de_json(data.get('playlist_absense'), client) data.pop('playlist_absense') + data['custom_wave'] = CustomWave.de_json(data.get('custom_wave'), client) + data['pager'] = Pager.de_json(data.get('pager'), client) + return cls(client=client, **data) @classmethod @@ -408,6 +489,10 @@ def de_list(cls, data: dict, client: 'Client') -> List['Playlist']: getRecommendations = get_recommendations #: Псевдоним для :attr:`get_recommendations_async` getRecommendationsAsync = get_recommendations_async + #: Псевдоним для :attr:`get_animated_cover_url` + getAnimatedCoverUrl = get_animated_cover_url + #: Псевдоним для :attr:`get_og_image_url` + getOgImageUrl = get_og_image_url #: Псевдоним для :attr:`download_animated_cover` downloadAnimatedCover = download_animated_cover #: Псевдоним для :attr:`download_animated_cover_async` @@ -416,6 +501,20 @@ def de_list(cls, data: dict, client: 'Client') -> List['Playlist']: downloadOgImage = download_og_image #: Псевдоним для :attr:`download_og_image_async` downloadOgImageAsync = download_og_image_async + #: Псевдоним для :attr:`download_animated_cover_bytes` + downloadAnimatedCoverBytes = download_animated_cover_bytes + #: Псевдоним для :attr:`download_animated_cover_bytes_async` + downloadAnimatedCoverBytesAsync = download_animated_cover_bytes_async + #: Псевдоним для :attr:`download_og_image_bytes` + downloadOgImageBytes = download_og_image_bytes + #: Псевдоним для :attr:`download_og_image_bytes_async` + downloadOgImageBytesAsync = download_og_image_bytes_async + #: Псевдоним для :attr:`rename_async` + renameAsync = rename_async + #: Псевдоним для :attr:`like_async` + likeAsync = like_async + #: Псевдоним для :attr:`dislike_async` + dislikeAsync = dislike_async #: Псевдоним для :attr:`fetch_tracks` fetchTracks = fetch_tracks #: Псевдоним для :attr:`fetch_tracks_async` @@ -428,11 +527,5 @@ def de_list(cls, data: dict, client: 'Client') -> List['Playlist']: deleteTracks = delete_tracks #: Псевдоним для :attr:`delete_tracks_async` deleteTracksAsync = delete_tracks_async - #: Псевдоним для :attr:`rename_async` - renameAsync = rename_async - #: Псевдоним для :attr:`like_async` - likeAsync = like_async - #: Псевдоним для :attr:`dislike_async` - dislikeAsync = dislike_async #: Псевдоним для :attr:`delete_async` deleteAsync = delete_async diff --git a/yandex_music/playlist/tag.py b/yandex_music/playlist/tag.py index 782f571e..fd88b1e7 100644 --- a/yandex_music/playlist/tag.py +++ b/yandex_music/playlist/tag.py @@ -49,3 +49,4 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['Tag']: return cls(client=client, **data) # TODO (MarshalX) add download_og_image shortcut? + # https://github.com/MarshalX/yandex-music-api/issues/556 diff --git a/yandex_music/playlist/tag_result.py b/yandex_music/playlist/tag_result.py index e810089a..8579c58a 100644 --- a/yandex_music/playlist/tag_result.py +++ b/yandex_music/playlist/tag_result.py @@ -47,3 +47,4 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['TagResult']: return cls(client=client, **data) # TODO (MarshalX) add fetch_playlists shortcut? + # https://github.com/MarshalX/yandex-music-api/issues/551 diff --git a/yandex_music/rotor/discrete_scale.py b/yandex_music/rotor/discrete_scale.py index 68d12fb8..13881c32 100644 --- a/yandex_music/rotor/discrete_scale.py +++ b/yandex_music/rotor/discrete_scale.py @@ -19,7 +19,7 @@ class DiscreteScale(YandexMusicObject): name (:obj:`str`): Название. min (:obj:`yandex_music.Value`): Минимальное значение. max (:obj:`yandex_music.Value`): Максимальное значение. - client (:obj:`yandex_music.Client` optional): Клиент Yandex Music. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. """ type: str diff --git a/yandex_music/rotor/enum.py b/yandex_music/rotor/enum.py index 91e7612a..6ac19c91 100644 --- a/yandex_music/rotor/enum.py +++ b/yandex_music/rotor/enum.py @@ -14,7 +14,7 @@ class Enum(YandexMusicObject): Attributes: type (:obj:`str`): Тип перечисления. name (:obj:`str`): Название перечисления. - possible_values (:obj:`list` из :obj:`yandex_Music.Value`): Доступные значения. + possible_values (:obj:`list` из :obj:`yandex_music.Value`): Доступные значения. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. **kwargs: Произвольные ключевые аргументы полученные от API. """ diff --git a/yandex_music/rotor/station_result.py b/yandex_music/rotor/station_result.py index 9a5a0e02..74c8b338 100644 --- a/yandex_music/rotor/station_result.py +++ b/yandex_music/rotor/station_result.py @@ -11,6 +11,9 @@ class StationResult(YandexMusicObject): """Класс, представляющий радиостанцию с настройками. + Note: + Известные значения `custom_name`: `Танцую`, `R'n'B`, `Отдыхаю`, `Просыпаюсь`, `Тренируюсь`, `В дороге`, `Работаю`, `Засыпаю`. + Attributes: station (:obj:`yandex_music.Station` | :obj:`None`): Станция. settings (:obj:`yandex_music.RotorSettings` | :obj:`None`): Первый набор настроек. @@ -18,6 +21,9 @@ class StationResult(YandexMusicObject): ad_params (:obj:`yandex_music.AdParams` | :obj:`None`): Настройки рекламы. explanation (:obj:`str`, optional): TODO. prerolls (:obj:`list` из :obj:`str`, optional): Прероллы TODO. + rup_title (:obj:`str`): Название станции / Моя волна TODO. + rup_description (:obj:`str`): Описание станции. + custom_name (:obj:`str`, optional): Название станции TODO. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. """ @@ -27,6 +33,9 @@ class StationResult(YandexMusicObject): ad_params: Optional['AdParams'] explanation: Optional[str] = None prerolls: Optional[list] = None + rup_title: str = None + rup_description: str = None + custom_name: Optional[str] = None client: Optional['Client'] = None def __post_init__(self): diff --git a/yandex_music/search/search.py b/yandex_music/search/search.py index 883d3756..cceeb17c 100644 --- a/yandex_music/search/search.py +++ b/yandex_music/search/search.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Optional -from yandex_music import YandexMusicObject +from yandex_music import YandexMusicObject, Album, Artist, Playlist, Track, Video, User from yandex_music.utils import model if TYPE_CHECKING: @@ -36,14 +36,14 @@ class Search(YandexMusicObject): search_request_id: str text: str best: Optional['Best'] - albums: Optional['SearchResult'] - artists: Optional['SearchResult'] - playlists: Optional['SearchResult'] - tracks: Optional['SearchResult'] - videos: Optional['SearchResult'] - users: Optional['SearchResult'] - podcasts: Optional['SearchResult'] - podcast_episodes: Optional['SearchResult'] + albums: Optional['SearchResult[Album]'] + artists: Optional['SearchResult[Artist]'] + playlists: Optional['SearchResult[Playlist]'] + tracks: Optional['SearchResult[Track]'] + videos: Optional['SearchResult[Video]'] + users: Optional['SearchResult[User]'] + podcasts: Optional['SearchResult[Album]'] + podcast_episodes: Optional['SearchResult[Track]'] type: Optional[str] = None page: Optional[int] = None per_page: Optional[int] = None @@ -165,7 +165,7 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['Search']: #: Псевдоним для :attr:`next_page` nextPage = next_page #: Псевдоним для :attr:`next_page_async` - nextPageASync = next_page_async + nextPageAsync = next_page_async #: Псевдоним для :attr:`prev_page` prevPage = prev_page #: Псевдоним для :attr:`prev_page_async` diff --git a/yandex_music/search/search_result.py b/yandex_music/search/search_result.py index 9fe73104..03c7b8cf 100644 --- a/yandex_music/search/search_result.py +++ b/yandex_music/search/search_result.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional, List, Union +from typing import TYPE_CHECKING, Optional, List, Union, TypeVar, Generic, Type, Dict from yandex_music import YandexMusicObject, Artist, Album, Track, Playlist, Video, User from yandex_music.utils import model @@ -6,21 +6,23 @@ if TYPE_CHECKING: from yandex_music import Client +T = TypeVar('T', bound=Union[Track, Artist, Album, Playlist, Video]) -de_json_result = { - 'track': Track.de_list, - 'artist': Artist.de_list, - 'album': Album.de_list, - 'playlist': Playlist.de_list, - 'video': Video.de_list, - 'user': User.de_list, - 'podcast': Album.de_list, - 'podcast_episode': Track.de_list, + +type_class_by_str: Dict[str, Type[T]] = { + 'track': Track, + 'artist': Artist, + 'album': Album, + 'playlist': Playlist, + 'video': Video, + 'user': User, + 'podcast': Album, + 'podcast_episode': Track, } @model -class SearchResult(YandexMusicObject): +class SearchResult(YandexMusicObject, Generic[T]): """Класс, представляющий результаты поиска. Note: @@ -40,14 +42,14 @@ class SearchResult(YandexMusicObject): total: int per_page: int order: int - results: List[Union[Track, Artist, Album, Playlist, Video]] + results: List[T] client: Optional['Client'] = None def __post_init__(self): self._id_attrs = (self.total, self.per_page, self.order, self.results) @classmethod - def de_json(cls, data: dict, client: 'Client', type_: str = None) -> Optional['SearchResult']: + def de_json(cls, data: Dict[str, Dict], client: 'Client', type_: str = None) -> Optional['SearchResult']: """Десериализация объекта. Args: @@ -63,6 +65,7 @@ def de_json(cls, data: dict, client: 'Client', type_: str = None) -> Optional['S data = super(SearchResult, cls).de_json(data, client) data['type'] = type_ - data['results'] = de_json_result.get(type_)(data.get('results'), client) + type_class = type_class_by_str.get(type_) + data['results'] = type_class.de_list(data.get('results'), client) return cls(client=client, **data) diff --git a/yandex_music/shot/shot_data.py b/yandex_music/shot/shot_data.py index 12a6e9c1..5aaf61c0 100644 --- a/yandex_music/shot/shot_data.py +++ b/yandex_music/shot/shot_data.py @@ -28,6 +28,17 @@ class ShotData(YandexMusicObject): def __post_init__(self): self._id_attrs = (self.cover_uri, self.mds_url, self.shot_text, self.shot_type) + def get_cover_url(self, size: str = '200x200') -> str: + """Возвращает URL обложки. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`str`: URL обложки. + """ + return f'https://{self.cover_uri.replace("%%", size)}' + def download_cover(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -35,7 +46,7 @@ def download_cover(self, filename: str, size: str = '200x200') -> None: filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + self.client.request.download(self.get_cover_url(size), filename) async def download_cover_async(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -44,7 +55,7 @@ async def download_cover_async(self, filename: str, size: str = '200x200') -> No filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - await self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + await self.client.request.download(self.get_cover_url(size), filename) def download_mds(self, filename: str) -> None: """Загрузка аудиоверсии шота. @@ -62,6 +73,44 @@ async def download_mds_async(self, filename: str) -> None: """ await self.client.request.download(self.mds_url, filename) + def download_cover_bytes(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов + """ + return self.client.request.retrieve(self.get_cover_url(size)) + + async def download_cover_bytes_async(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов + """ + return await self.client.request.retrieve(self.get_cover_url(size)) + + def download_mds_bytes(self) -> bytes: + """Загрузка аудиоверсии шота и возврат в виде байтов. + + Returns: + :obj:`bytes`: Аудиоверсия шота в виде байтов + """ + return self.client.request.retrieve(self.mds_url) + + async def download_mds_bytes_async(self) -> bytes: + """Загрузка аудиоверсии шота и возврат в виде байтов. + + Returns: + :obj:`bytes`: Аудиоверсия шота в виде байтов + """ + return await self.client.request.retrieve(self.mds_url) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['ShotData']: """Десериализация объекта. @@ -85,6 +134,8 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['ShotData']: # camelCase псевдонимы + #: Псевдоним для :attr:`get_cover_url` + getCoverUrl = get_cover_url #: Псевдоним для :attr:`download_cover` downloadCover = download_cover #: Псевдоним для :attr:`download_cover_async` @@ -93,3 +144,11 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['ShotData']: downloadMds = download_mds #: Псевдоним для :attr:`download_mds_async` downloadMdsAsync = download_mds_async + #: Псевдоним для :attr:`download_cover_bytes` + downloadCoverBytes = download_cover_bytes + #: Псевдоним для :attr:`download_cover_bytes_async` + downloadCoverBytesAsync = download_cover_bytes_async + #: Псевдоним для :attr:`download_mds_bytes` + downloadMdsBytes = download_mds_bytes + #: Псевдоним для :attr:`download_mds_bytes_async` + downloadMdsBytesAsync = download_mds_bytes_async diff --git a/yandex_music/supplement/lyrics.py b/yandex_music/supplement/lyrics.py index 28cdccb9..4f72a41f 100644 --- a/yandex_music/supplement/lyrics.py +++ b/yandex_music/supplement/lyrics.py @@ -11,6 +11,10 @@ class Lyrics(YandexMusicObject): """Класс, представляющий текст трека. + Warning: + Получение текста из дополнительной информации устарело. Используйте + :func:`yandex_music.Client.tracks_lyrics`. + Attributes: id (:obj:`int`): Уникальный идентификатор текста трека. lyrics (:obj:`str`): Первые строки текст песни. diff --git a/yandex_music/supplement/supplement.py b/yandex_music/supplement/supplement.py index 6e43ac4d..33e5edb0 100644 --- a/yandex_music/supplement/supplement.py +++ b/yandex_music/supplement/supplement.py @@ -11,6 +11,10 @@ class Supplement(YandexMusicObject): """Класс, представляющий дополнительную информацию о треке. + Warning: + Получение текста из дополнительной информации устарело. Используйте + :func:`yandex_music.Client.tracks_lyrics`. + Attributes: id (:obj:`int`): Уникальный идентификатор дополнительной информации. lyrics (:obj:`yandex_music.Lyrics`): Текст песни. diff --git a/yandex_music/track/lyrics_info.py b/yandex_music/track/lyrics_info.py new file mode 100644 index 00000000..79b5257e --- /dev/null +++ b/yandex_music/track/lyrics_info.py @@ -0,0 +1,43 @@ +from typing import TYPE_CHECKING, Optional + +from yandex_music import YandexMusicObject +from yandex_music.utils import model + +if TYPE_CHECKING: + from yandex_music import Client + + +@model +class LyricsInfo(YandexMusicObject): + """Класс, описывающий доступные тексты трека. + + Attributes: + has_available_sync_lyrics (:obj:`bool`): Наличие синхронизированного текста. + has_available_text_lyrics (:obj:`bool`): Наличие текста трека. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + """ + + has_available_sync_lyrics: bool + has_available_text_lyrics: bool + client: Optional['Client'] = None + + def __post_init__(self): + self._id_attrs = (self.has_available_sync_lyrics, self.has_available_text_lyrics) + + @classmethod + def de_json(cls, data: dict, client: 'Client') -> Optional['LyricsInfo']: + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + + Returns: + :obj:`yandex_music.LyricsInfo`: Типы доступных текстов трека. + """ + if not data: + return None + + data = super(LyricsInfo, cls).de_json(data, client) + + return cls(client=client, **data) diff --git a/yandex_music/track/lyrics_major.py b/yandex_music/track/lyrics_major.py new file mode 100644 index 00000000..d1445fe4 --- /dev/null +++ b/yandex_music/track/lyrics_major.py @@ -0,0 +1,46 @@ +from typing import TYPE_CHECKING, Optional + +from yandex_music import YandexMusicObject +from yandex_music.utils import model + + +if TYPE_CHECKING: + from yandex_music import Client + + +@model +class LyricsMajor(YandexMusicObject): + """Класс, представляющий сервис-источник текстов к трекам. + + Args: + id (:obj:`int`): Уникальный идентификатор сервиса. + name (:obj:`str`): Имя сервиса. + pretty_name (:obj:`str`): Человекочитаемое имя сервиса. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + """ + + id: int + name: str + pretty_name: str + client: Optional['Client'] = None + + def __post_init__(self): + self._id_attrs = (self.id,) + + @classmethod + def de_json(cls, data: dict, client: 'Client') -> Optional['LyricsMajor']: + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + + Returns: + :obj:`yandex_music.LyricsMajor`: Сервис-источник текстов к трекам. + """ + if not data: + return None + + data = super(LyricsMajor, cls).de_json(data, client) + + return cls(client=client, **data) diff --git a/yandex_music/track/r128.py b/yandex_music/track/r128.py new file mode 100644 index 00000000..7b489fed --- /dev/null +++ b/yandex_music/track/r128.py @@ -0,0 +1,45 @@ +from typing import TYPE_CHECKING, Optional + +from yandex_music import YandexMusicObject +from yandex_music.utils import model + +if TYPE_CHECKING: + from yandex_music import Client + + +@model +class R128(YandexMusicObject): + """Класс, описывающий параметры нормализации громкости трека в соответствии с рекомендацией EBU R 128. + + Attributes: + i (:obj:`float`): Интегрированная громкость. Совокупная громкость от начала до конца. + tp (:obj:`float`): True Peak. Реконструкция пикового уровня сигнала между выборками + (пикового уровня, генерируемого между двумя выборками ), рассчитанного с помощью + передискретизации. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + """ + + i: float + tp: float + client: Optional['Client'] = None + + def __post_init__(self): + self._id_attrs = (self.i, self.tp) + + @classmethod + def de_json(cls, data: dict, client: 'Client') -> Optional['R128']: + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + + Returns: + :obj:`yandex_music.R128`: Параметры нормализации громкости трека в соответствии с рекомендацией EBU R 128. + """ + if not data: + return None + + data = super(R128, cls).de_json(data, client) + + return cls(client=client, **data) diff --git a/yandex_music/track/track.py b/yandex_music/track/track.py index aba42f71..1725b2ac 100644 --- a/yandex_music/track/track.py +++ b/yandex_music/track/track.py @@ -16,6 +16,9 @@ User, MetaData, PoetryLoverMatch, + TrackLyrics, + LyricsInfo, + R128, ) @@ -28,6 +31,12 @@ class Track(YandexMusicObject): Известные значения поля `type`: `music`. + Известные значения поля `track_sharing_flag`: `VIDEO_ALLOWED`, `COVER_ONLY`. + + Известные значения поля `track_source`: `OWN`, `OWN_REPLACED_TO_UGC`. + + Известные значения поля `available_for_options`: `bookmate`. + Поля `can_publish`, `state`, `desired_visibility`, `filename`, `user_info` доступны только у треков что были загружены пользователем. @@ -70,11 +79,17 @@ class Track(YandexMusicObject): preview_duration_ms (:obj:`int`, optional): TODO. available_full_without_permission (:obj:`bool`, optional): Доступен ли без подписки. version (:obj:`str`, optional): Версия. - remember_position (:obj:`bool`, optional): Если :obj:`True`, то запоминатся последняя позиция прослушивания, + remember_position (:obj:`bool`, optional): Если :obj:`True`, то запоминается последняя позиция прослушивания, иначе позиция не запоминается. background_video_uri (:obj:`str`, optional): Ссылка на видеошот. - short_description (:obj:`str`, optional): Краткое опсание эпизода подкаста. + short_description (:obj:`str`, optional): Краткое описание эпизода подкаста. is_suitable_for_children (:obj:`bool`, optional): Подходит ли для детей TODO. + track_source (:obj:`str`, optional): Источник трека. + available_for_options (:obj:`list` из :obj:`str`, optional): Возможные опции для трека. + r128 (:obj:`yandex_music.R128`, optional): Параметры нормализации громкости трека + в соответствии с рекомендацией EBU R 128. + lyrics_info (:obj:`yandex_music.LyricsInfo`, optional): Данные о наличии текстов трека. + track_sharing_flag (:obj:`str`, optional): TODO. client (:obj:`yandex_music.Client`): Клиент Yandex Music. """ @@ -116,6 +131,11 @@ class Track(YandexMusicObject): background_video_uri: Optional[str] = None short_description: Optional[str] = None is_suitable_for_children: Optional[bool] = None + track_source: Optional[str] = None + available_for_options: Optional[List[str]] = None + r128: Optional['R128'] = None + lyrics_info: Optional['LyricsInfo'] = None + track_sharing_flag: Optional[str] = None client: Optional['Client'] = None def __post_init__(self): @@ -154,6 +174,42 @@ async def get_supplement_async(self, *args, **kwargs) -> Optional['Supplement']: """ return await self.client.track_supplement(self.id, *args, **kwargs) + def get_lyrics(self, *args, **kwargs) -> Optional['TrackLyrics']: + """Сокращение для:: + + client.tracks_lyrics(track.id, *args, **kwargs) + """ + return self.client.tracks_lyrics(self.id, *args, **kwargs) + + async def get_lyrics_async(self, *args, **kwargs) -> Optional['TrackLyrics']: + """Сокращение для:: + + client.tracks_lyrics(track.id, *args, **kwargs) + """ + return await self.client.tracks_lyrics(self.id, *args, **kwargs) + + def get_cover_url(self, size: str = '200x200') -> str: + """Возвращает URL обложки. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`str`: URL обложки. + """ + return f'https://{self.cover_uri.replace("%%", size)}' + + def get_og_image_url(self, size: str = '200x200') -> str: + """Возвращает URL OG обложки. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`str`: URL обложки. + """ + return f'https://{self.og_image.replace("%%", size)}' + def download_cover(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -161,7 +217,7 @@ def download_cover(self, filename: str, size: str = '200x200') -> None: filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + self.client.request.download(self.get_cover_url(size), filename) async def download_cover_async(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -170,7 +226,7 @@ async def download_cover_async(self, filename: str, size: str = '200x200') -> No filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - await self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + await self.client.request.download(self.get_cover_url(size), filename) def download_og_image(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -181,7 +237,7 @@ def download_og_image(self, filename: str, size: str = '200x200') -> None: filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + self.client.request.download(self.get_og_image_url(size), filename) async def download_og_image_async(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -192,7 +248,91 @@ async def download_og_image_async(self, filename: str, size: str = '200x200') -> filename (:obj:`str`): Путь для сохранения файла с названием и расширением. size (:obj:`str`, optional): Размер обложки. """ - await self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + await self.client.request.download(self.get_og_image_url(size), filename) + + def download_cover_bytes(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return self.client.request.retrieve(self.get_cover_url(size)) + + async def download_cover_bytes_async(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return await self.client.request.retrieve(self.get_cover_url(size)) + + def download_og_image_bytes(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Предпочтительнее использовать `self.download_cover()`. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return self.client.request.retrieve(self.get_og_image_url(size)) + + async def download_og_image_bytes_async(self, size: str = '200x200') -> bytes: + """Загрузка обложки и возврат в виде байтов. + + Предпочтительнее использовать `self.download_cover_async()`. + + Args: + size (:obj:`str`, optional): Размер обложки. + + Returns: + :obj:`bytes`: Обложка в виде байтов. + """ + return await self.client.request.retrieve(self.get_og_image_url(size)) + + def get_specific_download_info(self, codec: str, bitrate_in_kbps: int) -> Optional['DownloadInfo']: + """Возвращает вариант загрузки по критериям. + + Args: + codec (:obj:`str`, optional): Кодек из доступных в `self.download_info`. + bitrate_in_kbps (:obj:`int`, optional): Битрейт из доступных в `self.download_info` для данного кодека. + + Returns: + :obj:`yandex_music.DownloadInfo` | :obj:`None`: Вариант загрузки трека или :obj:`None`. + """ + if self.download_info is None: + self.get_download_info() + + for info in self.download_info: + if info.codec == codec and info.bitrate_in_kbps == bitrate_in_kbps: + return info + return None + + async def get_specific_download_info_async(self, codec: str, bitrate_in_kbps: int) -> Optional['DownloadInfo']: + """Возвращает вариант загрузки по критериям. + + Args: + codec (:obj:`str`, optional): Кодек из доступных в `self.download_info`. + bitrate_in_kbps (:obj:`int`, optional): Битрейт из доступных в `self.download_info` для данного кодека. + + Returns: + :obj:`yandex_music.DownloadInfo` | :obj:`None`: Вариант загрузки трека или :obj:`None`. + """ + if self.download_info is None: + await self.get_download_info_async() + + for info in self.download_info: + if info.codec == codec and info.bitrate_in_kbps == bitrate_in_kbps: + return info + return None def download(self, filename: str, codec: str = 'mp3', bitrate_in_kbps: int = 192) -> None: """Загрузка трека. @@ -210,13 +350,9 @@ def download(self, filename: str, codec: str = 'mp3', bitrate_in_kbps: int = 192 Raises: :class:`yandex_music.exceptions.InvalidBitrateError`: Если в `self.download_info` не найден подходящий трек. """ - if self.download_info is None: - self.get_download_info() - - for info in self.download_info: - if info.codec == codec and info.bitrate_in_kbps == bitrate_in_kbps: - info.download(filename) - break + info = self.get_specific_download_info(codec, bitrate_in_kbps) + if info: + info.download(filename) else: raise InvalidBitrateError('Unavailable bitrate') @@ -236,13 +372,57 @@ async def download_async(self, filename: str, codec: str = 'mp3', bitrate_in_kbp Raises: :class:`yandex_music.exceptions.InvalidBitrateError`: Если в `self.download_info` не найден подходящий трек. """ - if self.download_info is None: - await self.get_download_info_async() + info = await self.get_specific_download_info_async(codec, bitrate_in_kbps) + if info: + await info.download_async(filename) + else: + raise InvalidBitrateError('Unavailable bitrate') - for info in self.download_info: - if info.codec == codec and info.bitrate_in_kbps == bitrate_in_kbps: - await info.download_async(filename) - break + def download_bytes(self, codec: str = 'mp3', bitrate_in_kbps: int = 192) -> bytes: + """Загрузка трека и возврат в виде байтов. + + Note: + Известные значения `codec`: `mp3`, `aac`. + + Известные значения `bitrate_in_kbps`: `64`, `128`, `192`, `320`. + + Args: + codec (:obj:`str`, optional): Кодек из доступных в `self.download_info`. + bitrate_in_kbps (:obj:`int`, optional): Битрейт из доступных в `self.download_info` для данного кодека. + + Raises: + :class:`yandex_music.exceptions.InvalidBitrateError`: Если в `self.download_info` не найден подходящий трек. + + Returns: + :obj:`bytes`: Трек в виде байтов. + """ + info = self.get_specific_download_info(codec, bitrate_in_kbps) + if info: + return info.download_bytes() + else: + raise InvalidBitrateError('Unavailable bitrate') + + async def download_bytes_async(self, codec: str = 'mp3', bitrate_in_kbps: int = 192) -> bytes: + """Загрузка трека и возврат в виде байтов. + + Note: + Известные значения `codec`: `mp3`, `aac`. + + Известные значения `bitrate_in_kbps`: `64`, `128`, `192`, `320`. + + Args: + codec (:obj:`str`, optional): Кодек из доступных в `self.download_info`. + bitrate_in_kbps (:obj:`int`, optional): Битрейт из доступных в `self.download_info` для данного кодека. + + Raises: + :class:`yandex_music.exceptions.InvalidBitrateError`: Если в `self.download_info` не найден подходящий трек. + + Returns: + :obj:`bytes`: Трек в виде байтов. + """ + info = await self.get_specific_download_info_async(codec, bitrate_in_kbps) + if info: + return await info.download_bytes_async() else: raise InvalidBitrateError('Unavailable bitrate') @@ -305,7 +485,7 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['Track']: return None data = super(Track, cls).de_json(data, client) - from yandex_music import Normalization, Major, Album, Artist, User, MetaData, PoetryLoverMatch + from yandex_music import Normalization, Major, Album, Artist, User, MetaData, PoetryLoverMatch, R128, LyricsInfo data['albums'] = Album.de_list(data.get('albums'), client) data['artists'] = Artist.de_list(data.get('artists'), client) @@ -316,6 +496,8 @@ def de_json(cls, data: dict, client: 'Client') -> Optional['Track']: data['user_info'] = User.de_json(data.get('user_info'), client) data['meta_data'] = MetaData.de_json(data.get('meta_data'), client) data['poetry_lover_matches'] = PoetryLoverMatch.de_list(data.get('poetry_lover_matches'), client) + data['r128'] = R128.de_json(data.get('r128'), client) + data['lyrics_info'] = LyricsInfo.de_json(data.get('lyrics_info'), client) return cls(client=client, **data) @@ -345,6 +527,14 @@ def de_list(cls, data: dict, client: 'Client') -> List['Track']: getSupplement = get_supplement #: Псевдоним для :attr:`get_supplement_async` getSupplementAsync = get_supplement_async + #: Псевдоним для :attr:`get_lyrics` + getLyrics = get_lyrics + #: Псевдоним для :attr:`get_lyrics_async` + getLyricsAsync = get_lyrics_async + #: Псевдоним для :attr:`get_cover_url` + getCoverUrl = get_cover_url + #: Псевдоним для :attr:`get_og_image_url` + getOgImageUrl = get_og_image_url #: Псевдоним для :attr:`download_cover` downloadCover = download_cover #: Псевдоним для :attr:`download_cover_async` @@ -353,11 +543,29 @@ def de_list(cls, data: dict, client: 'Client') -> List['Track']: downloadOgImage = download_og_image #: Псевдоним для :attr:`download_og_image_async` downloadOgImageAsync = download_og_image_async - #: Псевдоним для :attr:`track_id` - trackId = track_id + #: Псевдоним для :attr:`download_cover_bytes` + downloadCoverBytes = download_cover_bytes + #: Псевдоним для :attr:`download_cover_bytes_async` + downloadCoverBytesAsync = download_cover_bytes_async + #: Псевдоним для :attr:`download_og_image_bytes` + downloadOgImageBytes = download_og_image_bytes + #: Псевдоним для :attr:`download_og_image_bytes_async` + downloadOgImageBytesAsync = download_og_image_bytes_async + #: Псевдоним для :attr:`get_specific_download_info` + getSpecificDownloadInfo = get_specific_download_info + #: Псевдоним для :attr:`get_specific_download_info_async` + getSpecificDownloadInfoAsync = get_specific_download_info_async + #: Псевдоним для :attr:`download_async` + downloadAsync = download_async + #: Псевдоним для :attr:`download_bytes` + downloadBytes = download_bytes + #: Псевдоним для :attr:`download_bytes_async` + downloadBytesAsync = download_bytes_async #: Псевдоним для :attr:`like_async` likeAsync = like_async #: Псевдоним для :attr:`dislike_async` - dislike_async = dislike_async - #: Псевдоним для :attr:`download_async` - downloadAsync = download_async + dislikeAsync = dislike_async + #: Псевдоним для :attr:`artists_name` + artistsName = artists_name + #: Псевдоним для :attr:`track_id` + trackId = track_id diff --git a/yandex_music/track/track_lyrics.py b/yandex_music/track/track_lyrics.py new file mode 100644 index 00000000..642d7b1c --- /dev/null +++ b/yandex_music/track/track_lyrics.py @@ -0,0 +1,79 @@ +from typing import TYPE_CHECKING, List, Optional + +from yandex_music import YandexMusicObject +from yandex_music.utils import model + + +if TYPE_CHECKING: + from yandex_music import Client, LyricsMajor + + +@model +class TrackLyrics(YandexMusicObject): + """Класс, представляющий текст трека. + + Attributes: + download_url (:obj:`str`): Ссылка на скачивание текста. + lyric_id (:obj:`int`): Уникальный идентификатор текста. + external_lyric_id (:obj:`str`): Уникальный идентификатор текста на сервисе предоставляющий текст. + writers (:obj:`list` из :obj:`str`): Авторы текста. + major (:obj:`yandex_music.LyricsMajor`): Сервис, откуда был получен текст. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + """ + + download_url: str + lyric_id: int + external_lyric_id: str + writers: List[str] + major: 'LyricsMajor' + client: Optional['Client'] = None + + def __post_init__(self): + self._id_attrs = ( + self.lyric_id, + self.external_lyric_id, + ) + + def fetch_lyrics(self) -> str: + """Получает текст песни по ссылке :attr:`yandex_music.TrackLyrics.download_url`. + + Returns: + :obj:`str`: Текст песни. + """ + return self.client.request.retrieve(self.download_url).decode('UTF-8') + + async def fetch_lyrics_async(self) -> str: + """Получает текст песни по ссылке :attr:`yandex_music.TrackLyrics.download_url`. + + Returns: + :obj:`str`: Текст песни. + """ + return await self.client.request.retrieve(self.download_url).decode('UTF-8') + + @classmethod + def de_json(cls, data: dict, client: 'Client') -> Optional['TrackLyrics']: + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + + Returns: + :obj:`yandex_music.TrackLyrics`: Текст трека. + """ + if not data: + return None + + data = super(TrackLyrics, cls).de_json(data, client) + from yandex_music import LyricsMajor + + data['major'] = LyricsMajor.de_json(data.get('major'), client) + + return cls(client=client, **data) + + # camelCase псевдонимы + + #: Псевдоним для :attr:`fetch_lyrics` + fetchLyrics = fetch_lyrics + #: Псевдоним для :attr:`fetch_lyrics_async` + fetchLyricsAsync = fetch_lyrics_async diff --git a/yandex_music/track_short.py b/yandex_music/track_short.py index 4c67cb9b..cc2313ca 100644 --- a/yandex_music/track_short.py +++ b/yandex_music/track_short.py @@ -22,6 +22,7 @@ class TrackShort(YandexMusicObject): recent (:obj:`bool`, optional): Недавний. chart (:obj:`yandex_music.Chart`, optional): Позиция в чарте. track (:obj:`yandex_music.Track`, optional): Полная версия трека. + original_index (:obj:`int`, optional): Индекс в плейлисте или альбоме. TODO уточнить про альбом. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. """ @@ -32,6 +33,7 @@ class TrackShort(YandexMusicObject): recent: Optional[bool] = None chart: Optional['Chart'] = None track: Optional['Track'] = None + original_index: Optional[int] = None client: Optional['Client'] = None def __post_init__(self): diff --git a/yandex_music/utils/convert_track_id.py b/yandex_music/utils/convert_track_id.py new file mode 100644 index 00000000..70fb403d --- /dev/null +++ b/yandex_music/utils/convert_track_id.py @@ -0,0 +1,21 @@ +from typing import Union + + +def convert_track_id_to_number(track_id: Union[str, int]) -> int: + """Переобразование идентификатора трека в номерной формат. + + Note: + Преобразует ID в формате "{track_id}:{album}" в track_id. + + Преобразует ID в формате "{track_id}" в track_id. + + Args: + track_id (:obj:`str` | :obj:`int`): Уникальный идентификатора трека. + + Returns: + :obj:`int`: Уникальный идентификатора трека в номерном формате. + """ + if isinstance(track_id, str): + track_id = int(track_id.split(':')[0]) + + return track_id diff --git a/yandex_music/utils/difference.py b/yandex_music/utils/difference.py index 42e3c8d3..71137b5b 100644 --- a/yandex_music/utils/difference.py +++ b/yandex_music/utils/difference.py @@ -75,14 +75,18 @@ def add_insert(self, at: int, tracks: Union[dict, List[dict]]) -> 'Difference': Returns: :obj:`yandex_music.utils.difference.Difference`: Набор операций над плейлистом. """ - # TODO принимать TrackId, а так же строку и сплитить её по ":". При отсутствии album_id кидать исключение. + # TODO (MarshalX) принимать TrackId, а так же строку и сплитить её по ":". + # При отсутствии album_id кидать исключение. + # https://github.com/MarshalX/yandex-music-api/issues/558 if not isinstance(tracks, list): tracks = [tracks] operation = {'op': Operation.INSERT.value, 'at': at, 'tracks': []} for track in tracks: - track = type('TrackId', (), track) # TODO replace to normal TrackId object + # TODO (MarshalX) replace to normal TrackId object + # https://github.com/MarshalX/yandex-music-api/issues/558 + track = type('TrackId', (), track) operation['tracks'].append({'id': track.id, 'albumId': track.album_id}) diff --git a/yandex_music/utils/request.py b/yandex_music/utils/request.py index 71257b51..331038ed 100644 --- a/yandex_music/utils/request.py +++ b/yandex_music/utils/request.py @@ -27,13 +27,16 @@ USER_AGENT = 'Yandex-Music-API' HEADERS = { - 'X-Yandex-Music-Client': 'YandexMusicAndroid/23020251', + 'X-Yandex-Music-Client': 'YandexMusicAndroid/24023231', } +DEFAULT_TIMEOUT = 5 reserved_names = keyword.kwlist + ['client'] logging.getLogger('urllib3').setLevel(logging.WARNING) +default_timeout = object() + class Request: """Вспомогательный класс для yandex_music, представляющий методы для выполнения POST и GET запросов, скачивания @@ -45,9 +48,12 @@ class Request: proxy_url (:obj:`str`, optional): Прокси. """ - def __init__(self, client=None, headers=None, proxy_url=None): + def __init__(self, client=None, headers=None, proxy_url=None, timeout=default_timeout): self.headers = headers or HEADERS.copy() + self._timeout = DEFAULT_TIMEOUT + self.set_timeout(timeout) + self.client = self.set_and_return_client(client) # aiohttp @@ -67,6 +73,16 @@ def set_language(self, lang: str) -> None: """ self.headers.update({'Accept-Language': lang}) + def set_timeout(self, timeout: Union[int, float, object] = default_timeout): + """Устанавливает время ожидания для всех запросов. + + Args: + timeout (:obj:`int` | :obj:`float`): Время ожидания от сервера. + """ + self._timeout = timeout + if timeout is default_timeout: + self._timeout = DEFAULT_TIMEOUT + def set_authorization(self, token: str) -> None: """Добавляет заголовок авторизации для каждого запроса. @@ -192,6 +208,9 @@ def _request_wrapper(self, *args, **kwargs): kwargs['headers']['User-Agent'] = USER_AGENT + if kwargs['timeout'] is default_timeout: + kwargs['timeout'] = self._timeout + try: resp = requests.request(*args, **kwargs) except requests.Timeout: @@ -222,7 +241,9 @@ def _request_wrapper(self, *args, **kwargs): else: raise NetworkError(f'{message} ({resp.status_code}): {resp.content}') - def get(self, url: str, params: dict = None, timeout: Union[int, float] = 5, *args, **kwargs) -> Union[dict, str]: + def get( + self, url: str, params: dict = None, timeout: Union[int, float] = default_timeout, *args, **kwargs + ) -> Union[dict, str]: """Отправка GET запроса. Args: @@ -245,7 +266,7 @@ def get(self, url: str, params: dict = None, timeout: Union[int, float] = 5, *ar return self._parse(result).get_result() - def post(self, url, data=None, timeout=5, *args, **kwargs) -> Union[dict, str]: + def post(self, url, data=None, timeout=default_timeout, *args, **kwargs) -> Union[dict, str]: """Отправка POST запроса. Args: @@ -268,7 +289,7 @@ def post(self, url, data=None, timeout=5, *args, **kwargs) -> Union[dict, str]: return self._parse(result).get_result() - def retrieve(self, url, timeout=5, *args, **kwargs) -> bytes: + def retrieve(self, url, timeout=default_timeout, *args, **kwargs) -> bytes: """Отправка GET запроса и получение содержимого без обработки (парсинга). Args: @@ -286,7 +307,7 @@ def retrieve(self, url, timeout=5, *args, **kwargs) -> bytes: """ return self._request_wrapper('GET', url, proxies=self.proxies, timeout=timeout, *args, **kwargs) - def download(self, url, filename, timeout=5, *args, **kwargs) -> None: + def download(self, url, filename, timeout=default_timeout, *args, **kwargs) -> None: """Отправка запроса на получение содержимого и его запись в файл. Args: diff --git a/yandex_music/utils/request_async.py b/yandex_music/utils/request_async.py index 62d2d0af..23def98b 100644 --- a/yandex_music/utils/request_async.py +++ b/yandex_music/utils/request_async.py @@ -33,13 +33,16 @@ USER_AGENT = 'Yandex-Music-API' HEADERS = { - 'X-Yandex-Music-Client': 'YandexMusicAndroid/23020251', + 'X-Yandex-Music-Client': 'YandexMusicAndroid/24023231', } +DEFAULT_TIMEOUT = 5 reserved_names = keyword.kwlist + ['client'] logging.getLogger('urllib3').setLevel(logging.WARNING) +default_timeout = object() + class Request: """Вспомогательный класс для yandex_music, представляющий методы для выполнения POST и GET запросов, скачивания @@ -51,9 +54,12 @@ class Request: proxy_url (:obj:`str`, optional): Прокси. """ - def __init__(self, client=None, headers=None, proxy_url=None): + def __init__(self, client=None, headers=None, proxy_url=None, timeout=default_timeout): self.headers = headers or HEADERS.copy() + self._timeout = DEFAULT_TIMEOUT + self.set_timeout(timeout) + self.client = self.set_and_return_client(client) # aiohttp @@ -73,6 +79,16 @@ def set_language(self, lang: str) -> None: """ self.headers.update({'Accept-Language': lang}) + def set_timeout(self, timeout: Union[int, float, object] = default_timeout): + """Устанавливает время ожидания для всех запросов. + + Args: + timeout (:obj:`int` | :obj:`float`): Время ожидания от сервера. + """ + self._timeout = timeout + if timeout is default_timeout: + self._timeout = DEFAULT_TIMEOUT + def set_authorization(self, token: str) -> None: """Добавляет заголовок авторизации для каждого запроса. @@ -198,6 +214,11 @@ async def _request_wrapper(self, *args, **kwargs): kwargs['headers']['User-Agent'] = USER_AGENT + if kwargs['timeout'] is default_timeout: + kwargs['timeout'] = aiohttp.ClientTimeout(total=self._timeout) + else: + kwargs['timeout'] = aiohttp.ClientTimeout(total=kwargs['timeout']) + try: async with aiohttp.request(*args, **kwargs) as _resp: resp = _resp @@ -231,7 +252,7 @@ async def _request_wrapper(self, *args, **kwargs): raise NetworkError(f'{message} ({resp.status}): {content}') async def get( - self, url: str, params: dict = None, timeout: Union[int, float] = 5, *args, **kwargs + self, url: str, params: dict = None, timeout: Union[int, float] = default_timeout, *args, **kwargs ) -> Union[dict, str]: """Отправка GET запроса. @@ -250,19 +271,12 @@ async def get( :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ result = await self._request_wrapper( - 'GET', - url, - params=params, - headers=self.headers, - proxy=self.proxy_url, - timeout=aiohttp.ClientTimeout(total=timeout), - *args, - **kwargs, + 'GET', url, params=params, headers=self.headers, proxy=self.proxy_url, timeout=timeout, *args, **kwargs ) return self._parse(result).get_result() - async def post(self, url, data=None, timeout=5, *args, **kwargs) -> Union[dict, str]: + async def post(self, url, data=None, timeout=default_timeout, *args, **kwargs) -> Union[dict, str]: """Отправка POST запроса. Args: @@ -280,19 +294,12 @@ async def post(self, url, data=None, timeout=5, *args, **kwargs) -> Union[dict, :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ result = await self._request_wrapper( - 'POST', - url, - headers=self.headers, - proxy=self.proxy_url, - data=data, - timeout=aiohttp.ClientTimeout(total=timeout), - *args, - **kwargs, + 'POST', url, headers=self.headers, proxy=self.proxy_url, data=data, timeout=timeout, *args, **kwargs ) return self._parse(result).get_result() - async def retrieve(self, url, timeout=5, *args, **kwargs) -> bytes: + async def retrieve(self, url, timeout=default_timeout, *args, **kwargs) -> bytes: """Отправка GET запроса и получение содержимого без обработки (парсинга). Args: @@ -308,11 +315,9 @@ async def retrieve(self, url, timeout=5, *args, **kwargs) -> bytes: Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ - return await self._request_wrapper( - 'GET', url, proxy=self.proxy_url, timeout=aiohttp.ClientTimeout(total=timeout), *args, **kwargs - ) + return await self._request_wrapper('GET', url, proxy=self.proxy_url, timeout=timeout, *args, **kwargs) - async def download(self, url, filename, timeout=5, *args, **kwargs) -> None: + async def download(self, url, filename, timeout=default_timeout, *args, **kwargs) -> None: """Отправка запроса на получение содержимого и его запись в файл. Args: diff --git a/yandex_music/utils/sign_request.py b/yandex_music/utils/sign_request.py new file mode 100644 index 00000000..78b51166 --- /dev/null +++ b/yandex_music/utils/sign_request.py @@ -0,0 +1,46 @@ +from typing import Union +from dataclasses import dataclass +import datetime +import hmac +import hashlib +import base64 + +from yandex_music.utils.convert_track_id import convert_track_id_to_number + + +DEFAULT_SIGN_KEY = 'p93jhgh689SBReK6ghtw62' +""":obj:`str`: Ключ для подписи из Android приложения.""" + + +@dataclass +class Sign: + """Подпись запроса. + + Attributes: + timestamp (:obj:`int`): Время создания подписи. + value (:obj:`str`): Подпись. + """ + + timestamp: int + value: str + + +def get_sign_request(track_id: Union[int, str], key: str = DEFAULT_SIGN_KEY) -> Sign: + """Создает подпись для запроса. + + Args: + track_id (:obj:`str` | :obj:`int`): Уникальный идентификатора трека. + key (:obj:`str`, optional): Ключ для подписи. + + Returns: + :obj:`Sign`: Подпись. + """ + track_id = convert_track_id_to_number(track_id) + + timestamp = int(datetime.datetime.now().timestamp()) + message = f'{track_id}{timestamp}' + + hmac_sign = hmac.new(key.encode('UTF-8'), message.encode('UTF-8'), hashlib.sha256).digest() + sign = base64.b64encode(hmac_sign).decode('UTF-8') + + return Sign(timestamp, sign)