diff --git a/CHANGES.rst b/CHANGES.rst index c9c33f43..c2ffbe4b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,54 @@ Список изменений ================ +Версия 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 ============= @@ -26,7 +74,7 @@ **Незначительные изменения и/или исправления** - Поле ``cover_uri`` класса ``Album`` теперь опциональное. -- Поле ``region`` у класса Account теперь не обязательное. +- Поле ``region`` у класса ``Account`` теперь не обязательное. - Исправлен баг в ``.to_dict()`` методе, связанный с десериализцией объектов списков и словарей. - Исправлен баг в ``.to_dict()`` методе, связанный с не рекурсивной десериализацией. - Исправлена десериализация ``similar_artists`` в ``BriefInfo``. diff --git a/Pipfile b/Pipfile index a82d7185..889c4e40 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,7 @@ pytest-cov = "*" codecov = "*" [packages] -requests = "*" +requests = { extras = ["socks"] } [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 7f560722..faee4012 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c889ddb9cbefe66436c2a82bf83cac14e9a8f1a85ad8b2e9d0d8aa891f33a5fc" + "sha256": "4af012e674da3f8f42b23516f2f3bdd4571bc89b117efec071530308cf97e478" }, "pipfile-spec": 6, "requires": { @@ -37,7 +37,18 @@ ], "version": "==2.8" }, + "pysocks": { + "hashes": [ + "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299", + "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", + "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0" + ], + "version": "==1.7.1" + }, "requests": { + "extras": [ + "socks" + ], "hashes": [ "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" @@ -47,10 +58,10 @@ }, "urllib3": { "hashes": [ - "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", - "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" + "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", + "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" ], - "version": "==1.25.6" + "version": "==1.25.7" } }, "develop": { @@ -104,6 +115,14 @@ "index": "pypi", "version": "==2.0.15" }, + "colorama": { + "hashes": [ + "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", + "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.4.1" + }, "coverage": { "hashes": [ "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", @@ -249,18 +268,18 @@ }, "pyparsing": { "hashes": [ - "sha256:4acadc9a2b96c19fe00932a38ca63e601180c39a189a696abce1eaab641447e1", - "sha256:61b5ed888beab19ddccab3478910e2076a6b5a0295dffc43021890e136edf764" + "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f", + "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a" ], - "version": "==2.4.4" + "version": "==2.4.5" }, "pytest": { "hashes": [ - "sha256:27abc3fef618a01bebb1f0d6d303d2816a99aa87a5968ebc32fe971be91eb1e6", - "sha256:58cee9e09242937e136dbb3dab466116ba20d6b7828c7620f23947f37eb4dae4" + "sha256:8e256fe71eb74e14a4d20a5987bb5e1488f0511ee800680aaedc62b9358714e8", + "sha256:ff0090819f669aaa0284d0f4aad1a6d9d67a6efdc6dd4eb4ac56b704f890a0d6" ], "index": "pypi", - "version": "==5.2.2" + "version": "==5.2.4" }, "pytest-cov": { "hashes": [ @@ -278,6 +297,9 @@ "version": "==2019.3" }, "requests": { + "extras": [ + "socks" + ], "hashes": [ "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" @@ -359,10 +381,10 @@ }, "urllib3": { "hashes": [ - "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", - "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" + "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", + "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" ], - "version": "==1.25.6" + "version": "==1.25.7" }, "wcwidth": { "hashes": [ diff --git a/README.rst b/README.rst index 633f5efd..adbf96d1 100644 --- a/README.rst +++ b/README.rst @@ -176,6 +176,52 @@ Microsoft Store. Так как API является закрытым и испо Первым треком из примера является следующий трек: music.yandex.ru/album/**1193829**/track/**10994777** +Выполнение запросов с использование прокси: + +.. code:: python + + from yandex_music.utils.request import Request + from yandex_music.client import Client + + request = Request(proxy_url='socks5://user:password@host:port') + client = Client(request=request) + +Примеры proxy url: + +- socks5://user:password@host:port +- http://host:port +- https://host:port +- http://user:password@host + +Больше примеров тут: `proxies - advanced usage - requests `_ + +Пример инициализации клиента с обработкой капчи: + +.. code:: python + + def init_client(): + client = captcha_key = captcha_answer = None + while not client: + try: + client = Client.from_credentials('login', 'pass', captcha_answer, captcha_key) + except Captcha as e: + e.captcha.download('captcha.png') + + captcha_key = e.captcha.x_captcha_key + captcha_answer = input('Число с картинки: ') + + return client + +Пример инициализации клиента с обработкой капчи при помощи callback-функции: + +.. code:: python + + def proc_captcha(captcha): + captcha.download('captcha.png') + return input('Число с картинки: ') + + client = Client.from_credentials('login', 'pass', captcha_callback=proc_captcha) + -------------------- Изучение по примерам -------------------- diff --git a/SECURITY.md b/SECURITY.md index a5e893da..834fc8dd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ | Версия | Поддержка | | ------- | ------------------ | -| 0.0.13 | :white_check_mark: | -| < 0.0.13 | :x: | +| 0.0.15 | :white_check_mark: | +| < 0.0.15 | :x: | ## Сообщение об уязвимости diff --git a/docs/source/yandex_music.artist.artist_albums.rst b/docs/source/yandex_music.artist.artist_albums.rst new file mode 100644 index 00000000..c734d47b --- /dev/null +++ b/docs/source/yandex_music.artist.artist_albums.rst @@ -0,0 +1,7 @@ +yandex_music.ArtistAlbums +========================= + +.. autoclass:: yandex_music.ArtistAlbums + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/yandex_music.artist.rst b/docs/source/yandex_music.artist.rst index 483b4be8..63446e86 100644 --- a/docs/source/yandex_music.artist.rst +++ b/docs/source/yandex_music.artist.rst @@ -11,4 +11,5 @@ yandex_music.artist.description yandex_music.artist.brief_info yandex_music.artist.artist_tracks + yandex_music.artist.artist_albums yandex_music.artist.pager diff --git a/docs/source/yandex_music.playlist.playlist_absence.rst b/docs/source/yandex_music.playlist.playlist_absence.rst new file mode 100644 index 00000000..b5408ba0 --- /dev/null +++ b/docs/source/yandex_music.playlist.playlist_absence.rst @@ -0,0 +1,7 @@ +yandex_music.PlaylistAbsence +============================ + +.. autoclass:: yandex_music.PlaylistAbsence + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/yandex_music.playlist.rst b/docs/source/yandex_music.playlist.rst index 45936190..29a84555 100644 --- a/docs/source/yandex_music.playlist.rst +++ b/docs/source/yandex_music.playlist.rst @@ -6,6 +6,7 @@ yandex_music.playlist.user yandex_music.playlist.made_for yandex_music.playlist.play_counter + yandex_music.playlist.playlist_absence yandex_music.playlist.playlist yandex_music.playlist.case_forms yandex_music.playlist.playlist_id diff --git a/docs/source/yandex_music.search.album_search_result.rst b/docs/source/yandex_music.search.album_search_result.rst deleted file mode 100644 index db3b03f9..00000000 --- a/docs/source/yandex_music.search.album_search_result.rst +++ /dev/null @@ -1,7 +0,0 @@ -yandex_music.AlbumSearchResult -============================== - -.. autoclass:: yandex_music.AlbumSearchResult - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/yandex_music.search.artist_search_result.rst b/docs/source/yandex_music.search.artist_search_result.rst deleted file mode 100644 index d5619e2e..00000000 --- a/docs/source/yandex_music.search.artist_search_result.rst +++ /dev/null @@ -1,7 +0,0 @@ -yandex_music.ArtistSearchResult -=============================== - -.. autoclass:: yandex_music.ArtistSearchResult - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/yandex_music.search.playlist_search_result.rst b/docs/source/yandex_music.search.playlist_search_result.rst deleted file mode 100644 index fe26c4e6..00000000 --- a/docs/source/yandex_music.search.playlist_search_result.rst +++ /dev/null @@ -1,7 +0,0 @@ -yandex_music.PlaylistSearchResult -================================= - -.. autoclass:: yandex_music.PlaylistSearchResult - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/yandex_music.search.rst b/docs/source/yandex_music.search.rst index 0069fde1..ede5bab6 100644 --- a/docs/source/yandex_music.search.rst +++ b/docs/source/yandex_music.search.rst @@ -4,11 +4,6 @@ .. toctree:: yandex_music.search.suggestions - yandex_music.search.track_search_result yandex_music.search.search - yandex_music.search.artist_search_result yandex_music.search.search_result - yandex_music.search.video_search_result yandex_music.search.best - yandex_music.search.playlist_search_result - yandex_music.search.album_search_result diff --git a/docs/source/yandex_music.search.track_search_result.rst b/docs/source/yandex_music.search.track_search_result.rst deleted file mode 100644 index aed130fd..00000000 --- a/docs/source/yandex_music.search.track_search_result.rst +++ /dev/null @@ -1,7 +0,0 @@ -yandex_music.TrackSearchResult -============================== - -.. autoclass:: yandex_music.TrackSearchResult - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/yandex_music.search.video_search_result.rst b/docs/source/yandex_music.search.video_search_result.rst deleted file mode 100644 index 4c541c1f..00000000 --- a/docs/source/yandex_music.search.video_search_result.rst +++ /dev/null @@ -1,7 +0,0 @@ -yandex_music.VideoSearchResult -============================== - -.. autoclass:: yandex_music.VideoSearchResult - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/yandex_music.utils.captcha_response.rst b/docs/source/yandex_music.utils.captcha_response.rst new file mode 100644 index 00000000..183d0b1a --- /dev/null +++ b/docs/source/yandex_music.utils.captcha_response.rst @@ -0,0 +1,7 @@ +yandex_music.utils.captcha_response.CaptchaResponse +=================================================== + +.. autoclass:: yandex_music.utils.captcha_response.CaptchaResponse + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/yandex_music.utils.rst b/docs/source/yandex_music.utils.rst index 6f89caa1..d196cc89 100644 --- a/docs/source/yandex_music.utils.rst +++ b/docs/source/yandex_music.utils.rst @@ -5,4 +5,5 @@ yandex_music.utils.request yandex_music.utils.response + yandex_music.utils.captcha_response yandex_music.utils.difference \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 663bd1f6..617cee6a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -requests \ No newline at end of file +requests[socks] \ No newline at end of file diff --git a/setup.py b/setup.py index f4e50a57..70ad57bb 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ def requirements(): with open('README.rst', 'r', encoding='utf-8') as f: setup(name='yandex-music', - version='0.0.14', + version='0.0.15', author='Il`ya Semyonov', author_email='Ilya@marshal.by', license='LGPLv3', diff --git a/tests/__init__.py b/tests/__init__.py index 6c942b1c..4609f792 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -39,6 +39,7 @@ from .test_play_contexts_data import TestPlayContextsData from .test_play_counter import TestPlayCounter from .test_playlist import TestPlaylist +from .test_playlist_absence import TestPlaylistAbsence from .test_playlist_id import TestPlaylistId from .test_playlist_id import TestPlaylistId from .test_plus import TestPlus diff --git a/tests/conftest.py b/tests/conftest.py index 398741b1..0bd1b3b8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ Account, Client, TrackShort, Value, DiscreteScale, PlaylistId, MixLink, Link, PassportPhone, User, Promotion, \ PersonalPlaylistsData, RotorSettings, TrackShortOld, PlayContextsData, Status, Settings, StationResult, Enum, \ TrackWithAds, VideoSupplement, ArtistEvent, ChartItem, Event, AlbumEvent, Day, PlayContext, Plus, Title, Label, \ - GeneratedPlaylist, Video, Vinyl, SearchResult, BlockEntity, Block + GeneratedPlaylist, Video, Vinyl, SearchResult, BlockEntity, Block, PlaylistAbsence from . import TestCounts, TestTrackId, TestCaseForms, TestRatings, TestIcon, TestAlbum, TestLyrics, \ TestTrack, TestInvocationInfo, TestPlaylist, TestAutoRenewable, TestStation, TestNormalization, TestMajor, \ TestTrackPosition, TestBest, TestChart, TestPermissions, TestPlus, TestProduct, TestCover, TestPlayCounter, \ @@ -15,14 +15,14 @@ TestUser, TestPassportPhone, TestPromotion, TestTitle, TestPersonalPlaylistsData, TestRotorSettings, \ TestTrackShortOld, TestPager, TestStatus, TestSettings, TestStationResult, TestLabel, TestTrackWithAds, \ TestVideoSupplement, TestEvent, TestDay, TestPlayContext, TestGeneratedPlaylist, TestVideo, TestVinyl, \ - TestSearchResult, TestBlockEntity, TestBlock + TestSearchResult, TestBlockEntity, TestBlock, TestPlaylistAbsence @pytest.fixture(scope='session') def artist_factory(cover, counts, ratings, link, description): class ArtistFactory: def get(self, popular_tracks): - return Artist(TestArtist.id, TestArtist.name, TestArtist.various, TestArtist.composer, cover, + return Artist(TestArtist.id, TestArtist.name, cover, TestArtist.various, TestArtist.composer, TestArtist.genres, TestArtist.op_image, TestArtist.no_pictures_from_search, counts, TestArtist.available, ratings, [link], TestArtist.tickets_available, TestArtist.likes_count, popular_tracks, TestArtist.regions, TestArtist.decomposed, TestArtist.full_names, description, @@ -46,10 +46,10 @@ def artist_without_tracks(artist_factory): def track_factory(major, normalization): class TrackFactory: def get(self, artists, albums): - return Track(TestTrack.id, TestTrack.title, TestTrack.available, TestTrack.available_for_premium_users, - artists, albums, TestTrack.lyrics_available, TestTrack.real_id, TestTrack.og_image, - TestTrack.type, TestTrack.cover_uri, major, TestTrack.duration_ms, TestTrack.storage_dir, - TestTrack.file_size, normalization, TestTrack.error, TestTrack.regions, + return Track(TestTrack.id, TestTrack.title, TestTrack.available, artists, albums, + TestTrack.available_for_premium_users, TestTrack.lyrics_available, TestTrack.real_id, + TestTrack.og_image, TestTrack.type, TestTrack.cover_uri, major, TestTrack.duration_ms, + TestTrack.storage_dir, TestTrack.file_size, normalization, TestTrack.error, TestTrack.regions, TestTrack.available_as_rbt, TestTrack.content_warning, TestTrack.explicit, TestTrack.preview_duration_ms, TestTrack.available_full_without_permission) @@ -102,11 +102,11 @@ def album_without_tracks(album_factory, artist_without_tracks): @pytest.fixture(scope='session') -def playlist_factory(user, cover, made_for, track_short, play_counter): +def playlist_factory(user, cover, made_for, track_short, play_counter, playlist_absence): class PlaylistFactory: def get(self): - return Playlist(user, TestPlaylist.uid, TestPlaylist.kind, TestPlaylist.title, TestPlaylist.track_count, - cover, made_for, play_counter, TestPlaylist.tags, TestPlaylist.revision, + return Playlist(user, cover, made_for, play_counter, playlist_absence, TestPlaylist.uid, TestPlaylist.kind, + TestPlaylist.title, TestPlaylist.track_count, TestPlaylist.tags, TestPlaylist.revision, TestPlaylist.snapshot, TestPlaylist.visibility, TestPlaylist.collective, TestPlaylist.created, TestPlaylist.modified, TestPlaylist.available, TestPlaylist.is_banner, TestPlaylist.is_premiere, TestPlaylist.duration_ms, TestPlaylist.og_image, [track_short], @@ -255,6 +255,11 @@ def play_counter(): return PlayCounter(TestPlayCounter.value, TestPlayCounter.description, TestPlayCounter.updated) +@pytest.fixture(scope='session') +def playlist_absence(): + return PlaylistAbsence(TestPlaylistAbsence.kind, TestPlaylistAbsence.reason) + + @pytest.fixture(scope='session') def results(playlist): return [playlist] diff --git a/tests/test_artist.py b/tests/test_artist.py index b6b58591..23f371df 100644 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -50,14 +50,11 @@ def test_expected_values(self, artist, cover, counts, ratings, link, track_witho assert artist.end_date == self.end_date def test_de_json_required(self, client, cover): - json_dict = {'id': self.id, 'name': self.name, 'various': self.various, 'composer': self.composer, - 'cover': cover.to_dict()} + json_dict = {'id': self.id, 'name': self.name, 'cover': cover.to_dict()} artist = Artist.de_json(json_dict, client) assert artist.id == self.id assert artist.name == self.name - assert artist.various == self.various - assert artist.composer == self.composer assert artist.cover == cover def test_de_json_all(self, client, cover, counts, ratings, link, track_without_artists, description): @@ -100,9 +97,9 @@ def test_de_json_all(self, client, cover, counts, ratings, link, track_without_a assert artist.end_date == self.end_date def test_equality(self, cover): - a = Artist(self.id, self.name, self.various, self.composer, cover) - b = Artist(self.id, '', self.various, self.composer, None) - c = Artist(self.id, self.name, self.various, self.composer, cover) + a = Artist(self.id, self.name, cover) + b = Artist(self.id, '', None) + c = Artist(self.id, self.name, cover) assert a != b assert hash(a) != hash(b) diff --git a/tests/test_artist_albums.py b/tests/test_artist_albums.py new file mode 100644 index 00000000..5b864691 --- /dev/null +++ b/tests/test_artist_albums.py @@ -0,0 +1,39 @@ +import pytest + +from yandex_music import ArtistAlbums + + +@pytest.fixture(scope='class') +def artist_albums(album, pager): + return ArtistAlbums([album], pager) + + +class TestArtistAlbums: + def test_expected_values(self, artist_albums, album, pager): + assert artist_albums.albums == [album] + assert artist_albums.pager == pager + + def test_de_json_required(self, client, album, pager): + json_dict = {'albums': [album.to_dict()], 'pager': pager.to_dict()} + artist_albums = ArtistAlbums.de_json(json_dict, client) + + assert artist_albums.albums == [album] + assert artist_albums.pager == pager + + def test_de_json_all(self, client, album, pager): + json_dict = {'albums': [album.to_dict()], 'pager': pager.to_dict()} + artist_albums = ArtistAlbums.de_json(json_dict, client) + + assert artist_albums.albums == [album] + assert artist_albums.pager == pager + + def test_equality(self, album, pager): + a = ArtistAlbums([album], pager) + b = ArtistAlbums([], pager) + c = ArtistAlbums([album], pager) + + assert a != b + assert hash(a) != hash(b) + assert a is not b + + assert a == c diff --git a/tests/test_playlist.py b/tests/test_playlist.py index e19cbc04..879da403 100644 --- a/tests/test_playlist.py +++ b/tests/test_playlist.py @@ -28,7 +28,7 @@ class TestPlaylist: is_for_from = None regions = None - def test_expected_values(self, playlist, user, cover, made_for, track_short, play_counter): + def test_expected_values(self, playlist, user, cover, made_for, track_short, play_counter, playlist_absence): assert playlist.owner == user assert playlist.uid == self.uid assert playlist.kind == self.kind @@ -37,6 +37,7 @@ def test_expected_values(self, playlist, user, cover, made_for, track_short, pla assert playlist.cover == cover assert playlist.made_for == made_for assert playlist.play_counter == play_counter + assert playlist.playlist_absence == playlist_absence assert playlist.tags == self.tags assert playlist.revision == self.revision assert playlist.snapshot == self.snapshot @@ -60,10 +61,10 @@ def test_expected_values(self, playlist, user, cover, made_for, track_short, pla assert playlist.is_for_from == self.is_for_from assert playlist.regions == self.regions - def test_de_json_required(self, client, user, cover, made_for, play_counter): + def test_de_json_required(self, client, user, cover, made_for, play_counter, playlist_absence): json_dict = {'owner': user.to_dict(), 'uid': self.uid, 'kind': self.kind, 'title': self.title, 'track_count': self.track_count, 'cover': cover.to_dict(), 'made_for': made_for.to_dict(), - 'play_counter': play_counter.to_dict()} + 'play_counter': play_counter.to_dict(), 'playlist_absence': playlist_absence.to_dict()} playlist = Playlist.de_json(json_dict, client) assert playlist.owner == user @@ -75,15 +76,16 @@ def test_de_json_required(self, client, user, cover, made_for, play_counter): assert playlist.made_for == made_for assert playlist.play_counter == play_counter - def test_de_json_all(self, client, user, cover, made_for, track_short, play_counter): + def test_de_json_all(self, client, user, cover, made_for, track_short, play_counter, playlist_absence): json_dict = {'owner': user.to_dict(), 'uid': self.uid, 'kind': self.kind, 'title': self.title, 'track_count': self.track_count, 'cover': cover.to_dict(), 'made_for': made_for.to_dict(), - 'play_counter': play_counter.to_dict(), 'tags': self.tags, 'revision': self.revision, - 'snapshot': self.snapshot, 'visibility': self.visibility, 'collective': self.collective, - 'created': self.created, 'modified': self.modified, 'available': self.available, - 'is_banner': self.is_banner, 'is_premiere': self.is_premiere, 'duration_ms': self.duration_ms, - 'og_image': self.og_image, 'tracks': [track_short.to_dict()], 'prerolls': self.prerolls, - 'likes_count': self.likes_count, 'generated_playlist_type': self.generated_playlist_type, + 'play_counter': play_counter.to_dict(), 'playlist_absence': playlist_absence.to_dict(), + 'tags': self.tags, 'revision': self.revision, 'snapshot': self.snapshot, + 'visibility': self.visibility, 'collective': self.collective, 'created': self.created, + 'modified': self.modified, 'available': self.available, 'is_banner': self.is_banner, + 'is_premiere': self.is_premiere, 'duration_ms': self.duration_ms, 'og_image': self.og_image, + 'tracks': [track_short.to_dict()], 'prerolls': self.prerolls, 'likes_count': self.likes_count, + 'generated_playlist_type': self.generated_playlist_type, 'animated_cover_uri': self.animated_cover_uri, 'ever_played': self.ever_played, 'description': self.description, 'description_formatted': self.description_formatted, 'is_for_from': self.is_for_from, 'regions': self.regions} @@ -97,6 +99,7 @@ def test_de_json_all(self, client, user, cover, made_for, track_short, play_coun assert playlist.cover == cover assert playlist.made_for == made_for assert playlist.play_counter == play_counter + assert playlist.playlist_absence == playlist_absence assert playlist.tags == self.tags assert playlist.revision == self.revision assert playlist.snapshot == self.snapshot @@ -120,11 +123,11 @@ def test_de_json_all(self, client, user, cover, made_for, track_short, play_coun assert playlist.is_for_from == self.is_for_from assert playlist.regions == self.regions - def test_equality(self, user, cover, made_for, play_counter): - a = Playlist(user, self.uid, self.kind, self.title, self.track_count, cover, made_for, play_counter) - b = Playlist(user, 123, self.kind, self.title, 10, cover, made_for, play_counter) - c = Playlist(user, self.uid, 321, self.title, self.track_count, None, made_for, play_counter) - d = Playlist(user, self.uid, self.kind, self.title, self.track_count, cover, made_for, play_counter) + def test_equality(self, user, cover, made_for, play_counter, playlist_absence): + a = Playlist(user, cover, made_for, play_counter, playlist_absence) + b = Playlist(user, cover, made_for, play_counter, None) + c = Playlist(user, None, made_for, play_counter, playlist_absence) + d = Playlist(user, cover, made_for, play_counter, playlist_absence) assert a != b != c assert hash(a) != hash(b) != hash(c) diff --git a/tests/test_playlist_absence.py b/tests/test_playlist_absence.py new file mode 100644 index 00000000..4ce6ca3e --- /dev/null +++ b/tests/test_playlist_absence.py @@ -0,0 +1,35 @@ +from yandex_music import PlaylistAbsence + + +class TestPlaylistAbsence: + kind = 1003 + reason = 'playlist-is-deleted' + + def test_expected_values(self, playlist_absence): + assert playlist_absence.kind == self.kind + assert playlist_absence.reason == self.reason + + def test_de_json_required(self, client): + json_dict = {'kind': self.kind, 'reason': self.reason} + playlist_absence = PlaylistAbsence.de_json(json_dict, client) + + assert playlist_absence.kind == self.kind + assert playlist_absence.reason == self.reason + + def test_de_json_all(self, client): + json_dict = {'kind': self.kind, 'reason': self.reason} + playlist_absence = PlaylistAbsence.de_json(json_dict, client) + + assert playlist_absence.kind == self.kind + assert playlist_absence.reason == self.reason + + def test_equality(self): + a = PlaylistAbsence(self.kind, self.reason) + b = PlaylistAbsence(10, self.reason) + c = PlaylistAbsence(self.kind, self.reason) + + assert a != b + assert hash(a) != hash(b) + assert a is not b + + assert a == c diff --git a/tests/test_track.py b/tests/test_track.py index 84e53838..a37e7016 100644 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -49,18 +49,14 @@ def test_expected_values(self, track, artist, album, major, normalization): def test_de_json_required(self, client, artist, album): json_dict = {'id': self.id, 'title': self.title, 'available': self.available, - 'available_for_premium_users': self.available_for_premium_users, - 'artists': [artist.to_dict()], 'albums': [album.to_dict()], - 'lyrics_available': self.lyrics_available} + 'artists': [artist.to_dict()], 'albums': [album.to_dict()]} track = Track.de_json(json_dict, client) assert track.id == self.id assert track.title == self.title assert track.available == self.available - assert track.available_for_premium_users == self.available_for_premium_users assert track.artists == [artist] assert track.albums == [album] - assert track.lyrics_available == self.lyrics_available def test_de_json_all(self, client, artist, album, major, normalization): json_dict = {'id': self.id, 'title': self.title, 'available': self.available, @@ -101,11 +97,9 @@ def test_de_json_all(self, client, artist, album, major, normalization): assert track.available_full_without_permission == self.available_full_without_permission def test_equality(self, artist, album): - a = Track(self.id, self.title, self.available, self.available_for_premium_users, [artist], [album], - self.lyrics_available) - b = Track(self.id, '', self.available, self.available_for_premium_users, [artist], [album], False) - c = Track(self.id, self.title, self.available, self.available_for_premium_users, [artist], [album], - self.lyrics_available) + a = Track(self.id, self.title, self.available, [artist], [album]) + b = Track(self.id, '', self.available, [artist], [None]) + c = Track(self.id, self.title, self.available, [artist], [album]) assert a != b assert hash(a) != hash(b) diff --git a/yandex_music/__init__.py b/yandex_music/__init__.py index f216bcb1..8a16d51d 100644 --- a/yandex_music/__init__.py +++ b/yandex_music/__init__.py @@ -19,11 +19,11 @@ from .album.track_position import TrackPosition from .artist.artist import Artist from .artist.artist_tracks import ArtistTracks +from .artist.artist_albums import ArtistAlbums from .artist.brief_info import BriefInfo from .artist.counts import Counts from .artist.description import Description from .artist.link import Link -from .artist.pager import Pager from .artist.ratings import Ratings from .artist.vinyl import Vinyl @@ -32,6 +32,7 @@ from .playlist.user import User from .playlist.play_counter import PlayCounter from .playlist.playlist_id import PlaylistId +from .playlist.playlist_absence import PlaylistAbsence from .playlist.playlist import Playlist from .tracks_list import TracksList @@ -94,6 +95,7 @@ from .supplement.lyrics import Lyrics from .supplement.video_supplement import VideoSupplement +from .pager import Pager from .cover import Cover from .experiments import Experiments from .invocation_info import InvocationInfo @@ -112,4 +114,5 @@ 'PersonalPlaylistsData', 'Promotion', 'Landing', 'Chart', 'ChartItem', 'PlayContext', 'Title', 'Genre', 'Icon', 'Images', 'Id', 'Station', 'Dashboard', 'RotorSettings', 'AdParams', 'Restrictions', 'Value', 'Enum', 'DiscreteScale', 'StationResult', 'Sequence', 'StationTracksResult', 'BriefInfo', 'Description', 'PlaylistId', - 'Vinyl', 'Supplement', 'Lyrics', 'VideoSupplement', 'ArtistTracks', 'Pager'] + 'Vinyl', 'Supplement', 'Lyrics', 'VideoSupplement', 'ArtistTracks', 'Pager', 'ArtistAlbums', + 'PlaylistAbsence'] diff --git a/yandex_music/artist/artist.py b/yandex_music/artist/artist.py index 53ffe1dc..f8cb00f3 100644 --- a/yandex_music/artist/artist.py +++ b/yandex_music/artist/artist.py @@ -5,9 +5,9 @@ class Artist(YandexMusicObject): def __init__(self, id, name, - various, - composer, cover, + various=None, + composer=None, genres=None, op_image=None, no_pictures_from_search=None, @@ -32,10 +32,10 @@ def __init__(self, **kwargs): self.id = id self.name = name - self.various = various - self.composer = composer self.cover = cover + self.various = various + self.composer = composer self.genres = genres self.op_image = op_image self.no_pictures_from_search = no_pictures_from_search @@ -55,12 +55,12 @@ def __init__(self, self.db_aliases = db_aliases self.aliases = aliases - # Оставлено строкой потому что может прийти конкретная дата или просто год + # Может прийти конкретная дата или просто год self.init_date = init_date self.end_date = end_date self.client = client - self._id_attrs = (self.id, self.name, self.various, self.composer, self.cover) + self._id_attrs = (self.id, self.name, self.cover) def download_op_image(self, filename, size='200x200'): """Загрузка обложки. @@ -95,6 +95,13 @@ def get_tracks(self, page=0, page_size=20, *args, **kwargs): """ return self.client.artists_tracks(self.id, page, page_size, *args, **kwargs) + def get_albums(self, page=0, page_size=20, sort_by='year', *args, **kwargs): + """Сокращение для:: + + client.artists_direct_albums(artist.id, page, page_size, sort_by, *args, **kwargs) + """ + return self.client.artists_direct_albums(self.id, page, page_size, sort_by, *args, **kwargs) + @classmethod def de_json(cls, data, client): if not data: @@ -129,3 +136,5 @@ def de_list(cls, data, client): downloadOpImage = download_op_image #: Псевдоним для :attr:`get_tracks` getTracks = get_tracks + #: Псевдоним для :attr:`get_albums` + getAlbums = get_albums diff --git a/yandex_music/artist/artist_albums.py b/yandex_music/artist/artist_albums.py new file mode 100644 index 00000000..be6c9c34 --- /dev/null +++ b/yandex_music/artist/artist_albums.py @@ -0,0 +1,52 @@ +from yandex_music import YandexMusicObject + + +class ArtistAlbums(YandexMusicObject): + """Класс представляющий страницу списка альбомов артиста. + + Attributes: + albums (:obj:`list` из :obj:`yandex_music.Album`): Список альбомов артиста. + pager (:obj:`yandex_music.Pager`): Объект класса :class:`yandex_music.Pager` представляющий пагинатор. + client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex + Music. + + Args: + albums (:obj:`list` из :obj:`yandex_music.Album`): Список альбомов артиста. + pager (:obj:`yandex_music.Pager`): Объект класса :class:`yandex_music.Pager` представляющий пагинатор. + client (:obj:`yandex_music.Client`, optional): Объект класса :class:`yandex_music.Client` представляющий клиент + Yandex Music. + **kwargs: Произвольные ключевые аргументы полученные от API. + """ + + def __init__(self, + albums, + pager, + client=None, + **kwargs): + self.albums = albums + self.pager = pager + + self.client = client + self._id_attrs = (self.pager, self.albums) + + @classmethod + def de_json(cls, data, client): + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex + Music. + + Returns: + :obj:`yandex_music.ArtistAlbums`: Объект класса :class:`yandex_music.ArtistAlbums`. + """ + if not data: + return None + + data = super(ArtistAlbums, cls).de_json(data, client) + from yandex_music import Album, Pager + data['albums'] = Album.de_list(data.get('albums'), client) + data['pager'] = Pager.de_json(data.get('pager'), client) + + return cls(client=client, **data) diff --git a/yandex_music/client.py b/yandex_music/client.py index 550289d6..82b0f51e 100644 --- a/yandex_music/client.py +++ b/yandex_music/client.py @@ -4,10 +4,11 @@ from yandex_music import YandexMusicObject, Status, Settings, PermissionAlerts, Experiments, Artist, Album, Playlist, \ TracksList, Track, AlbumsLikes, ArtistsLikes, PlaylistsLikes, Feed, PromoCodeStatus, DownloadInfo, Search, \ - Suggestions, Landing, Genre, Dashboard, StationResult, StationTracksResult, BriefInfo, Supplement, ArtistTracks + Suggestions, Landing, Genre, Dashboard, StationResult, StationTracksResult, BriefInfo, Supplement, ArtistTracks, \ + ArtistAlbums from yandex_music.utils.request import Request from yandex_music.utils.difference import Difference -from yandex_music.exceptions import InvalidToken +from yandex_music.exceptions import InvalidToken, Captcha CLIENT_ID = '23cabbbdc6cd418abb4b39c32c41195d' CLIENT_SECRET = '53bc75238f0c4d08a118e51fe9203300' @@ -25,7 +26,6 @@ 'playlist': PlaylistsLikes.de_list, } - logging.getLogger(__name__).addHandler(logging.NullHandler()) @@ -77,27 +77,53 @@ def __init__(self, token=None, base_url=None, oauth_url=None, request=None): self.base_url = base_url self.oauth_url = oauth_url - self._request = request or Request(self) + if request: + self._request = request + self._request.set_and_return_client(self) + else: + self._request = Request(self) self.account = self.account_status().account @classmethod - def from_credentials(cls, username, password, *args, **kwargs): + def from_credentials(cls, username, password, x_captcha_answer=None, x_captcha_key=None, captcha_callback=None, + *args, **kwargs): """Инициализция клиента по логину и паролю. - Данный метод получает токен каждый раз при вызове. Рекомендуется сгенерировать его самостоятельно, сохранить и - использовать при следующих инициализациях клиента. Не храните логины и пароли! + Note: + Данный метод получает токен каждый раз при вызове. Рекомендуется сгенерировать его самостоятельно, сохранить + и использовать при следующих инициализациях клиента. Не храните логины и пароли! Args: username (:obj:`str`): Логин клиента (идентификатор). password (:obj:`str`): Пароль клиента (аутентификатор). + x_captcha_answer (:obj:`str`, optional): Ответ на капчу (цифры с картинки). + x_captcha_key (:obj:`str`, optional): Уникальный ключ капчи. + captcha_callback (:obj:`function`, optional): Функция обратного вызова для обработки капчи, должна + принимать объект класса :class:`yandex_music.exceptions.Captcha` и возвращать проверочный код. **kwargs (:obj:`dict`, optional): Аргументы для конструктора клиента. Returns: :obj:`yandex_music.Client`. + + Raises: + :class:`yandex_music.YandexMusicError` """ - return cls(cls().generate_token_by_username_and_password(username, password), *args, **kwargs) + token = None + while not token: + try: + token = cls().generate_token_by_username_and_password(username, password, + x_captcha_answer=x_captcha_answer, + x_captcha_key=x_captcha_key) + except Captcha as e: + if not captcha_callback: + raise e + + x_captcha_answer = captcha_callback(e.captcha) + x_captcha_key = e.captcha.x_captcha_key + + return cls(token, *args, **kwargs) @classmethod def from_token(cls, token, *args, **kwargs): @@ -116,14 +142,16 @@ def from_token(cls, token, *args, **kwargs): return cls(token=token, *args, **kwargs) @log - def generate_token_by_username_and_password(self, username, password, grant_type='password', - timeout=None, *args, **kwargs): + def generate_token_by_username_and_password(self, username, password, grant_type='password', x_captcha_answer=None, + x_captcha_key=None, timeout=None, *args, **kwargs): """Метод получения OAuth токена по логину и паролю. Args: username (:obj:`str`): Логин клиента (идентификатор). password (:obj:`str`): Пароль клиента (аутентификатор). grant_type (:obj:`str`, optional): Тип разрешения OAuth. + x_captcha_answer (:obj:`str`, optional): Ответ на капчу (цифры с картинки). + x_captcha_key (:obj:`str`, optional): Уникальный ключ капчи. timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания ответа от сервера вместо указанного при создании пула. **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). @@ -145,6 +173,9 @@ def generate_token_by_username_and_password(self, username, password, grant_type 'password': password } + if x_captcha_answer and x_captcha_key: + data.update({'x_captcha_answer': x_captcha_answer, 'x_captcha_key': x_captcha_key}) + result = self._request.post(url, data, timeout=timeout, *args, **kwargs) return result.get('access_token') @@ -902,7 +933,10 @@ def artists_tracks(self, artist_id: str or int, page=0, page_size=20, timeout=No Returns: :obj:`yandex_music.ArtistsTracks`: Объекта класса :class:`yandex_music.ArtistsTracks` - представляющий страницу списка треков артиста + представляющий страницу списка треков артиста, иначе :obj:`None`. + + Raises: + :class:`yandex_music.YandexMusicError` """ url = f'{self.base_url}/artists/{artist_id}/tracks' @@ -916,6 +950,42 @@ def artists_tracks(self, artist_id: str or int, page=0, page_size=20, timeout=No return ArtistTracks.de_json(result, self) + @log + def artists_direct_albums(self, artist_id: str or int, page=0, page_size=20, sort_by='year', + timeout=None, *args, **kwargs): + """Получение альбомов артиста. + + Известные значения для sort_by: year, rating. + + Args: + artist_id (:obj:`str` | :obj:`int`): Уникальный идентификатор артиста. + 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: + :obj:`yandex_music.ArtistAlbums`: Объекта класса :class:`yandex_music.ArtistsTracks` + представляющий страницу списка альбомов артиста, иначе :obj:`None`. + + Raises: + :class:`yandex_music.YandexMusicError` + """ + + url = f'{self.base_url}/artists/{artist_id}/direct-albums' + + params = { + 'sort-by': sort_by, + 'page': page, + 'page-size': page_size + } + + result = self._request.get(url, params, timeout=timeout, *args, **kwargs) + + return ArtistAlbums.de_json(result, self) + def _like_action(self, object_type: str, ids: str or int or list, remove: bool = False, user_id: str or int = None, timeout=None, *args, **kwargs): if user_id is None: @@ -1136,6 +1206,8 @@ def users_dislikes_tracks_remove(self, track_ids: str or list, user_id: str or i artistsBriefInfo = artists_brief_info #: Псевдоним для :attr:`artists_tracks` artistsTracks = artists_tracks + #: Псевдоним для :attr:`artists_direct_albums` + artistsDirectAlbums = artists_direct_albums #: Псевдоним для :attr:`users_likes_tracks_add` usersLikesTracksAdd = users_likes_tracks_add #: Псевдоним для :attr:`users_likes_tracks_remove` diff --git a/yandex_music/exceptions.py b/yandex_music/exceptions.py index 39f82c9a..6eb0949f 100644 --- a/yandex_music/exceptions.py +++ b/yandex_music/exceptions.py @@ -20,6 +20,38 @@ class Unauthorized(YandexMusicError): pass +class Captcha(YandexMusicError): + """Базовый класс, представляющий исключение связанное с капчей. + + Attributes: + captcha (:obj:`yandex_music.utils.captcha_response.CaptchaResponse`): Объект класса + :class:`yandex_music.utils.captcha_response.CaptchaResponse` представляющий капчу. + + Args: + msg (:obj:`str`): Сообщение с ошибкой. + captcha (:obj:`yandex_music.utils.captcha_response.CaptchaResponse`): Объект класса + :class:`yandex_music.utils.captcha_response.CaptchaResponse` представляющий капчу. + """ + + def __init__(self, msg, captcha, *args, **kwargs): + self.captcha = captcha + super().__init__(msg, *args, **kwargs) + + +class CaptchaRequired(Captcha): + """Класс исключения, вызываемый в случае необходимости ввода проверочного кода. + """ + + pass + + +class CaptchaWrong(Captcha): + """Класс исключения, вызываемый в случае неправильного ввода капчи. + """ + + pass + + class NetworkError(YandexMusicError): """Базовый класс исключений, вызываемых для ошибок, связанных с запросами к серверу. @@ -37,4 +69,4 @@ class TimedOut(NetworkError): """ def __init__(self): - super(TimedOut, self).__init__('Timed out') + super().__init__('Timed out') diff --git a/yandex_music/artist/pager.py b/yandex_music/pager.py similarity index 100% rename from yandex_music/artist/pager.py rename to yandex_music/pager.py diff --git a/yandex_music/playlist/playlist.py b/yandex_music/playlist/playlist.py index ddcd82ed..ad2bf471 100644 --- a/yandex_music/playlist/playlist.py +++ b/yandex_music/playlist/playlist.py @@ -4,13 +4,14 @@ class Playlist(YandexMusicObject): def __init__(self, owner, - uid, - kind, - title, - track_count, cover, made_for, play_counter, + playlist_absence, + uid=None, + kind=None, + title=None, + track_count=None, tags=None, revision=None, snapshot=None, @@ -36,14 +37,15 @@ def __init__(self, client=None, **kwargs): self.owner = owner - self.uid = uid - self.kind = kind - self.title = title - self.track_count = track_count self.cover = cover self.made_for = made_for self.play_counter = play_counter + self.playlist_absence = playlist_absence + self.uid = uid + self.kind = kind + self.title = title + self.track_count = track_count self.revision = revision self.snapshot = snapshot self.visibility = visibility @@ -68,8 +70,7 @@ def __init__(self, self.tags = tags self.client = client - self._id_attrs = (self.uid, self.kind, self.title, self.track_count, self.cover, - self.made_for, self.play_counter) + self._id_attrs = (self.uid, self.kind, self.title, self.playlist_absence) @property def is_mine(self): @@ -127,13 +128,18 @@ def de_json(cls, data, client): return None data = super(Playlist, cls).de_json(data, client) - from yandex_music import User, MadeFor, Cover, PlayCounter, TrackShort + from yandex_music import User, MadeFor, Cover, PlayCounter, TrackShort, PlaylistAbsence data['owner'] = User.de_json(data.get('owner'), client) data['cover'] = Cover.de_json(data.get('cover'), client) data['made_for'] = MadeFor.de_json(data.get('made_for'), client) data['tracks'] = TrackShort.de_list(data.get('tracks'), client) data['play_counter'] = PlayCounter.de_json(data.get('play_counter'), client) + data['playlist_absence'] = PlaylistAbsence.de_json(data.get('playlist_absence'), client) # на случай фикса + if data.get('playlist_absense'): # очепятка яндуха + data['playlist_absence'] = PlaylistAbsence.de_json(data.get('playlist_absense'), client) + data.pop('playlist_absense') + return cls(client=client, **data) @classmethod diff --git a/yandex_music/playlist/playlist_absence.py b/yandex_music/playlist/playlist_absence.py new file mode 100644 index 00000000..ef6d8554 --- /dev/null +++ b/yandex_music/playlist/playlist_absence.py @@ -0,0 +1,49 @@ +from yandex_music import YandexMusicObject + + +class PlaylistAbsence(YandexMusicObject): + """Класс представляющий причину отсутствия плейлиста. + + Attributes: + kind (:obj:`int`): Уникальный идентификатор плейлиста. + reason (:obj:`str`): Причина отсутствия. + client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex + Music. + + Args: + kind (:obj:`int`): Уникальный идентификатор плейлиста. + reason (:obj:`str`): Причина отсутствия. + client (:obj:`yandex_music.Client`, optional): Объект класса :class:`yandex_music.Client` представляющий клиент + Yandex Music. + **kwargs: Произвольные ключевые аргументы полученные от API. + """ + + def __init__(self, + kind, + reason, + client=None, + **kwargs): + self.kind = kind + self.reason = reason + + self.client = client + self._id_attrs = (self.kind, self.reason) + + @classmethod + def de_json(cls, data, client): + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex + Music. + + Returns: + :obj:`yandex_music.PlaylistAbsence`: Объект класса :class:`yandex_music.PlaylistAbsence`. + """ + if not data: + return None + + data = super(PlaylistAbsence, cls).de_json(data, client) + + return cls(client=client, **data) diff --git a/yandex_music/search/search.py b/yandex_music/search/search.py index ad9dce1e..09e1c892 100644 --- a/yandex_music/search/search.py +++ b/yandex_music/search/search.py @@ -2,6 +2,38 @@ class Search(YandexMusicObject): + """Класс представляющий результаты поиска. + + Attributes: + search_request_id (:obj:`str`): ID запроса. + text (:obj:`str`): Текст запроса. + best (:obj:`yandex_music.Best`): Объект класса :class:`yandex_music.Best` представляющий лучший результат. + albums (:obj:`yandex_music.SearchResult`): Объект класса :class:`yandex_music.SearchResult` представляющий найденные альбомы. + artists (:obj:`yandex_music.SearchResult`): Объект класса :class:`yandex_music.SearchResult` представляющий найденных исполнителей. + playlists (:obj:`yandex_music.SearchResult`): Объект класса :class:`yandex_music.SearchResult` представляющий найденные плейлисты. + tracks (:obj:`yandex_music.SearchResult`): Объект класса :class:`yandex_music.SearchResult` представляющий найденные треки. + videos (:obj:`yandex_music.SearchResult`): Объект класса :class:`yandex_music.SearchResult` представляющий найденные видео. + misspell_corrected (:obj:`bool`): Был ли исправлен запрос. + nocorrect (:obj:`bool`): Было ли отключено исправление результата. + client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex + Music. + + Args: + search_request_id (:obj:`str`): ID запроса. + text (:obj:`str`): Текст запроса. + best (:obj:`yandex_music.Best`): Объект класса :class:`yandex_music.Best` представляющий лучший результат. + albums (:obj:`yandex_music.SearchResult`): Объект класса :class:`yandex_music.SearchResult` представляющий найденные альбомы. + artists (:obj:`yandex_music.SearchResult`): Объект класса :class:`yandex_music.SearchResult` представляющий найденных исполнителей. + playlists (:obj:`yandex_music.SearchResult`): Объект класса :class:`yandex_music.SearchResult` представляющий найденные плейлисты. + tracks (:obj:`yandex_music.SearchResult`): Объект класса :class:`yandex_music.SearchResult` представляющий найденные треки. + videos (:obj:`yandex_music.SearchResult`): Объект класса :class:`yandex_music.SearchResult` представляющий найденные видео. + misspell_corrected (:obj:`bool`, optional): Был ли исправлен запрос. + nocorrect (:obj:`bool`, optional): Было ли отключено исправление результата. + client (:obj:`yandex_music.Client`, optional): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex + Music. + **kwargs: Произвольные ключевые аргументы полученные от API. + """ + def __init__(self, search_request_id, text, @@ -33,6 +65,16 @@ def __init__(self, @classmethod def de_json(cls, data, client): + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex + Music. + + Returns: + :obj:`yandex_music.Search`: Объект класса :class:`yandex_music.Search`. + """ if not data: return None diff --git a/yandex_music/track/track.py b/yandex_music/track/track.py index da936d30..ffaea58a 100644 --- a/yandex_music/track/track.py +++ b/yandex_music/track/track.py @@ -6,10 +6,10 @@ def __init__(self, id, title, available, - available_for_premium_users, artists, albums, - lyrics_available, + available_for_premium_users=None, + lyrics_available=None, real_id=None, og_image=None, type=None, @@ -31,11 +31,11 @@ def __init__(self, self.id = id self.title = title self.available = available - self.available_for_premium_users = available_for_premium_users self.artists = artists self.albums = albums - self.lyrics_available = lyrics_available + self.available_for_premium_users = available_for_premium_users + self.lyrics_available = lyrics_available self.real_id = real_id self.og_image = og_image self.type = type @@ -56,8 +56,7 @@ def __init__(self, self.download_info = None self.client = client - self._id_attrs = (self.id, self.title, self.available, self.available_for_premium_users, - self.artists, self.albums, self.lyrics_available) + self._id_attrs = (self.id, self.title, self.available, self.artists, self.albums) def get_download_info(self, get_direct_links=False): self.download_info = self.client.tracks_download_info(self.track_id, get_direct_links) diff --git a/yandex_music/utils/captcha_response.py b/yandex_music/utils/captcha_response.py new file mode 100644 index 00000000..b41feb51 --- /dev/null +++ b/yandex_music/utils/captcha_response.py @@ -0,0 +1,71 @@ +from yandex_music import YandexMusicObject + + +class CaptchaResponse(YandexMusicObject): + """Класс представляющий ответ сервера с запросом на ввод капчи. + + Attributes: + x_captcha_url (:obj:`str`): Ссылка на изображение с капчей. + x_captcha_key (:obj:`str`): Уникальный ключ капчи. + error_description (:obj:`str`): Описание ошибки. + error (:obj:`str`): Код ошибки. + client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex + Music. + + Args: + x_captcha_url (:obj:`str`): Ссылка на изображение с капчей. + x_captcha_key (:obj:`str`): Уникальный ключ капчи. + error_description (:obj:`str`): Описание ошибки. + error (:obj:`str`): Код ошибки. + client (:obj:`yandex_music.Client`, optional): Объект класса :class:`yandex_music.Client` представляющий клиент + Yandex Music. + **kwargs: Произвольные ключевые аргументы полученные от API. + """ + + def __init__(self, + x_captcha_url, + x_captcha_key, + error_description, + error, + client=None, + **kwargs): + self.x_captcha_url = x_captcha_url + self.x_captcha_key = x_captcha_key + self.error_description = error_description + self.error = error + + self.client = client + self._id_attrs = (self.x_captcha_key, self.x_captcha_url) + + def download(self, filename=None): + """Загрузка изображения с капчей. + + Args: + filename (:obj:`str`, optional): Путь и(или) название файла вместе с расширением. По умолчанию ключ + капчи и расширение `.gif`. + """ + + if not filename: + filename = f'{self.x_captcha_key}.gif' + + self.client.request.download(self.x_captcha_url, filename) + + @classmethod + def de_json(cls, data, client): + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex + Music. + + Returns: + :obj:`yandex_music.utils.captcha_response.CaptchaResponse`: Объект класса + :class:`yandex_music.utils.captcha_response.CaptchaResponse`. + """ + if not data: + return None + + data = super(CaptchaResponse, cls).de_json(data, client) + + return cls(client=client, **data) diff --git a/yandex_music/utils/request.py b/yandex_music/utils/request.py index 3f20d8f3..6c2dd4f8 100644 --- a/yandex_music/utils/request.py +++ b/yandex_music/utils/request.py @@ -3,9 +3,10 @@ import logging import requests +from yandex_music.utils.captcha_response import CaptchaResponse from yandex_music.utils.response import Response -from yandex_music.exceptions import Unauthorized, BadRequest, NetworkError, YandexMusicError - +from yandex_music.exceptions import Unauthorized, BadRequest, NetworkError, YandexMusicError, CaptchaRequired, \ + CaptchaWrong USER_AGENT = 'Yandex-Music-API' HEADERS = { @@ -24,26 +25,30 @@ class Request: client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex Music. headers (:obj:`dict`, optional): Заголовки передаваемые с каждым запросом. - proxies (:obj:`dict`, optional): Прокси. + proxy_url (:obj:`str`, optional): Прокси. """ def __init__(self, - client, + client=None, headers=None, - proxies=None): - - self.client = client - + proxy_url=None): self.headers = headers or HEADERS.copy() - if self.client.token: - self.set_authorization(self.client.token) + self.client = self.set_and_return_client(client) - self.proxies = proxies # TODO + self.proxies = {'http': proxy_url, 'https': proxy_url} if proxy_url else None def set_authorization(self, token): self.headers.update({'Authorization': f'OAuth {token}'}) + def set_and_return_client(self, client): + self.client = client + + if self.client and self.client.token: + self.set_authorization(self.client.token) + + return self.client + @staticmethod def _convert_camel_to_snake(text): s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text) @@ -74,6 +79,9 @@ def _parse(self, json_data) -> Response: except (AttributeError, ValueError): raise YandexMusicError('Invalid server response') + if data.get('result') is None: + data = {'result': data, 'error': data.get('error'), 'error_description': data.get('error_description')} + return Response.de_json(data, self.client) def _request_wrapper(self, *args, **kwargs): @@ -92,9 +100,13 @@ def _request_wrapper(self, *args, **kwargs): if 200 <= resp.status_code <= 299: return resp - message = self._parse(resp.content).error or 'Unknown HTTPError' + parse = self._parse(resp.content) + message = parse.error or 'Unknown HTTPError' - if resp.status_code in (401, 403): + if 'CAPTCHA' in message: + exception = CaptchaWrong if 'Wrong' in message else CaptchaRequired + raise exception(message, CaptchaResponse.de_json(parse.result, self.client)) + elif resp.status_code in (401, 403): raise Unauthorized(message) elif resp.status_code == 400: raise BadRequest(message) @@ -107,19 +119,19 @@ def _request_wrapper(self, *args, **kwargs): raise NetworkError(f'{message} ({resp.status_code})') def get(self, url, params=None, timeout=5, *args, **kwargs): - result = self._request_wrapper('GET', url, params=params, headers=self.headers, timeout=timeout, - *args, **kwargs) + result = self._request_wrapper('GET', url, params=params, headers=self.headers, proxies=self.proxies, + timeout=timeout, *args, **kwargs) return self._parse(result.content).result def post(self, url, data=None, timeout=5, *args, **kwargs): - result = self._request_wrapper('POST', url, headers=self.headers, data=data, timeout=timeout, - *args, **kwargs) + result = self._request_wrapper('POST', url, headers=self.headers, proxies=self.proxies, data=data, + timeout=timeout, *args, **kwargs) return self._parse(result.content).result def retrieve(self, url, timeout=5, *args, **kwargs): - return self._request_wrapper('GET', url, timeout=timeout, *args, **kwargs) + return self._request_wrapper('GET', url, proxies=self.proxies, timeout=timeout, *args, **kwargs) def download(self, url, filename, timeout=5, *args, **kwargs): result = self.retrieve(url, timeout=timeout, *args, *kwargs) diff --git a/yandex_music/utils/response.py b/yandex_music/utils/response.py index 6978393d..650de28b 100644 --- a/yandex_music/utils/response.py +++ b/yandex_music/utils/response.py @@ -24,7 +24,7 @@ def error(self): @property def result(self): - return self._result or self.data + return self.data if self._result is None else self._result @classmethod def de_json(cls, data, client):