gmusicapi-12.1.1/0000755000175000017500000000000013512455420014052 5ustar simonsimon00000000000000gmusicapi-12.1.1/HISTORY.rst0000644000175000017500000003430713512455274015763 0ustar simonsimon00000000000000.. :changelog: History ------- As of 1.0.0, `semantic versioning `__ is used. 12.1.1 ++++++ released 2019-07-13 - open default log file as utf-8 to avoid windows encoding problems 12.1.0 ++++++ released 2019-04-20 - deprecate Mobileclient.get_promoted_songs in favor of Mobileclient.get_top_songs to clarify its behavior. Functionality remains the same. 12.0.0 ++++++ released 2019-01-08 - breaking: remove gmusicapi.clients.OAUTH_FILEPATH. Use Musicmanager.OAUTH_FILEPATH instead. 11.1.1 ++++++ released 2018-12-04 - add back gmusicapi.clients.OAUTH_FILEPATH after it was removed in 11.1.0 (a breaking change) - deprecate gmusicapi.clients.OAUTH_FILEPATH in favor of Musicmanager.OAUTH_FILEPATH 11.1.0 ++++++ released 2018-11-30 - add Mobileclient OAuth support (``perform_oauth`` and ``oauth_login``) and deprecate email/password auth, which Google now often rejects. It works the same way as the Musicmanager; see `the docs `__ for an example. - update Mobileclient.search schema 11.0.4 ++++++ released 2018-11-19 - fix a number of bugs with Mobileclient.search 11.0.3 ++++++ released 2018-09-19 - fix an "__init__() takes at most 4..." warning coming from oauth2client 11.0.2 ++++++ released 2018-09-09 - fix validation of "ios:..." format device ids - add inLibrary field to station docs 11.0.1 ++++++ released 2018-03-18 - update schemas 11.0.0 ++++++ released 2017-12-09 - breaking: list calls now default to max_results=None, increasing the default number of results from 100 to 999 - add updated_after param to song/playlist listing to support differential updates - add support for free radio stations - add filepath+extension to unsupported file exception message - fix "I'm Feeling Lucky" station never refreshing its seed - fix crashes caused by some 503s during uploading - fix gmtools for https://github.com/simon-weber/Google-Music-Playlist-Importer - fix AAC and ALAC content type upload detection - blacklist requests 2.8.2 - improve id documentation - update schemas 10.1.2 ++++++ released 2017-04-03 - validate device ids to prevent 403s during streaming - fix LocalUnboundError during login for some environments - update schemas 10.1.1 ++++++ released 2017-02-10 - deprecate include_deleted param to greatly speed up responses for Mobileclient.get_all_* - Mobileclient.search now works on non-subscription accounts - fix logging IOError on read-only filesystems - fix problems caused by broken requests IDNA support 10.1.0 ++++++ released 2016-10-31 - deprecate the Webclient - add podcast support to Mobileclient: - get_all_podcast_series - get_all_podcast_episodes - add_podcast_series - delete_podcast_series - edit_podcast_series - get_podcast_episode_stream_url - get_podcast_episode_info - get_podcast_series_info - get_browse_podcast_hierarchy - get_browse_podcast_series - add Mobileclient.add_store_tracks - add Mobileclient.rate_songs - add Musicmanager.get_quota - fix get_all_user_playlist_contents hanging for large playlists - fix is_authenticated status after uploader_id exceptions - fix upload progress tracker remaining after upload - various internal improvements and schema updates 10.0.1 ++++++ released 2016-06-04 - switch to pycryptodomex - minor schema adjustments 10.0.0 ++++++ released 2016-05-01 - breaking: Mobileclient.search_all_access is now Mobileclient.search - breaking: Mobileclient.add_aa_track is now Mobileclient.add_store_track - add situation_hits and video_hits to Mobclient.search - add methods Mobileclient.deauthorize_device, .get_listen_now_items, and .get_listen_now_situations - add property Mobileclient.is_subscribed - add playlists and curated stations as station seeds - add params locale and subscription to Mobileclient.login - add param enable_transcoding to Musicmanager.upload - update to newer Google apis, returning more data in responses - reduce memory usage during uploading - fix a variety of bugs, mostly python2/3 type errors 9.0.0 +++++ released 2016-03-05 - breaking: attempting to reupload a file after changing only its tags will result in a rejection as a duplicate upload (it used to upload successfully) - fix webclient login after Google changes - fix ``'str' object has no attribute 'refresh'`` - prevent upstream protobufs TypeError by locking version - a 'matched' value may be returned even if matching is not enabled if we were unable to disallow matching 8.0.0 +++++ released 2016-02-08 - breaking: drop support for python < 2.7.9 - add (experimental) python 3 support! - add Musicmanager.get_purchased_songs - add station_hits to search_all_access results - add disc_number and total_disc_count to Musicmanager.get_uploaded_songs - add a prompt for device id in tests - upgrade gpsoauth, removing dependency on pycrypto - deprecate Webclient.create_playlist and Webclient.get_registered_devices - fix various packaging problems - fix KeyError in Mobileclient.get_station_tracks - fix a TypeError from requests - fix various bits of the docs 7.0.0 +++++ released 2015-09-19 - breaking: python 2.6 is no longer supported - breaking: webclient.get_registered_devices has a slightly different schema - fix Webclient authentication and get_stream_urls - fix MusicManager uploading: Google shut down the rupio endpoint - fix certificate validation - fix album artist metadata not being upload 6.0.0 +++++ released 2015-06-20 - fix creation of multiple android devices from android_id=None; support creating device ids from mac address. - android_id is now optional for mobileclient.get_stream_url, defaulting to android_id from login() 5.0.0 +++++ released 2015-06-02 - breaking: Webclient.login temporarily broken after clientlogin deprecation - breaking: Mobileclient.get_thumbs_up_songs renamed to mobileclient.get_promoted_songs - breaking: Mobileclient.change_playlist_name is now edit_playlist - fix Mobileclient.login breakage due to clientlogin deprecation - fix Mobileclient.get_genres: return a list and handle invalid parent genres - add support for filtering out recently played station tracks to Mobileclient.get_station_tracks - add public playlist results to Mobileclient.search_all_access - add Mobileclient.get_registered_devices - add quality option to Mobileclient.get_stream_url - add support for public playlist creation to Mobileclient.create_playlist - make optional description param for Webclient.create_playlist - better handle locating mp3 transcoder 4.0.0 +++++ released 2014-06-08 - breaking: remove webclient.change_song_metadata; use mobileclient.change_song_metadata instead - breaking: remove webclient.get_all_songs; use mobileclient.get_all_songs instead - breaking: remove webclient.get_playlist_songs; use mobileclient.get_all_user_playlist_contents instead - breaking: remove webclient.get_all_playlist_ids; use mobileclient.get_all_user_playlists instead - breaking: webclient.upload_album_art now returns a url to the uploaded image - breaking: due to backend changes, mobileclient.change_song_metadata can only change ratings - add mobileclient.get_thumbs_up_songs - add mobileclient.increment_song_playcount - add webclient.create_playlist, which is capable of creating public playlists - add webclient.get_shared_playlist_info 3.1.0 +++++ released 2014-01-20 - add verify_ssl option to client init - greatly loosen dependency version requirements 3.0.1 +++++ released 2013-12-11 - remove extraneous logging introduced in 3.0.0 -- this could have logged auth details, so it's recommended to delete old logs 3.0.0 +++++ released 2013-11-03 - Musicmanager.get_all_songs is now Musicmanager.get_uploaded_songs - Mobileclient.get_all_playlist_contents is now Mobileclient.get_all_user_playlist_contents, and will no longer return results for subscribed playlists - add Mobileclient.get_shared_playlist_contents - add Mobileclient.reorder_playlist_entry - add Mobileclient.change_song_metadata - add Mobileclient.get_album_info - add Mobileclient.get_track_info - add Mobileclient.get_genres - compatibility fixes 2.0.0 +++++ released 2013-08-01 - remove broken Webclient.{create_playlist, change_playlist, copy_playlist, search, change_playlist_name} - add Mobileclient; this will slowly replace most of the Webclient, so prefer it when possible - add support for streaming All Access songs - add Webclient.get_registered_devices - add a toggle to turn off validation per client - raise an exception when a song dictionary is passed instead of an id 1.2.0 +++++ released 2013-05-16 - add support for listing/downloading songs with the Musicmanager. When possible, this should be preferred to the Webclient's method, since it does not have a download quota. - fix a bug where the string representing a machine's mac was not properly formed for use as an uploader_id. This will cause another machine to be registered for some users; the old device can be identified from its lack of a version number. - verify user-provided uploader_ids 1.1.0 +++++ released 2013-04-19 - get_all_songs can optionally return a generator - compatibility updates for AddPlaylist call - log to appdirs.user_log_dir by default - add open_browser param to perform_oauth 1.0.0 +++++ released 2013-04-02 - breaking: Api has been split into Webclient and Musicmanager - breaking: semantic versioning (previous versions removed from PyPi) - Music Manager OAuth support - faster uploading when matching is disabled - faster login 2013.03.04 ++++++++++ - add artistMatchedId to metadata - tests are no longer a mess 2013.02.27 ++++++++++ - add support for uploading album art (`docs `__) - add support for .m4b files - add CancelUploadJobs call (not exposed in api yet) - Python 2.6 compatibility - reduced peak memory usage when uploading - logging improvements - improved error messages when uploading 2013.02.15 ++++++++++ - user now controls logging (`docs `__) - documentation overhaul 2013.02.14 ++++++++++ - fix international logins 2013.02.12 ++++++++++ - fix packaging issues 2013.02.11 ++++++++++ - improve handling of strange metadata when uploading - add a dependency on `dateutil `__ 2013.02.09 ++++++++++ - breaking: upload returns a 3-tuple (`docs `__) - breaking: get_all_playlist_ids always returns lists of ids; remove always_id_lists option (`docs `__) - breaking: remove suppress_failure option in Api.__init__ - breaking: copy_playlist ``orig_id`` argument renamed to ``playlist_id`` (`docs `__) - new: report_incorrect_match (only useful for Music Manager uploads) (`docs `__) - uploading fixed - avconv replaces ffmpeg - scan and match is supported - huge code improvements 2013.01.05 ++++++++++ - compatibility update for playlist mutation - various metadata compatibility updates 2012.11.09 ++++++++++ - bugfix: support for uploading uppercase filenames (Tom Graham) - bugfix: fix typo in multidownload validation, and add test 2012.08.31 ++++++++++ - metadata compatibility updates (storeId, lastPlayed) - fix uploading of unicode filenames without tags 2012.05.04 ++++++++++ - update allowed rating values to 1-5 (David Dooling) - update metajamId to matchedId (David Dooling) - fix broken expectation about disc/track numbering metadata 2012.04.03 ++++++++++ - change to the 3-clause BSD license - add Kevin Kwok to AUTHORS 2012.04.01 ++++++++++ - improve code in example.py - support uploading of all Google-supported formats: m4a, ogg, flac, wma, mp3. Non-mp3 are transcoded to 320kbs abr mp3 using ffmpeg - introduce dependency on ffmpeg. for non-mp3 uploading, it needs to be in path and have the needed transcoders available - get_playlists is now get_all_playlist_ids, and is faster - add an exception CallFailure. Api functions raise it if the server says their request failed - add suppress_failure (default False) option to Api.__init__() - change_playlist now returns the changed playlistId (pid) - change_song_metadata now returns a list of changed songIds (sids) - create_playlist now returns the new pid - delete_playlist now returns the deleted pid - delete_songs now returns a list of deleted sids - change_playlist now returns the pid of the playlist - which may differ from the one passed in - add_songs_to_playlist now returns a list of (sid, new playlistEntryId aka eid) tuples of added songs - remove_songs_from_playlist now returns a list of removed (sid, eid) pairs - search dictionary is now flattened, without the "results" key. see documentation for example 2012.03.27 ++++++++++ - package for pip/pypi - add AUTHORS file - remove session.py; the sessions are now just api.PlaySession (Darryl Pogue) - protocol.Metadata_Expectations.get_expectation will return UnknownExpectation when queried for unknown keys; this should prevent future problems - add immutable 'subjectToCuration' and 'metajamId' fields - use unknown 2012.03.16 ++++++++++ - add change_playlist for playlist modifications - get_playlists supports multiple playlists of the same name by returning lists of playlist ids. By default, it will return a single string (the id) for unique playlist names; see the always_id_lists parameter. - api.login now attempts to bump Music Manager authentication first, bypassing browser emulation. This allows for much faster authentication. - urls updated for the change to Google Play Music - remove_songs_from_playlist now takes (playlist_id, song_ids), for consistency with other playlist mutations 2012.03.04 ++++++++++ - change name to gmusicapi to avoid ambiguity - change delete_song and remove_song_from_playlist to delete_songs and remove_songs_from_playlist, for consistency with other functions - add verification of WC json responses - setup a sane branch model. see http://nvie.com/posts/a-successful-git-branching-model/ - improve logging gmusicapi-12.1.1/LICENSE0000664000175000017500000000273713374625453015105 0ustar simonsimon00000000000000Copyright (c) 2018, Simon Weber All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of the contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. gmusicapi-12.1.1/MANIFEST.in0000600000175000017500000000042212667715127015613 0ustar simonsimon00000000000000include HISTORY.rst LICENSE example.py README.rst recursive-include gmusicapi *.py include docs/Makefile recursive-include docs/source *.py *.rst include gmusicapi/test/audiotest* include gmusicapi/test/imagetest* include gmusicapi/test/*.jsarray global-exclude _local_*.py gmusicapi-12.1.1/PKG-INFO0000644000175000017500000006041113512455420015151 0ustar simonsimon00000000000000Metadata-Version: 1.1 Name: gmusicapi Version: 12.1.1 Summary: An unofficial api for Google Play Music. Home-page: http://pypi.python.org/pypi/gmusicapi/ Author: Simon Weber Author-email: simon@simonmweber.com License: Copyright (c) 2018, Simon Weber All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of the contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Description: gmusicapi: an unofficial API for Google Play Music ================================================== gmusicapi allows control of `Google Music `__ with Python. .. code-block:: python from gmusicapi import Mobileclient api = Mobileclient() # after running api.perform_oauth() once: api.oauth_login('') # => True library = api.get_all_songs() sweet_track_ids = [track['id'] for track in library if track['artist'] == 'The Cat Empire'] playlist_id = api.create_playlist('Rad muzak') api.add_songs_to_playlist(playlist_id, sweet_track_ids) **gmusicapi is not supported nor endorsed by Google.** That said, it's actively maintained, and powers a bunch of cool projects: - alternate clients, including `one designed for the visually impaired `__, `a web-based jukebox which ships with its own server `__, `command line `__ `clients `__, `a FUSE filesystem `__, and `an Alexa skill `__ - library management tools for `syncing tracks `__, `syncing playlists `__, and `migrating to a different account `__ - proxies for media players, such as `gmusicproxy `__ and `gmusicprocurator `__, as well as plugins for `Mopidy `__, `Squeezebox `__ and `Tizonia `__. - enhancements like `autoplaylists / smart playlists `__ Getting started --------------- Start with `the usage docs `__, which will guide you through installation and the available apis. Once you're up and running, you can explore the rest of the docs at http://unofficial-google-music-api.readthedocs.io. If the documentation doesn't answer your questions, or you just want to get in touch, either `drop by #gmusicapi on Freenode `__ or shoot me an email. Status and updates ------------------ |build_status| |repominder_status| .. |build_status| image:: https://travis-ci.org/simon-weber/gmusicapi.png?branch=develop :target: https://travis-ci.org/simon-weber/gmusicapi .. |repominder_status| image:: https://img.shields.io/badge/dynamic/json.svg?label=release&query=%24.status&maxAge=43200&uri=https%3A%2F%2Fwww.repominder.com%2Fbadge%2FeyJyZXBvX2lkIjogMTEsICJ1c2VyX2lkIjogMn0%3D%2F&link=https%3A%2F%2Fwww.repominder.com%2F :target: https://www.repominder.com * November 2018: proper OAuth support for the mobileclient. * February 2016: Python 3 support! * September 2015: Google switched to a new music uploading endpoint, breaking uploading for outdated versions of gmusicapi. * June 2015: Full mobileclient and webclient functionality was restored. * May 2015: Limited mobileclient functionality was restored. * April 2015: Google deprecated clientlogin, breaking both the webclient and mobileclient. * November 2013: I started working fulltime at Venmo, meaning this project is back to night and weekend development. For fine-grained development updates, follow me on Twitter: `@simonmweber `__. .. :changelog: History ------- As of 1.0.0, `semantic versioning `__ is used. 12.1.1 ++++++ released 2019-07-13 - open default log file as utf-8 to avoid windows encoding problems 12.1.0 ++++++ released 2019-04-20 - deprecate Mobileclient.get_promoted_songs in favor of Mobileclient.get_top_songs to clarify its behavior. Functionality remains the same. 12.0.0 ++++++ released 2019-01-08 - breaking: remove gmusicapi.clients.OAUTH_FILEPATH. Use Musicmanager.OAUTH_FILEPATH instead. 11.1.1 ++++++ released 2018-12-04 - add back gmusicapi.clients.OAUTH_FILEPATH after it was removed in 11.1.0 (a breaking change) - deprecate gmusicapi.clients.OAUTH_FILEPATH in favor of Musicmanager.OAUTH_FILEPATH 11.1.0 ++++++ released 2018-11-30 - add Mobileclient OAuth support (``perform_oauth`` and ``oauth_login``) and deprecate email/password auth, which Google now often rejects. It works the same way as the Musicmanager; see `the docs `__ for an example. - update Mobileclient.search schema 11.0.4 ++++++ released 2018-11-19 - fix a number of bugs with Mobileclient.search 11.0.3 ++++++ released 2018-09-19 - fix an "__init__() takes at most 4..." warning coming from oauth2client 11.0.2 ++++++ released 2018-09-09 - fix validation of "ios:..." format device ids - add inLibrary field to station docs 11.0.1 ++++++ released 2018-03-18 - update schemas 11.0.0 ++++++ released 2017-12-09 - breaking: list calls now default to max_results=None, increasing the default number of results from 100 to 999 - add updated_after param to song/playlist listing to support differential updates - add support for free radio stations - add filepath+extension to unsupported file exception message - fix "I'm Feeling Lucky" station never refreshing its seed - fix crashes caused by some 503s during uploading - fix gmtools for https://github.com/simon-weber/Google-Music-Playlist-Importer - fix AAC and ALAC content type upload detection - blacklist requests 2.8.2 - improve id documentation - update schemas 10.1.2 ++++++ released 2017-04-03 - validate device ids to prevent 403s during streaming - fix LocalUnboundError during login for some environments - update schemas 10.1.1 ++++++ released 2017-02-10 - deprecate include_deleted param to greatly speed up responses for Mobileclient.get_all_* - Mobileclient.search now works on non-subscription accounts - fix logging IOError on read-only filesystems - fix problems caused by broken requests IDNA support 10.1.0 ++++++ released 2016-10-31 - deprecate the Webclient - add podcast support to Mobileclient: - get_all_podcast_series - get_all_podcast_episodes - add_podcast_series - delete_podcast_series - edit_podcast_series - get_podcast_episode_stream_url - get_podcast_episode_info - get_podcast_series_info - get_browse_podcast_hierarchy - get_browse_podcast_series - add Mobileclient.add_store_tracks - add Mobileclient.rate_songs - add Musicmanager.get_quota - fix get_all_user_playlist_contents hanging for large playlists - fix is_authenticated status after uploader_id exceptions - fix upload progress tracker remaining after upload - various internal improvements and schema updates 10.0.1 ++++++ released 2016-06-04 - switch to pycryptodomex - minor schema adjustments 10.0.0 ++++++ released 2016-05-01 - breaking: Mobileclient.search_all_access is now Mobileclient.search - breaking: Mobileclient.add_aa_track is now Mobileclient.add_store_track - add situation_hits and video_hits to Mobclient.search - add methods Mobileclient.deauthorize_device, .get_listen_now_items, and .get_listen_now_situations - add property Mobileclient.is_subscribed - add playlists and curated stations as station seeds - add params locale and subscription to Mobileclient.login - add param enable_transcoding to Musicmanager.upload - update to newer Google apis, returning more data in responses - reduce memory usage during uploading - fix a variety of bugs, mostly python2/3 type errors 9.0.0 +++++ released 2016-03-05 - breaking: attempting to reupload a file after changing only its tags will result in a rejection as a duplicate upload (it used to upload successfully) - fix webclient login after Google changes - fix ``'str' object has no attribute 'refresh'`` - prevent upstream protobufs TypeError by locking version - a 'matched' value may be returned even if matching is not enabled if we were unable to disallow matching 8.0.0 +++++ released 2016-02-08 - breaking: drop support for python < 2.7.9 - add (experimental) python 3 support! - add Musicmanager.get_purchased_songs - add station_hits to search_all_access results - add disc_number and total_disc_count to Musicmanager.get_uploaded_songs - add a prompt for device id in tests - upgrade gpsoauth, removing dependency on pycrypto - deprecate Webclient.create_playlist and Webclient.get_registered_devices - fix various packaging problems - fix KeyError in Mobileclient.get_station_tracks - fix a TypeError from requests - fix various bits of the docs 7.0.0 +++++ released 2015-09-19 - breaking: python 2.6 is no longer supported - breaking: webclient.get_registered_devices has a slightly different schema - fix Webclient authentication and get_stream_urls - fix MusicManager uploading: Google shut down the rupio endpoint - fix certificate validation - fix album artist metadata not being upload 6.0.0 +++++ released 2015-06-20 - fix creation of multiple android devices from android_id=None; support creating device ids from mac address. - android_id is now optional for mobileclient.get_stream_url, defaulting to android_id from login() 5.0.0 +++++ released 2015-06-02 - breaking: Webclient.login temporarily broken after clientlogin deprecation - breaking: Mobileclient.get_thumbs_up_songs renamed to mobileclient.get_promoted_songs - breaking: Mobileclient.change_playlist_name is now edit_playlist - fix Mobileclient.login breakage due to clientlogin deprecation - fix Mobileclient.get_genres: return a list and handle invalid parent genres - add support for filtering out recently played station tracks to Mobileclient.get_station_tracks - add public playlist results to Mobileclient.search_all_access - add Mobileclient.get_registered_devices - add quality option to Mobileclient.get_stream_url - add support for public playlist creation to Mobileclient.create_playlist - make optional description param for Webclient.create_playlist - better handle locating mp3 transcoder 4.0.0 +++++ released 2014-06-08 - breaking: remove webclient.change_song_metadata; use mobileclient.change_song_metadata instead - breaking: remove webclient.get_all_songs; use mobileclient.get_all_songs instead - breaking: remove webclient.get_playlist_songs; use mobileclient.get_all_user_playlist_contents instead - breaking: remove webclient.get_all_playlist_ids; use mobileclient.get_all_user_playlists instead - breaking: webclient.upload_album_art now returns a url to the uploaded image - breaking: due to backend changes, mobileclient.change_song_metadata can only change ratings - add mobileclient.get_thumbs_up_songs - add mobileclient.increment_song_playcount - add webclient.create_playlist, which is capable of creating public playlists - add webclient.get_shared_playlist_info 3.1.0 +++++ released 2014-01-20 - add verify_ssl option to client init - greatly loosen dependency version requirements 3.0.1 +++++ released 2013-12-11 - remove extraneous logging introduced in 3.0.0 -- this could have logged auth details, so it's recommended to delete old logs 3.0.0 +++++ released 2013-11-03 - Musicmanager.get_all_songs is now Musicmanager.get_uploaded_songs - Mobileclient.get_all_playlist_contents is now Mobileclient.get_all_user_playlist_contents, and will no longer return results for subscribed playlists - add Mobileclient.get_shared_playlist_contents - add Mobileclient.reorder_playlist_entry - add Mobileclient.change_song_metadata - add Mobileclient.get_album_info - add Mobileclient.get_track_info - add Mobileclient.get_genres - compatibility fixes 2.0.0 +++++ released 2013-08-01 - remove broken Webclient.{create_playlist, change_playlist, copy_playlist, search, change_playlist_name} - add Mobileclient; this will slowly replace most of the Webclient, so prefer it when possible - add support for streaming All Access songs - add Webclient.get_registered_devices - add a toggle to turn off validation per client - raise an exception when a song dictionary is passed instead of an id 1.2.0 +++++ released 2013-05-16 - add support for listing/downloading songs with the Musicmanager. When possible, this should be preferred to the Webclient's method, since it does not have a download quota. - fix a bug where the string representing a machine's mac was not properly formed for use as an uploader_id. This will cause another machine to be registered for some users; the old device can be identified from its lack of a version number. - verify user-provided uploader_ids 1.1.0 +++++ released 2013-04-19 - get_all_songs can optionally return a generator - compatibility updates for AddPlaylist call - log to appdirs.user_log_dir by default - add open_browser param to perform_oauth 1.0.0 +++++ released 2013-04-02 - breaking: Api has been split into Webclient and Musicmanager - breaking: semantic versioning (previous versions removed from PyPi) - Music Manager OAuth support - faster uploading when matching is disabled - faster login 2013.03.04 ++++++++++ - add artistMatchedId to metadata - tests are no longer a mess 2013.02.27 ++++++++++ - add support for uploading album art (`docs `__) - add support for .m4b files - add CancelUploadJobs call (not exposed in api yet) - Python 2.6 compatibility - reduced peak memory usage when uploading - logging improvements - improved error messages when uploading 2013.02.15 ++++++++++ - user now controls logging (`docs `__) - documentation overhaul 2013.02.14 ++++++++++ - fix international logins 2013.02.12 ++++++++++ - fix packaging issues 2013.02.11 ++++++++++ - improve handling of strange metadata when uploading - add a dependency on `dateutil `__ 2013.02.09 ++++++++++ - breaking: upload returns a 3-tuple (`docs `__) - breaking: get_all_playlist_ids always returns lists of ids; remove always_id_lists option (`docs `__) - breaking: remove suppress_failure option in Api.__init__ - breaking: copy_playlist ``orig_id`` argument renamed to ``playlist_id`` (`docs `__) - new: report_incorrect_match (only useful for Music Manager uploads) (`docs `__) - uploading fixed - avconv replaces ffmpeg - scan and match is supported - huge code improvements 2013.01.05 ++++++++++ - compatibility update for playlist mutation - various metadata compatibility updates 2012.11.09 ++++++++++ - bugfix: support for uploading uppercase filenames (Tom Graham) - bugfix: fix typo in multidownload validation, and add test 2012.08.31 ++++++++++ - metadata compatibility updates (storeId, lastPlayed) - fix uploading of unicode filenames without tags 2012.05.04 ++++++++++ - update allowed rating values to 1-5 (David Dooling) - update metajamId to matchedId (David Dooling) - fix broken expectation about disc/track numbering metadata 2012.04.03 ++++++++++ - change to the 3-clause BSD license - add Kevin Kwok to AUTHORS 2012.04.01 ++++++++++ - improve code in example.py - support uploading of all Google-supported formats: m4a, ogg, flac, wma, mp3. Non-mp3 are transcoded to 320kbs abr mp3 using ffmpeg - introduce dependency on ffmpeg. for non-mp3 uploading, it needs to be in path and have the needed transcoders available - get_playlists is now get_all_playlist_ids, and is faster - add an exception CallFailure. Api functions raise it if the server says their request failed - add suppress_failure (default False) option to Api.__init__() - change_playlist now returns the changed playlistId (pid) - change_song_metadata now returns a list of changed songIds (sids) - create_playlist now returns the new pid - delete_playlist now returns the deleted pid - delete_songs now returns a list of deleted sids - change_playlist now returns the pid of the playlist - which may differ from the one passed in - add_songs_to_playlist now returns a list of (sid, new playlistEntryId aka eid) tuples of added songs - remove_songs_from_playlist now returns a list of removed (sid, eid) pairs - search dictionary is now flattened, without the "results" key. see documentation for example 2012.03.27 ++++++++++ - package for pip/pypi - add AUTHORS file - remove session.py; the sessions are now just api.PlaySession (Darryl Pogue) - protocol.Metadata_Expectations.get_expectation will return UnknownExpectation when queried for unknown keys; this should prevent future problems - add immutable 'subjectToCuration' and 'metajamId' fields - use unknown 2012.03.16 ++++++++++ - add change_playlist for playlist modifications - get_playlists supports multiple playlists of the same name by returning lists of playlist ids. By default, it will return a single string (the id) for unique playlist names; see the always_id_lists parameter. - api.login now attempts to bump Music Manager authentication first, bypassing browser emulation. This allows for much faster authentication. - urls updated for the change to Google Play Music - remove_songs_from_playlist now takes (playlist_id, song_ids), for consistency with other playlist mutations 2012.03.04 ++++++++++ - change name to gmusicapi to avoid ambiguity - change delete_song and remove_song_from_playlist to delete_songs and remove_songs_from_playlist, for consistency with other functions - add verification of WC json responses - setup a sane branch model. see http://nvie.com/posts/a-successful-git-branching-model/ - improve logging Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Multimedia :: Sound/Audio Classifier: Topic :: Software Development :: Libraries :: Python Modules gmusicapi-12.1.1/README.rst0000664000175000017500000000732113415264447015557 0ustar simonsimon00000000000000gmusicapi: an unofficial API for Google Play Music ================================================== gmusicapi allows control of `Google Music `__ with Python. .. code-block:: python from gmusicapi import Mobileclient api = Mobileclient() # after running api.perform_oauth() once: api.oauth_login('') # => True library = api.get_all_songs() sweet_track_ids = [track['id'] for track in library if track['artist'] == 'The Cat Empire'] playlist_id = api.create_playlist('Rad muzak') api.add_songs_to_playlist(playlist_id, sweet_track_ids) **gmusicapi is not supported nor endorsed by Google.** That said, it's actively maintained, and powers a bunch of cool projects: - alternate clients, including `one designed for the visually impaired `__, `a web-based jukebox which ships with its own server `__, `command line `__ `clients `__, `a FUSE filesystem `__, and `an Alexa skill `__ - library management tools for `syncing tracks `__, `syncing playlists `__, and `migrating to a different account `__ - proxies for media players, such as `gmusicproxy `__ and `gmusicprocurator `__, as well as plugins for `Mopidy `__, `Squeezebox `__ and `Tizonia `__. - enhancements like `autoplaylists / smart playlists `__ Getting started --------------- Start with `the usage docs `__, which will guide you through installation and the available apis. Once you're up and running, you can explore the rest of the docs at http://unofficial-google-music-api.readthedocs.io. If the documentation doesn't answer your questions, or you just want to get in touch, either `drop by #gmusicapi on Freenode `__ or shoot me an email. Status and updates ------------------ |build_status| |repominder_status| .. |build_status| image:: https://travis-ci.org/simon-weber/gmusicapi.png?branch=develop :target: https://travis-ci.org/simon-weber/gmusicapi .. |repominder_status| image:: https://img.shields.io/badge/dynamic/json.svg?label=release&query=%24.status&maxAge=43200&uri=https%3A%2F%2Fwww.repominder.com%2Fbadge%2FeyJyZXBvX2lkIjogMTEsICJ1c2VyX2lkIjogMn0%3D%2F&link=https%3A%2F%2Fwww.repominder.com%2F :target: https://www.repominder.com * November 2018: proper OAuth support for the mobileclient. * February 2016: Python 3 support! * September 2015: Google switched to a new music uploading endpoint, breaking uploading for outdated versions of gmusicapi. * June 2015: Full mobileclient and webclient functionality was restored. * May 2015: Limited mobileclient functionality was restored. * April 2015: Google deprecated clientlogin, breaking both the webclient and mobileclient. * November 2013: I started working fulltime at Venmo, meaning this project is back to night and weekend development. For fine-grained development updates, follow me on Twitter: `@simonmweber `__. gmusicapi-12.1.1/docs/0000755000175000017500000000000013512455420015002 5ustar simonsimon00000000000000gmusicapi-12.1.1/docs/Makefile0000600000175000017500000001301512667715127016447 0ustar simonsimon00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/UnofficialGoogleMusicApi.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/UnofficialGoogleMusicApi.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/UnofficialGoogleMusicApi" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/UnofficialGoogleMusicApi" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." gmusicapi-12.1.1/docs/source/0000755000175000017500000000000013512455420016302 5ustar simonsimon00000000000000gmusicapi-12.1.1/docs/source/conf.py0000664000175000017500000001761413374625453017627 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- # # Unofficial Google Music Api documentation build configuration file, created by # sphinx-quickstart on Sat Feb 25 00:36:45 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) on_rtd = os.environ.get('READTHEDOCS', None) == 'True' from gmusicapi import __version__ # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # 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"] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'gmusicapi' copyright = u'2016 Simon Weber' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = __version__ # The full version, including alpha/beta/rc tags. release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. if not on_rtd: # Try to use the ReadTheDocs theme if installed. # Default to the default alabaster theme if not. try: import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] except ImportError: html_theme = 'alabaster' else: # Set theme to 'default' for ReadTheDocs. html_theme = 'default' # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'UnofficialGoogleMusicApidoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'gmusicapi.tex', u'gmusicapi Documentation', u'Simon Weber', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'gmusicapi', u'gmusicapi Documentation', [u'Simon Weber'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'gmusicapi', u'gmusicapi Documentation', u'Simon Weber', 'gmusicapi', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' gmusicapi-12.1.1/docs/source/contributing.rst0000600000175000017500000000136512667715127021555 0ustar simonsimon00000000000000.. _contributing: Contributing to gmusicapi ========================= The easiest way to start contributing is to help `triage issues `__. Development ----------- Please make pull requests to the ``develop`` branch. ``master`` is currently used to hold the code of the most recent release. Building the docs locally is straightforward. First, make sure Sphinx is installed: ``$ pip install sphinx``. Next, simply do ``$ cd gmusicapi/docs`` followed by ``$ make html``. If there weren't any problems, the docs are now in ``build/html``. You can serve them up locally with ``$ python -m SimpleHTTPServer``, then view them in your web browser at ``http://127.0.0.1:8000/build/html/``. gmusicapi-12.1.1/docs/source/index.rst0000664000175000017500000000444613400371522020151 0ustar simonsimon00000000000000gmusicapi: an unofficial API for Google Play Music ================================================== This library allows control of `Google Music `__ with Python. .. code-block:: python from gmusicapi import Mobileclient api = Mobileclient() # after running api.perform_oauth() once: api.oauth_login('') # => True library = api.get_all_songs() sweet_track_ids = [track['id'] for track in library if track['artist'] == 'The Cat Empire'] playlist_id = api.create_playlist('Rad muzak') api.add_songs_to_playlist(playlist_id, sweet_track_ids) **This project is not supported nor endorsed by Google.** Use common sense (protocol compliance, reasonable load, etc) and don't ruin the fun for everyone else. Features -------- All major functionality is supported: - Library management: list, create, delete, and modify songs and playlists - Streaming and single-song downloading - Music Manager uploading/scan-and-match and library downloading - Most All Access features See `the changelog `__ for changes by version. Using gmusicapi --------------- .. toctree:: :hidden: usage ports contributing Getting started +++++++++++++++ The :ref:`usage section ` has installation instructions and some simple examples. Api and data reference ++++++++++++++++++++++ The reference has details for all classes and functions, as well as information on the Google Music data you'll encounter: .. toctree:: :maxdepth: 2 reference/api reference/protocol Making gmusicapi better +++++++++++++++++++++++ Contributions are always welcome! The :ref:`contributing section ` has more details. `The code `__ might also be useful. Ports and other languages +++++++++++++++++++++++++ The :ref:`ports section ` lists known ports and information for making ports. Getting help ++++++++++++ Start by searching for `existing issues `__ on GitHub. If you don't find any describing what you're seeing, go ahead and open one. There's also ``#gmusicapi`` on Freenode, though it's pretty quiet these days. gmusicapi-12.1.1/docs/source/ports.rst0000664000175000017500000000347713374625453020233 0ustar simonsimon00000000000000.. _ports: Support for Other Languages =========================== Here are the ports I'm currently aware of: - C++: `dvirtz `__ and `Greg Wicks `__ - C#: `ffleischer `__ and `Taylor Finnell `__ - Go: `Lari Rasku `__ - Java: `Jens Villadsen `__ and `Nick Martin `__ - Javascript: `Lari Rasku `__. There's also my `Google Music Turntable uploader `__; it's not a port, but may be useful as an example. - Kotlin: `timtimmahh `__ - Node: `Jamon Terrell `__ - Objective-C: `Gregory Wicks `__ - PHP: `raydanhk `__ - Ruby: `Loic Nageleisen `__ They're in various states of completion and maintenance. Alternatively, consider using `GMusicProxy `__ or copying its approach. Building a Port --------------- Get in touch if you're working on a port. I'm happy to answer questions and point you to relevant bits of code. Generally, though, the `protocol package `__ is what you'll want to look at. It contains all of the call schemas in a psuedo-dsl that's explained `here `__. gmusicapi-12.1.1/docs/source/reference/0000755000175000017500000000000013512455420020240 5ustar simonsimon00000000000000gmusicapi-12.1.1/docs/source/reference/api.rst0000664000175000017500000000166713374625453021572 0ustar simonsimon00000000000000.. _api: .. currentmodule:: gmusicapi.clients Client Interfaces ================= gmusicapi currently has three main interfaces: one for the music.google.com webclient, one for the Android App, and one for the Music Manager. The big differences are: * :py:class:`Webclient` development has mostly ceased, with the :py:class:`Mobileclient` superceding it. It is not tested nor well supported. * :py:class:`Musicmanager` is used for uploading and downloading, while :py:class:`Mobileclient` supports everything but uploading. * :py:class:`Webclient`/:py:class:`Mobileclient` require a plaintext email and password to login, while :py:class:`Musicmanager` uses `OAuth2 `__. * :py:class:`Mobileclient` supports streaming but requires that the Google Play Music app has been installed and run before use. .. toctree:: :maxdepth: 2 webclient mobileclient musicmanager gmusicapi-12.1.1/docs/source/reference/mobileclient.rst0000664000175000017500000001037513456665572023472 0ustar simonsimon00000000000000.. _mobileclient: .. currentmodule:: gmusicapi.clients Mobileclient Interface ====================== .. autoclass:: Mobileclient Setup and login --------------- .. automethod:: Mobileclient.__init__ .. automethod:: Mobileclient.perform_oauth .. automethod:: Mobileclient.oauth_login .. automethod:: Mobileclient.login .. automethod:: Mobileclient.logout .. automethod:: Mobileclient.is_authenticated .. attribute:: Mobileclient.locale The locale of the Mobileclient session used to localize some responses. Should be an `ICU `__ locale supported by Android. Set during login but can be changed at any time. Account Management ------------------ .. attribute:: Mobileclient.is_subscribed Returns the subscription status of the Google Music account. Result is cached with a TTL of 10 minutes. To get live status before the TTL is up, delete the ``is_subscribed`` property of the Mobileclient instance. >>> mc = Mobileclient() >>> mc.is_subscribed # Live status. >>> mc.is_subscribed # Cached status. >>> del mc.is_subscribed # Delete is_subscribed property. >>> mc.is_subscribed # Live status. .. automethod:: Mobileclient.get_registered_devices .. automethod:: Mobileclient.deauthorize_device Songs ----- Songs are uniquely referred to within a library with a track id in uuid format. Store tracks also have track ids, but they are in a different format than library track ids. ``song_id.startswith('T')`` is always ``True`` for store track ids and ``False`` for library track ids. Adding a store track to a library will yield a normal song id. Store track ids can be used in most places that normal song ids can (e.g. playlist addition or streaming). Note that sometimes they are stored under the ``'nid'`` key, not the ``'id'`` key. .. automethod:: Mobileclient.get_all_songs .. automethod:: Mobileclient.get_stream_url .. automethod:: Mobileclient.rate_songs .. automethod:: Mobileclient.change_song_metadata .. automethod:: Mobileclient.delete_songs .. automethod:: Mobileclient.get_top_songs .. automethod:: Mobileclient.increment_song_playcount .. automethod:: Mobileclient.add_store_track .. automethod:: Mobileclient.add_store_tracks .. automethod:: Mobileclient.get_station_track_stream_url Playlists --------- Like songs, playlists have unique ids within a library. However, their names do not need to be unique. The tracks making up a playlist are referred to as 'playlist entries', and have unique entry ids within the entire library (not just their containing playlist). .. automethod:: Mobileclient.get_all_playlists .. automethod:: Mobileclient.get_all_user_playlist_contents .. automethod:: Mobileclient.get_shared_playlist_contents .. automethod:: Mobileclient.create_playlist .. automethod:: Mobileclient.delete_playlist .. automethod:: Mobileclient.edit_playlist .. automethod:: Mobileclient.add_songs_to_playlist .. automethod:: Mobileclient.remove_entries_from_playlist .. automethod:: Mobileclient.reorder_playlist_entry Radio Stations -------------- Radio Stations are available for free in the US only. A subscription is required in other countries. .. automethod:: Mobileclient.get_all_stations .. automethod:: Mobileclient.create_station .. automethod:: Mobileclient.delete_stations .. automethod:: Mobileclient.get_station_tracks Podcasts -------- .. automethod:: Mobileclient.get_all_podcast_series .. automethod:: Mobileclient.get_all_podcast_episodes .. automethod:: Mobileclient.add_podcast_series .. automethod:: Mobileclient.delete_podcast_series .. automethod:: Mobileclient.edit_podcast_series .. automethod:: Mobileclient.get_podcast_episode_stream_url Search ------ Search Google Play for information about artists, albums, tracks, and more. .. automethod:: Mobileclient.search .. automethod:: Mobileclient.get_genres .. automethod:: Mobileclient.get_album_info .. automethod:: Mobileclient.get_artist_info .. automethod:: Mobileclient.get_podcast_episode_info .. automethod:: Mobileclient.get_podcast_series_info .. automethod:: Mobileclient.get_track_info .. automethod:: Mobileclient.get_station_info Misc ---- .. automethod:: Mobileclient.get_browse_podcast_hierarchy .. automethod:: Mobileclient.get_browse_podcast_series .. automethod:: Mobileclient.get_listen_now_items .. automethod:: Mobileclient.get_listen_now_situations gmusicapi-12.1.1/docs/source/reference/musicmanager.rst0000664000175000017500000000114313374625453023461 0ustar simonsimon00000000000000.. _musicmanager: .. currentmodule:: gmusicapi.clients Musicmanager Interface ====================== .. autoclass:: Musicmanager Setup and login --------------- .. automethod:: Musicmanager.perform_oauth .. automethod:: Musicmanager.__init__ .. automethod:: Musicmanager.login .. automethod:: Musicmanager.logout Uploading Songs --------------- .. automethod:: Musicmanager.upload Downloading Songs ----------------- .. automethod:: Musicmanager.get_uploaded_songs .. automethod:: Musicmanager.get_purchased_songs .. automethod:: Musicmanager.download_song Misc ---- ..automethod:: Musicmanager.get_quota gmusicapi-12.1.1/docs/source/reference/protocol.rst0000664000175000017500000000263413400371522022636 0ustar simonsimon00000000000000.. _protocol: .. currentmodule:: gmusicapi.protocol Protocol Details ================ The following definitions represent known endpoints relating to Google Music. They are organized by the client that uses them. The names of these classes are semantic, and may not match their actual endpoint. Most of the time, endusers will want to use one of the :ref:`api`. However, any of the definitions listed here can be called by using the ``_make_call`` member of a Client and providing the parameters needed by ``dynamic_*`` functions. It's tough to generate the exact schema of every call in a readable fashion, so this information is left out. If you need exact specifications, look at `the code `__ - or submit a pull request to generate the docs =) Android Client -------------- .. automodule:: gmusicapi.protocol.mobileclient :members: :undoc-members: :exclude-members: McCall, build_request, filter_response, validate, filter_text, item_schema, genre_schema, shared_plentry Music Manager ------------- .. automodule:: gmusicapi.protocol.musicmanager :members: :undoc-members: :exclude-members: MmCall, build_request, filter_response, validate Web Client ---------- .. automodule:: gmusicapi.protocol.webclient :members: :undoc-members: :exclude-members: WcCall, build_request, filter_response, validate, expected_response gmusicapi-12.1.1/docs/source/reference/webclient.rst0000664000175000017500000000212613374625453022764 0ustar simonsimon00000000000000.. _webclient: .. currentmodule:: gmusicapi.clients .. |br| raw:: html
Webclient Interface =================== **WARNING** |br| **Webclient functionality is not tested nor well supported.** |br| **Use** :class:`Mobileclient` **or** :class:`Musicmanager` **if possible.** .. autoclass:: Webclient Setup and login --------------- .. automethod:: Webclient.__init__ .. automethod:: Webclient.login .. automethod:: Webclient.logout Song downloading and streaming ------------------------------ .. automethod:: Webclient.get_song_download_info .. automethod:: Webclient.get_stream_audio .. automethod:: Webclient.get_stream_urls .. automethod:: Webclient.report_incorrect_match Song manipulation ----------------- .. automethod:: Webclient.upload_album_art .. automethod:: Webclient.delete_songs Playlist manipulation --------------------- .. automethod:: Webclient.create_playlist .. automethod:: Webclient.add_songs_to_playlist .. automethod:: Webclient.remove_songs_from_playlist .. automethod:: Webclient.get_shared_playlist_info Other ----- .. automethod:: Webclient.get_registered_devices gmusicapi-12.1.1/docs/source/usage.rst0000664000175000017500000000623213400371522020141 0ustar simonsimon00000000000000.. _usage: .. currentmodule:: gmusicapi.clients Usage ===== Installation ------------ Use `pip `__: ``$ pip install gmusicapi``. To install the yet-to-be-released development version, use ``$ pip install git+https://github.com/simon-weber/gmusicapi.git@develop#egg=gmusicapi``. If you're going to be uploading music, you'll likely need `avconv `__ or `ffmpeg `__ installed and in your system path, along with at least libmp3lame: - Linux - Use your distro's package manager: e.g ``$ sudo apt-get install libav-tools libavcodec-extra-53`` (ffmpeg requires extra steps on `Debian `__/`Ubuntu `__). - Download pre-built binaries of `avconv `__ or `ffmpeg `__ and `edit your path `__ to include the directory that contains avconv/ffmpeg. - Mac - Use `Homebrew `__ to install `libav (avconv) `__ or `ffmpeg `__. - Windows - Download pre-built binaries of `avconv `__ or `ffmpeg `__ and `edit your path `__ to include the directory that contains avconv.exe/ffmpeg.exe. - Google App Engine - See `this thread `__ for instructions. The only time avconv or ffmpeg is not required is when uploading mp3s without scan-and-match enabled. If you need to install avconv/ffmpeg from source, be sure to use ``$ ./configure --enable-gpl --enable-nonfree --enable-libmp3lame``. Quickstart ---------- There are two supported client classes based on different Google apis. The :py:class:`Mobileclient` uses the Android app's apis to handle library management and playback. The :py:class:`Musicmanager` uses the desktop Music Manager's apis to handle uploading and downloading. Both have similar command-line `OAuth2 `__ interfaces for logging in. For example: .. code-block:: python from gmusicapi import Musicmanager mm = Musicmanager() mm.perform_oauth() This only needs to be run once, and if successful will save a refresh token to disk. Then, future runs can start with: .. code-block:: python from gmusicapi import Musicmanager mm = Musicmanager() mm.login() # currently named oauth_login for the Mobileclient If you need both library management and uploading, just create multiple client instances. There is also the :py:class:`Webclient`, which is uses the webapp's apis to handle similar tasks to the Mobileclient. It is not tested nor well supported, and requires providing full account credentials to use. Avoid it if possible. The reference section has complete information on all clients: .. toctree:: :maxdepth: 2 reference/api gmusicapi-12.1.1/example.py0000664000175000017500000000530013374625453016072 0ustar simonsimon00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import, unicode_literals from builtins import * # noqa from getpass import getpass from gmusicapi import Mobileclient def ask_for_credentials(): """Make an instance of the api and attempts to login with it. Return the authenticated api. """ # We're not going to upload anything, so the Mobileclient is what we want. api = Mobileclient() logged_in = False attempts = 0 while not logged_in and attempts < 3: email = input('Email: ') password = getpass() logged_in = api.login(email, password, Mobileclient.FROM_MAC_ADDRESS) attempts += 1 return api def demonstrate(): """Demonstrate some api features.""" api = ask_for_credentials() if not api.is_authenticated(): print("Sorry, those credentials weren't accepted.") return print('Successfully logged in.') print() # Get all of the users songs. # library is a big list of dictionaries, each of which contains a single song. print('Loading library...', end=' ') library = api.get_all_songs() print('done.') print(len(library), 'tracks detected.') print() # Show some info about a song. There is no guaranteed order; # this is essentially a random song. first_song = library[0] print("The first song I see is '{}' by '{}'.".format( first_song['title'], first_song['artist'])) # We're going to create a new playlist and add a song to it. # Songs are uniquely identified by 'song ids', so let's get the id: song_id = first_song['id'] print("I'm going to make a new playlist and add that song to it.") print("I'll delete it when we're finished.") print() playlist_name = input('Enter a name for the playlist: ') # Like songs, playlists have unique ids. # Google Music allows more than one playlist of the same name; # these ids are necessary. playlist_id = api.create_playlist(playlist_name) print('Made the playlist.') print() # Now let's add the song to the playlist, using their ids: api.add_songs_to_playlist(playlist_id, song_id) print('Added the song to the playlist.') print() # We're all done! The user can now go and see that the playlist is there. # The web client syncs our changes in real time. input('You can now check on Google Music that the playlist exists.\n' 'When done, press enter to delete the playlist:') api.delete_playlist(playlist_id) print('Deleted the playlist.') # It's good practice to logout when finished. api.logout() print('All done!') if __name__ == '__main__': demonstrate() gmusicapi-12.1.1/gmusicapi/0000755000175000017500000000000013512455420016033 5ustar simonsimon00000000000000gmusicapi-12.1.1/gmusicapi/__init__.py0000664000175000017500000000061013375050363020147 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- from gmusicapi._version import __version__ from gmusicapi.clients import Webclient, Musicmanager, Mobileclient from gmusicapi.exceptions import CallFailure __copyright__ = 'Copyright 2018 Simon Weber' __license__ = 'BSD 3-Clause' __title__ = 'gmusicapi' # appease flake8: the imports are purposeful (__version__, Webclient, Musicmanager, Mobileclient, CallFailure) gmusicapi-12.1.1/gmusicapi/_version.py0000644000175000017500000000006113512455274020235 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- __version__ = u"12.1.1" gmusicapi-12.1.1/gmusicapi/appdirs.py0000664000175000017500000000146013374625453020065 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- """ Mock version of appdirs for use in cases without the real version """ from __future__ import print_function, division, absolute_import, unicode_literals from builtins import * # noqa try: from appdirs import AppDirs my_appdirs = AppDirs('gmusicapi', 'Simon Weber') except ImportError: print('warning: could not import appdirs; will use current directory') class FakeAppDirs(object): to_spoof = set(base + '_dir' for base in ('user_data', 'site_data', 'user_config', 'site_config', 'user_cache', 'user_log')) def __getattr__(self, name): if name in self.to_spoof: return '.' # current dir else: raise AttributeError my_appdirs = FakeAppDirs() gmusicapi-12.1.1/gmusicapi/clients/0000755000175000017500000000000013512455420017474 5ustar simonsimon00000000000000gmusicapi-12.1.1/gmusicapi/clients/__init__.py0000664000175000017500000000047713415264447021630 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import, unicode_literals from gmusicapi.clients.webclient import Webclient from gmusicapi.clients.musicmanager import Musicmanager from gmusicapi.clients.mobileclient import Mobileclient (Webclient, Musicmanager, Mobileclient) # noqa gmusicapi-12.1.1/gmusicapi/clients/mobileclient.py0000664000175000017500000027515213456665572022554 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import, unicode_literals from past.builtins import basestring from builtins import * # noqa from collections import defaultdict import datetime from functools import partial from operator import itemgetter import os import re from uuid import getnode as getmac from gmusicapi import session from gmusicapi.appdirs import my_appdirs from gmusicapi.clients.shared import _OAuthClient from gmusicapi.exceptions import CallFailure, NotSubscribed, InvalidDeviceId from gmusicapi.protocol import mobileclient from gmusicapi.protocol.shared import authtypes from gmusicapi.utils import utils class Mobileclient(_OAuthClient): """Allows library management and streaming by posing as the googleapis.com mobile clients. Uploading is not supported by this client (use the :class:`Musicmanager` to upload). """ _session_class = session.Mobileclient _authtype = None FROM_MAC_ADDRESS = object() OAUTH_FILEPATH = os.path.join(my_appdirs.user_data_dir, 'mobileclient.cred') def __init__(self, debug_logging=True, validate=True, verify_ssl=True): super(Mobileclient, self).__init__(self.__class__.__name__, debug_logging, validate, verify_ssl) def _make_call(self, protocol, *args, **kwargs): """Switch the required_auth at runtime.""" if self._authtype is not None: kwargs['required_auth'] = authtypes(**{self._authtype: True}) return super(Mobileclient, self)._make_call(protocol, *args, **kwargs) def _ensure_device_id(self, device_id=None): if device_id is None: device_id = self.android_id if len(device_id) == 16 and re.match('^[a-z0-9]*$', device_id): # android device ids are now sent in base 10 device_id = str(int(device_id, 16)) return device_id def _validate_device_id(self, device_id, is_mac=False): """Ensure that a given device_id belongs to the user supplying it.""" if is_mac: # Always allow logins with MAC address. return device_id device_ids = [] for d in self.get_registered_devices(): if d['id'].startswith('ios:'): device_ids.append(d['id']) elif d['id'].startswith('0x'): # old android format device_ids.append(d['id'][2:]) else: # mac address format device_ids.append(d['id'].replace(':', '')) if device_id in device_ids: return device_id else: self.logout() raise InvalidDeviceId('Invalid device_id %s.' % device_id, device_ids) @property def locale(self): """The locale of the Mobileclient session used to localize some responses. Should be an `ICU `__ locale supported by Android. Set on authentication with :func:`login` but can be changed at any time. """ return self.session._locale @locale.setter def locale(self, locale): self.session._locale = locale @utils.cached_property(ttl=600) def is_subscribed(self): """Returns the subscription status of the Google Music account. Result is cached with a TTL of 10 minutes. To get live status before the TTL is up, delete the ``is_subscribed`` property of the Mobileclient instance. >>> mc = Mobileclient() >>> mc.is_subscribed # Live status. >>> mc.is_subscribed # Cached status. >>> del mc.is_subscribed # Delete is_subscribed property. >>> mc.is_subscribed # Live status. """ res = self._make_call(mobileclient.Config) for item in res['data']['entries']: if item['key'] == 'isNautilusUser' and item['value'] == 'true': self.session._is_subscribed = True break else: self.session._is_subscribed = False return self.session._is_subscribed def _login(self, session_login, android_id, locale): if android_id is None: raise ValueError("android_id cannot be None.") is_mac = android_id is self.FROM_MAC_ADDRESS if is_mac: mac_int = getmac() if (mac_int >> 40) % 2: raise OSError("a valid MAC could not be determined." " Provide an android_id (and be" " sure to provide the same one on future runs).") device_id = utils.create_mac_string(mac_int) device_id = device_id.replace(':', '') else: device_id = android_id if not session_login(): self.logger.info("failed to authenticate") return False self.android_id = self._validate_device_id(device_id, is_mac=is_mac) self.logger.info("authenticated") self.locale = locale if self.is_subscribed: self.logger.info("subscribed") return True def oauth_login(self, device_id, oauth_credentials=OAUTH_FILEPATH, locale='en_US'): """Authenticates the mobileclient with pre-existing OAuth credentials. Returns ``True`` on success, ``False`` on failure. :param oauth_credentials: ``oauth2client.client.OAuth2Credentials`` or the path to a ``oauth2client.file.Storage`` file. By default, the same default path used by :func:`perform_oauth` is used. Endusers will likely call :func:`perform_oauth` once to write credentials to disk and then ignore this parameter. This param is mostly intended to allow flexibility for developers of a 3rd party service who intend to perform their own OAuth flow (eg on their website). :param device_id: A string of 16 hex digits for Android or "ios:" for iOS. Alternatively, pass ``Mobileclient.FROM_MAC_ADDRESS`` to attempt to use this machine's MAC address as an id. If a valid MAC address cannot be determined on this machine (which is often the case when running on a VPS), raise OSError. This will likely be deprecated, since Google now rejects ids of this form. :param locale: `ICU `__ locale used to localize certain responses. This must be a locale supported by Android. Defaults to ``'en_US'``. """ self._authtype = 'oauth' session_login = partial(self._oauth_login, oauth_credentials) return self._login(session_login, device_id, locale) @utils.deprecated('prefer Mobileclient.oauth_login') def login(self, email, password, android_id, locale='en_US'): """Authenticates the Mobileclient using full account credentials. Returns ``True`` on success, ``False`` on failure. Behind the scenes, this performs a Google-specific Android login flow with the provided credentials, then trades those for a Google Music auth token. It is deprecated in favor of the more robust :func:`oauth_login`, which performs a normal OAuth flow instead of taking account credentials. :param email: eg ``'test@gmail.com'`` or just ``'test'``. :param password: password or app-specific password for 2-factor users. This is not stored locally, and is sent securely over SSL. :param android_id: 16 hex digits, eg ``'1234567890abcdef'``. Pass Mobileclient.FROM_MAC_ADDRESS instead to attempt to use this machine's MAC address as an android id. **Use this at your own risk**: the id will be a non-standard 12 characters, but appears to work fine in testing. If a valid MAC address cannot be determined on this machine (which is often the case when running on a VPS), raise OSError. :param locale: `ICU `__ locale used to localize certain responses. This must be a locale supported by Android. Defaults to ``'en_US'``. """ self._authtype = 'gpsoauth' session_login = partial(self.session.gpsoauth_login, email, password, android_id) return self._login(session_login, android_id, locale) # TODO expose max/page-results, etc for list operations def get_all_songs(self, incremental=False, include_deleted=None, updated_after=None): """Returns a list of dictionaries that each represent a song. :param incremental: if True, return a generator that yields lists of at most 1000 tracks as they are retrieved from the server. This can be useful for presenting a loading bar to a user. :param include_deleted: ignored. Will be removed in a future release. :param updated_after: a datetime.datetime; defaults to unix epoch. If provided, deleted songs may be returned. Here is an example song dictionary:: { 'comment':'', 'rating':'0', 'albumArtRef':[ { 'url': 'http://lh6.ggpht.com/...' } ], 'artistId':[ 'Aod62yyj3u3xsjtooghh2glwsdi' ], 'composer':'', 'year':2011, 'creationTimestamp':'1330879409467830', 'id':'5924d75a-931c-30ed-8790-f7fce8943c85', 'album':'Heritage ', 'totalDiscCount':0, 'title':'Haxprocess', 'recentTimestamp':'1372040508935000', 'albumArtist':'', 'trackNumber':6, 'discNumber':0, 'deleted':False, 'storeId':'Txsffypukmmeg3iwl3w5a5s3vzy', 'nid':'Txsffypukmmeg3iwl3w5a5s3vzy', 'totalTrackCount':10, 'estimatedSize':'17229205', 'albumId':'Bdkf6ywxmrhflvtasnayxlkgpcm', 'beatsPerMinute':0, 'genre':'Progressive Metal', 'playCount':7, 'artistArtRef':[ { 'url': 'http://lh3.ggpht.com/...' } ], 'kind':'sj#track', 'artist':'Opeth', 'lastModifiedTimestamp':'1330881158830924', 'clientId':'+eGFGTbiyMktbPuvB5MfsA', 'durationMillis':'418000' } """ tracks = self._get_all_items(mobileclient.ListTracks, incremental, updated_after=updated_after) return tracks @utils.accept_singleton(dict) @utils.empty_arg_shortcircuit def rate_songs(self, songs, rating): """Rate library or store songs. Returns rated song ids. :param songs: a list of song dictionaries or a single song dictionary. required keys: 'id' for library songs or 'nid' and 'trackType' for store songs. :param rating: set to ``'0'`` (no thumb), ``'1'`` (down thumb), or ``'5'`` (up thumb). """ mutate_call = mobileclient.BatchMutateTracks mutations = [] for song in songs: song['rating'] = rating mutations.append({'update': song}) self._make_call(mutate_call, mutations) # TODO # store tracks don't send back their id, so we're # forced to spoof this return [utils.id_or_nid(song) for song in songs] @utils.accept_singleton(dict) @utils.empty_arg_shortcircuit @utils.deprecated('prefer Mobileclient.rate_songs') def change_song_metadata(self, songs): """Changes the metadata of tracks. Returns a list of the song ids changed. :param songs: a list of song dictionaries or a single song dictionary. Currently, only the ``rating`` key can be changed. Set it to ``'0'`` (no thumb), ``'1'`` (down thumb), or ``'5'`` (up thumb). You can also use this to rate store tracks that aren't in your library, eg:: song = mc.get_track_info('') song['rating'] = '5' mc.change_song_metadata(song) """ mutate_call = mobileclient.BatchMutateTracks mutations = [{'update': s} for s in songs] self._make_call(mutate_call, mutations) # TODO # store tracks don't send back their id, so we're # forced to spoof this return [utils.id_or_nid(d) for d in songs] def increment_song_playcount(self, song_id, plays=1, playtime=None): """Increments a song's playcount and returns its song id. :params song_id: a song id. Providing the id of a store track that has been added to the library will *not* increment the corresponding library song's playcount. To do this, use the 'id' field (which looks like a uuid and doesn't begin with 'T'), not the 'nid' field. :params plays: (optional) positive number of plays to increment by. The default is 1. :params playtime: (optional) a datetime.datetime of the time the song was played. It will default to the time of the call. """ if playtime is None: playtime = datetime.datetime.now() self._make_call(mobileclient.IncrementPlayCount, song_id, plays, playtime) return song_id @utils.require_subscription @utils.enforce_id_param @utils.deprecated('prefer Mobileclient.add_store_tracks') def add_store_track(self, store_song_id): """Adds a store track to the library Returns the library track id of added store track. :param store_song_id: store song id """ return self.add_store_tracks(store_song_id)[0] @utils.require_subscription @utils.accept_singleton(basestring) @utils.enforce_ids_param def add_store_tracks(self, store_song_ids): """Add store tracks to the library Returns a list of the library track ids of added store tracks. :param store_song_ids: a list of store song ids or a single store song id """ mutate_call = mobileclient.BatchMutateTracks add_mutations = [mutate_call.build_track_add(self.get_track_info(store_song_id)) for store_song_id in store_song_ids] res = self._make_call(mutate_call, add_mutations) return [r['id'] for r in res['mutate_response']] @utils.accept_singleton(basestring) @utils.enforce_ids_param @utils.empty_arg_shortcircuit def delete_songs(self, library_song_ids): """Deletes songs from the library. Returns a list of deleted song ids. :param song_ids: a list of song ids, or a single song id. """ mutate_call = mobileclient.BatchMutateTracks del_mutations = mutate_call.build_track_deletes(library_song_ids) res = self._make_call(mutate_call, del_mutations) return [d['id'] for d in res['mutate_response']] @utils.enforce_id_param def get_stream_url(self, song_id, device_id=None, quality='hi'): """Returns a url that will point to an mp3 file. :param song_id: A single song id. This can be ``'storeId'`` from a store song, ``'id'`` from an uploaded song, or ``'trackId'`` from a playlist entry. :param device_id: (optional) defaults to ``android_id`` from login. Otherwise, provide a mobile device id as a string. Android device ids are 16 characters, while iOS ids are uuids with 'ios:' prepended. If you have already used Google Music on a mobile device, :func:`Mobileclient.get_registered_devices ` will provide at least one working id. Omit ``'0x'`` from the start of the string if present. Registered computer ids (a MAC address) will not be accepted and will 403. Providing an unregistered mobile device id will register it to your account, subject to Google's `device limits `__. **Registering a device id that you do not own is likely a violation of the TOS.** :param quality: (optional) stream bits per second quality One of three possible values, hi: 320kbps, med: 160kbps, low: 128kbps. The default is hi When handling the resulting url, keep in mind that: * you will likely need to handle redirects * the url expires after a minute * only one IP can be streaming music at once. This can result in an http 403 with ``X-Rejected-Reason: ANOTHER_STREAM_BEING_PLAYED``. The file will not contain metadata. Use :func:`Webclient.get_song_download_info ` or :func:`Musicmanager.download_song ` to download files with metadata. """ if song_id.startswith('T') and not self.is_subscribed: raise NotSubscribed("Store tracks require a subscription to stream.") device_id = self._ensure_device_id(device_id) return self._make_call(mobileclient.GetStreamUrl, song_id, device_id, quality) def get_station_track_stream_url(self, song_id, wentry_id, session_token, quality='hi'): """Returns a url that will point to an mp3 file. This is only for use by free accounts, and requires a call to :func:`get_station_info` first to provide `wentry_id` and `session_token`. Subscribers should instead use :func:`get_stream_url`. :param song_id: a single song id :param wentry_id: a free radio station track entry id (`wentryid` from :func:`get_station_info`) :param session_token: a free radio station session token (`sessionToken` from :func:`get_station_info`) :param quality: (optional) stream bits per second quality One of three possible values, hi: 320kbps, med: 160kbps, low: 128kbps. The default is hi """ return self._make_call(mobileclient.GetStationTrackStreamUrl, song_id, wentry_id, session_token, quality) def get_all_playlists(self, incremental=False, include_deleted=None, updated_after=None): """Returns a list of dictionaries that each represent a playlist. :param incremental: if True, return a generator that yields lists of at most 1000 playlists as they are retrieved from the server. This can be useful for presenting a loading bar to a user. :param include_deleted: ignored. Will be removed in a future release. :param updated_after: a datetime.datetime; defaults to unix epoch If provided, deleted playlists may be returned. Here is an example playlist dictionary:: { # can also be SHARED (public/subscribed to), MAGIC or omitted 'type': 'USER_GENERATED', 'kind': 'sj#playlist', 'name': 'Something Mix', 'deleted': False, 'lastModifiedTimestamp': '1325458766483033', 'recentTimestamp': '1325458766479000', 'shareToken': '', 'ownerProfilePhotoUrl': 'http://lh3.googleusercontent.com/...', 'ownerName': 'Simon Weber', 'accessControlled': False, # has to do with shared playlists 'creationTimestamp': '1325285553626172', 'id': '3d72c9b5-baad-4ff7-815d-cdef717e5d61' } """ playlists = self._get_all_items(mobileclient.ListPlaylists, incremental, updated_after=updated_after) return playlists # these could trivially support multiple creation/edits/deletion, but # I chose to match the old webclient interface (at least for now). def create_playlist(self, name, description=None, public=False): """Creates a new empty playlist and returns its id. :param name: the desired title. Creating multiple playlists with the same name is allowed. :param description: (optional) the desired description :param public: (optional) if True and the user has a subscription, share playlist. """ share_state = 'PUBLIC' if public else 'PRIVATE' mutate_call = mobileclient.BatchMutatePlaylists add_mutations = mutate_call.build_playlist_adds([{'name': name, 'description': description, 'public': share_state}]) res = self._make_call(mutate_call, add_mutations) return res['mutate_response'][0]['id'] @utils.enforce_id_param def edit_playlist(self, playlist_id, new_name=None, new_description=None, public=None): """Changes the name of a playlist and returns its id. :param playlist_id: the id of the playlist :param new_name: (optional) desired title :param new_description: (optional) desired description :param public: (optional) if True and the user has a subscription, share playlist. """ if all(value is None for value in (new_name, new_description, public)): raise ValueError('new_name, new_description, or public must be provided') if public is None: share_state = public else: share_state = 'PUBLIC' if public else 'PRIVATE' mutate_call = mobileclient.BatchMutatePlaylists update_mutations = mutate_call.build_playlist_updates([ {'id': playlist_id, 'name': new_name, 'description': new_description, 'public': share_state} ]) res = self._make_call(mutate_call, update_mutations) return res['mutate_response'][0]['id'] @utils.enforce_id_param def delete_playlist(self, playlist_id): """Deletes a playlist and returns its id. :param playlist_id: the id to delete. """ # TODO accept multiple? mutate_call = mobileclient.BatchMutatePlaylists del_mutations = mutate_call.build_playlist_deletes([playlist_id]) res = self._make_call(mutate_call, del_mutations) return res['mutate_response'][0]['id'] def get_all_user_playlist_contents(self): """ Retrieves the contents of *all* user-created playlists -- the Mobileclient does not support retrieving only the contents of one playlist. This will not return results for public playlists that the user is subscribed to; use :func:`get_shared_playlist_contents` instead. The same structure as :func:`get_all_playlists` will be returned, but with the addition of a ``'tracks'`` key in each dict set to a list of properly-ordered playlist entry dicts. Here is an example playlist entry for an individual track:: { 'kind': 'sj#playlistEntry', 'deleted': False, 'trackId': '2bb0ab1c-ce1a-3c0f-9217-a06da207b7a7', 'lastModifiedTimestamp': '1325285553655027', 'playlistId': '3d72c9b5-baad-4ff7-815d-cdef717e5d61', 'absolutePosition': '01729382256910287871', # denotes playlist ordering 'source': '1', # '2' if hosted on Google Music, '1' otherwise (see below) 'creationTimestamp': '1325285553655027', 'id': 'c9f1aff5-f93d-4b98-b13a-429cc7972fea' ## see below } If a user uploads local music to Google Music using the Music Manager, Google will attempt to match each uploaded track to a track already hosted on its servers. If a match is found for a track, the playlist entry key ``'source'`` has the value ``'2'``, and the entry will have a key ``'track'`` with a value that is a dict of track metadata (title, artist, etc). If a track is not hosted on Google Music, then the playlist entry key ``'source'`` has the value ``'1'``, and may not have a ``'track'`` key (e.g., for an MP3 without ID3 tags). In this case, the key ``'trackId'`` corresponds to the column ``ServerId`` in the table ``XFILES`` in Music Manager's local SQLite database (stored, e.g., at ~/Library/Application\ Support/Google/MusicManager/ServerDatabase.db on OS X). Among other things, the SQLite database exposes the track's local file path, and Music Manager's imputed metadata. (Note that the above behavior is documented for the Music Manager set to sync from local Folders, and may differ if it instead syncs from iTunes.) """ user_playlists = [p for p in self.get_all_playlists() if (p.get('type') == 'USER_GENERATED' or p.get('type') != 'SHARED' or 'type' not in p)] all_entries = self._get_all_items(mobileclient.ListPlaylistEntries, incremental=False, updated_after=None) for playlist in user_playlists: # TODO could use a dict to make this faster entries = [e for e in all_entries if e['playlistId'] == playlist['id']] entries.sort(key=itemgetter('absolutePosition')) playlist['tracks'] = entries return user_playlists def get_shared_playlist_contents(self, share_token): """ Retrieves the contents of a public playlist. :param share_token: from ``playlist['shareToken']``, or a playlist share url (``https://play.google.com/music/playlist/``). Note that tokens from urls will need to be url-decoded, eg ``AM...%3D%3D`` becomes ``AM...==``. For example, to retrieve the contents of a playlist that the user is subscribed to:: subscribed_to = [p for p in mc.get_all_playlists() if p.get('type') == 'SHARED'] share_tok = subscribed_to[0]['shareToken'] tracks = mc.get_shared_playlist_contents(share_tok) The user need not be subscribed to a playlist to list its tracks. Returns a list of playlist entries with structure the same as those returned by :func:`get_all_user_playlist_contents`, but without the ``'clientId'`` or ``'playlistId'`` keys. """ res = self._make_call(mobileclient.ListSharedPlaylistEntries, updated_after=None, share_token=share_token) entries = res['entries'][0]['playlistEntry'] entries.sort(key=itemgetter('absolutePosition')) return entries @utils.accept_singleton(basestring, 2) @utils.enforce_id_param @utils.enforce_ids_param(position=2) @utils.empty_arg_shortcircuit(position=2) def add_songs_to_playlist(self, playlist_id, song_ids): """Appends songs to the end of a playlist. Returns a list of playlist entry ids that were added. :param playlist_id: the id of the playlist to add to. :param song_ids: a list of song ids, or a single song id. These can be ``'storeId'`` from a store song, ``'id'`` from an uploaded song, or ``'trackId'`` from a playlist entry. Playlists have a maximum size of 1000 songs. Calls may fail before that point (presumably) due to an error on Google's end (see `#239 `__). """ mutate_call = mobileclient.BatchMutatePlaylistEntries add_mutations = mutate_call.build_plentry_adds(playlist_id, song_ids) res = self._make_call(mutate_call, add_mutations) return [e['id'] for e in res['mutate_response']] @utils.accept_singleton(basestring, 1) @utils.enforce_ids_param(position=1) @utils.empty_arg_shortcircuit(position=1) def remove_entries_from_playlist(self, entry_ids): """Removes specific entries from a playlist. Returns a list of entry ids that were removed. :param entry_ids: a list of entry ids, or a single entry id. """ mutate_call = mobileclient.BatchMutatePlaylistEntries del_mutations = mutate_call.build_plentry_deletes(entry_ids) res = self._make_call(mutate_call, del_mutations) return [e['id'] for e in res['mutate_response']] def reorder_playlist_entry(self, entry, to_follow_entry=None, to_precede_entry=None): """Reorders a single entry in a playlist and returns its id. Read ``reorder_playlist_entry(foo, bar, gaz)`` as "reorder playlist entry *foo* to follow entry *bar* and precede entry *gaz*." :param entry: the playlist entry to move. :param to_follow_entry: the playlist entry that will come before *entry* in the resulting playlist, or None if *entry* is to be the first entry in the playlist. :param to_precede_entry: the playlist entry that will come after *entry* in the resulting playlist or None if *entry* is to be the last entry in the playlist. ``reorder_playlist_entry(foo)`` is invalid and will raise ValueError; provide at least one of *to_follow_entry* or *to_precede_entry*. Leaving *to_follow_entry* or *to_precede_entry* as None when *entry* is not to be the first or last entry in the playlist is undefined. All params are dicts returned by :func:`get_all_user_playlist_contents` or :func:`get_shared_playlist_contents`. """ if to_follow_entry is None and to_precede_entry is None: raise ValueError('either to_follow_entry or to_precede_entry must be provided') mutate_call = mobileclient.BatchMutatePlaylistEntries before = to_follow_entry['clientId'] if to_follow_entry else None after = to_precede_entry['clientId'] if to_precede_entry else None reorder_mutation = mutate_call.build_plentry_reorder(entry, before, after) res = self._make_call(mutate_call, [reorder_mutation]) return [e['id'] for e in res['mutate_response']] # WIP, see issue #179 # def reorder_playlist(self, reordered_playlist, orig_playlist=None): # """TODO""" # if not reordered_playlist['tracks']: # #TODO what to return? # return # if orig_playlist is None: # #TODO get pl from server # pass # if len(reordered_playlist['tracks']) != len(orig_playlist['tracks']): # raise ValueError('the original playlist does not have the same number of' # ' tracks as the reordered playlist') # # find the minimum number of mutations to match the orig playlist # orig_tracks = orig_playlist['tracks'] # orig_tracks_id_to_idx = dict([(t['id'], i) for (i, t) in enumerate(orig_tracks)]) # re_tracks = reordered_playlist['tracks'] # re_tracks_id_to_idx = dict([(t['id'], i) for (i, t) in enumerate(re_tracks)]) # translated_re_tracks = [orig_tracks_id_to_idx[t['id']] for t in re_tracks] # lis = utils.longest_increasing_subseq(translated_re_tracks) # idx_to_move = set(range(len(orig_tracks))) - set(lis) # idx_pos_pairs = [(i, re_tracks_id_to_idx[orig_tracks[i]['id']]) # for i in idx_to_move] # #TODO build out mutations # return idx_pos_pairs # @staticmethod # def _create_ple_reorder_mutations(tracks, from_to_idx_pairs): # """ # Return a list of mutations. # :param tracks: orig_playlist['tracks'] # :param from_to_idx_pairs: [(from_index, to_index)] # """ # for from_idx, to_idx in sorted(key=itemgetter(1) # playlist_len = len(self.plentry_ids) # for from_pos, to_pos in [pair for pair in # itertools.product(range(playlist_len), repeat=2) # if pair[0] < pair[1]]: # pl = self.mc_get_playlist_songs(self.playlist_id) # from_e = pl[from_pos] # e_before_new_pos, e_after_new_pos = None, None # if to_pos - 1 >= 0: # e_before_new_pos = pl[to_pos] # if to_pos + 1 < playlist_len: # e_after_new_pos = pl[to_pos + 1] # self.mc.reorder_playlist_entry(from_e, # to_follow_entry=e_before_new_pos, # to_precede_entry=e_after_new_pos) # self._mc_assert_ple_position(from_e, to_pos) # if e_before_new_pos: # self._mc_assert_ple_position(e_before_new_pos, to_pos - 1) # if e_after_new_pos: # self._mc_assert_ple_position(e_after_new_pos, to_pos + 1) def get_registered_devices(self): """ Returns a list of dictionaries representing devices associated with the account. Performing the :class:`Musicmanager` OAuth flow will register a device of type ``'DESKTOP_APP'``. Installing the Android or iOS Google Music app and logging into it will register a device of type ``'ANDROID'`` or ``'IOS'`` respectively, which is required for streaming with the :class:`Mobileclient`. Here is an example response:: [ { u'kind': u'sj#devicemanagementinfo', u'friendlyName': u'my-hostname', u'id': u'AA:BB:CC:11:22:33', u'lastAccessedTimeMs': u'1394138679694', u'type': u'DESKTOP_APP' }, { u"kind": u"sj#devicemanagementinfo", u'friendlyName': u'Nexus 7', u'id': u'0x00112233aabbccdd', # remove 0x when streaming u'lastAccessedTimeMs': u'1344808742774', u'type': u'ANDROID' u'smartPhone': True }, { u"kind": u"sj#devicemanagementinfo", u'friendlyName': u'iPhone 6', u'id': u'ios:01234567-0123-0123-0123-0123456789AB', u'lastAccessedTimeMs': 1394138679694, u'type': u'IOS' u'smartPhone': True } { u'kind': u'sj#devicemanagementinfo', u'friendlyName': u'Google Play Music for Chrome on Windows', u'id': u'rt2qfkh0qjhos4bxrgc0oae...', # 64 characters, alphanumeric u'lastAccessedTimeMs': u'1425602805052', u'type': u'DESKTOP_APP' }, ] """ res = self._make_call(mobileclient.GetDeviceManagementInfo) return res['data']['items'] if 'data' in res else [] def deauthorize_device(self, device_id): """Deauthorize a registered device. Returns ``True`` on success, ``False`` on failure. :param device_id: A mobile device id as a string. Android ids are 16 characters with '0x' prepended, iOS ids are uuids with 'ios:' prepended, while desktop ids are in the form of a MAC address. Providing an invalid or unregistered device id will result in a 400 HTTP error. Google limits the number of device deauthorizations to 4 per year. Attempts to deauthorize a device when that limit is reached results in a 403 HTTP error with: ``X-Rejected-Reason: TOO_MANY_DEAUTHORIZATIONS``. """ try: self._make_call(mobileclient.DeauthDevice, device_id) except CallFailure: self.logger.exception("Deauthorization failure.") return False return True def get_top_songs(self): """Returns a list of dictionaries that each represent a track. Only store tracks will be returned. Tops songs seem to be an auto-generated playlist of positively-rated songs (Thumbs up). See :func:`get_track_info` for the format of a track dictionary. """ return self._get_all_items(mobileclient.ListPromotedTracks, incremental=False, updated_after=None) @utils.deprecated('prefer Mobileclient.get_top_songs') def get_promoted_songs(self): """Returns a list of dictionaries that each represent a track. Only store tracks will be returned. Promoted tracks are determined in an unknown fashion, but positively-rated library tracks are common. It is deprecated in favor of the more clearly scoped :func:`get_tops_songs`. See :func:`get_track_info` for the format of a track dictionary. """ return self.get_top_songs() def get_listen_now_items(self): """Returns a list of dictionaries of Listen Now albums and stations. See :func:`get_listen_now_situations` for Listen Now situations. Here is an example Listen Now album:: { 'album': { 'artist_metajam_id': 'A2mfgoustq7iqjdbvlenw7pnap4', 'artist_name': 'Justin Bieber', 'artist_profile_image': { 'url': 'http://lh3.googleusercontent.com/XgktDR74DWE9xD...'', }, 'description': 'Purpose is the fourth studio album by Canadian...', 'description_attribution': { 'kind': 'sj#attribution', 'license_title': 'Creative Commons Attribution CC-BY-SA 4.0', 'license_url': 'http://creativecommons.org/licenses/by-sa/4.0/legalcode', 'source_title': 'Wikipedia', 'source_url': 'http://en.wikipedia.org/wiki/Purpose_(Justin_Bieber_album)', }, 'id': { 'artist': 'Justin Bieber', 'metajamCompactKey': 'Bqpez5cimsze2fh6w7j2rcf55xa', 'title': 'Purpose (Deluxe)', }, 'title': 'Purpose (Deluxe)' 'images': [ { 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/m66cbl4Jl3VNz...', }, ], } 'kind': 'sj#listennowitem', 'suggestion_reason': '9', 'suggestion_text': 'Popular album on Google Play Music', 'type': '1' } Here is an example Listen Now station:: { 'radio_station': { 'id': { 'seeds': [ { 'artistId': 'Ax6ociylvowozcz2iepfqsar54i', 'kind': 'sj#radioSeed', 'metadataSeed': { 'artist': { 'artistArtRef': 'http://lh3.googleusercontent.com/x9qukAx...', 'artistArtRefs': [ { 'aspectRatio': '2', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/x9qukAx...', }, ], 'artistId': 'Ax6ociylvowozcz2iepfqsar54i', 'artist_bio_attribution': { 'kind': 'sj#attribution', 'source_title': 'artist representative', }, 'kind': 'sj#artist', 'name': 'Drake', }, 'kind': 'sj#radioSeedMetadata', }, 'seedType': '3', }, ] }, 'title': 'Drake', }, 'compositeArtRefs': [ { 'aspectRatio': '2', 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/rE39ky1yZN...', }, { 'aspectRatio': '1', 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/Pcwg_HngBr...', }, ], 'images': [ { 'aspectRatio': '2', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/x9qukAx_TMam...', }, ], 'suggestion_reason': '9', 'suggestion_text': 'Popular artist on Google Play Music', 'type': '3' } """ res = self._make_call(mobileclient.ListListenNowItems) return res['listennow_items'] def get_listen_now_situations(self): """Returns a list of dictionaries that each represent a Listen Now situation. See :func:`get_listen_now_items` for Listen Now albums and stations. A situation contains a list of related stations or other situations. Here is an example situation:: { 'description': 'Select a station of today's most popular songs.', 'id': 'Ntiiwllegkw73p27o236mfsj674', 'imageUrl': 'http://lh3.googleusercontent.com/egm4NgIK-Cmh84GjVgH...', 'stations': [ { 'compositeArtRefs': [ { 'aspectRatio': '2', 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/ffDI377y...', }, ], 'contentTypes': ['1'], 'description': "This playlist features today's biggest pop songs...", 'imageUrls': [ { 'aspectRatio': '1', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/B4iKX23Z...', }, ], 'kind': 'sj#radioStation', 'name': "Today's Pop Hits", 'seed': { 'curatedStationId': 'Lgen6kdn43tz5b3edimqd5e4ckq', 'kind': 'sj#radioSeed', 'seedType': '9', }, 'skipEventHistory': [], 'stationSeeds': [ { 'curatedStationId': 'Lgen6kdn43tz5b3edimqd5e4ckq', 'kind': 'sj#radioSeed', 'seedType': '9', }, ], } ], 'title': "Today's Biggest Hits", 'wideImageUrl': 'http://lh3.googleusercontent.com/13W-bm3sNmSfOjUkEqY...' } """ return self._make_call(mobileclient.ListListenNowSituations)['situations'] def get_browse_podcast_hierarchy(self): """Retrieve the hierarchy of podcast browse genres. Returns a list of podcast genres and subgenres:: { "groups": [ { "id": "JZCpodcasttopchart", "displayName": "Top Charts", "subgroups": [ { "id": "JZCpodcasttopchartall", "displayName": "All categories" }, { "id": "JZCpodcasttopchartarts", "displayName": "Arts" }, { "id": "JZCpodcasttopchartbusiness", "displayName": "Business" }, { "id": "JZCpodcasttopchartcomedy", "displayName": "Comedy" }, { "id": "JZCpodcasttopcharteducation", "displayName": "Education" }, { "id": "JZCpodcasttopchartgames", "displayName": "Games & hobbies" }, { "id": "JZCpodcasttopchartgovernment", "displayName": "Government & organizations" }, { "id": "JZCpodcasttopcharthealth", "displayName": "Health" }, { "id": "JZCpodcasttopchartkids", "displayName": "Kids & families" }, { "id": "JZCpodcasttopchartmusic", "displayName": "Music" }, { "id": "JZCpodcasttopchartnews", "displayName": "News & politics" }, { "id": "JZCpodcasttopchartreligion", "displayName": "Religion & spirituality" }, { "id": "JZCpodcasttopchartscience", "displayName": "Science & medicine" }, { "id": "JZCpodcasttopchartsociety", "displayName": "Society & culture" }, { "id": "JZCpodcasttopchartsports", "displayName": "Sports & recreation" }, { "id": "JZCpodcasttopcharttechnology", "displayName": "Technology" }, { "id": "JZCpodcasttopcharttv", "displayName": "TV & film" } ] } ] } """ res = self._make_call(mobileclient.GetBrowsePodcastHierarchy) return res.get('groups', []) def get_browse_podcast_series(self, genre_id='JZCpodcasttopchartall'): """Retrieve podcast series from browse podcasts by genre. :param genre_id: A podcast genre id as returned by :func:`get_podcast_browse_hierarchy`. Defaults to Top Chart 'All categories'. Returns a list of podcast series dicts. Here is an example podcast series dict:: { 'art': [ { 'aspectRatio': '1', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/liR-Pm7EhB58wrAa4uo9Y33LcJJ8keU...' } ], 'author': 'NBC Sports Radio', 'continuationToken': '', 'description': 'Mike Florio talks about the biggest NFL topics with the ' 'people who are most passionate about the game: League execs, ' 'players, coaches and the journalists who cover pro football.', 'explicitType': '2', 'link': 'https://audioboom.com/channel/pro-football-talk-live-with-mike-florio', 'seriesId': 'I3iad5heqorm3nck6yp7giruc5i', 'title': 'Pro Football Talk Live with Mike Florio', 'totalNumEpisodes': 0 } """ res = self._make_call(mobileclient.ListBrowsePodcastSeries, id=genre_id) return res.get('series', []) def get_all_podcast_series(self, device_id=None, incremental=False, include_deleted=None, updated_after=None): """Retrieve list of user-subscribed podcast series. :param device_id: (optional) defaults to ``android_id`` from login. Otherwise, provide a mobile device id as a string. Android device ids are 16 characters, while iOS ids are uuids with 'ios:' prepended. If you have already used Google Music on a mobile device, :func:`Mobileclient.get_registered_devices ` will provide at least one working id. Omit ``'0x'`` from the start of the string if present. Registered computer ids (a MAC address) will not be accepted and will 403. Providing an unregistered mobile device id will register it to your account, subject to Google's `device limits `__. **Registering a device id that you do not own is likely a violation of the TOS.** :param incremental: if True, return a generator that yields lists of at most 1000 podcast series as they are retrieved from the server. This can be useful for presenting a loading bar to a user. :param include_deleted: ignored. Will be removed in a future release. :param updated_after: a datetime.datetime; defaults to unix epoch Returns a list of podcast series dicts. Here is an example podcast series dict:: { 'art': [ { 'aspectRatio': '1', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/bNoyxoGTwCGkUscMjHsvKe5W80uMOfq...' } ], 'author': 'Chris Hardwick', 'continuationToken': '', 'description': 'I am Chris Hardwick. I am on TV a lot and have a blog at ' 'nerdist.com. This podcast is basically just me talking about ' 'stuff and things with my two nerdy friends Jonah Ray and Matt ' 'Mira, and usually someone more famous than all of us. ' 'Occasionally we swear because that is fun. I hope you like ' "it, but if you don't I'm sure you will not hesitate to unfurl " "your rage in the 'reviews' section because that's how the " 'Internet works.', 'explicitType': '1', 'link': 'http://nerdist.com/', 'seriesId': 'Iliyrhelw74vdqrro77kq2vrdhy', 'title': 'The Nerdist', 'totalNumEpisodes': 829, 'userPreferences': { 'autoDownload': False, 'notifyOnNewEpisode': False, 'subscribed': True } } """ device_id = self._ensure_device_id(device_id) return self._get_all_items(mobileclient.ListPodcastSeries, incremental=incremental, updated_after=updated_after, device_id=device_id) def get_all_podcast_episodes(self, device_id=None, incremental=False, include_deleted=None, updated_after=None): """Retrieve list of episodes from user-subscribed podcast series. :param device_id: (optional) defaults to ``android_id`` from login. Otherwise, provide a mobile device id as a string. Android device ids are 16 characters, while iOS ids are uuids with 'ios:' prepended. If you have already used Google Music on a mobile device, :func:`Mobileclient.get_registered_devices ` will provide at least one working id. Omit ``'0x'`` from the start of the string if present. Registered computer ids (a MAC address) will not be accepted and will 403. Providing an unregistered mobile device id will register it to your account, subject to Google's `device limits `__. **Registering a device id that you do not own is likely a violation of the TOS.** :param incremental: if True, return a generator that yields lists of at most 1000 podcast episodes as they are retrieved from the server. This can be useful for presenting a loading bar to a user. :param include_deleted: ignored. Will be removed in a future release. :param updated_after: a datetime.datetime; defaults to unix epoch Returns a list of podcast episode dicts. Here is an example podcast episode dict:: { 'art': [ { 'aspectRatio': '1', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/bNoyxoGTwCGkUscMjHsvKe5W80uMOfq...' } ], 'deleted': False, 'description': 'Comedian Bill Burr yelled at Philadelphia, Chris vaguely ' 'understands hockey, Jonah understands it even less, and Matt ' 'is weirdly not tired of the running "Matt loves the Dave ' 'Matthews Band" joke, though I\'m sure all of you are.', 'durationMillis': '4310000', 'episodeId': 'D6i26frpxu53t2ws3lpbjtpovum', 'explicitType': '2', 'fileSize': '69064793', 'publicationTimestampMillis': '1277791500000', 'seriesId': 'Iliyrhelw74vdqrro77kq2vrdhy', 'seriesTitle': 'The Nerdist', 'title': 'Bill Burr' } """ device_id = self._ensure_device_id(device_id) return self._get_all_items(mobileclient.ListPodcastEpisodes, incremental=incremental, updated_after=updated_after, device_id=device_id) # TODO: Support multiple. @utils.enforce_id_param def add_podcast_series(self, podcast_id, notify_on_new_episode=False): """Subscribe to a podcast series. :param podcast_id: A podcast series id (hint: they always start with 'I'). :param notify_on_new_episode: Get device notifications on new episodes. Returns podcast series id of added podcast series """ mutate_call = mobileclient.BatchMutatePodcastSeries update_mutations = mutate_call.build_podcast_updates([ { 'seriesId': podcast_id, 'subscribed': True, 'userPreferences': { 'subscribed': True, 'notifyOnNewEpisode': notify_on_new_episode } } ]) res = self._make_call(mutate_call, update_mutations) return res['mutate_response'][0]['id'] # TODO: Support multiple. @utils.enforce_id_param def delete_podcast_series(self, podcast_id): """Unsubscribe to a podcast series. :param podcast_id: A podcast series id (hint: they always start with 'I'). Returns podcast series id of removed podcast series """ mutate_call = mobileclient.BatchMutatePodcastSeries update_mutations = mutate_call.build_podcast_updates([ { 'seriesId': podcast_id, 'subscribed': False, 'userPreferences': { 'subscribed': False, 'notifyOnNewEpisode': False } } ]) res = self._make_call(mutate_call, update_mutations) return res['mutate_response'][0]['id'] # TODO: Support multiple. @utils.enforce_id_param def edit_podcast_series(self, podcast_id, subscribe=True, notify_on_new_episode=False): """Edit a podcast series subscription. :param podcast_id: A podcast series id (hint: they always start with 'I'). :param subscribe: Subscribe to podcast. :param notify_on_new_episode: Get device notifications on new episodes. Returns podcast series id of edited podcast series """ mutate_call = mobileclient.BatchMutatePodcastSeries update_mutations = mutate_call.build_podcast_updates([ { 'seriesId': podcast_id, 'subscribed': subscribe, 'userPreferences': { 'subscribed': subscribe, 'notifyOnNewEpisode': notify_on_new_episode } } ]) res = self._make_call(mutate_call, update_mutations) return res['mutate_response'][0]['id'] @utils.enforce_id_param def get_podcast_episode_stream_url(self, podcast_episode_id, device_id=None, quality='hi'): """Returns a url that will point to an mp3 file. :param podcast_episde_id: a single podcast episode id (hint: they always start with 'D'). :param device_id: (optional) defaults to ``android_id`` from login. Otherwise, provide a mobile device id as a string. Android device ids are 16 characters, while iOS ids are uuids with 'ios:' prepended. If you have already used Google Music on a mobile device, :func:`Mobileclient.get_registered_devices ` will provide at least one working id. Omit ``'0x'`` from the start of the string if present. Registered computer ids (a MAC address) will not be accepted and will 403. Providing an unregistered mobile device id will register it to your account, subject to Google's `device limits `__. **Registering a device id that you do not own is likely a violation of the TOS.** :param quality: (optional) stream bits per second quality One of three possible values, hi: 320kbps, med: 160kbps, low: 128kbps. The default is hi When handling the resulting url, keep in mind that: * you will likely need to handle redirects * the url expires after a minute * only one IP can be streaming music at once. This can result in an http 403 with ``X-Rejected-Reason: ANOTHER_STREAM_BEING_PLAYED``. The file will not contain metadata. """ device_id = self._ensure_device_id(device_id) return self._make_call( mobileclient.GetPodcastEpisodeStreamUrl, podcast_episode_id, device_id, quality) def get_podcast_series_info(self, podcast_series_id, max_episodes=50): """Retrieves information about a podcast series. :param podcast_series_id: A podcast series id (hint: they always start with 'I'). :param max_episodes: Maximum number of episodes to retrieve Returns a dict, eg:: { 'art': [ { 'aspectRatio': '1', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/bNoyxoGTwCGkUscMjHsvKe5W80uMOfq...' } ], 'author': 'Chris Hardwick', 'continuationToken': '', 'description': 'I am Chris Hardwick. I am on TV a lot and have a blog at ' 'nerdist.com. This podcast is basically just me talking about ' 'stuff and things with my two nerdy friends Jonah Ray and Matt ' 'Mira, and usually someone more famous than all of us. ' 'Occasionally we swear because that is fun. I hope you like ' "it, but if you don't I'm sure you will not hesitate to unfurl " "your rage in the 'reviews' section because that's how the " 'Internet works.', 'episodes': [ { 'art': [ { 'aspectRatio': '1', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/bNoyxoGTwCGkUscMjHsvKe5...' } ], 'description': 'Sarah Jessica Parker (Sex and the City) ' 'chats with Chris about growing up without ' 'television, her time on Square Pegs and ' 'her character in L.A. Story. Sarah Jessica ' 'then talks about how she felt when she first ' 'got the part of Carrie on Sex and the ' 'City, how she dealt with her sudden ' 'celebrity of being Carrie Bradshaw and they ' 'come up with a crazy theory about the show! ' 'They also talk about Sarah Jessica’s new ' 'show Divorce on HBO!', 'durationMillis': '5101000', 'episodeId': 'Dcz67vtkhrerzh4hptfqpadt5vm', 'explicitType': '1', 'fileSize': '40995252', 'publicationTimestampMillis': '1475640000000', 'seriesId': 'Iliyrhelw74vdqrro77kq2vrdhy', 'seriesTitle': 'The Nerdist', 'title': 'Sarah Jessica Parker' }, ] 'explicitType': '1', 'link': 'http://nerdist.com/', 'seriesId': 'Iliyrhelw74vdqrro77kq2vrdhy', 'title': 'The Nerdist', 'totalNumEpisodes': 829, 'userPreferences': { 'autoDownload': False, 'notifyOnNewEpisode': False, 'subscribed': True } } """ return self._make_call(mobileclient.GetPodcastSeries, podcast_series_id, max_episodes) def get_podcast_episode_info(self, podcast_episode_id): """Retrieves information about a podcast episode. :param podcast_episode_id: A podcast episode id (hint: they always start with 'D'). Returns a dict, eg:: { 'art': [ { 'aspectRatio': '1', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/bNoyxoGTwCGkUscMjHsvKe5...' } ], 'description': 'Sarah Jessica Parker (Sex and the City) ' 'chats with Chris about growing up without ' 'television, her time on Square Pegs and ' 'her character in L.A. Story. Sarah Jessica ' 'then talks about how she felt when she first ' 'got the part of Carrie on Sex and the ' 'City, how she dealt with her sudden ' 'celebrity of being Carrie Bradshaw and they ' 'come up with a crazy theory about the show! ' 'They also talk about Sarah Jessica’s new ' 'show Divorce on HBO!', 'durationMillis': '5101000', 'episodeId': 'Dcz67vtkhrerzh4hptfqpadt5vm', 'explicitType': '1', 'fileSize': '40995252', 'publicationTimestampMillis': '1475640000000', 'seriesId': 'Iliyrhelw74vdqrro77kq2vrdhy', 'seriesTitle': 'The Nerdist', 'title': 'Sarah Jessica Parker' } """ return self._make_call(mobileclient.GetPodcastEpisode, podcast_episode_id) def create_station(self, name, track_id=None, artist_id=None, album_id=None, genre_id=None, playlist_token=None, curated_station_id=None): """Creates a radio station and returns its id. :param name: the name of the station to create :param \*_id: the id of an item to seed the station from. :param playlist_token: The shareToken of a playlist to seed the station from. Exactly one of the id/token params must be provided, or ValueError will be raised. """ # TODO could expose include_tracks seed = {} if track_id is not None: if track_id[0] == 'T': seed['trackId'] = track_id seed['seedType'] = 2 else: seed['trackLockerId'] = track_id seed['seedType'] = 1 if artist_id is not None: seed['artistId'] = artist_id seed['seedType'] = 3 if album_id is not None: seed['albumId'] = album_id seed['seedType'] = 4 if genre_id is not None: seed['genreId'] = genre_id seed['seedType'] = 5 if playlist_token is not None: seed['playlistShareToken'] = playlist_token seed['seedType'] = 8 if curated_station_id is not None: seed['curatedStationId'] = curated_station_id seed['seedType'] = 9 if len(seed) > 2: raise ValueError('exactly one {track,artist,album,genre}_id must be provided') mutate_call = mobileclient.BatchMutateStations add_mutation = mutate_call.build_add(name, seed, include_tracks=False, num_tracks=0) res = self._make_call(mutate_call, [add_mutation]) return res['mutate_response'][0]['id'] @utils.accept_singleton(basestring) @utils.enforce_ids_param @utils.empty_arg_shortcircuit def delete_stations(self, station_ids): """Deletes radio stations and returns their ids. :param station_ids: a single id, or a list of ids to delete """ mutate_call = mobileclient.BatchMutateStations delete_mutations = mutate_call.build_deletes(station_ids) res = self._make_call(mutate_call, delete_mutations) return [s['id'] for s in res['mutate_response']] def get_all_stations(self, incremental=False, include_deleted=None, updated_after=None): """Retrieve all library stations. Returns a list of dictionaries that each represent a radio station. This includes any stations listened to recently, which might not be in the library. :param incremental: if True, return a generator that yields lists of at most 1000 stations as they are retrieved from the server. This can be useful for presenting a loading bar to a user. :param include_deleted: ignored. Will be removed in a future release. :param updated_after: a datetime.datetime; defaults to unix epoch Here is an example station dictionary:: { 'imageUrl': 'http://lh6.ggpht.com/...', 'kind': 'sj#radioStation', 'name': 'station', 'deleted': False, 'lastModifiedTimestamp': '1370796487455005', 'recentTimestamp': '1370796487454000', 'clientId': 'c2639bf4-af24-4e4f-ab37-855fc89d15a1', 'seed': { 'kind': 'sj#radioSeed', 'trackLockerId': '7df3aadd-9a18-3dc1-b92e-a7cf7619da7e' # possible keys: # albumId, artistId, genreId, trackId, trackLockerId }, 'id': '69f1bfce-308a-313e-9ed2-e50abe33a25d', 'inLibrary': True }, """ return self._get_all_items(mobileclient.ListStations, incremental, updated_after=updated_after) def get_station_tracks(self, station_id, num_tracks=25, recently_played_ids=None): """Returns a list of dictionaries that each represent a track. Each call performs a separate sampling (with replacement?) from all possible tracks for the station. Nonexistent stations will return an empty list. :param station_id: the id of a radio station to retrieve tracks from. Use the special id ``'IFL'`` for the "I'm Feeling Lucky" station. :param num_tracks: the number of tracks to retrieve :param recently_played_ids: a list of recently played track ids retrieved from this station. This avoids playing duplicates. See :func:`get_all_songs` for the format of a track dictionary. """ if recently_played_ids is None: recently_played_ids = [] def add_track_type(track_id): if track_id[0] == 'T': return {'id': track_id, 'type': 1} else: return {'id': track_id, 'type': 0} recently_played = [add_track_type(track_id) for track_id in recently_played_ids] res = self._make_call(mobileclient.ListStationTracks, station_id, num_tracks, recently_played=recently_played) stations = res.get('data', {}).get('stations') if not stations: return [] return stations[0].get('tracks', []) def search(self, query, max_results=100): """Queries Google Music for content. Most result types will be empty unless using a subscription account. :param query: a string keyword to search with. Capitalization and punctuation are ignored. :param max_results: Maximum number of items to be retrieved. The maximum accepted value is 100. If set higher, results are limited to 10. A value of ``None`` allows up to 1000 results per type but won't return playlist nor situation results. Default is ``100``. The results are returned as a dictionary of lists mapped by result type: * album_hits * artist_hits * genre_hits * playlist_hits * podcast_hits * situation_hits * song_hits * station_hits * video_hits Here is a sample of results for a search of ``'workout'`` on a subscription account:: { 'album_hits': [{ 'album': { 'albumArtRef': 'http://lh5.ggpht.com/DVIg4GiD6msHfgPs_Vu_2eRxCyAoz0fF...', 'albumArtist': 'J.Cole', 'albumId': 'Bfp2tuhynyqppnp6zennhmf6w3y', 'artist': 'J.Cole', 'artistId': ['Ajgnxme45wcqqv44vykrleifpji'], 'description_attribution': { 'kind': 'sj#attribution', 'license_title': 'Creative Commons Attribution CC-BY', 'license_url': 'http://creativecommons.org/licenses/by/4.0/legalcode', 'source_title': 'Freebase', 'source_url': '' }, 'explicitType': '1', 'kind': 'sj#album', 'name': 'Work Out', 'year': 2011 }, 'type': '3' }], 'artist_hits': [{ 'artist': { 'artistArtRef': 'http://lh3.googleusercontent.com/MJe-cDw9uQ-pUagoLlm...', 'artistArtRefs': [{ 'aspectRatio': '2', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/MJe-cDw9uQ-pUagoLlmKX3x_K...' }], 'artistId': 'Ajgnxme45wcqqv44vykrleifpji', 'artist_bio_attribution': { 'kind': 'sj#attribution', 'source_title': 'David Jeffries, Rovi' }, 'kind': 'sj#artist', 'name': 'J. Cole' }, 'type': '2' }], 'playlist_hits': [{ 'playlist': { 'albumArtRef': [ {'url': 'http://lh3.googleusercontent.com/KJsAhrg8Jk_5A4xYLA68LFC...'} ], 'description': 'Workout Plan ', 'kind': 'sj#playlist', 'name': 'Workout', 'ownerName': 'Ida Sarver', 'shareToken': 'AMaBXyktyF6Yy_G-8wQy8Rru0tkueIbIFblt2h0BpkvTzHDz-fFj6P...', 'type': 'SHARED' }, 'type': '4' }], 'podcast_hits': [{ 'series': { 'art': [ { 'aspectRatio': '1', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'https://lh3.googleusercontent.com/je4lsaiQCdfcOWoYm3Z_mC...' } ], 'author': 'Steve Boyett', 'continuationToken': '', 'copyright': 'Music copyright c the respective artists. All other ' 'material c2006, 2016 by Podrunner, LLC. All rights ' 'reserved. For personal use only. Unauthorized ' 'reproduction, sale, rental, exchange, public ' 'performance, or broadcast of this audio is ' 'prohibited.', 'description': 'Nonstop, one-hour, high-energy workout music mixes ' "to help you groove while you move. Podrunner's " 'fixed-tempo and interval exercise mixes are ' 'perfect for power walking, jogging, running, ' 'spinning, elliptical, aerobics, and many other ' 'tempo-based forms of exercise. An iTunes ' 'award-winner six years in a row!', 'explicitType': '2', 'link': 'http://www.podrunner.com/', 'seriesId': 'Ilx4ufdua5rdvzplnojtloulo3a', 'title': 'PODRUNNER: Workout Music', 'totalNumEpisodes': 0 }, 'type': '9' }], 'situation_hits': [{ 'situation': { 'description': 'Level up and enter beast mode with some loud, aggressive music.', 'id': 'Nrklpcyfewwrmodvtds5qlfp5ve', 'imageUrl': 'http://lh3.googleusercontent.com/Cd8WRMaG_pDwjTC_dSPIIuf...', 'title': 'Entering Beast Mode', 'wideImageUrl': 'http://lh3.googleusercontent.com/8A9S-nTb5pfJLcpS8P...'}, 'type': '7' }], 'song_hits': [{ 'track': { 'album': 'Work Out', 'albumArtRef': [{ 'aspectRatio': '1', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'http://lh5.ggpht.com/DVIg4GiD6msHfgPs_Vu_2eRxCyAoz0fFdxj5w...' }], 'albumArtist': 'J.Cole', 'albumAvailableForPurchase': True, 'albumId': 'Bfp2tuhynyqppnp6zennhmf6w3y', 'artist': 'J Cole', 'artistId': ['Ajgnxme45wcqqv44vykrleifpji', 'Ampniqsqcwxk7btbgh5ycujij5i'], 'composer': '', 'discNumber': 1, 'durationMillis': '234000', 'estimatedSize': '9368582', 'explicitType': '1', 'genre': 'Pop', 'kind': 'sj#track', 'nid': 'Tq3nsmzeumhilpegkimjcnbr6aq', 'primaryVideo': { 'id': '6PN78PS_QsM', 'kind': 'sj#video', 'thumbnails': [{ 'height': 180, 'url': 'https://i.ytimg.com/vi/6PN78PS_QsM/mqdefault.jpg', 'width': 320 }] }, 'storeId': 'Tq3nsmzeumhilpegkimjcnbr6aq', 'title': 'Work Out', 'trackAvailableForPurchase': True, 'trackAvailableForSubscription': True, 'trackNumber': 1, 'trackType': '7', 'year': 2011 }, 'type': '1' }], 'station_hits': [{ 'station': { 'compositeArtRefs': [{ 'aspectRatio': '1', 'kind': 'sj#imageRef', 'url': 'http://lh3.googleusercontent.com/3aD9mFppy6PwjADnjwv_w...' }], 'contentTypes': ['1'], 'description': 'These riff-tastic metal tracks are perfect ' 'for getting the blood pumping.', 'imageUrls': [{ 'aspectRatio': '1', 'autogen': False, 'kind': 'sj#imageRef', 'url': 'http://lh5.ggpht.com/YNGkFdrtk43e8H941fuAHjflrNZ1CJUeqdoys...' }], 'kind': 'sj#radioStation', 'name': 'Heavy Metal Workout', 'seed': { 'curatedStationId': 'Lcwg73w3bd64hsrgarnorif52r', 'kind': 'sj#radioSeed', 'seedType': '9' }, 'skipEventHistory': [], 'stationSeeds': [{ 'curatedStationId': 'Lcwg73w3bd64hsrgarnorif52r', 'kind': 'sj#radioSeed', 'seedType': '9'} ]}, 'type': '6' }], 'video_hits': [{ 'score': 629.6226806640625, 'type': '8', 'youtube_video': { 'id': '6PN78PS_QsM', 'kind': 'sj#video', 'thumbnails': [{ 'height': 180, 'url': 'https://i.ytimg.com/vi/6PN78PS_QsM/mqdefault.jpg', 'width': 320 }], 'title': 'J. Cole - Work Out' } }] } """ res = self._make_call(mobileclient.Search, query, max_results) clusters = res.get('clusterDetail', []) hits_by_type = defaultdict(list) for cluster in clusters: hit_type = cluster['cluster']['type'] hits = cluster.get('entries', []) hits_by_type[hit_type].extend(hits) return {'album_hits': hits_by_type['3'], 'artist_hits': hits_by_type['2'], 'playlist_hits': hits_by_type['4'], 'genre_hits': hits_by_type['5'], 'podcast_hits': hits_by_type['9'], 'situation_hits': hits_by_type['7'], 'song_hits': hits_by_type['1'], 'station_hits': hits_by_type['6'], 'video_hits': hits_by_type['8']} @utils.enforce_id_param def get_artist_info(self, artist_id, include_albums=True, max_top_tracks=5, max_rel_artist=5): """Retrieves details on an artist. :param artist_id: an artist id (hint: they always start with 'A') :param include_albums: when True, create the ``'albums'`` substructure :param max_top_tracks: maximum number of top tracks to retrieve :param max_rel_artist: maximum number of related artists to retrieve Returns a dict, eg:: { 'albums':[ # only if include_albums is True { 'albumArtRef':'http://lh6.ggpht.com/...', 'albumArtist':'Amorphis', 'albumId':'Bfr2onjv7g7tm4rzosewnnwxxyy', 'artist':'Amorphis', 'artistId':[ 'Apoecs6off3y6k4h5nvqqos4b5e' ], 'kind':'sj#album', 'name':'Circle', 'year':2013 }, ], 'artistArtRef': 'http://lh6.ggpht.com/...', 'artistId':'Apoecs6off3y6k4h5nvqqos4b5e', 'kind':'sj#artist', 'name':'Amorphis', 'related_artists':[ # only if max_rel_artists > 0 { 'artistArtRef': 'http://lh5.ggpht.com/...', 'artistId':'Aheqc7kveljtq7rptd7cy5gvk2q', 'kind':'sj#artist', 'name':'Dark Tranquillity' } ], 'topTracks':[ # only if max_top_tracks > 0 { 'album':'Skyforger', 'albumArtRef':[ { 'url': 'http://lh4.ggpht.com/...' } ], 'albumArtist':'Amorphis', 'albumAvailableForPurchase':True, 'albumId':'B5nc22xlcmdwi3zn5htkohstg44', 'artist':'Amorphis', 'artistId':[ 'Apoecs6off3y6k4h5nvqqos4b5e' ], 'discNumber':1, 'durationMillis':'253000', 'estimatedSize':'10137633', 'kind':'sj#track', 'nid':'Tn2ugrgkeinrrb2a4ji7khungoy', 'playCount':1, 'storeId':'Tn2ugrgkeinrrb2a4ji7khungoy', 'title':'Silver Bride', 'trackAvailableForPurchase':True, 'trackNumber':2, 'trackType':'7' } ], 'total_albums':21 } """ res = self._make_call(mobileclient.GetArtist, artist_id, include_albums, max_top_tracks, max_rel_artist) return res def _get_all_items(self, call, incremental, **kwargs): """ :param call: protocol.McCall :param incremental: bool kwargs are passed to the call. """ if not incremental: # slight optimization: get more items in a page kwargs.setdefault('max_results', 20000) generator = self._get_all_items_incremental(call, **kwargs) if incremental: return generator return [s for chunk in generator for s in chunk] def _get_all_items_incremental(self, call, **kwargs): """Return a generator of lists of tracks. kwargs are passed to the call.""" get_next_chunk = True lib_chunk = {} next_page_token = None while get_next_chunk: lib_chunk = self._make_call(call, start_token=next_page_token, **kwargs) items = [] for item in lib_chunk['data']['items']: if 'userPreferences' in item: if item['userPreferences'].get('subscribed', False): items.append(item) elif ('updated_after' in kwargs) or (not item.get('deleted', False)): items.append(item) # Conditional prevents generator from yielding empty # list for last page of podcast list calls. if items: yield items # Podcast list calls always include 'nextPageToken' in responses. # We have to check to make sure we don't get stuck in an infinite loop # by comparing the previous and next page tokens. prev_page_token = next_page_token next_page_token = lib_chunk.get('nextPageToken') get_next_chunk = (next_page_token and next_page_token != prev_page_token) @utils.enforce_id_param def get_album_info(self, album_id, include_tracks=True): """Retrieves details on an album. :param album_id: an album id (hint: they always start with 'B') :param include_tracks: when True, create the ``'tracks'`` substructure Returns a dict, eg:: { 'kind': 'sj#album', 'name': 'Circle', 'artist': 'Amorphis', 'albumArtRef': 'http://lh6.ggpht.com/...', 'tracks': [ # if `include_tracks` is True { 'album': 'Circle', 'kind': 'sj#track', 'storeId': 'T5zb7luo2vkroozmj57g2nljdsy', # can be used as a song id 'artist': 'Amorphis', 'albumArtRef': [ { 'url': 'http://lh6.ggpht.com/...' }], 'title': 'Shades of Grey', 'nid': 'T5zb7luo2vkroozmj57g2nljdsy', 'estimatedSize': '13115591', 'albumId': 'Bfr2onjv7g7tm4rzosewnnwxxyy', 'artistId': ['Apoecs6off3y6k4h5nvqqos4b5e'], 'albumArtist': 'Amorphis', 'durationMillis': '327000', 'composer': '', 'genre': 'Metal', 'trackNumber': 1, 'discNumber': 1, 'trackAvailableForPurchase': True, 'trackType': '7', 'albumAvailableForPurchase': True }, # ... ], 'albumId': 'Bfr2onjv7g7tm4rzosewnnwxxyy', 'artistId': ['Apoecs6off3y6k4h5nvqqos4b5e'], 'albumArtist': 'Amorphis', 'year': 2013 } """ return self._make_call(mobileclient.GetAlbum, album_id, include_tracks) @utils.enforce_id_param def get_track_info(self, store_track_id): """Retrieves information about a store track. :param store_track_id: a store track id (hint: they always start with 'T') Returns a dict, eg:: { 'album': 'Best Of', 'kind': 'sj#track', 'storeId': 'Te2qokfjmhqxw4bnkswbfphzs4m', 'artist': 'Amorphis', 'albumArtRef': [ { 'url': 'http://lh5.ggpht.com/...' }], 'title': 'Hopeless Days', 'nid': 'Te2qokfjmhqxw4bnkswbfphzs4m', 'estimatedSize': '12325643', 'albumId': 'Bsbjjc24a5xutbutvbvg3h4y2k4', 'artistId': ['Apoecs6off3y6k4h5nvqqos4b5e'], 'albumArtist': 'Amorphis', 'durationMillis': '308000', 'composer': '', 'genre': 'Metal', 'trackNumber': 2, 'discNumber': 1, 'trackAvailableForPurchase': True, 'trackType': '7', 'albumAvailableForPurchase': True } """ return self._make_call(mobileclient.GetStoreTrack, store_track_id) @utils.enforce_id_param def get_station_info(self, station_id, num_tracks=25): """Retrieves information about a station. :param station_id: a station id :param include_tracks: when True, create the ``'tracks'`` substructure :param num_tracks: maximum number of tracks to return Returns a dict, eg:: { 'kind':'sj#radioStation', 'byline':'By Google Play Music', 'name':'Neo Soul', 'compositeArtRefs':[ { 'url':'http://lh3.googleusercontent.com/Aa-WBVTbKegp6McwqeHlj6KX5EYHKOBag74uwNl4xIHSv1g7Mi-NkMzwig', 'kind':'sj#imageRef', 'aspectRatio':'2' } ], 'deleted':False, 'enforcementResult':{ 'sessionInvalidated':False }, 'lastModifiedTimestamp':'1497410517701000', 'recentTimestamp':'1497410516945000', 'clientId':'9e66e89e-50b0-11e7-aaa3-bc5ff4545c4b', 'sessionToken':'AFTSR9PtB_PbyqZ3jsnl-PFma4upK1MEtlhVnIlxRNynGalctoJF4TpgzaxymOnk0Gv5DQG7gb_W3eLamPU_Mg1cWylhrowQi1EFMBKWHeDDYWzpU1cEOF-D3c_gnwsBRHIuOetph2veY2Fd-dKVzjOkN6mtidE-XPR2VnpR9PG83wRLVRtJq5593-Vvbu6wjCHD9f23ohxg-ki0tyD3fjFW1463zy63YzN5Aa2SpbvOskEWhwhS3u9ASgEoX08lePE-ZZAq1XtmVvLa8DnDMVb7i95Qhp0dM2it1uruKHH85u7tMYnttbAW4022d0rqrp3ULDKOYMvIIouXH44-bkbKLuVIADiqeNavwTVzcoJxWo4mMKjCaxM=', 'tracks':[ { 'albumArtRef':[ { 'url':'http://lh5.ggpht.com/vWRj9DkKZ7cFj-qXoGoBGsv7ngUWdtGNl1SSOdzj2efDwdAs3F0kJ3Xq6zLxKjgv1v3ive5S', 'kind':'sj#imageRef', 'aspectRatio':'1', 'autogen':False } ], 'artistId':[ 'Atmjrctnubes5zhftrey2xjkzl' ], 'composer':'', 'year':1996, 'trackAvailableForSubscription':True, 'trackType':'7', 'album':u"Maxwell's Urban Hang Suite", 'title':u"Ascension (Don't Ever Wonder)", 'albumArtist':'Maxwell', 'trackNumber':4, 'discNumber':1, 'albumAvailableForPurchase':False, 'explicitType':'2', 'trackAvailableForPurchase':True, 'storeId':'T6utayayrlyfmpovgj4ulacpat', 'nid':'T6utayayrlyfmpovgj4ulacpat', 'estimatedSize':'13848059', 'albumId':'Bpwzztxynfjwtnrtgiugem3b56e', 'genre':'Neo-Soul', 'kind':'sj#track', 'primaryVideo':{ 'kind':'sj#video', 'id':'D7rm9t5S4uE', 'thumbnails':[ { 'url':'https://i.ytimg.com/vi/D7rm9t5S4uE/mqdefault.jpg', 'width':320, 'height':180 } ] }, 'artist':'Maxwell', 'wentryid':'ec9428eb-2676-4e92-901d-2de9a72fe581', 'durationMillis':'346000' } ], 'seed':{ 'kind':'sj#radioSeed', 'curatedStationId':'L3lu7bpcqtd3e7pa7w37rf7gdu', 'seedType':'9' }, 'skipEventHistory':[ ], 'inLibrary':False, 'imageUrls':[ { 'url':'http://lh3.googleusercontent.com/iceDDsQjQ683AD4w21WWlekg115Ixy_kMTivkFJTjo3w7vuW4-SSs3F3KQOaR8qoI-QYVuOQoA', 'kind':'sj#imageRef', 'aspectRatio':'1', 'autogen':False } ], 'id':'1a9ec96c-6c98-3c43-b123-9e2743203f5d' } """ res = self._make_call(mobileclient.ListStationTracks, station_id, num_tracks, []) return res.get('data', {'stations': [{}]})['stations'][0] def get_genres(self, parent_genre_id=None): """Retrieves information on Google Music genres. :param parent_genre_id: (optional) If provided, only child genres will be returned. By default, all root genres are returned. If this id is invalid, an empty list will be returned. Returns a list of dicts of the form, eg:: { 'name': 'Alternative/Indie', 'id': 'ALTERNATIVE_INDIE' 'kind': 'sj#musicGenre', 'children': [ # this key may not be present 'ALTERNATIVE_80S', # these are ids 'ALT_COUNTRY', # ... ], 'images': [ { # these are album covers representative of the genre 'url': 'http://lh6.ggpht.com/...' }, # ... ], } Note that the id can be used with :func:`create_station` to seed a radio station. """ res = self._make_call(mobileclient.GetGenres, parent_genre_id) # An invalid parent genre won't respond with a genres key. return res.get('genres', []) gmusicapi-12.1.1/gmusicapi/clients/musicmanager.py0000664000175000017500000005602113400371522022523 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import, unicode_literals from future.utils import PY3 from past.builtins import basestring from builtins import * # noqa import os from socket import gethostname import time from uuid import getnode as getmac if PY3: from urllib.parse import unquote else: from urllib import unquote import httplib2 # included with oauth2client from oauth2client.client import TokenRevokeError import gmusicapi from gmusicapi.clients.shared import _OAuthClient from gmusicapi.appdirs import my_appdirs from gmusicapi.exceptions import CallFailure, NotLoggedIn from gmusicapi.protocol import musicmanager, upload_pb2, locker_pb2 from gmusicapi.utils import utils from gmusicapi import session class Musicmanager(_OAuthClient): """Allows uploading by posing as Google's Music Manager. Musicmanager uses OAuth, so a plaintext email and password are not required when logging in. For most authors and users of gmusicapi scripts, :func:`perform_oauth` should be run once per machine to store credentials to disk. Future calls to :func:`login` can use use the stored credentials by default. Some authors may want more control over the OAuth flow. In this case, credentials can be directly provided to :func:`login`. """ OAUTH_FILEPATH = os.path.join(my_appdirs.user_data_dir, 'oauth.cred') _session_class = session.Musicmanager def __init__(self, debug_logging=True, validate=True, verify_ssl=True): super(Musicmanager, self).__init__(self.__class__.__name__, debug_logging, validate, verify_ssl) def login(self, oauth_credentials=OAUTH_FILEPATH, uploader_id=None, uploader_name=None): """Authenticates the Music Manager using OAuth. Returns ``True`` on success, ``False`` on failure. Unlike the :class:`Webclient`, OAuth allows authentication without providing plaintext credentials to the application. In most cases, the default parameters should be acceptable. Users on virtual machines will want to provide `uploader_id`. :param oauth_credentials: ``oauth2client.client.OAuth2Credentials`` or the path to a ``oauth2client.file.Storage`` file. By default, the same default path used by :func:`perform_oauth` is used. Endusers will likely call :func:`perform_oauth` once to write credentials to disk and then ignore this parameter. This param is mostly intended to allow flexibility for developers of a 3rd party service who intend to perform their own OAuth flow (eg on their website). :param uploader_id: a unique id as a MAC address, eg ``'00:11:22:33:AA:BB'``. This should only be provided in cases where the default (host MAC address incremented by 1) will not work. Upload behavior is undefined if a Music Manager uses the same id, especially when reporting bad matches. ``ValueError`` will be raised if this is provided but not in the proper form. ``OSError`` will be raised if this is not provided and a real MAC could not be determined (most common when running on a VPS). If provided, use the same id on all future runs for this machine, because of the upload device limit explained below. :param uploader_name: human-readable non-unique id; default is ``" (gmusicapi-{version})"``. This doesn't appear to be a part of authentication at all. Registering with (id, name = X, Y) and logging in with (id, name = X, Z) works, and does not change the server-stored uploader_name. There are hard limits on how many upload devices can be registered; refer to `Google's docs `__. There have been limits on deauthorizing devices in the past, so it's smart not to register more devices than necessary. """ return (self._oauth_login(oauth_credentials) and self._perform_upauth(uploader_id, uploader_name)) def _perform_upauth(self, uploader_id, uploader_name): """Auth or register ourselves as an upload client. Return True on success; see :py:func:`login` for params. """ if uploader_id is None: mac_int = getmac() if (mac_int >> 40) % 2: self.session.logout() raise OSError('a valid MAC could not be determined.' ' Provide uploader_id (and be' ' sure to provide the same one on future runs).') else: # distinguish us from a Music Manager on this machine mac_int = (mac_int + 1) % (1 << 48) uploader_id = utils.create_mac_string(mac_int) if not utils.is_valid_mac(uploader_id): self.session.logout() raise ValueError('uploader_id is not in a valid form.' '\nProvide 6 pairs of hex digits' ' with capital letters', ' (eg "00:11:22:33:AA:BB")') if uploader_name is None: uploader_name = gethostname() + u" (gmusicapi-%s)" % gmusicapi.__version__ try: # this is a MM-specific step that might register a new device. self._make_call(musicmanager.AuthenticateUploader, uploader_id, uploader_name) self.logger.info("successful upauth") self.uploader_id = uploader_id self.uploader_name = uploader_name except CallFailure: self.logger.exception("upauth failure") self.session.logout() return False return True def logout(self, revoke_oauth=False): """Forgets local authentication in this Client instance. :param revoke_oauth: if True, oauth credentials will be permanently revoked. If credentials came from a file, it will be deleted. Returns ``True`` on success.""" # TODO the login/logout stuff is all over the place success = True if revoke_oauth: try: # this automatically deletes a Storage file, if present self.session._oauth_creds.revoke(httplib2.Http()) except TokenRevokeError: self.logger.exception("could not revoke oauth credentials") success = False self.uploader_id = None self.uploader_name = None return success and super(Musicmanager, self).logout() # mostly copy-paste from Webclient.get_all_songs. # not worried about overlap in this case; the logic of either could change. def get_uploaded_songs(self, incremental=False): """Returns a list of dictionaries, each with the following keys: ``('id', 'title', 'album', 'album_artist', 'artist', 'track_number', 'track_size', 'disc_number', 'total_disc_count')``. All Access tracks that were added to the library will not be included, only tracks uploaded/matched by the user. :param incremental: if True, return a generator that yields lists of at most 1000 dictionaries as they are retrieved from the server. This can be useful for presenting a loading bar to a user. """ to_return = self._get_all_songs() if not incremental: to_return = [song for chunk in to_return for song in chunk] return to_return # mostly copy-paste from Webclient.get_all_songs. # not worried about overlap in this case; the logic of either could change. def get_purchased_songs(self, incremental=False): """Returns a list of dictionaries, each with the following keys: ``('id', 'title', 'album', 'album_artist', 'artist', 'track_number', 'track_size', 'disc_number', 'total_disc_count')``. :param incremental: if True, return a generator that yields lists of at most 1000 dictionaries as they are retrieved from the server. This can be useful for presenting a loading bar to a user. """ to_return = self._get_all_songs(export_type=2) if not incremental: to_return = [song for chunk in to_return for song in chunk] return to_return @staticmethod def _track_info_to_dict(track_info): """Given a download_pb2.DownloadTrackInfo, return a dictionary.""" # figure it's better to hardcode keys here than use introspection # and risk returning a new field all of a sudden. return dict((field, getattr(track_info, field)) for field in ('id', 'title', 'album', 'album_artist', 'artist', 'track_number', 'track_size', 'disc_number', 'total_disc_count')) def _get_all_songs(self, export_type=1): """Return a generator of song chunks.""" get_next_chunk = True # need to spoof .continuation_token access, and # can't add attrs to object(). Can with functions. lib_chunk = lambda: 0 # noqa lib_chunk.continuation_token = None while get_next_chunk: lib_chunk = self._make_call(musicmanager.ListTracks, self.uploader_id, lib_chunk.continuation_token, export_type) yield [self._track_info_to_dict(info) for info in lib_chunk.download_track_info] get_next_chunk = lib_chunk.HasField('continuation_token') @utils.enforce_id_param def download_song(self, song_id): """Download an uploaded or purchased song from your library. Subscription tracks can't be downloaded with this method. Returns a tuple ``(u'suggested_filename', 'audio_bytestring')``. The filename will be what the Music Manager would save the file as, presented as a unicode string with the proper file extension. You don't have to use it if you don't want. :param song_id: a single uploaded or purchased song id. To write the song to disk, use something like:: filename, audio = mm.download_song(an_id) # if open() throws a UnicodeEncodeError, either use # filename.encode('utf-8') # or change your default encoding to something sane =) with open(filename, 'wb') as f: f.write(audio) Unlike with :py:func:`Webclient.get_song_download_info `, there is no download limit when using this interface. Also unlike the Webclient, downloading a track requires authentication. Returning a url does not suffice, since retrieving a track without auth will produce an http 500. """ url = self._make_call(musicmanager.GetDownloadLink, song_id, self.uploader_id)['url'] response = self._make_call(musicmanager.DownloadTrack, url) cd_header = response.headers['content-disposition'] filename = unquote(cd_header.split("filename*=UTF-8''")[-1]) return (filename, response.content) def get_quota(self): """Returns a tuple of (number of uploaded tracks, allowed number of uploaded tracks).""" if self.uploader_id is None: raise NotLoggedIn("Not authenticated as an upload device;" " run Musicmanager.login(...perform_upload_auth=True...)" " first.") client_state = self._make_call( musicmanager.GetClientState, self.uploader_id).clientstate_response return (client_state.total_track_count, client_state.locker_track_limit) @utils.accept_singleton(basestring) @utils.empty_arg_shortcircuit(return_code='{}') def upload(self, filepaths, enable_matching=False, enable_transcoding=True, transcode_quality='320k'): """Uploads the given filepaths. All non-mp3 files will be transcoded before being uploaded. This is a limitation of Google's backend. An available installation of ffmpeg or avconv is required in most cases: see `the installation page `__ for details. Returns a 3-tuple ``(uploaded, matched, not_uploaded)`` of dictionaries, eg:: ( {'': ''}, # uploaded {'': ''}, # matched {'': ''} # not uploaded ) :param filepaths: a list of filepaths, or a single filepath. :param enable_matching: if ``True``, attempt to use `scan and match `__ to avoid uploading every song. This requires ffmpeg or avconv. **WARNING**: currently, mismatched songs can *not* be fixed with the 'Fix Incorrect Match' button nor :py:func:`report_incorrect_match `. They would have to be deleted and reuploaded with matching disabled (or with the Music Manager). Fixing matches from gmusicapi may be supported in a future release; see issue `#89 `__. :param enable_transcoding: if ``False``, non-MP3 files that aren't matched using `scan and match `__ will not be uploaded. :param transcode_quality: if int, pass to ffmpeg/avconv ``-q:a`` for libmp3lame (`lower-better int, `__). If string, pass to ffmpeg/avconv ``-b:a`` (eg ``'128k'`` for an average bitrate of 128k). The default is 320kbps cbr (the highest possible quality). All Google-supported filetypes are supported; see `Google's documentation `__. If ``PERMANENT_ERROR`` is given as a not_uploaded reason, attempts to reupload will never succeed. The file will need to be changed before the server will reconsider it; the easiest way is to change metadata tags (it's not important that the tag be uploaded, just that the contents of the file change somehow). """ if self.uploader_id is None or self.uploader_name is None: raise NotLoggedIn("Not authenticated as an upload device;" " run Api.login(...perform_upload_auth=True...)" " first.") # TODO there is way too much code in this function. # To return. uploaded = {} matched = {} not_uploaded = {} # Gather local information on the files. local_info = {} # {clientid: (path, Track)} for path in filepaths: try: track = musicmanager.UploadMetadata.fill_track_info(path) except BaseException as e: self.logger.exception("problem gathering local info of '%r'", path) user_err_msg = str(e) if 'Non-ASCII strings must be converted to unicode' in str(e): # This is a protobuf-specific error; they require either ascii or unicode. # To keep behavior consistent, make no effort to guess - require users # to decode first. user_err_msg = ("nonascii bytestrings must be decoded to unicode" " (error: '%s')" % user_err_msg) not_uploaded[path] = user_err_msg else: local_info[track.client_id] = (path, track) if not local_info: return uploaded, matched, not_uploaded # TODO allow metadata faking # Upload metadata; the server tells us what to do next. res = self._make_call(musicmanager.UploadMetadata, [t for (path, t) in local_info.values()], self.uploader_id) # TODO checking for proper contents should be handled in verification md_res = res.metadata_response responses = [r for r in md_res.track_sample_response] sample_requests = [req for req in md_res.signed_challenge_info] # Send scan and match samples if requested. for sample_request in sample_requests: path, track = local_info[sample_request.challenge_info.client_track_id] bogus_sample = None if not enable_matching: bogus_sample = b'' # just send empty bytes try: res = self._make_call(musicmanager.ProvideSample, path, sample_request, track, self.uploader_id, bogus_sample) except (IOError, ValueError) as e: self.logger.warning("couldn't create scan and match sample for '%r': %s", path, str(e)) not_uploaded[path] = str(e) else: responses.extend(res.sample_response.track_sample_response) # Read sample responses and prep upload requests. to_upload = {} # {serverid: (path, Track, do_not_rematch?)} for sample_res in responses: path, track = local_info[sample_res.client_track_id] if sample_res.response_code == upload_pb2.TrackSampleResponse.MATCHED: self.logger.info("matched '%r' to sid %s", path, sample_res.server_track_id) matched[path] = sample_res.server_track_id if not enable_matching: self.logger.error("'%r' was matched without matching enabled", path) elif sample_res.response_code == upload_pb2.TrackSampleResponse.UPLOAD_REQUESTED: to_upload[sample_res.server_track_id] = (path, track, False) else: # there was a problem # report the symbolic name of the response code enum for debugging enum_desc = upload_pb2._TRACKSAMPLERESPONSE.enum_types[0] res_name = enum_desc.values_by_number[sample_res.response_code].name err_msg = "TrackSampleResponse code %s: %s" % (sample_res.response_code, res_name) if res_name == 'ALREADY_EXISTS': # include the sid, too # this shouldn't be relied on externally, but I use it in # tests - being surrounded by parens is how it's matched err_msg += "(%s)" % sample_res.server_track_id self.logger.warning("upload of '%r' rejected: %s", path, err_msg) not_uploaded[path] = err_msg # Send upload requests. if to_upload: # TODO reordering requests could avoid wasting time waiting for reup sync self._make_call(musicmanager.UpdateUploadState, 'start', self.uploader_id) for server_id, (path, track, do_not_rematch) in to_upload.items(): # It can take a few tries to get an session. should_retry = True attempts = 0 while should_retry and attempts < 10: session = self._make_call(musicmanager.GetUploadSession, self.uploader_id, len(uploaded), track, path, server_id, do_not_rematch) attempts += 1 got_session, error_details = \ musicmanager.GetUploadSession.process_session(session) if got_session: self.logger.info("got an upload session for '%r'", path) break should_retry, reason, error_code = error_details self.logger.debug("problem getting upload session: %s\ncode=%s retrying=%s", reason, error_code, should_retry) if error_code == 200 and do_not_rematch: # reupload requests need to wait on a server sync # 200 == already uploaded, so force a retry in this case should_retry = True time.sleep(6) # wait before retrying else: err_msg = "GetUploadSession error %s: %s" % (error_code, reason) self.logger.warning("giving up on upload session for '%r': %s", path, err_msg) not_uploaded[path] = err_msg continue # to next upload # got a session, do the upload # this terribly inconsistent naming isn't my fault: Google-- session = session['sessionStatus'] external = session['externalFieldTransfers'][0] session_url = external['putInfo']['url'] content_type = external.get('content_type', 'audio/mpeg') if track.original_content_type != locker_pb2.Track.MP3: if enable_transcoding: try: self.logger.info("transcoding '%r' to mp3", path) contents = utils.transcode_to_mp3(path, quality=transcode_quality) except (IOError, ValueError) as e: self.logger.warning("error transcoding %r: %s", path, e) not_uploaded[path] = "transcoding error: %s" % e continue else: not_uploaded[path] = "transcoding disabled" continue else: with open(path, 'rb') as f: contents = f.read() upload_response = self._make_call(musicmanager.UploadFile, session_url, content_type, contents) success = upload_response.get('sessionStatus', {}).get('state') if success: uploaded[path] = server_id else: # 404 == already uploaded? serverside check on clientid? self.logger.debug("could not finalize upload of '%r'. response: %s", path, upload_response) not_uploaded[path] = 'could not finalize upload; details in log' self._make_call(musicmanager.UpdateUploadState, 'stopped', self.uploader_id) return uploaded, matched, not_uploaded gmusicapi-12.1.1/gmusicapi/clients/shared.py0000664000175000017500000001555713400371522021327 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import, unicode_literals from builtins import * # noqa from past.builtins import basestring import logging import os from gmusicapi.utils import utils from future.utils import with_metaclass from oauth2client.client import OAuth2WebServerFlow import oauth2client.file import webbrowser class _Base(with_metaclass(utils.DocstringInheritMeta, object)): """Factors out common client setup.""" _session_class = utils.NotImplementedField num_clients = 0 # used to disambiguate loggers def __init__(self, logger_basename, debug_logging, validate, verify_ssl): """ :param debug_logging: each Client has a ``logger`` member. The logger is named ``gmusicapi.`` and will propogate to the ``gmusicapi`` root logger. If this param is ``True``, handlers will be configured to send this client's debug log output to disk, with warnings and above printed to stderr. `Appdirs `__ ``user_log_dir`` is used by default. Users can run:: from gmusicapi.utils import utils print utils.log_filepath to see the exact location on their system. If ``False``, no handlers will be configured; users must create their own handlers. Completely ignoring logging is dangerous and not recommended. The Google Music protocol can change at any time; if something were to go wrong, the logs would be necessary for recovery. :param validate: if False, do not validate server responses against known schemas. This helps to catch protocol changes, but requires significant cpu work. This arg is stored as ``self.validate`` and can be safely modified at runtime. :param verify_ssl: if False, exceptions will not be raised if there are problems verifying SSL certificates. Be wary of using this option; it's almost always better to fix the machine's SSL configuration than to ignore errors. """ # this isn't correct if init is called more than once, so we log the # client name below to avoid confusion for people reading logs _Base.num_clients += 1 logger_name = "gmusicapi.%s%s" % (logger_basename, _Base.num_clients) self._cache = {} self.logger = logging.getLogger(logger_name) self.validate = validate self._verify_ssl = verify_ssl def setup_session(s): s.verify = self._verify_ssl self.session = self._session_class(rsession_setup=setup_session) if debug_logging: utils.configure_debug_log_handlers(self.logger) self.logger.info("initialized") self.logout() def _make_call(self, protocol, *args, **kwargs): """Returns the response of a protocol.Call. args/kwargs are passed to protocol.perform. CallFailure may be raised.""" return protocol.perform(self.session, self.validate, *args, **kwargs) def is_authenticated(self): """Returns ``True`` if the Api can make an authenticated request.""" return self.session.is_authenticated def logout(self): """Forgets local authentication and cached properties in this Api instance. Returns ``True`` on success.""" # note to clients: this will be called during __init__. self.session.logout() self._cache.clear() # Clear the instance of all cached properties. self.logger.info("logged out") return True class _OAuthClient(_Base): _path_sentinel = object() # the default path for credential storage OAUTH_FILEPATH = utils.NotImplementedField @classmethod def perform_oauth(cls, storage_filepath=_path_sentinel, open_browser=False): """Provides a series of prompts for a user to follow to authenticate. Returns ``oauth2client.client.OAuth2Credentials`` when successful. In most cases, this should only be run once per machine to store credentials to disk, then never be needed again. If the user refuses to give access, ``oauth2client.client.FlowExchangeError`` is raised. :param storage_filepath: a filepath to write the credentials to, or ``None`` to not write the credentials to disk (which is not recommended). `Appdirs `__ ``user_data_dir`` is used by default. Check the OAUTH_FILEPATH field on this class to see the exact location that will be used. :param open_browser: if True, attempt to open the auth url in the system default web browser. The url will be printed regardless of this param's setting. This flow is intentionally very simple. For complete control over the OAuth flow, pass an ``oauth2client.client.OAuth2Credentials`` to :func:`login` instead. """ if storage_filepath is cls._path_sentinel: storage_filepath = cls.OAUTH_FILEPATH flow = OAuth2WebServerFlow(**cls._session_class.oauth._asdict()) auth_uri = flow.step1_get_authorize_url() print() print("Visit the following url:\n %s" % auth_uri) if open_browser: print() print('Opening your browser to it now...', end=' ') webbrowser.open(auth_uri) print('done.') print("If you don't see your browser, you can just copy and paste the url.") print() code = input("Follow the prompts, then paste the auth code here and hit enter: ") credentials = flow.step2_exchange(code) if storage_filepath is not None: if storage_filepath == cls.OAUTH_FILEPATH: utils.make_sure_path_exists(os.path.dirname(cls.OAUTH_FILEPATH), 0o700) storage = oauth2client.file.Storage(storage_filepath) storage.put(credentials) return credentials def _oauth_login(self, oauth_credentials): """Return True on success.""" if isinstance(oauth_credentials, basestring): oauth_file = oauth_credentials if oauth_file == self.OAUTH_FILEPATH: utils.make_sure_path_exists(os.path.dirname(self.OAUTH_FILEPATH), 0o700) storage = oauth2client.file.Storage(oauth_file) oauth_credentials = storage.get() if oauth_credentials is None: self.logger.warning("could not retrieve oauth credentials from '%r'", oauth_file) return False if not self.session.login(oauth_credentials): self.logger.warning("failed to authenticate") return False self.logger.info("oauth successful") return True gmusicapi-12.1.1/gmusicapi/clients/webclient.py0000664000175000017500000004034513374625453022045 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import, unicode_literals from future.utils import PY3 from past.builtins import basestring from builtins import * # noqa import warnings if PY3: from urllib.parse import parse_qsl, urlparse else: from urlparse import parse_qsl, urlparse import gmusicapi from gmusicapi.clients.shared import _Base from gmusicapi.exceptions import GmusicapiWarning from gmusicapi.protocol import webclient from gmusicapi.utils import utils import gmusicapi.session class Webclient(_Base): """Allows library management and streaming by posing as the music.google.com webclient. Uploading is not supported by this client (use the :class:`Musicmanager` to upload). Any methods in this class that are duplicated by the :class:`Mobileclient` are deprecated, and will generate a warning at runtime. The following methods are *not* deprecated: * :func:`get_shared_playlist_info` * :func:`get_song_download_info` * :func:`get_stream_urls` * :func:`get_stream_audio` * :func:`report_incorrect_match` * :func:`upload_album_art` """ _session_class = gmusicapi.session.Webclient def __init__(self, debug_logging=True, validate=True, verify_ssl=True): warnings.warn( "Webclient functionality is not tested nor well supported. " "Use Mobileclient or Musicmanager if possible.", GmusicapiWarning ) super(Webclient, self).__init__(self.__class__.__name__, debug_logging, validate, verify_ssl) def login(self, email, password): """Authenticates the webclient. Returns ``True`` on success, ``False`` on failure. :param email: eg ``'test@gmail.com'`` or just ``'test'``. :param password: the account's password. This is not stored locally, and is sent securely over SSL. App-specific passwords are not supported on the webclient. Users who don't use two-factor auth will likely need to enable `less secure login `__. If this is needed, a warning will be logged during login (which will print to stderr in the default logging configuration). """ if not self.session.login(email, password): self.logger.info("failed to authenticate") return False self.logger.info("authenticated") return True def logout(self): return super(Webclient, self).logout() def get_shared_playlist_info(self, share_token): """ Returns a dictionary with four keys: author, description, num_tracks, and title. :param share_token: from ``playlist['shareToken']``, or a playlist share url (``https://play.google.com/music/playlist/``). Note that tokens from urls will need to be url-decoded, eg ``AM...%3D%3D`` becomes ``AM...==``. """ res = self._make_call(webclient.GetSharedPlaylist, '', share_token) num_tracks = len(res[1][0]) md = res[1][1] return { u'author': md[8], u'description': md[7], u'num_tracks': num_tracks, u'title': md[1], } @utils.enforce_id_param def get_song_download_info(self, song_id): """Returns a tuple: ``('', )``. :param song_id: a single song id. ``url`` will be ``None`` if the download limit is exceeded. GM allows 2 downloads per song. The download count may not always be accurate, and the 2 download limit seems to be loosely enforced. This call alone does not count towards a download - the count is incremented when ``url`` is retrieved. """ # TODO the protocol expects a list of songs - could extend with accept_singleton info = self._make_call(webclient.GetDownloadInfo, [song_id]) url = info.get('url') return (url, info["downloadCounts"][song_id]) @utils.enforce_id_param def get_stream_urls(self, song_id): """Returns a list of urls that point to a streamable version of this song. If you just need the audio and are ok with gmusicapi doing the download, consider using :func:`get_stream_audio` instead. This abstracts away the differences between different kinds of tracks: * normal tracks return a single url * All Access tracks return multiple urls, which must be combined :param song_id: a single song id. While acquiring the urls requires authentication, retreiving the contents does not. However, there are limitations on how the stream urls can be used: * the urls expire after a minute * only one IP can be streaming music at once. Other attempts will get an http 403 with ``X-Rejected-Reason: ANOTHER_STREAM_BEING_PLAYED``. *This is only intended for streaming*. The streamed audio does not contain metadata. Use :func:`get_song_download_info` or :func:`Musicmanager.download_song ` to download files with metadata. """ res = self._make_call(webclient.GetStreamUrl, song_id) try: return [res['url']] except KeyError: return res['urls'] @utils.enforce_id_param def get_stream_audio(self, song_id, use_range_header=None): """Returns a bytestring containing mp3 audio for this song. :param song_id: a single song id :param use_range_header: in some cases, an HTTP range header can be used to save some bandwidth. However, there's no guarantee that the server will respect it, meaning that the client may get back an unexpected response when using it. There are three possible values for this argument: * None: (default) send header; fix response locally on problems * True: send header; raise IOError on problems * False: do not send header """ urls = self.get_stream_urls(song_id) # TODO shouldn't session.send be used throughout? if len(urls) == 1: return self.session._rsession.get(urls[0]).content # AA tracks are separated into multiple files. # the url contains the range of each file to be used. range_pairs = [[int(s) for s in val.split('-')] for url in urls for key, val in parse_qsl(urlparse(url)[4]) if key == 'range'] stream_pieces = bytearray() prev_end = 0 headers = None for url, (start, end) in zip(urls, range_pairs): if use_range_header or use_range_header is None: headers = {'Range': 'bytes=' + str(prev_end - start) + '-'} audio = self.session._rsession.get(url, headers=headers).content if end - prev_end != len(audio) - 1: # content length is not in the right range if use_range_header: # the user didn't want automatic response fixup raise IOError('use_range_header is True but the response' ' was not the correct content length.' ' This might be caused by a (poorly-written) http proxy.') # trim to the proper range audio = audio[prev_end - start:] stream_pieces.extend(audio) prev_end = end + 1 return bytes(stream_pieces) @utils.accept_singleton(basestring) @utils.enforce_ids_param @utils.empty_arg_shortcircuit def report_incorrect_match(self, song_ids): """Equivalent to the 'Fix Incorrect Match' button, this requests re-uploading of songs. Returns the song_ids provided. :param song_ids: a list of song ids to report, or a single song id. Note that if you uploaded a song through gmusicapi, it won't be reuploaded automatically - this currently only works for songs uploaded with the Music Manager. See issue `#89 `__. This should only be used on matched tracks (``song['type'] == 6``). """ self._make_call(webclient.ReportBadSongMatch, song_ids) return song_ids @utils.accept_singleton(basestring) @utils.enforce_ids_param @utils.empty_arg_shortcircuit def upload_album_art(self, song_ids, image_filepath): """Uploads an image and sets it as the album art for songs. Returns a url to the image on Google's servers. :param song_ids: a list of song ids, or a single song id. :param image_filepath: filepath of the art to use. jpg and png are known to work. This function will *always* upload the provided image, even if it's already uploaded. If the art is already uploaded and set for another song, copy over the value of the ``'albumArtUrl'`` key using :func:`Mobileclient.change_song_metadata` instead. """ res = self._make_call(webclient.UploadImage, image_filepath) url = res['imageUrl'] song_dicts = [dict((('id', id), ('albumArtUrl', url))) for id in song_ids] self._make_call(webclient.ChangeSongMetadata, song_dicts) return url @utils.accept_singleton(dict) @utils.empty_arg_shortcircuit def change_song_metadata(self, songs): """Changes metadata of songs. Returns a list of the song ids changed. :param songs: a list of song dictionaries, each dictionary must contain valid song 'id' The following fields are supported: title, album, albumArtist, artist """ self._make_call(webclient.ChangeSongMetadata, songs) return list(song['id'] for song in songs) # deprecated methods follow: @utils.deprecated('prefer Mobileclient.create_playlist') def create_playlist(self, name, description=None, public=False): """ Creates a playlist and returns its id. :param name: the name of the playlist. :param description: (optional) the description of the playlist. :param public: if True and the user has All Access, create a shared playlist. """ res = self._make_call(webclient.CreatePlaylist, name, description, public) return res[1][0] @utils.deprecated('prefer Mobileclient.get_registered_devices') def get_registered_devices(self): """ Returns a list of dictionaries representing devices associated with the account. Performing the :class:`Musicmanager` OAuth flow will register a device of type 1. Installing the Google Music app on an android or ios device and logging into it will register a device of type 2 or 3, which is used for streaming with the :class:`Mobileclient`. Here is an example response:: [ { u'deviceType': 1, # laptop/desktop u'id': u'00:11:22:33:AA:BB', u'lastAccessedFormatted': u'May 24, 2015', u'lastAccessedTimeMillis': 1432468588200, # utc-millisecond u'lastEventTimeMillis': 1434211605335, u'name': u'my computer'}, }, { u'deviceType': 2, # android device u'carrier': u'Google', u'id': u'0x00112233aabbccdd', # remove 0x when streaming u'lastAccessedFormatted': u'September 19, 2015', u'lastAccessedTimeMillis': 1442706069906, u'lastEventTimeMillis': 1435271137193, u'manufacturer': u'Asus', u'model': u'Nexus 7', u'name': u'my nexus 7' }, { u'deviceType': 3, # ios device u'id': u'ios:01234567-0123-0123-0123-0123456789AB', u'lastAccessedFormatted': u'June 25, 2015', u'lastAccessedTimeMillis': 1435271588780, u'lastEventTimeMillis': 1435271442417, u'name': u'my iphone' } ] """ # TODO sessionid stuff res = self._make_call(webclient.GetSettings, '') return res['settings']['uploadDevice'] @utils.accept_singleton(basestring) @utils.enforce_ids_param @utils.empty_arg_shortcircuit @utils.deprecated('prefer Mobileclient.delete_songs') def delete_songs(self, song_ids): """**Deprecated**: prefer :func:`Mobileclient.delete_songs`. Deletes songs from the entire library. Returns a list of deleted song ids. :param song_ids: a list of song ids, or a single song id. """ res = self._make_call(webclient.DeleteSongs, song_ids) return res['deleteIds'] @utils.accept_singleton(basestring, 2) @utils.enforce_ids_param(2) @utils.enforce_id_param @utils.empty_arg_shortcircuit(position=2) @utils.deprecated('prefer Mobileclient.add_songs_to_playlist') def add_songs_to_playlist(self, playlist_id, song_ids): """**Deprecated**: prefer :func:`Mobileclient.add_songs_to_playlist`. Appends songs to a playlist. Returns a list of (song id, playlistEntryId) tuples that were added. :param playlist_id: id of the playlist to add to. :param song_ids: a list of song ids, or a single song id. Playlists have a maximum size of 1000 songs. """ res = self._make_call(webclient.AddToPlaylist, playlist_id, song_ids) new_entries = res['songIds'] return [(e['songId'], e['playlistEntryId']) for e in new_entries] @utils.accept_singleton(basestring, 2) @utils.enforce_ids_param(2) @utils.enforce_id_param @utils.empty_arg_shortcircuit(position=2) @utils.deprecated('prefer Mobileclient.remove_entries_from_playlist') def remove_songs_from_playlist(self, playlist_id, sids_to_match): """**Deprecated**: prefer :func:`Mobileclient.remove_entries_from_playlist`. Removes all copies of the given song ids from a playlist. Returns a list of removed (sid, eid) pairs. :param playlist_id: id of the playlist to remove songs from. :param sids_to_match: a list of song ids to match, or a single song id. This does *not always* the inverse of a call to :func:`add_songs_to_playlist`, since multiple copies of the same song are removed. """ playlist_tracks = self.get_playlist_songs(playlist_id) sid_set = set(sids_to_match) matching_eids = [t["playlistEntryId"] for t in playlist_tracks if t["id"] in sid_set] if matching_eids: # Call returns "sid_eid" strings. sid_eids = self._remove_entries_from_playlist(playlist_id, matching_eids) return [s.split("_") for s in sid_eids] else: return [] @utils.accept_singleton(basestring, 2) @utils.empty_arg_shortcircuit(position=2) def _remove_entries_from_playlist(self, playlist_id, entry_ids_to_remove): """Removes entries from a playlist. Returns a list of removed "sid_eid" strings. :param playlist_id: the playlist to be modified. :param entry_ids: a list of entry ids, or a single entry id. """ # GM requires the song ids in the call as well; find them. playlist_tracks = self.get_playlist_songs(playlist_id) remove_eid_set = set(entry_ids_to_remove) e_s_id_pairs = [(t["id"], t["playlistEntryId"]) for t in playlist_tracks if t["playlistEntryId"] in remove_eid_set] num_not_found = len(entry_ids_to_remove) - len(e_s_id_pairs) if num_not_found > 0: self.logger.warning("when removing, %d entry ids could not be found in playlist id %s", num_not_found, playlist_id) # Unzip the pairs. sids, eids = list(zip(*e_s_id_pairs)) res = self._make_call(webclient.DeleteSongs, sids, playlist_id, eids) return res['deleteIds'] gmusicapi-12.1.1/gmusicapi/exceptions.py0000664000175000017500000000324713374625453020611 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- """Custom exceptions used across the project.""" from __future__ import print_function, division, absolute_import, unicode_literals from future.utils import python_2_unicode_compatible from builtins import * # noqa @python_2_unicode_compatible class CallFailure(Exception): """Exception raised when a Google Music server responds that a call failed. Attributes: callname -- name of the protocol.Call that failed """ def __init__(self, message, callname): Exception.__init__(self, message) self.callname = callname def __str__(self): return "%s: %s" % (self.callname, Exception.__str__(self)) class ParseException(Exception): """Thrown by Call.parse_response on errors.""" pass class ValidationException(Exception): """Thrown by Transaction.verify_res_schema on errors.""" pass class AlreadyLoggedIn(Exception): pass class NotLoggedIn(Exception): pass class NotSubscribed(Exception): def __init__(self, *args): if len(args) >= 1: args = list(args) args[0] += " (https://goo.gl/v1wVHT)" args = tuple(args) else: args = ("Subscription required. (https://goo.gl/v1wVHT)",) self.args = args class GmusicapiWarning(UserWarning): pass class InvalidDeviceId(Exception): def __init__(self, message, ids): if ids: message += 'Your valid device IDs are:\n* %s' % '\n* '.join(ids) else: message += 'It looks like your account does not have any ' 'valid device IDs.' super(InvalidDeviceId, self).__init__(message) self.valid_device_ids = ids gmusicapi-12.1.1/gmusicapi/gmtools/0000755000175000017500000000000013512455420017517 5ustar simonsimon00000000000000gmusicapi-12.1.1/gmusicapi/gmtools/__init__.py0000600000175000017500000000003012667715127021626 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- gmusicapi-12.1.1/gmusicapi/gmtools/tools.py0000664000175000017500000003450413374625453021254 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- """Tools for manipulating client-received Google Music data.""" from __future__ import print_function, division, absolute_import, unicode_literals from builtins import * # noqa import operator import re import collections from collections import Counter from functools import reduce def get_id_pairs(track_list): """Create a list of (sid, eid) tuples from a list of tracks. Tracks without an eid will have an eid of None.""" return [(t["id"], t.get("playlistEntryId")) for t in track_list] def find_playlist_changes(orig_tracks, modified_tracks): """Finds the changes between two playlists. Returns a tuple of (deletions, additions, staying). Deletions and additions are both Counters of (sid, eid) tuples; staying is a set of (sid, eid) tuples. :param old: the original playlist. :param modified: the modified playlist.""" s_pairs = get_id_pairs(orig_tracks) # Three cases for desired pairs: # 1: (sid, eid from this playlist): either no action or add # (if someone adds a dupe from the same playlist) # 2: (sid, eid not from this playlist): add # 3: (sid, None): add d_pairs = get_id_pairs(modified_tracks) # Counters are multisets. s_count = Counter(s_pairs) d_count = Counter(d_pairs) to_del = s_count - d_count to_add = d_count - s_count to_keep = set(s_count & d_count) # guaranteed to be counts of 1 return (to_del, to_add, to_keep) def filter_song_md(song, md_list=['id'], no_singletons=True): """Returns a list of desired metadata from a song. Does not modify the given song. :param song: Dictionary representing a GM song. :param md_list: (optional) the ordered list of metadata to select. :param no_singletons: (optional) if md_list is of length 1, return the data, not a singleton list. """ filtered = [song[md_type] for md_type in md_list] if len(md_list) == 1 and no_singletons: return filtered[0] else: return filtered def build_song_rep(song, md_list=['title', 'artist', 'album'], divider=" - "): """Returns a string of the requested metadata types. The order of md_list determines order in the string. :param song: Dictionary representing a GM song. :param md_list: (optional) list of valid GM metadata types. :param divider: (optional) string to join the metadata. """ filtered = filter_song_md(song, md_list, no_singletons=False) return divider.join(filtered) def reorder_to(l, order): """Returns a list, reordered to a specific ordering. :param l: the list to reorder. It is not modified. :param order: a list containing the new ordering, eg [2,1,0] to reverse a list of length 3 """ # Zip on ordering, sort by it, then remove ordering. return [el[1] for el in sorted(zip(order, l), key=lambda el: el[0])] def build_queries_from(f, regex, cap_types, cap_pr, encoding='ascii'): """Returns a list of queries from the given file. Queries have the form [(, ), ...] :param f: opened file, ready to read. :param regex: a compiled regex to capture query info from file lines. :param cap_types: the GM metadata types of the regex captures. :param cap_pr: the priority of the captures. :param encoding: (optional) encoding of the file. """ queries = [] for line in f: matches = regex.match(line) if matches: # Zip captures to their types and order by priority to build a query. query = reorder_to( list(zip(matches.groups(), cap_types)), cap_pr) queries.append(query) return queries def build_query_rep(query, divider=" - "): """Build a string representation of a query, without metadata types""" return divider.join([el[0] for el in query]) # Not mine. From: http://en.wikipedia.org/wiki/Function_composition_(computer_science) def compose(*funcs, **kfuncs): """Compose a group of functions (f(g(h(..)))) into (fogoh...)(...)""" return reduce(lambda f, g: lambda *args, **kaargs: f(g(*args, **kaargs)), funcs) class SongMatcher(object): """Matches GM songs to user-provided metadata.""" def __init__(self, songs, log_metadata=['title', 'artist', 'album']): """Prepares songs for matching and determines logging options. :param songs: list of GM songs to match against. :param log_metadata: list of valid GM metadata types to show in the log. order given will be order outputted. """ # If match times are a problem, could # read to an indexed format here. self.library = songs # Lines of a log of how matching went. self.log_lines = [] self.log_metadata = log_metadata def build_log(self): """Returns a string built from the current log lines.""" encoded_lines = [line.encode('utf-8') for line in self.log_lines] return "\n".join(encoded_lines) def build_song_for_log(self, song): """Returns a string built from a song using log options. :param song: """ return build_song_rep(song, self.log_metadata) class SearchModifier(object): """Controls how to query the library. Implementations define a comparator, and 2 functions (transformers) to modify the query and song data on the fly. Sometimes it makes sense to chain implementations. In this case, transformers are composed and the most outward comparator is used. """ def __init__(self, q_t, s_t, comp): # Comparator - defines how to compare query and song data. # f(song data, query) -> truthy value self.comp = comp # Query and song transformers - # manipulate query, song before comparison. # f(unicode) -> unicode self.q_t = q_t self.s_t = s_t # Some modifiers that are useful in my library: # Ignore capitalization: ignore_caps = SearchModifier( # Change query and song to lowercase, # before comparing with ==. str.lower, str.lower, operator.eq ) # Wildcard punctuation (also non ascii chars): ignore_punc = SearchModifier( # Replace query with a regex, where punc matches any (or no) characters. lambda q: re.sub(r"[^a-zA-Z0-9\s]", ".*", q), # Don't change the song. lambda s: s, # The comparator becomes regex matching. lambda sd, q: re.search(q, sd) ) implemented_modifiers = (ignore_caps, ignore_punc) # The modifiers and order to be used in auto query mode. auto_modifiers = implemented_modifiers # Tiebreakers are used when there are multiple results from a query. @staticmethod def manual_tiebreak(query, results): """Prompts a user to choose a result from multiple. For use with query_library as a tiebreaker. Returns a singleton list or None. :param query: the original query. :param results: list of results. """ print() print("Manual tiebreak for query:") print(build_query_rep(query).encode('utf-8')) print() print("Enter the number next to your choice:") print() print("0: None of these.") menu_lines = [] key = 1 for song in results: menu_lines.append( str(key) + ": " + build_song_rep(song).encode('utf-8')) key += 1 print("\n".join(menu_lines)) choice = -1 while not (0 <= choice <= len(results)): try: choice = int(input("Choice: ")) except ValueError: pass return None if choice == 0 else [results[choice - 1]] # Tiebreaker which does nothing with results. @staticmethod def no_tiebreak(query, results): return results # Exception thrown when a tie is broken. class TieBroken(Exception): def __init__(self, results): self.results = results # A named tuple to hold the frozen args when querying recursively. QueryState = collections.namedtuple('QueryState', 'orig t_breaker mods auto') def query_library(self, query, tie_breaker=no_tiebreak, modifiers=None, auto=False): """Queries the library for songs. returns a list of matches, or None. """ if not modifiers: modifiers = [] try: if not auto: return self.query_library_rec(query, self.library, self.QueryState(query, tie_breaker, modifiers, auto)) else: # Auto mode attempts a search with the current modifiers. # If we get 1 result, we return it. # If we get no results, we add the next mod from auto_modifers and try again. # If we get many results, we branch and try with another modifier. # On no results, we tiebreak our old results. # Otherwise, we return the branched results. current_mods = modifiers[:] # Be ready to use any mods from the auto list which we aren't using already. future_mods = (m for m in self.auto_modifiers if m not in modifiers) while True: # broken when future_mods runs out # will not break ties in auto mode results = self.query_library_rec( query, self.library, self.QueryState(query, tie_breaker, current_mods, auto)) if not results: try: current_mods.append(next(future_mods)) except StopIteration: return results elif len(results) == 1: return results else: # Received many results from our current search. # Branch; try more modifers to try and improve. # If results, use them; otherwise tiebreak ours. try: current_mods.append(next(future_mods)) except StopIteration: raise self.TieBroken(tie_breaker.__func__(query, results)) next_results = self.query_library(query, tie_breaker, current_mods, auto) if not next_results: raise self.TieBroken(tie_breaker.__func__(query, results)) else: return next_results except self.TieBroken as tie: return tie.results def query_library_rec(self, query, library, state): """Returns a list of matches, or None. Recursive querying routine for query_library. """ if len(query) == 0: return None # Composing applies right to left; currently mods are left to right. # Reverse then append the default modifier for proper compose order. mods_to_apply = [sm for sm in reversed(state.mods)] mods_to_apply.append(self.SearchModifier( lambda q: q, lambda sd: sd, operator.eq)) # Create the transformers by composing all of them. q_t = compose(*list(map((lambda sm: sm.q_t), mods_to_apply))) s_t = compose(*list(map((lambda sm: sm.s_t), mods_to_apply))) # Use the most outward comparator. comp = mods_to_apply[0].comp q, md_type = query[0] # No need to repeatedly transform q. q_transformed = q_t(q) # GM limits libraries to 20k songs; this isn't a big performance hit. results = [s for s in library if comp(s_t(s[md_type]), q_transformed)] # Check for immediate return conditions. if not results: return None if len(results) == 1: return [results[0]] # Try to refine results by querying them with the next metadata in the query. next_query = query[1:] next_results = self.query_library_rec(next_query, results, state) if not next_results: # Don't break ties in auto mode; it's handled a level up. if not state.auto: raise self.TieBroken(state.t_breaker(state.orig, results)) else: return results # Now we have multiple for both our query and the next. # Always prefer the next query to ours. return next_results def match(self, queries, tie_breaker=manual_tiebreak, auto=True): """Runs queries against the library; returns a list of songs. Match success is logged. :param query: list of (query, metadata type) in order of precedence. eg [('The Car Song', 'title'), ('The Cat Empire', 'artist')] :param tie_breaker: (optional) tie breaker to use. :param modifiers: (optional) An ordered collection of SearchModifers. Applied during the query left to right. :param auto: (optional) When True, automagically manage modifiers to find results. """ matches = [] self.log_lines.append("## Starting match of " + str(len(queries)) + " queries ##") for query in queries: res = self.query_library(query, tie_breaker, auto=auto) if res: matches += res # Log the results. # The alert precedes the information for a quick view of what happened. alert = None if res is None: alert = "!!" elif len(res) == 1: alert = "==" else: alert = "??" # Each query shows the alert and the query. self.log_lines.append(alert + " " + build_query_rep(query)) # Displayed on the line below the alert (might be useful later). extra_info = None if res: for song in res: self.log_lines.append( (extra_info if extra_info else (' ' * len(alert))) + " " + self.build_song_for_log(song)) elif extra_info: self.log_lines.append(extra_info) return matches gmusicapi-12.1.1/gmusicapi/protocol/0000755000175000017500000000000013512455420017674 5ustar simonsimon00000000000000gmusicapi-12.1.1/gmusicapi/protocol/__init__.py0000600000175000017500000000003012667715127022003 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- gmusicapi-12.1.1/gmusicapi/protocol/download_pb2.py0000664000175000017500000003205713374625453022644 0ustar simonsimon00000000000000# Generated by the protocol buffer compiler. DO NOT EDIT! # source: download.proto import sys _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database from google.protobuf import descriptor_pb2 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor.FileDescriptor( name='download.proto', package='wireless_android_skyjam', syntax='proto2', serialized_pb=_b('\n\x0e\x64ownload.proto\x12\x17wireless_android_skyjam\"\xf7\x01\n\x18GetTracksToExportRequest\x12\x11\n\tclient_id\x18\x02 \x02(\t\x12\x1a\n\x12\x63ontinuation_token\x18\x03 \x01(\t\x12Y\n\x0b\x65xport_type\x18\x04 \x01(\x0e\x32\x44.wireless_android_skyjam.GetTracksToExportRequest.TracksToExportType\x12\x13\n\x0bupdated_min\x18\x05 \x01(\x03\"<\n\x12TracksToExportType\x12\x07\n\x03\x41LL\x10\x01\x12\x1d\n\x19PURCHASED_AND_PROMOTIONAL\x10\x02\"\xbc\x01\n\x11\x44ownloadTrackInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\r\n\x05\x61lbum\x18\x03 \x01(\t\x12\x14\n\x0c\x61lbum_artist\x18\x04 \x01(\t\x12\x0e\n\x06\x61rtist\x18\x05 \x01(\t\x12\x14\n\x0ctrack_number\x18\x06 \x01(\x05\x12\x12\n\ntrack_size\x18\x07 \x01(\x03\x12\x13\n\x0b\x64isc_number\x18\x08 \x01(\x05\x12\x18\n\x10total_disc_count\x18\t \x01(\x05\"\x83\x03\n\x19GetTracksToExportResponse\x12W\n\x06status\x18\x01 \x02(\x0e\x32G.wireless_android_skyjam.GetTracksToExportResponse.TracksToExportStatus\x12G\n\x13\x64ownload_track_info\x18\x02 \x03(\x0b\x32*.wireless_android_skyjam.DownloadTrackInfo\x12\x1a\n\x12\x63ontinuation_token\x18\x03 \x01(\t\x12\x13\n\x0bupdated_min\x18\x04 \x01(\x03\"\x92\x01\n\x14TracksToExportStatus\x12\x06\n\x02OK\x10\x01\x12\x13\n\x0fTRANSIENT_ERROR\x10\x02\x12\x1b\n\x17MAX_NUM_CLIENTS_REACHED\x10\x03\x12!\n\x1dUNABLE_TO_AUTHENTICATE_CLIENT\x10\x04\x12\x1d\n\x19UNABLE_TO_REGISTER_CLIENT\x10\x05') ) _sym_db.RegisterFileDescriptor(DESCRIPTOR) _GETTRACKSTOEXPORTREQUEST_TRACKSTOEXPORTTYPE = _descriptor.EnumDescriptor( name='TracksToExportType', full_name='wireless_android_skyjam.GetTracksToExportRequest.TracksToExportType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='ALL', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='PURCHASED_AND_PROMOTIONAL', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=231, serialized_end=291, ) _sym_db.RegisterEnumDescriptor(_GETTRACKSTOEXPORTREQUEST_TRACKSTOEXPORTTYPE) _GETTRACKSTOEXPORTRESPONSE_TRACKSTOEXPORTSTATUS = _descriptor.EnumDescriptor( name='TracksToExportStatus', full_name='wireless_android_skyjam.GetTracksToExportResponse.TracksToExportStatus', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='OK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='TRANSIENT_ERROR', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='MAX_NUM_CLIENTS_REACHED', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='UNABLE_TO_AUTHENTICATE_CLIENT', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='UNABLE_TO_REGISTER_CLIENT', index=4, number=5, options=None, type=None), ], containing_type=None, options=None, serialized_start=726, serialized_end=872, ) _sym_db.RegisterEnumDescriptor(_GETTRACKSTOEXPORTRESPONSE_TRACKSTOEXPORTSTATUS) _GETTRACKSTOEXPORTREQUEST = _descriptor.Descriptor( name='GetTracksToExportRequest', full_name='wireless_android_skyjam.GetTracksToExportRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='client_id', full_name='wireless_android_skyjam.GetTracksToExportRequest.client_id', index=0, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='continuation_token', full_name='wireless_android_skyjam.GetTracksToExportRequest.continuation_token', index=1, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='export_type', full_name='wireless_android_skyjam.GetTracksToExportRequest.export_type', index=2, number=4, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='updated_min', full_name='wireless_android_skyjam.GetTracksToExportRequest.updated_min', index=3, number=5, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _GETTRACKSTOEXPORTREQUEST_TRACKSTOEXPORTTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=44, serialized_end=291, ) _DOWNLOADTRACKINFO = _descriptor.Descriptor( name='DownloadTrackInfo', full_name='wireless_android_skyjam.DownloadTrackInfo', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='id', full_name='wireless_android_skyjam.DownloadTrackInfo.id', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='title', full_name='wireless_android_skyjam.DownloadTrackInfo.title', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album', full_name='wireless_android_skyjam.DownloadTrackInfo.album', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album_artist', full_name='wireless_android_skyjam.DownloadTrackInfo.album_artist', index=3, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='artist', full_name='wireless_android_skyjam.DownloadTrackInfo.artist', index=4, number=5, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track_number', full_name='wireless_android_skyjam.DownloadTrackInfo.track_number', index=5, number=6, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track_size', full_name='wireless_android_skyjam.DownloadTrackInfo.track_size', index=6, number=7, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='disc_number', full_name='wireless_android_skyjam.DownloadTrackInfo.disc_number', index=7, number=8, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='total_disc_count', full_name='wireless_android_skyjam.DownloadTrackInfo.total_disc_count', index=8, number=9, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=294, serialized_end=482, ) _GETTRACKSTOEXPORTRESPONSE = _descriptor.Descriptor( name='GetTracksToExportResponse', full_name='wireless_android_skyjam.GetTracksToExportResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='status', full_name='wireless_android_skyjam.GetTracksToExportResponse.status', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='download_track_info', full_name='wireless_android_skyjam.GetTracksToExportResponse.download_track_info', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='continuation_token', full_name='wireless_android_skyjam.GetTracksToExportResponse.continuation_token', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='updated_min', full_name='wireless_android_skyjam.GetTracksToExportResponse.updated_min', index=3, number=4, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _GETTRACKSTOEXPORTRESPONSE_TRACKSTOEXPORTSTATUS, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=485, serialized_end=872, ) _GETTRACKSTOEXPORTREQUEST.fields_by_name['export_type'].enum_type = _GETTRACKSTOEXPORTREQUEST_TRACKSTOEXPORTTYPE _GETTRACKSTOEXPORTREQUEST_TRACKSTOEXPORTTYPE.containing_type = _GETTRACKSTOEXPORTREQUEST _GETTRACKSTOEXPORTRESPONSE.fields_by_name['status'].enum_type = _GETTRACKSTOEXPORTRESPONSE_TRACKSTOEXPORTSTATUS _GETTRACKSTOEXPORTRESPONSE.fields_by_name['download_track_info'].message_type = _DOWNLOADTRACKINFO _GETTRACKSTOEXPORTRESPONSE_TRACKSTOEXPORTSTATUS.containing_type = _GETTRACKSTOEXPORTRESPONSE DESCRIPTOR.message_types_by_name['GetTracksToExportRequest'] = _GETTRACKSTOEXPORTREQUEST DESCRIPTOR.message_types_by_name['DownloadTrackInfo'] = _DOWNLOADTRACKINFO DESCRIPTOR.message_types_by_name['GetTracksToExportResponse'] = _GETTRACKSTOEXPORTRESPONSE GetTracksToExportRequest = _reflection.GeneratedProtocolMessageType('GetTracksToExportRequest', (_message.Message,), dict( DESCRIPTOR = _GETTRACKSTOEXPORTREQUEST, __module__ = 'download_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetTracksToExportRequest) )) _sym_db.RegisterMessage(GetTracksToExportRequest) DownloadTrackInfo = _reflection.GeneratedProtocolMessageType('DownloadTrackInfo', (_message.Message,), dict( DESCRIPTOR = _DOWNLOADTRACKINFO, __module__ = 'download_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.DownloadTrackInfo) )) _sym_db.RegisterMessage(DownloadTrackInfo) GetTracksToExportResponse = _reflection.GeneratedProtocolMessageType('GetTracksToExportResponse', (_message.Message,), dict( DESCRIPTOR = _GETTRACKSTOEXPORTRESPONSE, __module__ = 'download_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetTracksToExportResponse) )) _sym_db.RegisterMessage(GetTracksToExportResponse) # @@protoc_insertion_point(module_scope) gmusicapi-12.1.1/gmusicapi/protocol/locker_pb2.py0000664000175000017500000070711313374625453022316 0ustar simonsimon00000000000000# Generated by the protocol buffer compiler. DO NOT EDIT! # source: locker.proto import sys _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database from google.protobuf import descriptor_pb2 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from . import uits_pb2 as uits__pb2 DESCRIPTOR = _descriptor.FileDescriptor( name='locker.proto', package='wireless_android_skyjam', syntax='proto2', serialized_pb=_b('\n\x0clocker.proto\x12\x17wireless_android_skyjam\x1a\nuits.proto\"\x90\x02\n\x08\x41udioRef\x12\x36\n\x05store\x18\x01 \x02(\x0e\x32\'.wireless_android_skyjam.AudioRef.Store\x12\x0b\n\x03ref\x18\x02 \x02(\x0c\x12\x0b\n\x03url\x18\x04 \x01(\t\x12\x10\n\x08\x62it_rate\x18\x05 \x01(\x05\x12\x13\n\x0bsample_rate\x18\x06 \x01(\x05\x12\x14\n\x0c\x64ownloadable\x18\x07 \x01(\x08\x12\x17\n\x0f\x64uration_millis\x18\x08 \x01(\x03\x12\x19\n\x11rematch_timestamp\x18\t \x01(\x03\x12\x1e\n\x16invalid_due_to_wipeout\x18\n \x01(\x08\"!\n\x05Store\x12\r\n\tBLOBSTORE\x10\x01\x12\t\n\x05SM_V2\x10\x02\"\x81\x02\n\x08ImageRef\x12\x36\n\x05store\x18\x01 \x01(\x0e\x32\'.wireless_android_skyjam.ImageRef.Store\x12\r\n\x05width\x18\x02 \x01(\r\x12\x0e\n\x06height\x18\x03 \x01(\r\x12\x0b\n\x03url\x18\x06 \x01(\t\x12\x1e\n\x16invalid_due_to_wipeout\x18\x07 \x01(\x08\x12\x38\n\x06origin\x18\x08 \x01(\x0e\x32(.wireless_android_skyjam.ImageRef.Origin\"\x14\n\x05Store\x12\x0b\n\x07SHOEBOX\x10\x03\"!\n\x06Origin\x12\x0c\n\x08PERSONAL\x10\x01\x12\t\n\x05STORE\x10\x02\"1\n\x12UploadedUitsId3Tag\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"5\n\x12\x41\x64\x64itionalMetadata\x12\x10\n\x08tag_name\x18\x01 \x02(\t\x12\r\n\x05value\x18\x02 \x02(\x0c\"\x89\x01\n\x0bTrackExtras\x12H\n\x13\x61\x64\x64itional_metadata\x18\x02 \x03(\x0b\x32+.wireless_android_skyjam.AdditionalMetadata\x12\x12\n\ncandidates\x18\x03 \x03(\t\x12\x1c\n\x14original_sample_rate\x18\x04 \x01(\x05\"\xb1\x13\n\x05Track\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x1a\n\x12\x63reation_timestamp\x18\x03 \x01(\x03\x12\x1f\n\x17last_modified_timestamp\x18\x04 \x01(\x03\x12\x16\n\x07\x64\x65leted\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\r\n\x05title\x18\x06 \x01(\t\x12\x0e\n\x06\x61rtist\x18\x07 \x01(\t\x12\x13\n\x0b\x61rtist_hash\x18. \x01(\x03\x12\x10\n\x08\x63omposer\x18\x08 \x01(\t\x12\r\n\x05\x61lbum\x18\t \x01(\t\x12\x14\n\x0c\x61lbum_artist\x18\n \x01(\t\x12\x17\n\x0f\x63\x61nonical_album\x18\x38 \x01(\t\x12\x18\n\x10\x63\x61nonical_artist\x18\x39 \x01(\t\x12\x1d\n\x15\x63\x61nonical_genre_album\x18: \x01(\t\x12\x0c\n\x04year\x18\x0b \x01(\x05\x12\x0f\n\x07\x63omment\x18\x0c \x01(\t\x12\x14\n\x0ctrack_number\x18\r \x01(\x05\x12\r\n\x05genre\x18\x0e \x01(\t\x12\x17\n\x0f\x64uration_millis\x18\x0f \x01(\x03\x12\x18\n\x10\x62\x65\x61ts_per_minute\x18\x10 \x01(\x05\x12\x19\n\x11original_bit_rate\x18, \x01(\x05\x12\x34\n\taudio_ref\x18\x11 \x03(\x0b\x32!.wireless_android_skyjam.AudioRef\x12\x38\n\ralbum_art_ref\x18\x12 \x03(\x0b\x32!.wireless_android_skyjam.ImageRef\x12N\n\x13\x61vailability_status\x18\x13 \x01(\x0e\x32\x31.wireless_android_skyjam.Track.AvailabilityStatus\x12\x12\n\nplay_count\x18\x14 \x01(\x05\x12@\n\x0c\x63ontent_type\x18\x19 \x01(\x0e\x32*.wireless_android_skyjam.Track.ContentType\x12\x19\n\x11total_track_count\x18\x1a \x01(\x05\x12\x13\n\x0b\x64isc_number\x18\x1b \x01(\x05\x12\x18\n\x10total_disc_count\x18\x1c \x01(\x05\x12\x39\n\x08\x63hannels\x18\x1d \x01(\x0e\x32\'.wireless_android_skyjam.Track.Channels\x12<\n\ntrack_type\x18\x1e \x01(\x0e\x32(.wireless_android_skyjam.Track.TrackType\x12\x1e\n\x16use_single_server_copy\x18; \x01(\x08\x12\x35\n\x06rating\x18\x1f \x01(\x0e\x32%.wireless_android_skyjam.Track.Rating\x12\x16\n\x0e\x65stimated_size\x18 \x01(\x03\x12\x10\n\x08store_id\x18! \x01(\t\x12\x12\n\nmetajam_id\x18\" \x01(\t\x12 \n\x15metajam_id_confidence\x18+ \x01(\x01:\x01\x30\x12\x0c\n\x04uits\x18# \x01(\t\x12<\n\ruits_metadata\x18( \x01(\x0b\x32%.wireless_android_skyjam.UitsMetadata\x12\x13\n\x0b\x63ompilation\x18$ \x01(\x08\x12\x19\n\x11\x63lient_date_added\x18% \x01(\x03\x12\x18\n\x10recent_timestamp\x18& \x01(\x03\x12\x1d\n\x0e\x64o_not_rematch\x18\' \x01(\x08:\x05\x66\x61lse\x12\x1b\n\x13\x66rom_album_purchase\x18) \x01(\x08\x12\x18\n\x10\x61lbum_metajam_id\x18* \x01(\t\x12\x16\n\x0etransaction_id\x18- \x01(\t\x12\x13\n\x0b\x64\x65\x62ug_track\x18/ \x01(\x08\x12\x18\n\x10normalized_title\x18\x30 \x01(\t\x12\x19\n\x11normalized_artist\x18\x31 \x01(\t\x12\x18\n\x10normalized_album\x18\x32 \x01(\t\x12\x1f\n\x17normalized_album_artist\x18\x33 \x01(\t\x12\"\n\x1anormalized_canonical_album\x18\x36 \x01(\t\x12#\n\x1bnormalized_canonical_artist\x18\x37 \x01(\t\x12\x13\n\x0buploader_id\x18\x34 \x01(\t\x12\x17\n\x0f\x63lient_album_id\x18\x35 \x01(\t\x12\x18\n\x10label_owner_code\x18< \x01(\t\x12I\n\x15original_content_type\x18= \x01(\x0e\x32*.wireless_android_skyjam.Track.ContentType\x12\x42\n\ruploaded_uits\x18G \x03(\x0b\x32+.wireless_android_skyjam.UploadedUitsId3Tag\x12\x42\n\rexplicit_type\x18J \x01(\x0e\x32+.wireless_android_skyjam.Track.ExplicitType\x12:\n\x0ctrack_extras\x18K \x01(\x0b\x32$.wireless_android_skyjam.TrackExtras\"\x86\x01\n\x12\x41vailabilityStatus\x12\x0b\n\x07PENDING\x10\x01\x12\x0b\n\x07MATCHED\x10\x02\x12\x14\n\x10UPLOAD_REQUESTED\x10\x03\x12\r\n\tAVAILABLE\x10\x04\x12\x12\n\x0e\x46ORCE_REUPLOAD\x10\x05\x12\x1d\n\x19UPLOAD_PERMANENTLY_FAILED\x10\x06\"W\n\x0b\x43ontentType\x12\x07\n\x03MP3\x10\x01\x12\x07\n\x03M4A\x10\x02\x12\x07\n\x03\x41\x41\x43\x10\x03\x12\x08\n\x04\x46LAC\x10\x04\x12\x07\n\x03OGG\x10\x05\x12\x07\n\x03WMA\x10\x06\x12\x07\n\x03M4P\x10\x07\x12\x08\n\x04\x41LAC\x10\x08\" \n\x08\x43hannels\x12\x08\n\x04MONO\x10\x01\x12\n\n\x06STEREO\x10\x02\"\x8b\x01\n\tTrackType\x12\x11\n\rMATCHED_TRACK\x10\x01\x12\x13\n\x0fUNMATCHED_TRACK\x10\x02\x12\x0f\n\x0bLOCAL_TRACK\x10\x03\x12\x13\n\x0fPURCHASED_TRACK\x10\x04\x12\x1f\n\x1bMETADATA_ONLY_MATCHED_TRACK\x10\x05\x12\x0f\n\x0bPROMO_TRACK\x10\x06\"e\n\x06Rating\x12\r\n\tNOT_RATED\x10\x01\x12\x0c\n\x08ONE_STAR\x10\x02\x12\r\n\tTWO_STARS\x10\x03\x12\x0f\n\x0bTHREE_STARS\x10\x04\x12\x0e\n\nFOUR_STARS\x10\x05\x12\x0e\n\nFIVE_STARS\x10\x06\"3\n\x0c\x45xplicitType\x12\x0c\n\x08\x45XPLICIT\x10\x01\x12\t\n\x05\x43LEAN\x10\x02\x12\n\n\x06\x45\x44ITED\x10\x03\"7\n\x06Tracks\x12-\n\x05track\x18\x01 \x03(\x0b\x32\x1e.wireless_android_skyjam.Track\"\xe4\x02\n\x08Playlist\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x1a\n\x12\x63reation_timestamp\x18\x03 \x01(\x03\x12\x1f\n\x17last_modified_timestamp\x18\x04 \x01(\x03\x12\x16\n\x07\x64\x65leted\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x0c\n\x04name\x18\x06 \x01(\t\x12\x45\n\rplaylist_type\x18\x07 \x01(\x0e\x32..wireless_android_skyjam.Playlist.PlaylistType\x12;\n\x10playlist_art_ref\x18\x08 \x01(\x0b\x32!.wireless_android_skyjam.ImageRef\x12\x18\n\x10recent_timestamp\x18\t \x01(\x03\"8\n\x0cPlaylistType\x12\x12\n\x0eUSER_GENERATED\x10\x01\x12\t\n\x05MAGIC\x10\x02\x12\t\n\x05PROMO\x10\x03\"\xde\x03\n\rPlaylistEntry\x12\x13\n\x0bplaylist_id\x18\x01 \x01(\t\x12\x19\n\x11\x61\x62solute_position\x18\x02 \x01(\x03\x12\x1c\n\x14place_after_entry_id\x18\x03 \x01(\t\x12\x10\n\x08track_id\x18\x04 \x01(\t\x12\n\n\x02id\x18\x05 \x01(\t\x12\x11\n\tclient_id\x18\x06 \x01(\t\x12\x1a\n\x12\x63reation_timestamp\x18\x07 \x01(\x03\x12\x1f\n\x17last_modified_timestamp\x18\x08 \x01(\x03\x12\x16\n\x07\x64\x65leted\x18\t \x01(\x08:\x05\x66\x61lse\x12`\n\x19relative_position_id_type\x18\n \x01(\x0e\x32=.wireless_android_skyjam.PlaylistEntry.RelativePositionIdType\x12-\n\x05track\x18\x0f \x01(\x0b\x32\x1e.wireless_android_skyjam.Track\x12\x1d\n\x15place_before_entry_id\x18\x10 \x01(\t\x12\x17\n\x0fstring_position\x18\x11 \x01(\t\"0\n\x16RelativePositionIdType\x12\n\n\x06SERVER\x10\x01\x12\n\n\x06\x43LIENT\x10\x02\"\x80\x04\n\x16TrackSearchRestriction\x12Q\n\tattribute\x18\x01 \x02(\x0e\x32>.wireless_android_skyjam.TrackSearchRestriction.TrackAttribute\x12\r\n\x05value\x18\x02 \x02(\t\x12W\n\x0f\x63omparison_type\x18\x03 \x02(\x0e\x32>.wireless_android_skyjam.TrackSearchRestriction.ComparisonType\"\xa6\x01\n\x0eTrackAttribute\x12\t\n\x05TITLE\x10\x01\x12\n\n\x06\x41RTIST\x10\x02\x12\t\n\x05\x41LBUM\x10\x03\x12\x10\n\x0c\x41LBUM_ARTIST\x10\x04\x12\t\n\x05GENRE\x10\x05\x12\x17\n\x13\x41VAILABILITY_STATUS\x10\x06\x12\x0e\n\nTRACK_TYPE\x10\x07\x12\x08\n\x04YEAR\x10\x08\x12\x0c\n\x08STORE_ID\x10\t\x12\x14\n\x10\x41LBUM_METAJAM_ID\x10\n\"\x81\x01\n\x0e\x43omparisonType\x12\t\n\x05\x45QUAL\x10\x00\x12\r\n\tNOT_EQUAL\x10\x01\x12\x10\n\x0cGREATER_THAN\x10\x02\x12\x11\n\rGREATER_EQUAL\x10\x03\x12\r\n\tLESS_THAN\x10\x04\x12\x0e\n\nLESS_EQUAL\x10\x05\x12\x11\n\rPARTIAL_MATCH\x10\x06\"\xa2\x02\n\x19TrackSearchRestrictionSet\x12S\n\x04type\x18\x01 \x01(\x0e\x32\x45.wireless_android_skyjam.TrackSearchRestrictionSet.RestrictionSetType\x12\x44\n\x0brestriction\x18\x02 \x03(\x0b\x32/.wireless_android_skyjam.TrackSearchRestriction\x12\x43\n\x07sub_set\x18\x03 \x03(\x0b\x32\x32.wireless_android_skyjam.TrackSearchRestrictionSet\"%\n\x12RestrictionSetType\x12\x07\n\x03\x41ND\x10\x00\x12\x06\n\x02OR\x10\x01\"\x98\x02\n\x0eTrackSortOrder\x12I\n\tattribute\x18\x01 \x01(\x0e\x32\x36.wireless_android_skyjam.TrackSortOrder.TrackAttribute\x12\x18\n\ndescending\x18\x02 \x01(\x08:\x04true\"\xa0\x01\n\x0eTrackAttribute\x12\x16\n\x12LAST_MODIFIED_TIME\x10\x01\x12\n\n\x06\x41RTIST\x10\x02\x12\t\n\x05\x41LBUM\x10\x03\x12\t\n\x05TITLE\x10\x04\x12\x10\n\x0cTRACK_NUMBER\x10\x06\x12\x0e\n\nPLAY_COUNT\x10\t\x12\x13\n\x0f\x44URATION_MILLIS\x10\n\x12\n\n\x06RATING\x10\x0b\x12\x11\n\rCREATION_TIME\x10\x0c\"\xde\x03\n\x10GetTracksRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12\x13\n\x0bupdated_min\x18\x02 \x01(\x03\x12\x17\n\x0finclude_deleted\x18\x03 \x01(\x08\x12\x13\n\x0bmax_results\x18\x04 \x01(\x05\x12\x1a\n\x12\x63ontinuation_token\x18\x05 \x01(\t\x12K\n\x12search_restriction\x18\x06 \x03(\x0b\x32/.wireless_android_skyjam.TrackSearchRestriction\x12;\n\nsort_order\x18\x07 \x03(\x0b\x32\'.wireless_android_skyjam.TrackSortOrder\x12K\n\x0frestriction_set\x18\x08 \x01(\x0b\x32\x32.wireless_android_skyjam.TrackSearchRestrictionSet\x12S\n\x10track_projection\x18\t \x01(\x0e\x32\x39.wireless_android_skyjam.GetTracksRequest.TrackProjection\".\n\x0fTrackProjection\x12\x08\n\x04\x46ULL\x10\x01\x12\x11\n\rFRONTEND_VIEW\x10\x02\"\x83\x02\n\x11GetTracksResponse\x12N\n\rresponse_code\x18\x01 \x02(\x0e\x32\x37.wireless_android_skyjam.GetTracksResponse.ResponseCode\x12-\n\x05track\x18\x02 \x03(\x0b\x32\x1e.wireless_android_skyjam.Track\x12\x1f\n\x17\x65stimated_total_results\x18\x03 \x01(\x03\x12\x1a\n\x12\x63ontinuation_token\x18\x04 \x01(\t\"2\n\x0cResponseCode\x12\x06\n\x02OK\x10\x01\x12\x10\n\x0cNOT_MODIFIED\x10\x02\x12\x08\n\x04GONE\x10\x03\"\xfc\x01\n\x19GetPlaylistEntriesRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12\x13\n\x0bupdated_min\x18\x02 \x01(\x03\x12\x17\n\x0finclude_deleted\x18\x03 \x01(\x08\x12\x13\n\x0bmax_results\x18\x04 \x01(\x05\x12\x1a\n\x12\x63ontinuation_token\x18\x05 \x01(\t\x12\x1a\n\x12playlist_id_filter\x18\x06 \x01(\t\x12)\n\x1ainclude_all_track_metadata\x18\x07 \x01(\x08:\x05\x66\x61lse\x12(\n\x1aonly_show_available_tracks\x18\x08 \x01(\x08:\x04true\"\xa6\x02\n\x1aGetPlaylistEntriesResponse\x12W\n\rresponse_code\x18\x01 \x02(\x0e\x32@.wireless_android_skyjam.GetPlaylistEntriesResponse.ResponseCode\x12>\n\x0eplaylist_entry\x18\x02 \x03(\x0b\x32&.wireless_android_skyjam.PlaylistEntry\x12\x1f\n\x17\x65stimated_total_results\x18\x03 \x01(\x03\x12\x1a\n\x12\x63ontinuation_token\x18\x04 \x01(\t\"2\n\x0cResponseCode\x12\x06\n\x02OK\x10\x01\x12\x10\n\x0cNOT_MODIFIED\x10\x02\x12\x08\n\x04GONE\x10\x03\"\xe0\x01\n\x11PlaylistSortOrder\x12O\n\tattribute\x18\x01 \x01(\x0e\x32<.wireless_android_skyjam.PlaylistSortOrder.PlaylistAttribute\x12\x19\n\ndescending\x18\x02 \x01(\x08:\x05\x66\x61lse\"_\n\x11PlaylistAttribute\x12\x16\n\x12LAST_MODIFIED_TIME\x10\x01\x12\t\n\x05TITLE\x10\x02\x12\x11\n\rCREATION_TIME\x10\x03\x12\x14\n\x10RECENT_TIMESTAMP\x10\x04\"\xc5\x01\n\x13GetPlaylistsRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12\x13\n\x0bupdated_min\x18\x02 \x01(\x03\x12\x17\n\x0finclude_deleted\x18\x03 \x01(\x08\x12\x13\n\x0bmax_results\x18\x04 \x01(\x05\x12\x1a\n\x12\x63ontinuation_token\x18\x05 \x01(\t\x12>\n\nsort_order\x18\x06 \x01(\x0b\x32*.wireless_android_skyjam.PlaylistSortOrder\"\x8f\x02\n\x14GetPlaylistsResponse\x12Q\n\rresponse_code\x18\x01 \x02(\x0e\x32:.wireless_android_skyjam.GetPlaylistsResponse.ResponseCode\x12\x33\n\x08playlist\x18\x02 \x03(\x0b\x32!.wireless_android_skyjam.Playlist\x12\x1f\n\x17\x65stimated_total_results\x18\x03 \x01(\x03\x12\x1a\n\x12\x63ontinuation_token\x18\x04 \x01(\t\"2\n\x0cResponseCode\x12\x06\n\x02OK\x10\x01\x12\x10\n\x0cNOT_MODIFIED\x10\x02\x12\x08\n\x04GONE\x10\x03\"3\n\x12LookupTrackRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\";\n\x1aLookupPlaylistEntryRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\"6\n\x15LookupPlaylistRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\"\x9e\x03\n\x12\x42\x61tchLookupRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12:\n\x05track\x18\x02 \x03(\x0b\x32+.wireless_android_skyjam.LookupTrackRequest\x12@\n\x08playlist\x18\x03 \x03(\x0b\x32..wireless_android_skyjam.LookupPlaylistRequest\x12O\n\rmetadata_type\x18\x04 \x01(\x0e\x32\x38.wireless_android_skyjam.BatchLookupRequest.MetadataType\x12K\n\x0eplaylist_entry\x18\x05 \x03(\x0b\x32\x33.wireless_android_skyjam.LookupPlaylistEntryRequest\x12\x1e\n\x0finclude_deleted\x18\x06 \x01(\x08:\x05\x66\x61lse\";\n\x0cMetadataType\x12\t\n\x05TRACK\x10\x01\x12\x0c\n\x08PLAYLIST\x10\x02\x12\x12\n\x0ePLAYLIST_ENTRY\x10\x03\"\xb9\x01\n\x13\x42\x61tchLookupResponse\x12-\n\x05track\x18\x01 \x03(\x0b\x32\x1e.wireless_android_skyjam.Track\x12\x33\n\x08playlist\x18\x02 \x03(\x0b\x32!.wireless_android_skyjam.Playlist\x12>\n\x0eplaylist_entry\x18\x03 \x03(\x0b\x32&.wireless_android_skyjam.PlaylistEntry\"\xea\x01\n\x12MutateTrackRequest\x12\x34\n\x0c\x63reate_track\x18\x01 \x01(\x0b\x32\x1e.wireless_android_skyjam.Track\x12\x34\n\x0cupdate_track\x18\x02 \x01(\x0b\x32\x1e.wireless_android_skyjam.Track\x12\x14\n\x0c\x64\x65lete_track\x18\x03 \x01(\t\x12\x16\n\x0epartial_update\x18\x04 \x01(\x08\x12\"\n\x14update_last_modified\x18\x05 \x01(\x08:\x04true\x12\x16\n\x0eundelete_track\x18\x06 \x01(\t\"\xe6\x03\n\x0eMutateResponse\x12Q\n\rresponse_code\x18\x01 \x01(\x0e\x32:.wireless_android_skyjam.MutateResponse.MutateResponseCode\x12\n\n\x02id\x18\x02 \x01(\t\x12\x10\n\x08\x63hild_id\x18\x03 \x03(\t\x12\x11\n\tclient_id\x18\x04 \x01(\t\x12W\n\x13\x61vailability_status\x18\x05 \x01(\x0e\x32:.wireless_android_skyjam.MutateResponse.AvailabilityStatus\x12\x15\n\rerror_message\x18\x06 \x01(\t\"W\n\x12MutateResponseCode\x12\x06\n\x02OK\x10\x01\x12\x0c\n\x08\x43ONFLICT\x10\x02\x12\x13\n\x0fINVALID_REQUEST\x10\x03\x12\x16\n\x12METADATA_TOO_LARGE\x10\x04\"\x86\x01\n\x12\x41vailabilityStatus\x12\x0b\n\x07PENDING\x10\x01\x12\x0b\n\x07MATCHED\x10\x02\x12\x14\n\x10UPLOAD_REQUESTED\x10\x03\x12\r\n\tAVAILABLE\x10\x04\x12\x12\n\x0e\x46ORCE_REUPLOAD\x10\x05\x12\x1d\n\x19UPLOAD_PERMANENTLY_FAILED\x10\x06\"\xe5\x01\n\x18\x42\x61tchMutateTracksRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12\x43\n\x0etrack_mutation\x18\x02 \x03(\x0b\x32+.wireless_android_skyjam.MutateTrackRequest\x12\x1f\n\x11send_notification\x18\x03 \x01(\x08:\x04true\x12\'\n\x19\x64\x65tect_timestamp_conflict\x18\x04 \x01(\x08:\x04true\x12)\n\x1bnotify_fine_grained_updates\x18\x05 \x01(\x08:\x04true\"\xfd\x01\n\x19\x42\x61tchMutateTracksResponse\x12g\n\rresponse_code\x18\x01 \x03(\x0e\x32P.wireless_android_skyjam.BatchMutateTracksResponse.BatchMutateTracksResponseCode\x12@\n\x0fmutate_response\x18\x02 \x03(\x0b\x32\'.wireless_android_skyjam.MutateResponse\"5\n\x1d\x42\x61tchMutateTracksResponseCode\x12\x06\n\x02OK\x10\x01\x12\x0c\n\x08\x43ONFLICT\x10\x02\"\xbf\x02\n\x15MutatePlaylistRequest\x12:\n\x0f\x63reate_playlist\x18\x01 \x01(\x0b\x32!.wireless_android_skyjam.Playlist\x12:\n\x0fupdate_playlist\x18\x02 \x01(\x0b\x32!.wireless_android_skyjam.Playlist\x12\x17\n\x0f\x64\x65lete_playlist\x18\x03 \x01(\t\x12\x16\n\x0epartial_update\x18\x04 \x01(\x08\x12>\n\x0eplaylist_entry\x18\x05 \x03(\x0b\x32&.wireless_android_skyjam.PlaylistEntry\x12\"\n\x14update_last_modified\x18\x06 \x01(\x08:\x04true\x12\x19\n\x11undelete_playlist\x18\x07 \x01(\t\"\xef\x01\n\x1b\x42\x61tchMutatePlaylistsRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12I\n\x11playlist_mutation\x18\x02 \x03(\x0b\x32..wireless_android_skyjam.MutatePlaylistRequest\x12\x1f\n\x11send_notification\x18\x03 \x01(\x08:\x04true\x12\'\n\x19\x64\x65tect_timestamp_conflict\x18\x04 \x01(\x08:\x04true\x12*\n\x1bnotify_fine_grained_updates\x18\x05 \x01(\x08:\x05\x66\x61lse\"\x89\x02\n\x1c\x42\x61tchMutatePlaylistsResponse\x12m\n\rresponse_code\x18\x01 \x03(\x0e\x32V.wireless_android_skyjam.BatchMutatePlaylistsResponse.BatchMutatePlaylistsResponseCode\x12@\n\x0fmutate_response\x18\x02 \x03(\x0b\x32\'.wireless_android_skyjam.MutateResponse\"8\n BatchMutatePlaylistsResponseCode\x12\x06\n\x02OK\x10\x01\x12\x0c\n\x08\x43ONFLICT\x10\x02\"\xb6\x02\n\x1aMutatePlaylistEntryRequest\x12\x45\n\x15\x63reate_playlist_entry\x18\x01 \x01(\x0b\x32&.wireless_android_skyjam.PlaylistEntry\x12\x45\n\x15update_playlist_entry\x18\x02 \x01(\x0b\x32&.wireless_android_skyjam.PlaylistEntry\x12\x45\n\x15\x64\x65lete_playlist_entry\x18\x03 \x01(\x0b\x32&.wireless_android_skyjam.PlaylistEntry\x12\"\n\x14update_last_modified\x18\x04 \x01(\x08:\x04true\x12\x1f\n\x17undelete_playlist_entry\x18\x05 \x01(\t\"\x80\x02\n!BatchMutatePlaylistEntriesRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12T\n\x17playlist_entry_mutation\x18\x02 \x03(\x0b\x32\x33.wireless_android_skyjam.MutatePlaylistEntryRequest\x12\x1f\n\x11send_notification\x18\x03 \x01(\x08:\x04true\x12\'\n\x19\x64\x65tect_timestamp_conflict\x18\x04 \x01(\x08:\x04true\x12*\n\x1bnotify_fine_grained_updates\x18\x05 \x01(\x08:\x05\x66\x61lse\"\xa1\x02\n\"BatchMutatePlaylistEntriesResponse\x12y\n\rresponse_code\x18\x01 \x03(\x0e\x32\x62.wireless_android_skyjam.BatchMutatePlaylistEntriesResponse.BatchMutatePlaylistEntriesResponseCode\x12@\n\x0fmutate_response\x18\x02 \x03(\x0b\x32\'.wireless_android_skyjam.MutateResponse\">\n&BatchMutatePlaylistEntriesResponseCode\x12\x06\n\x02OK\x10\x01\x12\x0c\n\x08\x43ONFLICT\x10\x02\"\xa8\x01\n\x11MagicPlaylistSeed\x12\x46\n\tseed_type\x18\x01 \x02(\x0e\x32\x33.wireless_android_skyjam.MagicPlaylistSeed.SeedType\x12\x0c\n\x04seed\x18\x02 \x02(\t\"=\n\x08SeedType\x12\t\n\x05TRACK\x10\x00\x12\n\n\x06\x41RTIST\x10\x01\x12\t\n\x05\x41LBUM\x10\x02\x12\x0f\n\x0bOPAQUE_SEED\x10\x03\"\xe9\x01\n\x14MagicPlaylistRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12\x15\n\rplaylist_name\x18\x02 \x01(\t\x12\x13\n\x0bplaylist_id\x18\x03 \x01(\t\x12\x38\n\x04seed\x18\x04 \x03(\x0b\x32*.wireless_android_skyjam.MagicPlaylistSeed\x12\x1b\n\x13num_recommendations\x18\x05 \x01(\x05\x12)\n\x1ainclude_all_track_metadata\x18\x06 \x01(\x08:\x05\x66\x61lse\x12\x12\n\nmodel_name\x18\x07 \x01(\t\"\x8c\x01\n\x15MagicPlaylistResponse\x12\x33\n\x08playlist\x18\x01 \x01(\x0b\x32!.wireless_android_skyjam.Playlist\x12>\n\x0eplaylist_entry\x18\x02 \x03(\x0b\x32&.wireless_android_skyjam.PlaylistEntry\"\xf8\x01\n\x12\x46lushLockerRequest\x12\x0f\n\x07gaia_id\x18\x01 \x01(\x03\x12\x13\n\x0bgaia_cookie\x18\x02 \x01(\t\x12#\n\x15remove_audio_binaries\x18\x03 \x01(\x08:\x04true\x12#\n\x15remove_image_binaries\x18\x04 \x01(\x08:\x04true\x12\x1f\n\x11send_notification\x18\x05 \x01(\x08:\x04true\x12&\n\x17reset_subscription_type\x18\x06 \x01(\x08:\x05\x66\x61lse\x12)\n\x1bnotify_fine_grained_updates\x18\x08 \x01(\x08:\x04true\"\x8a\x01\n\x13\x46lushLockerResponse\x12\x16\n\x0etracks_removed\x18\x01 \x01(\x05\x12\x17\n\x0f\x65ntries_removed\x18\x02 \x01(\x05\x12\x19\n\x11playlists_removed\x18\x03 \x01(\x05\x12\'\n\x1fsuccess_reset_subscription_type\x18\x04 \x01(\x08\"6\n\x12LockerNotification\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\"\xee\x01\n\x05\x41lbum\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x61lbum_artist\x18\x02 \x01(\t\x12\x34\n\talbum_art\x18\x03 \x01(\x0b\x32!.wireless_android_skyjam.ImageRef\x12\x13\n\x0btrack_count\x18\x04 \x01(\x05\x12\x18\n\x10last_time_played\x18\x05 \x01(\x03\x12\x16\n\x0eis_compilation\x18\x06 \x01(\x08\x12\x18\n\x10\x61lbum_metajam_id\x18\x07 \x01(\t\x12\x1a\n\x12\x63reation_timestamp\x18\x08 \x01(\x03\x12\x0e\n\x06\x61rtist\x18\t \x01(\t\"\xbb\x01\n\x0e\x41lbumSortOrder\x12I\n\tattribute\x18\x01 \x01(\x0e\x32\x36.wireless_android_skyjam.AlbumSortOrder.AlbumAttribute\x12\x19\n\ndescending\x18\x02 \x01(\x08:\x05\x66\x61lse\"C\n\x0e\x41lbumAttribute\x12\x14\n\x10LAST_PLAYED_TIME\x10\x01\x12\x08\n\x04NAME\x10\x02\x12\x11\n\rCREATION_TIME\x10\x03\"u\n\x10GetAlbumsRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12;\n\nsort_order\x18\x02 \x01(\x0b\x32\'.wireless_android_skyjam.AlbumSortOrder\x12\x13\n\x0bmax_results\x18\x03 \x01(\x05\"B\n\x11GetAlbumsResponse\x12-\n\x05\x61lbum\x18\x01 \x03(\x0b\x32\x1e.wireless_android_skyjam.Album\"`\n\x06\x41rtist\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x19\n\x11total_track_count\x18\x02 \x01(\x05\x12-\n\x05\x61lbum\x18\x03 \x03(\x0b\x32\x1e.wireless_android_skyjam.Album\",\n\x0f\x41rtistSortOrder\x12\x19\n\ndescending\x18\x02 \x01(\x08:\x05\x66\x61lse\"w\n\x11GetArtistsRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12<\n\nsort_order\x18\x02 \x01(\x0b\x32(.wireless_android_skyjam.ArtistSortOrder\x12\x13\n\x0bmax_results\x18\x03 \x01(\x05\"E\n\x12GetArtistsResponse\x12/\n\x06\x61rtist\x18\x01 \x03(\x0b\x32\x1f.wireless_android_skyjam.Artist\"d\n\nMusicGenre\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x19\n\x11total_track_count\x18\x02 \x01(\x05\x12-\n\x05\x61lbum\x18\x03 \x03(\x0b\x32\x1e.wireless_android_skyjam.Album\"+\n\x0eGenreSortOrder\x12\x19\n\ndescending\x18\x02 \x01(\x08:\x05\x66\x61lse\"u\n\x10GetGenresRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12;\n\nsort_order\x18\x02 \x01(\x0b\x32\'.wireless_android_skyjam.GenreSortOrder\x12\x13\n\x0bmax_results\x18\x03 \x01(\x05\"G\n\x11GetGenresResponse\x12\x32\n\x05genre\x18\x01 \x03(\x0b\x32#.wireless_android_skyjam.MusicGenre\"\xfe\x02\n GetDynamicPlaylistEntriesRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12s\n\x15playlist_entries_type\x18\x04 \x02(\x0e\x32T.wireless_android_skyjam.GetDynamicPlaylistEntriesRequest.DynamicPlaylistEntriesType\x12\x13\n\x0bmax_results\x18\x02 \x01(\x05\x12\x1a\n\x12\x63ontinuation_token\x18\x03 \x01(\t\x12)\n\x1ainclude_all_track_metadata\x18\x05 \x01(\x08:\x05\x66\x61lse\"x\n\x1a\x44ynamicPlaylistEntriesType\x12\r\n\tPURCHASED\x10\x01\x12\r\n\tTHUMBS_UP\x10\x02\x12\x12\n\x0eRECENTLY_ADDED\x10\x03\x12\x0c\n\x08PROMOTED\x10\x04\x12\x1a\n\x16PROMOTED_AND_PURCHASED\x10\x05\"\xa2\x04\n!GetDynamicPlaylistEntriesResponse\x12^\n\rresponse_code\x18\x01 \x02(\x0e\x32G.wireless_android_skyjam.GetDynamicPlaylistEntriesResponse.ResponseCode\x12>\n\x0eplaylist_entry\x18\x02 \x03(\x0b\x32&.wireless_android_skyjam.PlaylistEntry\x12\x1f\n\x17\x65stimated_total_results\x18\x03 \x01(\x03\x12\x1a\n\x12\x63ontinuation_token\x18\x04 \x01(\t\x12t\n\x15playlist_entries_type\x18\x05 \x01(\x0e\x32U.wireless_android_skyjam.GetDynamicPlaylistEntriesResponse.DynamicPlaylistEntriesType\"\x85\x01\n\x1a\x44ynamicPlaylistEntriesType\x12\r\n\tPURCHASED\x10\x01\x12\r\n\tTHUMBS_UP\x10\x02\x12\x12\n\x0eRECENTLY_ADDED\x10\x03\x12\x0c\n\x08PROMOTED\x10\x04\x12\x0b\n\x07UNKNOWN\x10\x05\x12\x1a\n\x16PROMOTED_AND_PURCHASED\x10\x06\"\"\n\x0cResponseCode\x12\x06\n\x02OK\x10\x01\x12\n\n\x06NOT_OK\x10\x02\"4\n!GetAggregationsByTrackTypeRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\"\x82\x02\n\x12TrackTypeAggregate\x12O\n\x10track_type_value\x18\x01 \x01(\x0e\x32\x35.wireless_android_skyjam.TrackTypeAggregate.TrackType\x12\r\n\x05\x63ount\x18\x02 \x01(\x05\"\x8b\x01\n\tTrackType\x12\x11\n\rMATCHED_TRACK\x10\x01\x12\x13\n\x0fUNMATCHED_TRACK\x10\x02\x12\x0f\n\x0bLOCAL_TRACK\x10\x03\x12\x13\n\x0fPURCHASED_TRACK\x10\x04\x12\x1f\n\x1bMETADATA_ONLY_MATCHED_TRACK\x10\x05\x12\x0f\n\x0bPROMO_TRACK\x10\x06\"o\n\"GetAggregationsByTrackTypeResponse\x12I\n\x14track_type_aggregate\x18\x01 \x03(\x0b\x32+.wireless_android_skyjam.TrackTypeAggregate\"=\n*GetAggregationsByAvailabilityStatusRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\"\x9b\x02\n\x1b\x41vailabilityStatusAggregate\x12\x64\n\x13\x61vailability_status\x18\x01 \x01(\x0e\x32G.wireless_android_skyjam.AvailabilityStatusAggregate.AvailabilityStatus\x12\r\n\x05\x63ount\x18\x02 \x01(\x05\"\x86\x01\n\x12\x41vailabilityStatus\x12\x0b\n\x07PENDING\x10\x01\x12\x0b\n\x07MATCHED\x10\x02\x12\x14\n\x10UPLOAD_REQUESTED\x10\x03\x12\r\n\tAVAILABLE\x10\x04\x12\x12\n\x0e\x46ORCE_REUPLOAD\x10\x05\x12\x1d\n\x19UPLOAD_PERMANENTLY_FAILED\x10\x06\"\x8a\x01\n+GetAggregationsByAvailabilityStatusResponse\x12[\n\x1d\x61vailability_status_aggregate\x18\x01 \x03(\x0b\x32\x34.wireless_android_skyjam.AvailabilityStatusAggregate\"7\n\x15\x41\x64\x64PromoTracksRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12\r\n\x05genre\x18\x02 \x03(\t\"G\n\x16\x41\x64\x64PromoTracksResponse\x12-\n\x05track\x18\x01 \x03(\x0b\x32\x1e.wireless_android_skyjam.Track\"J\n\x1eGetPlaylistAggregationsRequest\x12\x0f\n\x07gaia_id\x18\x01 \x02(\x03\x12\x17\n\x0bmax_results\x18\x02 \x01(\x05:\x02\x31\x34\"\x9b\x01\n\x11PlaylistAggregate\x12\x13\n\x0bplaylist_id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x34\n\talbum_art\x18\x03 \x01(\x0b\x32!.wireless_android_skyjam.ImageRef\x12\x13\n\x0btrack_count\x18\x04 \x01(\x03\x12\x18\n\x10last_time_played\x18\x05 \x01(\x03\"i\n\x1fGetPlaylistAggregationsResponse\x12\x46\n\x12playlist_aggregate\x18\x01 \x03(\x0b\x32*.wireless_android_skyjam.PlaylistAggregate\"?\n\x1bRemoteControlCommandRequest\x12\x0f\n\x07gaia_id\x18\x01 \x01(\x03\x12\x0f\n\x07\x63ommand\x18\x02 \x01(\t\"\xcb\x01\n\x1cRemoteControlCommandResponse\x12Y\n\rresponse_code\x18\x01 \x01(\x0e\x32\x42.wireless_android_skyjam.RemoteControlCommandResponse.ResponseCode\"P\n\x0cResponseCode\x12\x06\n\x02OK\x10\x01\x12\x10\n\x0cNO_PUBLISHER\x10\x02\x12\x13\n\x0fINVALID_REQUEST\x10\x03\x12\x11\n\rPUBLISH_ERROR\x10\x04') , dependencies=[uits__pb2.DESCRIPTOR,]) _sym_db.RegisterFileDescriptor(DESCRIPTOR) _AUDIOREF_STORE = _descriptor.EnumDescriptor( name='Store', full_name='wireless_android_skyjam.AudioRef.Store', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='BLOBSTORE', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='SM_V2', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=293, serialized_end=326, ) _sym_db.RegisterEnumDescriptor(_AUDIOREF_STORE) _IMAGEREF_STORE = _descriptor.EnumDescriptor( name='Store', full_name='wireless_android_skyjam.ImageRef.Store', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='SHOEBOX', index=0, number=3, options=None, type=None), ], containing_type=None, options=None, serialized_start=531, serialized_end=551, ) _sym_db.RegisterEnumDescriptor(_IMAGEREF_STORE) _IMAGEREF_ORIGIN = _descriptor.EnumDescriptor( name='Origin', full_name='wireless_android_skyjam.ImageRef.Origin', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='PERSONAL', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='STORE', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=553, serialized_end=586, ) _sym_db.RegisterEnumDescriptor(_IMAGEREF_ORIGIN) _TRACK_AVAILABILITYSTATUS = _descriptor.EnumDescriptor( name='AvailabilityStatus', full_name='wireless_android_skyjam.Track.AvailabilityStatus', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='PENDING', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='MATCHED', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='UPLOAD_REQUESTED', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='AVAILABLE', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='FORCE_REUPLOAD', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='UPLOAD_PERMANENTLY_FAILED', index=5, number=6, options=None, type=None), ], containing_type=None, options=None, serialized_start=2761, serialized_end=2895, ) _sym_db.RegisterEnumDescriptor(_TRACK_AVAILABILITYSTATUS) _TRACK_CONTENTTYPE = _descriptor.EnumDescriptor( name='ContentType', full_name='wireless_android_skyjam.Track.ContentType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='MP3', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='M4A', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='AAC', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='FLAC', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='OGG', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='WMA', index=5, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='M4P', index=6, number=7, options=None, type=None), _descriptor.EnumValueDescriptor( name='ALAC', index=7, number=8, options=None, type=None), ], containing_type=None, options=None, serialized_start=2897, serialized_end=2984, ) _sym_db.RegisterEnumDescriptor(_TRACK_CONTENTTYPE) _TRACK_CHANNELS = _descriptor.EnumDescriptor( name='Channels', full_name='wireless_android_skyjam.Track.Channels', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='MONO', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='STEREO', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=2986, serialized_end=3018, ) _sym_db.RegisterEnumDescriptor(_TRACK_CHANNELS) _TRACK_TRACKTYPE = _descriptor.EnumDescriptor( name='TrackType', full_name='wireless_android_skyjam.Track.TrackType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='MATCHED_TRACK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='UNMATCHED_TRACK', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='LOCAL_TRACK', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='PURCHASED_TRACK', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='METADATA_ONLY_MATCHED_TRACK', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='PROMO_TRACK', index=5, number=6, options=None, type=None), ], containing_type=None, options=None, serialized_start=3021, serialized_end=3160, ) _sym_db.RegisterEnumDescriptor(_TRACK_TRACKTYPE) _TRACK_RATING = _descriptor.EnumDescriptor( name='Rating', full_name='wireless_android_skyjam.Track.Rating', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='NOT_RATED', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='ONE_STAR', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='TWO_STARS', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='THREE_STARS', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='FOUR_STARS', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='FIVE_STARS', index=5, number=6, options=None, type=None), ], containing_type=None, options=None, serialized_start=3162, serialized_end=3263, ) _sym_db.RegisterEnumDescriptor(_TRACK_RATING) _TRACK_EXPLICITTYPE = _descriptor.EnumDescriptor( name='ExplicitType', full_name='wireless_android_skyjam.Track.ExplicitType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='EXPLICIT', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='CLEAN', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='EDITED', index=2, number=3, options=None, type=None), ], containing_type=None, options=None, serialized_start=3265, serialized_end=3316, ) _sym_db.RegisterEnumDescriptor(_TRACK_EXPLICITTYPE) _PLAYLIST_PLAYLISTTYPE = _descriptor.EnumDescriptor( name='PlaylistType', full_name='wireless_android_skyjam.Playlist.PlaylistType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='USER_GENERATED', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='MAGIC', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='PROMO', index=2, number=3, options=None, type=None), ], containing_type=None, options=None, serialized_start=3676, serialized_end=3732, ) _sym_db.RegisterEnumDescriptor(_PLAYLIST_PLAYLISTTYPE) _PLAYLISTENTRY_RELATIVEPOSITIONIDTYPE = _descriptor.EnumDescriptor( name='RelativePositionIdType', full_name='wireless_android_skyjam.PlaylistEntry.RelativePositionIdType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='SERVER', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='CLIENT', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=4165, serialized_end=4213, ) _sym_db.RegisterEnumDescriptor(_PLAYLISTENTRY_RELATIVEPOSITIONIDTYPE) _TRACKSEARCHRESTRICTION_TRACKATTRIBUTE = _descriptor.EnumDescriptor( name='TrackAttribute', full_name='wireless_android_skyjam.TrackSearchRestriction.TrackAttribute', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='TITLE', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='ARTIST', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='ALBUM', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='ALBUM_ARTIST', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='GENRE', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='AVAILABILITY_STATUS', index=5, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='TRACK_TYPE', index=6, number=7, options=None, type=None), _descriptor.EnumValueDescriptor( name='YEAR', index=7, number=8, options=None, type=None), _descriptor.EnumValueDescriptor( name='STORE_ID', index=8, number=9, options=None, type=None), _descriptor.EnumValueDescriptor( name='ALBUM_METAJAM_ID', index=9, number=10, options=None, type=None), ], containing_type=None, options=None, serialized_start=4430, serialized_end=4596, ) _sym_db.RegisterEnumDescriptor(_TRACKSEARCHRESTRICTION_TRACKATTRIBUTE) _TRACKSEARCHRESTRICTION_COMPARISONTYPE = _descriptor.EnumDescriptor( name='ComparisonType', full_name='wireless_android_skyjam.TrackSearchRestriction.ComparisonType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='EQUAL', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='NOT_EQUAL', index=1, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='GREATER_THAN', index=2, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='GREATER_EQUAL', index=3, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='LESS_THAN', index=4, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='LESS_EQUAL', index=5, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='PARTIAL_MATCH', index=6, number=6, options=None, type=None), ], containing_type=None, options=None, serialized_start=4599, serialized_end=4728, ) _sym_db.RegisterEnumDescriptor(_TRACKSEARCHRESTRICTION_COMPARISONTYPE) _TRACKSEARCHRESTRICTIONSET_RESTRICTIONSETTYPE = _descriptor.EnumDescriptor( name='RestrictionSetType', full_name='wireless_android_skyjam.TrackSearchRestrictionSet.RestrictionSetType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='AND', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='OR', index=1, number=1, options=None, type=None), ], containing_type=None, options=None, serialized_start=4984, serialized_end=5021, ) _sym_db.RegisterEnumDescriptor(_TRACKSEARCHRESTRICTIONSET_RESTRICTIONSETTYPE) _TRACKSORTORDER_TRACKATTRIBUTE = _descriptor.EnumDescriptor( name='TrackAttribute', full_name='wireless_android_skyjam.TrackSortOrder.TrackAttribute', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='LAST_MODIFIED_TIME', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='ARTIST', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='ALBUM', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='TITLE', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='TRACK_NUMBER', index=4, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='PLAY_COUNT', index=5, number=9, options=None, type=None), _descriptor.EnumValueDescriptor( name='DURATION_MILLIS', index=6, number=10, options=None, type=None), _descriptor.EnumValueDescriptor( name='RATING', index=7, number=11, options=None, type=None), _descriptor.EnumValueDescriptor( name='CREATION_TIME', index=8, number=12, options=None, type=None), ], containing_type=None, options=None, serialized_start=5144, serialized_end=5304, ) _sym_db.RegisterEnumDescriptor(_TRACKSORTORDER_TRACKATTRIBUTE) _GETTRACKSREQUEST_TRACKPROJECTION = _descriptor.EnumDescriptor( name='TrackProjection', full_name='wireless_android_skyjam.GetTracksRequest.TrackProjection', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='FULL', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='FRONTEND_VIEW', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=5739, serialized_end=5785, ) _sym_db.RegisterEnumDescriptor(_GETTRACKSREQUEST_TRACKPROJECTION) _GETTRACKSRESPONSE_RESPONSECODE = _descriptor.EnumDescriptor( name='ResponseCode', full_name='wireless_android_skyjam.GetTracksResponse.ResponseCode', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='OK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='NOT_MODIFIED', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='GONE', index=2, number=3, options=None, type=None), ], containing_type=None, options=None, serialized_start=5997, serialized_end=6047, ) _sym_db.RegisterEnumDescriptor(_GETTRACKSRESPONSE_RESPONSECODE) _GETPLAYLISTENTRIESRESPONSE_RESPONSECODE = _descriptor.EnumDescriptor( name='ResponseCode', full_name='wireless_android_skyjam.GetPlaylistEntriesResponse.ResponseCode', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='OK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='NOT_MODIFIED', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='GONE', index=2, number=3, options=None, type=None), ], containing_type=None, options=None, serialized_start=5997, serialized_end=6047, ) _sym_db.RegisterEnumDescriptor(_GETPLAYLISTENTRIESRESPONSE_RESPONSECODE) _PLAYLISTSORTORDER_PLAYLISTATTRIBUTE = _descriptor.EnumDescriptor( name='PlaylistAttribute', full_name='wireless_android_skyjam.PlaylistSortOrder.PlaylistAttribute', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='LAST_MODIFIED_TIME', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='TITLE', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='CREATION_TIME', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='RECENT_TIMESTAMP', index=3, number=4, options=None, type=None), ], containing_type=None, options=None, serialized_start=6731, serialized_end=6826, ) _sym_db.RegisterEnumDescriptor(_PLAYLISTSORTORDER_PLAYLISTATTRIBUTE) _GETPLAYLISTSRESPONSE_RESPONSECODE = _descriptor.EnumDescriptor( name='ResponseCode', full_name='wireless_android_skyjam.GetPlaylistsResponse.ResponseCode', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='OK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='NOT_MODIFIED', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='GONE', index=2, number=3, options=None, type=None), ], containing_type=None, options=None, serialized_start=5997, serialized_end=6047, ) _sym_db.RegisterEnumDescriptor(_GETPLAYLISTSRESPONSE_RESPONSECODE) _BATCHLOOKUPREQUEST_METADATATYPE = _descriptor.EnumDescriptor( name='MetadataType', full_name='wireless_android_skyjam.BatchLookupRequest.MetadataType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='TRACK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='PLAYLIST', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='PLAYLIST_ENTRY', index=2, number=3, options=None, type=None), ], containing_type=None, options=None, serialized_start=7828, serialized_end=7887, ) _sym_db.RegisterEnumDescriptor(_BATCHLOOKUPREQUEST_METADATATYPE) _MUTATERESPONSE_MUTATERESPONSECODE = _descriptor.EnumDescriptor( name='MutateResponseCode', full_name='wireless_android_skyjam.MutateResponse.MutateResponseCode', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='OK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONFLICT', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='INVALID_REQUEST', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='METADATA_TOO_LARGE', index=3, number=4, options=None, type=None), ], containing_type=None, options=None, serialized_start=8577, serialized_end=8664, ) _sym_db.RegisterEnumDescriptor(_MUTATERESPONSE_MUTATERESPONSECODE) _MUTATERESPONSE_AVAILABILITYSTATUS = _descriptor.EnumDescriptor( name='AvailabilityStatus', full_name='wireless_android_skyjam.MutateResponse.AvailabilityStatus', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='PENDING', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='MATCHED', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='UPLOAD_REQUESTED', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='AVAILABLE', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='FORCE_REUPLOAD', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='UPLOAD_PERMANENTLY_FAILED', index=5, number=6, options=None, type=None), ], containing_type=None, options=None, serialized_start=2761, serialized_end=2895, ) _sym_db.RegisterEnumDescriptor(_MUTATERESPONSE_AVAILABILITYSTATUS) _BATCHMUTATETRACKSRESPONSE_BATCHMUTATETRACKSRESPONSECODE = _descriptor.EnumDescriptor( name='BatchMutateTracksResponseCode', full_name='wireless_android_skyjam.BatchMutateTracksResponse.BatchMutateTracksResponseCode', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='OK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONFLICT', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=9236, serialized_end=9289, ) _sym_db.RegisterEnumDescriptor(_BATCHMUTATETRACKSRESPONSE_BATCHMUTATETRACKSRESPONSECODE) _BATCHMUTATEPLAYLISTSRESPONSE_BATCHMUTATEPLAYLISTSRESPONSECODE = _descriptor.EnumDescriptor( name='BatchMutatePlaylistsResponseCode', full_name='wireless_android_skyjam.BatchMutatePlaylistsResponse.BatchMutatePlaylistsResponseCode', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='OK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONFLICT', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=10065, serialized_end=10121, ) _sym_db.RegisterEnumDescriptor(_BATCHMUTATEPLAYLISTSRESPONSE_BATCHMUTATEPLAYLISTSRESPONSECODE) _BATCHMUTATEPLAYLISTENTRIESRESPONSE_BATCHMUTATEPLAYLISTENTRIESRESPONSECODE = _descriptor.EnumDescriptor( name='BatchMutatePlaylistEntriesResponseCode', full_name='wireless_android_skyjam.BatchMutatePlaylistEntriesResponse.BatchMutatePlaylistEntriesResponseCode', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='OK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONFLICT', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=10923, serialized_end=10985, ) _sym_db.RegisterEnumDescriptor(_BATCHMUTATEPLAYLISTENTRIESRESPONSE_BATCHMUTATEPLAYLISTENTRIESRESPONSECODE) _MAGICPLAYLISTSEED_SEEDTYPE = _descriptor.EnumDescriptor( name='SeedType', full_name='wireless_android_skyjam.MagicPlaylistSeed.SeedType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='TRACK', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='ARTIST', index=1, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='ALBUM', index=2, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='OPAQUE_SEED', index=3, number=3, options=None, type=None), ], containing_type=None, options=None, serialized_start=11095, serialized_end=11156, ) _sym_db.RegisterEnumDescriptor(_MAGICPLAYLISTSEED_SEEDTYPE) _ALBUMSORTORDER_ALBUMATTRIBUTE = _descriptor.EnumDescriptor( name='AlbumAttribute', full_name='wireless_android_skyjam.AlbumSortOrder.AlbumAttribute', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='LAST_PLAYED_TIME', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='NAME', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='CREATION_TIME', index=2, number=3, options=None, type=None), ], containing_type=None, options=None, serialized_start=12347, serialized_end=12414, ) _sym_db.RegisterEnumDescriptor(_ALBUMSORTORDER_ALBUMATTRIBUTE) _GETDYNAMICPLAYLISTENTRIESREQUEST_DYNAMICPLAYLISTENTRIESTYPE = _descriptor.EnumDescriptor( name='DynamicPlaylistEntriesType', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesRequest.DynamicPlaylistEntriesType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='PURCHASED', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='THUMBS_UP', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='RECENTLY_ADDED', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='PROMOTED', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='PROMOTED_AND_PURCHASED', index=4, number=5, options=None, type=None), ], containing_type=None, options=None, serialized_start=13541, serialized_end=13661, ) _sym_db.RegisterEnumDescriptor(_GETDYNAMICPLAYLISTENTRIESREQUEST_DYNAMICPLAYLISTENTRIESTYPE) _GETDYNAMICPLAYLISTENTRIESRESPONSE_DYNAMICPLAYLISTENTRIESTYPE = _descriptor.EnumDescriptor( name='DynamicPlaylistEntriesType', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesResponse.DynamicPlaylistEntriesType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='PURCHASED', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='THUMBS_UP', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='RECENTLY_ADDED', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='PROMOTED', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='UNKNOWN', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='PROMOTED_AND_PURCHASED', index=5, number=6, options=None, type=None), ], containing_type=None, options=None, serialized_start=14041, serialized_end=14174, ) _sym_db.RegisterEnumDescriptor(_GETDYNAMICPLAYLISTENTRIESRESPONSE_DYNAMICPLAYLISTENTRIESTYPE) _GETDYNAMICPLAYLISTENTRIESRESPONSE_RESPONSECODE = _descriptor.EnumDescriptor( name='ResponseCode', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesResponse.ResponseCode', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='OK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='NOT_OK', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=14176, serialized_end=14210, ) _sym_db.RegisterEnumDescriptor(_GETDYNAMICPLAYLISTENTRIESRESPONSE_RESPONSECODE) _TRACKTYPEAGGREGATE_TRACKTYPE = _descriptor.EnumDescriptor( name='TrackType', full_name='wireless_android_skyjam.TrackTypeAggregate.TrackType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='MATCHED_TRACK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='UNMATCHED_TRACK', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='LOCAL_TRACK', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='PURCHASED_TRACK', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='METADATA_ONLY_MATCHED_TRACK', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='PROMO_TRACK', index=5, number=6, options=None, type=None), ], containing_type=None, options=None, serialized_start=3021, serialized_end=3160, ) _sym_db.RegisterEnumDescriptor(_TRACKTYPEAGGREGATE_TRACKTYPE) _AVAILABILITYSTATUSAGGREGATE_AVAILABILITYSTATUS = _descriptor.EnumDescriptor( name='AvailabilityStatus', full_name='wireless_android_skyjam.AvailabilityStatusAggregate.AvailabilityStatus', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='PENDING', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='MATCHED', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='UPLOAD_REQUESTED', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='AVAILABLE', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='FORCE_REUPLOAD', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='UPLOAD_PERMANENTLY_FAILED', index=5, number=6, options=None, type=None), ], containing_type=None, options=None, serialized_start=2761, serialized_end=2895, ) _sym_db.RegisterEnumDescriptor(_AVAILABILITYSTATUSAGGREGATE_AVAILABILITYSTATUS) _REMOTECONTROLCOMMANDRESPONSE_RESPONSECODE = _descriptor.EnumDescriptor( name='ResponseCode', full_name='wireless_android_skyjam.RemoteControlCommandResponse.ResponseCode', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='OK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='NO_PUBLISHER', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='INVALID_REQUEST', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='PUBLISH_ERROR', index=3, number=4, options=None, type=None), ], containing_type=None, options=None, serialized_start=15790, serialized_end=15870, ) _sym_db.RegisterEnumDescriptor(_REMOTECONTROLCOMMANDRESPONSE_RESPONSECODE) _AUDIOREF = _descriptor.Descriptor( name='AudioRef', full_name='wireless_android_skyjam.AudioRef', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='store', full_name='wireless_android_skyjam.AudioRef.store', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='ref', full_name='wireless_android_skyjam.AudioRef.ref', index=1, number=2, type=12, cpp_type=9, label=2, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='url', full_name='wireless_android_skyjam.AudioRef.url', index=2, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='bit_rate', full_name='wireless_android_skyjam.AudioRef.bit_rate', index=3, number=5, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sample_rate', full_name='wireless_android_skyjam.AudioRef.sample_rate', index=4, number=6, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='downloadable', full_name='wireless_android_skyjam.AudioRef.downloadable', index=5, number=7, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='duration_millis', full_name='wireless_android_skyjam.AudioRef.duration_millis', index=6, number=8, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='rematch_timestamp', full_name='wireless_android_skyjam.AudioRef.rematch_timestamp', index=7, number=9, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='invalid_due_to_wipeout', full_name='wireless_android_skyjam.AudioRef.invalid_due_to_wipeout', index=8, number=10, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _AUDIOREF_STORE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=54, serialized_end=326, ) _IMAGEREF = _descriptor.Descriptor( name='ImageRef', full_name='wireless_android_skyjam.ImageRef', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='store', full_name='wireless_android_skyjam.ImageRef.store', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=3, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='width', full_name='wireless_android_skyjam.ImageRef.width', index=1, number=2, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='height', full_name='wireless_android_skyjam.ImageRef.height', index=2, number=3, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='url', full_name='wireless_android_skyjam.ImageRef.url', index=3, number=6, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='invalid_due_to_wipeout', full_name='wireless_android_skyjam.ImageRef.invalid_due_to_wipeout', index=4, number=7, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='origin', full_name='wireless_android_skyjam.ImageRef.origin', index=5, number=8, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _IMAGEREF_STORE, _IMAGEREF_ORIGIN, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=329, serialized_end=586, ) _UPLOADEDUITSID3TAG = _descriptor.Descriptor( name='UploadedUitsId3Tag', full_name='wireless_android_skyjam.UploadedUitsId3Tag', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='owner', full_name='wireless_android_skyjam.UploadedUitsId3Tag.owner', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='data', full_name='wireless_android_skyjam.UploadedUitsId3Tag.data', index=1, number=2, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=588, serialized_end=637, ) _ADDITIONALMETADATA = _descriptor.Descriptor( name='AdditionalMetadata', full_name='wireless_android_skyjam.AdditionalMetadata', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='tag_name', full_name='wireless_android_skyjam.AdditionalMetadata.tag_name', index=0, number=1, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='value', full_name='wireless_android_skyjam.AdditionalMetadata.value', index=1, number=2, type=12, cpp_type=9, label=2, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=639, serialized_end=692, ) _TRACKEXTRAS = _descriptor.Descriptor( name='TrackExtras', full_name='wireless_android_skyjam.TrackExtras', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='additional_metadata', full_name='wireless_android_skyjam.TrackExtras.additional_metadata', index=0, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='candidates', full_name='wireless_android_skyjam.TrackExtras.candidates', index=1, number=3, type=9, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='original_sample_rate', full_name='wireless_android_skyjam.TrackExtras.original_sample_rate', index=2, number=4, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=695, serialized_end=832, ) _TRACK = _descriptor.Descriptor( name='Track', full_name='wireless_android_skyjam.Track', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='id', full_name='wireless_android_skyjam.Track.id', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_id', full_name='wireless_android_skyjam.Track.client_id', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='creation_timestamp', full_name='wireless_android_skyjam.Track.creation_timestamp', index=2, number=3, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='last_modified_timestamp', full_name='wireless_android_skyjam.Track.last_modified_timestamp', index=3, number=4, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='deleted', full_name='wireless_android_skyjam.Track.deleted', index=4, number=5, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='title', full_name='wireless_android_skyjam.Track.title', index=5, number=6, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='artist', full_name='wireless_android_skyjam.Track.artist', index=6, number=7, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='artist_hash', full_name='wireless_android_skyjam.Track.artist_hash', index=7, number=46, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='composer', full_name='wireless_android_skyjam.Track.composer', index=8, number=8, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album', full_name='wireless_android_skyjam.Track.album', index=9, number=9, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album_artist', full_name='wireless_android_skyjam.Track.album_artist', index=10, number=10, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='canonical_album', full_name='wireless_android_skyjam.Track.canonical_album', index=11, number=56, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='canonical_artist', full_name='wireless_android_skyjam.Track.canonical_artist', index=12, number=57, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='canonical_genre_album', full_name='wireless_android_skyjam.Track.canonical_genre_album', index=13, number=58, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='year', full_name='wireless_android_skyjam.Track.year', index=14, number=11, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='comment', full_name='wireless_android_skyjam.Track.comment', index=15, number=12, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track_number', full_name='wireless_android_skyjam.Track.track_number', index=16, number=13, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='genre', full_name='wireless_android_skyjam.Track.genre', index=17, number=14, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='duration_millis', full_name='wireless_android_skyjam.Track.duration_millis', index=18, number=15, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='beats_per_minute', full_name='wireless_android_skyjam.Track.beats_per_minute', index=19, number=16, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='original_bit_rate', full_name='wireless_android_skyjam.Track.original_bit_rate', index=20, number=44, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='audio_ref', full_name='wireless_android_skyjam.Track.audio_ref', index=21, number=17, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album_art_ref', full_name='wireless_android_skyjam.Track.album_art_ref', index=22, number=18, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='availability_status', full_name='wireless_android_skyjam.Track.availability_status', index=23, number=19, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='play_count', full_name='wireless_android_skyjam.Track.play_count', index=24, number=20, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='content_type', full_name='wireless_android_skyjam.Track.content_type', index=25, number=25, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='total_track_count', full_name='wireless_android_skyjam.Track.total_track_count', index=26, number=26, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='disc_number', full_name='wireless_android_skyjam.Track.disc_number', index=27, number=27, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='total_disc_count', full_name='wireless_android_skyjam.Track.total_disc_count', index=28, number=28, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='channels', full_name='wireless_android_skyjam.Track.channels', index=29, number=29, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track_type', full_name='wireless_android_skyjam.Track.track_type', index=30, number=30, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='use_single_server_copy', full_name='wireless_android_skyjam.Track.use_single_server_copy', index=31, number=59, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='rating', full_name='wireless_android_skyjam.Track.rating', index=32, number=31, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='estimated_size', full_name='wireless_android_skyjam.Track.estimated_size', index=33, number=32, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='store_id', full_name='wireless_android_skyjam.Track.store_id', index=34, number=33, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='metajam_id', full_name='wireless_android_skyjam.Track.metajam_id', index=35, number=34, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='metajam_id_confidence', full_name='wireless_android_skyjam.Track.metajam_id_confidence', index=36, number=43, type=1, cpp_type=5, label=1, has_default_value=True, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='uits', full_name='wireless_android_skyjam.Track.uits', index=37, number=35, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='uits_metadata', full_name='wireless_android_skyjam.Track.uits_metadata', index=38, number=40, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='compilation', full_name='wireless_android_skyjam.Track.compilation', index=39, number=36, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_date_added', full_name='wireless_android_skyjam.Track.client_date_added', index=40, number=37, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='recent_timestamp', full_name='wireless_android_skyjam.Track.recent_timestamp', index=41, number=38, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='do_not_rematch', full_name='wireless_android_skyjam.Track.do_not_rematch', index=42, number=39, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='from_album_purchase', full_name='wireless_android_skyjam.Track.from_album_purchase', index=43, number=41, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album_metajam_id', full_name='wireless_android_skyjam.Track.album_metajam_id', index=44, number=42, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='transaction_id', full_name='wireless_android_skyjam.Track.transaction_id', index=45, number=45, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='debug_track', full_name='wireless_android_skyjam.Track.debug_track', index=46, number=47, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='normalized_title', full_name='wireless_android_skyjam.Track.normalized_title', index=47, number=48, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='normalized_artist', full_name='wireless_android_skyjam.Track.normalized_artist', index=48, number=49, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='normalized_album', full_name='wireless_android_skyjam.Track.normalized_album', index=49, number=50, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='normalized_album_artist', full_name='wireless_android_skyjam.Track.normalized_album_artist', index=50, number=51, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='normalized_canonical_album', full_name='wireless_android_skyjam.Track.normalized_canonical_album', index=51, number=54, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='normalized_canonical_artist', full_name='wireless_android_skyjam.Track.normalized_canonical_artist', index=52, number=55, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='uploader_id', full_name='wireless_android_skyjam.Track.uploader_id', index=53, number=52, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_album_id', full_name='wireless_android_skyjam.Track.client_album_id', index=54, number=53, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='label_owner_code', full_name='wireless_android_skyjam.Track.label_owner_code', index=55, number=60, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='original_content_type', full_name='wireless_android_skyjam.Track.original_content_type', index=56, number=61, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='uploaded_uits', full_name='wireless_android_skyjam.Track.uploaded_uits', index=57, number=71, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='explicit_type', full_name='wireless_android_skyjam.Track.explicit_type', index=58, number=74, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track_extras', full_name='wireless_android_skyjam.Track.track_extras', index=59, number=75, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _TRACK_AVAILABILITYSTATUS, _TRACK_CONTENTTYPE, _TRACK_CHANNELS, _TRACK_TRACKTYPE, _TRACK_RATING, _TRACK_EXPLICITTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=835, serialized_end=3316, ) _TRACKS = _descriptor.Descriptor( name='Tracks', full_name='wireless_android_skyjam.Tracks', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='track', full_name='wireless_android_skyjam.Tracks.track', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=3318, serialized_end=3373, ) _PLAYLIST = _descriptor.Descriptor( name='Playlist', full_name='wireless_android_skyjam.Playlist', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='id', full_name='wireless_android_skyjam.Playlist.id', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_id', full_name='wireless_android_skyjam.Playlist.client_id', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='creation_timestamp', full_name='wireless_android_skyjam.Playlist.creation_timestamp', index=2, number=3, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='last_modified_timestamp', full_name='wireless_android_skyjam.Playlist.last_modified_timestamp', index=3, number=4, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='deleted', full_name='wireless_android_skyjam.Playlist.deleted', index=4, number=5, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='name', full_name='wireless_android_skyjam.Playlist.name', index=5, number=6, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_type', full_name='wireless_android_skyjam.Playlist.playlist_type', index=6, number=7, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_art_ref', full_name='wireless_android_skyjam.Playlist.playlist_art_ref', index=7, number=8, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='recent_timestamp', full_name='wireless_android_skyjam.Playlist.recent_timestamp', index=8, number=9, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _PLAYLIST_PLAYLISTTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=3376, serialized_end=3732, ) _PLAYLISTENTRY = _descriptor.Descriptor( name='PlaylistEntry', full_name='wireless_android_skyjam.PlaylistEntry', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='playlist_id', full_name='wireless_android_skyjam.PlaylistEntry.playlist_id', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='absolute_position', full_name='wireless_android_skyjam.PlaylistEntry.absolute_position', index=1, number=2, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='place_after_entry_id', full_name='wireless_android_skyjam.PlaylistEntry.place_after_entry_id', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track_id', full_name='wireless_android_skyjam.PlaylistEntry.track_id', index=3, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='id', full_name='wireless_android_skyjam.PlaylistEntry.id', index=4, number=5, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_id', full_name='wireless_android_skyjam.PlaylistEntry.client_id', index=5, number=6, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='creation_timestamp', full_name='wireless_android_skyjam.PlaylistEntry.creation_timestamp', index=6, number=7, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='last_modified_timestamp', full_name='wireless_android_skyjam.PlaylistEntry.last_modified_timestamp', index=7, number=8, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='deleted', full_name='wireless_android_skyjam.PlaylistEntry.deleted', index=8, number=9, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='relative_position_id_type', full_name='wireless_android_skyjam.PlaylistEntry.relative_position_id_type', index=9, number=10, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track', full_name='wireless_android_skyjam.PlaylistEntry.track', index=10, number=15, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='place_before_entry_id', full_name='wireless_android_skyjam.PlaylistEntry.place_before_entry_id', index=11, number=16, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='string_position', full_name='wireless_android_skyjam.PlaylistEntry.string_position', index=12, number=17, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _PLAYLISTENTRY_RELATIVEPOSITIONIDTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=3735, serialized_end=4213, ) _TRACKSEARCHRESTRICTION = _descriptor.Descriptor( name='TrackSearchRestriction', full_name='wireless_android_skyjam.TrackSearchRestriction', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='attribute', full_name='wireless_android_skyjam.TrackSearchRestriction.attribute', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='value', full_name='wireless_android_skyjam.TrackSearchRestriction.value', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='comparison_type', full_name='wireless_android_skyjam.TrackSearchRestriction.comparison_type', index=2, number=3, type=14, cpp_type=8, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _TRACKSEARCHRESTRICTION_TRACKATTRIBUTE, _TRACKSEARCHRESTRICTION_COMPARISONTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=4216, serialized_end=4728, ) _TRACKSEARCHRESTRICTIONSET = _descriptor.Descriptor( name='TrackSearchRestrictionSet', full_name='wireless_android_skyjam.TrackSearchRestrictionSet', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='type', full_name='wireless_android_skyjam.TrackSearchRestrictionSet.type', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='restriction', full_name='wireless_android_skyjam.TrackSearchRestrictionSet.restriction', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sub_set', full_name='wireless_android_skyjam.TrackSearchRestrictionSet.sub_set', index=2, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _TRACKSEARCHRESTRICTIONSET_RESTRICTIONSETTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=4731, serialized_end=5021, ) _TRACKSORTORDER = _descriptor.Descriptor( name='TrackSortOrder', full_name='wireless_android_skyjam.TrackSortOrder', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='attribute', full_name='wireless_android_skyjam.TrackSortOrder.attribute', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='descending', full_name='wireless_android_skyjam.TrackSortOrder.descending', index=1, number=2, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _TRACKSORTORDER_TRACKATTRIBUTE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=5024, serialized_end=5304, ) _GETTRACKSREQUEST = _descriptor.Descriptor( name='GetTracksRequest', full_name='wireless_android_skyjam.GetTracksRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.GetTracksRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='updated_min', full_name='wireless_android_skyjam.GetTracksRequest.updated_min', index=1, number=2, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='include_deleted', full_name='wireless_android_skyjam.GetTracksRequest.include_deleted', index=2, number=3, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='max_results', full_name='wireless_android_skyjam.GetTracksRequest.max_results', index=3, number=4, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='continuation_token', full_name='wireless_android_skyjam.GetTracksRequest.continuation_token', index=4, number=5, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='search_restriction', full_name='wireless_android_skyjam.GetTracksRequest.search_restriction', index=5, number=6, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sort_order', full_name='wireless_android_skyjam.GetTracksRequest.sort_order', index=6, number=7, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='restriction_set', full_name='wireless_android_skyjam.GetTracksRequest.restriction_set', index=7, number=8, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track_projection', full_name='wireless_android_skyjam.GetTracksRequest.track_projection', index=8, number=9, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _GETTRACKSREQUEST_TRACKPROJECTION, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=5307, serialized_end=5785, ) _GETTRACKSRESPONSE = _descriptor.Descriptor( name='GetTracksResponse', full_name='wireless_android_skyjam.GetTracksResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='response_code', full_name='wireless_android_skyjam.GetTracksResponse.response_code', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track', full_name='wireless_android_skyjam.GetTracksResponse.track', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='estimated_total_results', full_name='wireless_android_skyjam.GetTracksResponse.estimated_total_results', index=2, number=3, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='continuation_token', full_name='wireless_android_skyjam.GetTracksResponse.continuation_token', index=3, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _GETTRACKSRESPONSE_RESPONSECODE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=5788, serialized_end=6047, ) _GETPLAYLISTENTRIESREQUEST = _descriptor.Descriptor( name='GetPlaylistEntriesRequest', full_name='wireless_android_skyjam.GetPlaylistEntriesRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.GetPlaylistEntriesRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='updated_min', full_name='wireless_android_skyjam.GetPlaylistEntriesRequest.updated_min', index=1, number=2, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='include_deleted', full_name='wireless_android_skyjam.GetPlaylistEntriesRequest.include_deleted', index=2, number=3, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='max_results', full_name='wireless_android_skyjam.GetPlaylistEntriesRequest.max_results', index=3, number=4, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='continuation_token', full_name='wireless_android_skyjam.GetPlaylistEntriesRequest.continuation_token', index=4, number=5, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_id_filter', full_name='wireless_android_skyjam.GetPlaylistEntriesRequest.playlist_id_filter', index=5, number=6, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='include_all_track_metadata', full_name='wireless_android_skyjam.GetPlaylistEntriesRequest.include_all_track_metadata', index=6, number=7, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='only_show_available_tracks', full_name='wireless_android_skyjam.GetPlaylistEntriesRequest.only_show_available_tracks', index=7, number=8, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=6050, serialized_end=6302, ) _GETPLAYLISTENTRIESRESPONSE = _descriptor.Descriptor( name='GetPlaylistEntriesResponse', full_name='wireless_android_skyjam.GetPlaylistEntriesResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='response_code', full_name='wireless_android_skyjam.GetPlaylistEntriesResponse.response_code', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_entry', full_name='wireless_android_skyjam.GetPlaylistEntriesResponse.playlist_entry', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='estimated_total_results', full_name='wireless_android_skyjam.GetPlaylistEntriesResponse.estimated_total_results', index=2, number=3, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='continuation_token', full_name='wireless_android_skyjam.GetPlaylistEntriesResponse.continuation_token', index=3, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _GETPLAYLISTENTRIESRESPONSE_RESPONSECODE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=6305, serialized_end=6599, ) _PLAYLISTSORTORDER = _descriptor.Descriptor( name='PlaylistSortOrder', full_name='wireless_android_skyjam.PlaylistSortOrder', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='attribute', full_name='wireless_android_skyjam.PlaylistSortOrder.attribute', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='descending', full_name='wireless_android_skyjam.PlaylistSortOrder.descending', index=1, number=2, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _PLAYLISTSORTORDER_PLAYLISTATTRIBUTE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=6602, serialized_end=6826, ) _GETPLAYLISTSREQUEST = _descriptor.Descriptor( name='GetPlaylistsRequest', full_name='wireless_android_skyjam.GetPlaylistsRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.GetPlaylistsRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='updated_min', full_name='wireless_android_skyjam.GetPlaylistsRequest.updated_min', index=1, number=2, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='include_deleted', full_name='wireless_android_skyjam.GetPlaylistsRequest.include_deleted', index=2, number=3, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='max_results', full_name='wireless_android_skyjam.GetPlaylistsRequest.max_results', index=3, number=4, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='continuation_token', full_name='wireless_android_skyjam.GetPlaylistsRequest.continuation_token', index=4, number=5, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sort_order', full_name='wireless_android_skyjam.GetPlaylistsRequest.sort_order', index=5, number=6, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=6829, serialized_end=7026, ) _GETPLAYLISTSRESPONSE = _descriptor.Descriptor( name='GetPlaylistsResponse', full_name='wireless_android_skyjam.GetPlaylistsResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='response_code', full_name='wireless_android_skyjam.GetPlaylistsResponse.response_code', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist', full_name='wireless_android_skyjam.GetPlaylistsResponse.playlist', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='estimated_total_results', full_name='wireless_android_skyjam.GetPlaylistsResponse.estimated_total_results', index=2, number=3, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='continuation_token', full_name='wireless_android_skyjam.GetPlaylistsResponse.continuation_token', index=3, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _GETPLAYLISTSRESPONSE_RESPONSECODE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=7029, serialized_end=7300, ) _LOOKUPTRACKREQUEST = _descriptor.Descriptor( name='LookupTrackRequest', full_name='wireless_android_skyjam.LookupTrackRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='id', full_name='wireless_android_skyjam.LookupTrackRequest.id', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_id', full_name='wireless_android_skyjam.LookupTrackRequest.client_id', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=7302, serialized_end=7353, ) _LOOKUPPLAYLISTENTRYREQUEST = _descriptor.Descriptor( name='LookupPlaylistEntryRequest', full_name='wireless_android_skyjam.LookupPlaylistEntryRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='id', full_name='wireless_android_skyjam.LookupPlaylistEntryRequest.id', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_id', full_name='wireless_android_skyjam.LookupPlaylistEntryRequest.client_id', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=7355, serialized_end=7414, ) _LOOKUPPLAYLISTREQUEST = _descriptor.Descriptor( name='LookupPlaylistRequest', full_name='wireless_android_skyjam.LookupPlaylistRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='id', full_name='wireless_android_skyjam.LookupPlaylistRequest.id', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_id', full_name='wireless_android_skyjam.LookupPlaylistRequest.client_id', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=7416, serialized_end=7470, ) _BATCHLOOKUPREQUEST = _descriptor.Descriptor( name='BatchLookupRequest', full_name='wireless_android_skyjam.BatchLookupRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.BatchLookupRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track', full_name='wireless_android_skyjam.BatchLookupRequest.track', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist', full_name='wireless_android_skyjam.BatchLookupRequest.playlist', index=2, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='metadata_type', full_name='wireless_android_skyjam.BatchLookupRequest.metadata_type', index=3, number=4, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_entry', full_name='wireless_android_skyjam.BatchLookupRequest.playlist_entry', index=4, number=5, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='include_deleted', full_name='wireless_android_skyjam.BatchLookupRequest.include_deleted', index=5, number=6, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _BATCHLOOKUPREQUEST_METADATATYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=7473, serialized_end=7887, ) _BATCHLOOKUPRESPONSE = _descriptor.Descriptor( name='BatchLookupResponse', full_name='wireless_android_skyjam.BatchLookupResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='track', full_name='wireless_android_skyjam.BatchLookupResponse.track', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist', full_name='wireless_android_skyjam.BatchLookupResponse.playlist', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_entry', full_name='wireless_android_skyjam.BatchLookupResponse.playlist_entry', index=2, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=7890, serialized_end=8075, ) _MUTATETRACKREQUEST = _descriptor.Descriptor( name='MutateTrackRequest', full_name='wireless_android_skyjam.MutateTrackRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='create_track', full_name='wireless_android_skyjam.MutateTrackRequest.create_track', index=0, number=1, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='update_track', full_name='wireless_android_skyjam.MutateTrackRequest.update_track', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='delete_track', full_name='wireless_android_skyjam.MutateTrackRequest.delete_track', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='partial_update', full_name='wireless_android_skyjam.MutateTrackRequest.partial_update', index=3, number=4, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='update_last_modified', full_name='wireless_android_skyjam.MutateTrackRequest.update_last_modified', index=4, number=5, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='undelete_track', full_name='wireless_android_skyjam.MutateTrackRequest.undelete_track', index=5, number=6, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=8078, serialized_end=8312, ) _MUTATERESPONSE = _descriptor.Descriptor( name='MutateResponse', full_name='wireless_android_skyjam.MutateResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='response_code', full_name='wireless_android_skyjam.MutateResponse.response_code', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='id', full_name='wireless_android_skyjam.MutateResponse.id', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='child_id', full_name='wireless_android_skyjam.MutateResponse.child_id', index=2, number=3, type=9, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_id', full_name='wireless_android_skyjam.MutateResponse.client_id', index=3, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='availability_status', full_name='wireless_android_skyjam.MutateResponse.availability_status', index=4, number=5, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='error_message', full_name='wireless_android_skyjam.MutateResponse.error_message', index=5, number=6, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _MUTATERESPONSE_MUTATERESPONSECODE, _MUTATERESPONSE_AVAILABILITYSTATUS, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=8315, serialized_end=8801, ) _BATCHMUTATETRACKSREQUEST = _descriptor.Descriptor( name='BatchMutateTracksRequest', full_name='wireless_android_skyjam.BatchMutateTracksRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.BatchMutateTracksRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track_mutation', full_name='wireless_android_skyjam.BatchMutateTracksRequest.track_mutation', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='send_notification', full_name='wireless_android_skyjam.BatchMutateTracksRequest.send_notification', index=2, number=3, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='detect_timestamp_conflict', full_name='wireless_android_skyjam.BatchMutateTracksRequest.detect_timestamp_conflict', index=3, number=4, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='notify_fine_grained_updates', full_name='wireless_android_skyjam.BatchMutateTracksRequest.notify_fine_grained_updates', index=4, number=5, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=8804, serialized_end=9033, ) _BATCHMUTATETRACKSRESPONSE = _descriptor.Descriptor( name='BatchMutateTracksResponse', full_name='wireless_android_skyjam.BatchMutateTracksResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='response_code', full_name='wireless_android_skyjam.BatchMutateTracksResponse.response_code', index=0, number=1, type=14, cpp_type=8, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='mutate_response', full_name='wireless_android_skyjam.BatchMutateTracksResponse.mutate_response', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _BATCHMUTATETRACKSRESPONSE_BATCHMUTATETRACKSRESPONSECODE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=9036, serialized_end=9289, ) _MUTATEPLAYLISTREQUEST = _descriptor.Descriptor( name='MutatePlaylistRequest', full_name='wireless_android_skyjam.MutatePlaylistRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='create_playlist', full_name='wireless_android_skyjam.MutatePlaylistRequest.create_playlist', index=0, number=1, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='update_playlist', full_name='wireless_android_skyjam.MutatePlaylistRequest.update_playlist', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='delete_playlist', full_name='wireless_android_skyjam.MutatePlaylistRequest.delete_playlist', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='partial_update', full_name='wireless_android_skyjam.MutatePlaylistRequest.partial_update', index=3, number=4, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_entry', full_name='wireless_android_skyjam.MutatePlaylistRequest.playlist_entry', index=4, number=5, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='update_last_modified', full_name='wireless_android_skyjam.MutatePlaylistRequest.update_last_modified', index=5, number=6, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='undelete_playlist', full_name='wireless_android_skyjam.MutatePlaylistRequest.undelete_playlist', index=6, number=7, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=9292, serialized_end=9611, ) _BATCHMUTATEPLAYLISTSREQUEST = _descriptor.Descriptor( name='BatchMutatePlaylistsRequest', full_name='wireless_android_skyjam.BatchMutatePlaylistsRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.BatchMutatePlaylistsRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_mutation', full_name='wireless_android_skyjam.BatchMutatePlaylistsRequest.playlist_mutation', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='send_notification', full_name='wireless_android_skyjam.BatchMutatePlaylistsRequest.send_notification', index=2, number=3, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='detect_timestamp_conflict', full_name='wireless_android_skyjam.BatchMutatePlaylistsRequest.detect_timestamp_conflict', index=3, number=4, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='notify_fine_grained_updates', full_name='wireless_android_skyjam.BatchMutatePlaylistsRequest.notify_fine_grained_updates', index=4, number=5, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=9614, serialized_end=9853, ) _BATCHMUTATEPLAYLISTSRESPONSE = _descriptor.Descriptor( name='BatchMutatePlaylistsResponse', full_name='wireless_android_skyjam.BatchMutatePlaylistsResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='response_code', full_name='wireless_android_skyjam.BatchMutatePlaylistsResponse.response_code', index=0, number=1, type=14, cpp_type=8, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='mutate_response', full_name='wireless_android_skyjam.BatchMutatePlaylistsResponse.mutate_response', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _BATCHMUTATEPLAYLISTSRESPONSE_BATCHMUTATEPLAYLISTSRESPONSECODE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=9856, serialized_end=10121, ) _MUTATEPLAYLISTENTRYREQUEST = _descriptor.Descriptor( name='MutatePlaylistEntryRequest', full_name='wireless_android_skyjam.MutatePlaylistEntryRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='create_playlist_entry', full_name='wireless_android_skyjam.MutatePlaylistEntryRequest.create_playlist_entry', index=0, number=1, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='update_playlist_entry', full_name='wireless_android_skyjam.MutatePlaylistEntryRequest.update_playlist_entry', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='delete_playlist_entry', full_name='wireless_android_skyjam.MutatePlaylistEntryRequest.delete_playlist_entry', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='update_last_modified', full_name='wireless_android_skyjam.MutatePlaylistEntryRequest.update_last_modified', index=3, number=4, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='undelete_playlist_entry', full_name='wireless_android_skyjam.MutatePlaylistEntryRequest.undelete_playlist_entry', index=4, number=5, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=10124, serialized_end=10434, ) _BATCHMUTATEPLAYLISTENTRIESREQUEST = _descriptor.Descriptor( name='BatchMutatePlaylistEntriesRequest', full_name='wireless_android_skyjam.BatchMutatePlaylistEntriesRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.BatchMutatePlaylistEntriesRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_entry_mutation', full_name='wireless_android_skyjam.BatchMutatePlaylistEntriesRequest.playlist_entry_mutation', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='send_notification', full_name='wireless_android_skyjam.BatchMutatePlaylistEntriesRequest.send_notification', index=2, number=3, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='detect_timestamp_conflict', full_name='wireless_android_skyjam.BatchMutatePlaylistEntriesRequest.detect_timestamp_conflict', index=3, number=4, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='notify_fine_grained_updates', full_name='wireless_android_skyjam.BatchMutatePlaylistEntriesRequest.notify_fine_grained_updates', index=4, number=5, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=10437, serialized_end=10693, ) _BATCHMUTATEPLAYLISTENTRIESRESPONSE = _descriptor.Descriptor( name='BatchMutatePlaylistEntriesResponse', full_name='wireless_android_skyjam.BatchMutatePlaylistEntriesResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='response_code', full_name='wireless_android_skyjam.BatchMutatePlaylistEntriesResponse.response_code', index=0, number=1, type=14, cpp_type=8, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='mutate_response', full_name='wireless_android_skyjam.BatchMutatePlaylistEntriesResponse.mutate_response', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _BATCHMUTATEPLAYLISTENTRIESRESPONSE_BATCHMUTATEPLAYLISTENTRIESRESPONSECODE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=10696, serialized_end=10985, ) _MAGICPLAYLISTSEED = _descriptor.Descriptor( name='MagicPlaylistSeed', full_name='wireless_android_skyjam.MagicPlaylistSeed', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='seed_type', full_name='wireless_android_skyjam.MagicPlaylistSeed.seed_type', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='seed', full_name='wireless_android_skyjam.MagicPlaylistSeed.seed', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _MAGICPLAYLISTSEED_SEEDTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=10988, serialized_end=11156, ) _MAGICPLAYLISTREQUEST = _descriptor.Descriptor( name='MagicPlaylistRequest', full_name='wireless_android_skyjam.MagicPlaylistRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.MagicPlaylistRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_name', full_name='wireless_android_skyjam.MagicPlaylistRequest.playlist_name', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_id', full_name='wireless_android_skyjam.MagicPlaylistRequest.playlist_id', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='seed', full_name='wireless_android_skyjam.MagicPlaylistRequest.seed', index=3, number=4, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='num_recommendations', full_name='wireless_android_skyjam.MagicPlaylistRequest.num_recommendations', index=4, number=5, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='include_all_track_metadata', full_name='wireless_android_skyjam.MagicPlaylistRequest.include_all_track_metadata', index=5, number=6, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='model_name', full_name='wireless_android_skyjam.MagicPlaylistRequest.model_name', index=6, number=7, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=11159, serialized_end=11392, ) _MAGICPLAYLISTRESPONSE = _descriptor.Descriptor( name='MagicPlaylistResponse', full_name='wireless_android_skyjam.MagicPlaylistResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='playlist', full_name='wireless_android_skyjam.MagicPlaylistResponse.playlist', index=0, number=1, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_entry', full_name='wireless_android_skyjam.MagicPlaylistResponse.playlist_entry', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=11395, serialized_end=11535, ) _FLUSHLOCKERREQUEST = _descriptor.Descriptor( name='FlushLockerRequest', full_name='wireless_android_skyjam.FlushLockerRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.FlushLockerRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='gaia_cookie', full_name='wireless_android_skyjam.FlushLockerRequest.gaia_cookie', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='remove_audio_binaries', full_name='wireless_android_skyjam.FlushLockerRequest.remove_audio_binaries', index=2, number=3, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='remove_image_binaries', full_name='wireless_android_skyjam.FlushLockerRequest.remove_image_binaries', index=3, number=4, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='send_notification', full_name='wireless_android_skyjam.FlushLockerRequest.send_notification', index=4, number=5, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='reset_subscription_type', full_name='wireless_android_skyjam.FlushLockerRequest.reset_subscription_type', index=5, number=6, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='notify_fine_grained_updates', full_name='wireless_android_skyjam.FlushLockerRequest.notify_fine_grained_updates', index=6, number=8, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=11538, serialized_end=11786, ) _FLUSHLOCKERRESPONSE = _descriptor.Descriptor( name='FlushLockerResponse', full_name='wireless_android_skyjam.FlushLockerResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='tracks_removed', full_name='wireless_android_skyjam.FlushLockerResponse.tracks_removed', index=0, number=1, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='entries_removed', full_name='wireless_android_skyjam.FlushLockerResponse.entries_removed', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlists_removed', full_name='wireless_android_skyjam.FlushLockerResponse.playlists_removed', index=2, number=3, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='success_reset_subscription_type', full_name='wireless_android_skyjam.FlushLockerResponse.success_reset_subscription_type', index=3, number=4, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=11789, serialized_end=11927, ) _LOCKERNOTIFICATION = _descriptor.Descriptor( name='LockerNotification', full_name='wireless_android_skyjam.LockerNotification', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.LockerNotification.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='payload', full_name='wireless_android_skyjam.LockerNotification.payload', index=1, number=2, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=11929, serialized_end=11983, ) _ALBUM = _descriptor.Descriptor( name='Album', full_name='wireless_android_skyjam.Album', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='name', full_name='wireless_android_skyjam.Album.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album_artist', full_name='wireless_android_skyjam.Album.album_artist', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album_art', full_name='wireless_android_skyjam.Album.album_art', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track_count', full_name='wireless_android_skyjam.Album.track_count', index=3, number=4, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='last_time_played', full_name='wireless_android_skyjam.Album.last_time_played', index=4, number=5, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='is_compilation', full_name='wireless_android_skyjam.Album.is_compilation', index=5, number=6, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album_metajam_id', full_name='wireless_android_skyjam.Album.album_metajam_id', index=6, number=7, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='creation_timestamp', full_name='wireless_android_skyjam.Album.creation_timestamp', index=7, number=8, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='artist', full_name='wireless_android_skyjam.Album.artist', index=8, number=9, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=11986, serialized_end=12224, ) _ALBUMSORTORDER = _descriptor.Descriptor( name='AlbumSortOrder', full_name='wireless_android_skyjam.AlbumSortOrder', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='attribute', full_name='wireless_android_skyjam.AlbumSortOrder.attribute', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='descending', full_name='wireless_android_skyjam.AlbumSortOrder.descending', index=1, number=2, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _ALBUMSORTORDER_ALBUMATTRIBUTE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=12227, serialized_end=12414, ) _GETALBUMSREQUEST = _descriptor.Descriptor( name='GetAlbumsRequest', full_name='wireless_android_skyjam.GetAlbumsRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.GetAlbumsRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sort_order', full_name='wireless_android_skyjam.GetAlbumsRequest.sort_order', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='max_results', full_name='wireless_android_skyjam.GetAlbumsRequest.max_results', index=2, number=3, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=12416, serialized_end=12533, ) _GETALBUMSRESPONSE = _descriptor.Descriptor( name='GetAlbumsResponse', full_name='wireless_android_skyjam.GetAlbumsResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='album', full_name='wireless_android_skyjam.GetAlbumsResponse.album', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=12535, serialized_end=12601, ) _ARTIST = _descriptor.Descriptor( name='Artist', full_name='wireless_android_skyjam.Artist', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='name', full_name='wireless_android_skyjam.Artist.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='total_track_count', full_name='wireless_android_skyjam.Artist.total_track_count', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album', full_name='wireless_android_skyjam.Artist.album', index=2, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=12603, serialized_end=12699, ) _ARTISTSORTORDER = _descriptor.Descriptor( name='ArtistSortOrder', full_name='wireless_android_skyjam.ArtistSortOrder', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='descending', full_name='wireless_android_skyjam.ArtistSortOrder.descending', index=0, number=2, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=12701, serialized_end=12745, ) _GETARTISTSREQUEST = _descriptor.Descriptor( name='GetArtistsRequest', full_name='wireless_android_skyjam.GetArtistsRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.GetArtistsRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sort_order', full_name='wireless_android_skyjam.GetArtistsRequest.sort_order', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='max_results', full_name='wireless_android_skyjam.GetArtistsRequest.max_results', index=2, number=3, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=12747, serialized_end=12866, ) _GETARTISTSRESPONSE = _descriptor.Descriptor( name='GetArtistsResponse', full_name='wireless_android_skyjam.GetArtistsResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='artist', full_name='wireless_android_skyjam.GetArtistsResponse.artist', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=12868, serialized_end=12937, ) _MUSICGENRE = _descriptor.Descriptor( name='MusicGenre', full_name='wireless_android_skyjam.MusicGenre', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='name', full_name='wireless_android_skyjam.MusicGenre.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='total_track_count', full_name='wireless_android_skyjam.MusicGenre.total_track_count', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album', full_name='wireless_android_skyjam.MusicGenre.album', index=2, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=12939, serialized_end=13039, ) _GENRESORTORDER = _descriptor.Descriptor( name='GenreSortOrder', full_name='wireless_android_skyjam.GenreSortOrder', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='descending', full_name='wireless_android_skyjam.GenreSortOrder.descending', index=0, number=2, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=13041, serialized_end=13084, ) _GETGENRESREQUEST = _descriptor.Descriptor( name='GetGenresRequest', full_name='wireless_android_skyjam.GetGenresRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.GetGenresRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sort_order', full_name='wireless_android_skyjam.GetGenresRequest.sort_order', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='max_results', full_name='wireless_android_skyjam.GetGenresRequest.max_results', index=2, number=3, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=13086, serialized_end=13203, ) _GETGENRESRESPONSE = _descriptor.Descriptor( name='GetGenresResponse', full_name='wireless_android_skyjam.GetGenresResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='genre', full_name='wireless_android_skyjam.GetGenresResponse.genre', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=13205, serialized_end=13276, ) _GETDYNAMICPLAYLISTENTRIESREQUEST = _descriptor.Descriptor( name='GetDynamicPlaylistEntriesRequest', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_entries_type', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesRequest.playlist_entries_type', index=1, number=4, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='max_results', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesRequest.max_results', index=2, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='continuation_token', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesRequest.continuation_token', index=3, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='include_all_track_metadata', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesRequest.include_all_track_metadata', index=4, number=5, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _GETDYNAMICPLAYLISTENTRIESREQUEST_DYNAMICPLAYLISTENTRIESTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=13279, serialized_end=13661, ) _GETDYNAMICPLAYLISTENTRIESRESPONSE = _descriptor.Descriptor( name='GetDynamicPlaylistEntriesResponse', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='response_code', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesResponse.response_code', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_entry', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesResponse.playlist_entry', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='estimated_total_results', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesResponse.estimated_total_results', index=2, number=3, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='continuation_token', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesResponse.continuation_token', index=3, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_entries_type', full_name='wireless_android_skyjam.GetDynamicPlaylistEntriesResponse.playlist_entries_type', index=4, number=5, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _GETDYNAMICPLAYLISTENTRIESRESPONSE_DYNAMICPLAYLISTENTRIESTYPE, _GETDYNAMICPLAYLISTENTRIESRESPONSE_RESPONSECODE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=13664, serialized_end=14210, ) _GETAGGREGATIONSBYTRACKTYPEREQUEST = _descriptor.Descriptor( name='GetAggregationsByTrackTypeRequest', full_name='wireless_android_skyjam.GetAggregationsByTrackTypeRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.GetAggregationsByTrackTypeRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=14212, serialized_end=14264, ) _TRACKTYPEAGGREGATE = _descriptor.Descriptor( name='TrackTypeAggregate', full_name='wireless_android_skyjam.TrackTypeAggregate', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='track_type_value', full_name='wireless_android_skyjam.TrackTypeAggregate.track_type_value', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='count', full_name='wireless_android_skyjam.TrackTypeAggregate.count', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _TRACKTYPEAGGREGATE_TRACKTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=14267, serialized_end=14525, ) _GETAGGREGATIONSBYTRACKTYPERESPONSE = _descriptor.Descriptor( name='GetAggregationsByTrackTypeResponse', full_name='wireless_android_skyjam.GetAggregationsByTrackTypeResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='track_type_aggregate', full_name='wireless_android_skyjam.GetAggregationsByTrackTypeResponse.track_type_aggregate', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=14527, serialized_end=14638, ) _GETAGGREGATIONSBYAVAILABILITYSTATUSREQUEST = _descriptor.Descriptor( name='GetAggregationsByAvailabilityStatusRequest', full_name='wireless_android_skyjam.GetAggregationsByAvailabilityStatusRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.GetAggregationsByAvailabilityStatusRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=14640, serialized_end=14701, ) _AVAILABILITYSTATUSAGGREGATE = _descriptor.Descriptor( name='AvailabilityStatusAggregate', full_name='wireless_android_skyjam.AvailabilityStatusAggregate', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='availability_status', full_name='wireless_android_skyjam.AvailabilityStatusAggregate.availability_status', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='count', full_name='wireless_android_skyjam.AvailabilityStatusAggregate.count', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _AVAILABILITYSTATUSAGGREGATE_AVAILABILITYSTATUS, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=14704, serialized_end=14987, ) _GETAGGREGATIONSBYAVAILABILITYSTATUSRESPONSE = _descriptor.Descriptor( name='GetAggregationsByAvailabilityStatusResponse', full_name='wireless_android_skyjam.GetAggregationsByAvailabilityStatusResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='availability_status_aggregate', full_name='wireless_android_skyjam.GetAggregationsByAvailabilityStatusResponse.availability_status_aggregate', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=14990, serialized_end=15128, ) _ADDPROMOTRACKSREQUEST = _descriptor.Descriptor( name='AddPromoTracksRequest', full_name='wireless_android_skyjam.AddPromoTracksRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.AddPromoTracksRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='genre', full_name='wireless_android_skyjam.AddPromoTracksRequest.genre', index=1, number=2, type=9, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=15130, serialized_end=15185, ) _ADDPROMOTRACKSRESPONSE = _descriptor.Descriptor( name='AddPromoTracksResponse', full_name='wireless_android_skyjam.AddPromoTracksResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='track', full_name='wireless_android_skyjam.AddPromoTracksResponse.track', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=15187, serialized_end=15258, ) _GETPLAYLISTAGGREGATIONSREQUEST = _descriptor.Descriptor( name='GetPlaylistAggregationsRequest', full_name='wireless_android_skyjam.GetPlaylistAggregationsRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.GetPlaylistAggregationsRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='max_results', full_name='wireless_android_skyjam.GetPlaylistAggregationsRequest.max_results', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=True, default_value=14, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=15260, serialized_end=15334, ) _PLAYLISTAGGREGATE = _descriptor.Descriptor( name='PlaylistAggregate', full_name='wireless_android_skyjam.PlaylistAggregate', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='playlist_id', full_name='wireless_android_skyjam.PlaylistAggregate.playlist_id', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='name', full_name='wireless_android_skyjam.PlaylistAggregate.name', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album_art', full_name='wireless_android_skyjam.PlaylistAggregate.album_art', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track_count', full_name='wireless_android_skyjam.PlaylistAggregate.track_count', index=3, number=4, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='last_time_played', full_name='wireless_android_skyjam.PlaylistAggregate.last_time_played', index=4, number=5, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=15337, serialized_end=15492, ) _GETPLAYLISTAGGREGATIONSRESPONSE = _descriptor.Descriptor( name='GetPlaylistAggregationsResponse', full_name='wireless_android_skyjam.GetPlaylistAggregationsResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='playlist_aggregate', full_name='wireless_android_skyjam.GetPlaylistAggregationsResponse.playlist_aggregate', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=15494, serialized_end=15599, ) _REMOTECONTROLCOMMANDREQUEST = _descriptor.Descriptor( name='RemoteControlCommandRequest', full_name='wireless_android_skyjam.RemoteControlCommandRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaia_id', full_name='wireless_android_skyjam.RemoteControlCommandRequest.gaia_id', index=0, number=1, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='command', full_name='wireless_android_skyjam.RemoteControlCommandRequest.command', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=15601, serialized_end=15664, ) _REMOTECONTROLCOMMANDRESPONSE = _descriptor.Descriptor( name='RemoteControlCommandResponse', full_name='wireless_android_skyjam.RemoteControlCommandResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='response_code', full_name='wireless_android_skyjam.RemoteControlCommandResponse.response_code', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _REMOTECONTROLCOMMANDRESPONSE_RESPONSECODE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=15667, serialized_end=15870, ) _AUDIOREF.fields_by_name['store'].enum_type = _AUDIOREF_STORE _AUDIOREF_STORE.containing_type = _AUDIOREF _IMAGEREF.fields_by_name['store'].enum_type = _IMAGEREF_STORE _IMAGEREF.fields_by_name['origin'].enum_type = _IMAGEREF_ORIGIN _IMAGEREF_STORE.containing_type = _IMAGEREF _IMAGEREF_ORIGIN.containing_type = _IMAGEREF _TRACKEXTRAS.fields_by_name['additional_metadata'].message_type = _ADDITIONALMETADATA _TRACK.fields_by_name['audio_ref'].message_type = _AUDIOREF _TRACK.fields_by_name['album_art_ref'].message_type = _IMAGEREF _TRACK.fields_by_name['availability_status'].enum_type = _TRACK_AVAILABILITYSTATUS _TRACK.fields_by_name['content_type'].enum_type = _TRACK_CONTENTTYPE _TRACK.fields_by_name['channels'].enum_type = _TRACK_CHANNELS _TRACK.fields_by_name['track_type'].enum_type = _TRACK_TRACKTYPE _TRACK.fields_by_name['rating'].enum_type = _TRACK_RATING _TRACK.fields_by_name['uits_metadata'].message_type = uits__pb2._UITSMETADATA _TRACK.fields_by_name['original_content_type'].enum_type = _TRACK_CONTENTTYPE _TRACK.fields_by_name['uploaded_uits'].message_type = _UPLOADEDUITSID3TAG _TRACK.fields_by_name['explicit_type'].enum_type = _TRACK_EXPLICITTYPE _TRACK.fields_by_name['track_extras'].message_type = _TRACKEXTRAS _TRACK_AVAILABILITYSTATUS.containing_type = _TRACK _TRACK_CONTENTTYPE.containing_type = _TRACK _TRACK_CHANNELS.containing_type = _TRACK _TRACK_TRACKTYPE.containing_type = _TRACK _TRACK_RATING.containing_type = _TRACK _TRACK_EXPLICITTYPE.containing_type = _TRACK _TRACKS.fields_by_name['track'].message_type = _TRACK _PLAYLIST.fields_by_name['playlist_type'].enum_type = _PLAYLIST_PLAYLISTTYPE _PLAYLIST.fields_by_name['playlist_art_ref'].message_type = _IMAGEREF _PLAYLIST_PLAYLISTTYPE.containing_type = _PLAYLIST _PLAYLISTENTRY.fields_by_name['relative_position_id_type'].enum_type = _PLAYLISTENTRY_RELATIVEPOSITIONIDTYPE _PLAYLISTENTRY.fields_by_name['track'].message_type = _TRACK _PLAYLISTENTRY_RELATIVEPOSITIONIDTYPE.containing_type = _PLAYLISTENTRY _TRACKSEARCHRESTRICTION.fields_by_name['attribute'].enum_type = _TRACKSEARCHRESTRICTION_TRACKATTRIBUTE _TRACKSEARCHRESTRICTION.fields_by_name['comparison_type'].enum_type = _TRACKSEARCHRESTRICTION_COMPARISONTYPE _TRACKSEARCHRESTRICTION_TRACKATTRIBUTE.containing_type = _TRACKSEARCHRESTRICTION _TRACKSEARCHRESTRICTION_COMPARISONTYPE.containing_type = _TRACKSEARCHRESTRICTION _TRACKSEARCHRESTRICTIONSET.fields_by_name['type'].enum_type = _TRACKSEARCHRESTRICTIONSET_RESTRICTIONSETTYPE _TRACKSEARCHRESTRICTIONSET.fields_by_name['restriction'].message_type = _TRACKSEARCHRESTRICTION _TRACKSEARCHRESTRICTIONSET.fields_by_name['sub_set'].message_type = _TRACKSEARCHRESTRICTIONSET _TRACKSEARCHRESTRICTIONSET_RESTRICTIONSETTYPE.containing_type = _TRACKSEARCHRESTRICTIONSET _TRACKSORTORDER.fields_by_name['attribute'].enum_type = _TRACKSORTORDER_TRACKATTRIBUTE _TRACKSORTORDER_TRACKATTRIBUTE.containing_type = _TRACKSORTORDER _GETTRACKSREQUEST.fields_by_name['search_restriction'].message_type = _TRACKSEARCHRESTRICTION _GETTRACKSREQUEST.fields_by_name['sort_order'].message_type = _TRACKSORTORDER _GETTRACKSREQUEST.fields_by_name['restriction_set'].message_type = _TRACKSEARCHRESTRICTIONSET _GETTRACKSREQUEST.fields_by_name['track_projection'].enum_type = _GETTRACKSREQUEST_TRACKPROJECTION _GETTRACKSREQUEST_TRACKPROJECTION.containing_type = _GETTRACKSREQUEST _GETTRACKSRESPONSE.fields_by_name['response_code'].enum_type = _GETTRACKSRESPONSE_RESPONSECODE _GETTRACKSRESPONSE.fields_by_name['track'].message_type = _TRACK _GETTRACKSRESPONSE_RESPONSECODE.containing_type = _GETTRACKSRESPONSE _GETPLAYLISTENTRIESRESPONSE.fields_by_name['response_code'].enum_type = _GETPLAYLISTENTRIESRESPONSE_RESPONSECODE _GETPLAYLISTENTRIESRESPONSE.fields_by_name['playlist_entry'].message_type = _PLAYLISTENTRY _GETPLAYLISTENTRIESRESPONSE_RESPONSECODE.containing_type = _GETPLAYLISTENTRIESRESPONSE _PLAYLISTSORTORDER.fields_by_name['attribute'].enum_type = _PLAYLISTSORTORDER_PLAYLISTATTRIBUTE _PLAYLISTSORTORDER_PLAYLISTATTRIBUTE.containing_type = _PLAYLISTSORTORDER _GETPLAYLISTSREQUEST.fields_by_name['sort_order'].message_type = _PLAYLISTSORTORDER _GETPLAYLISTSRESPONSE.fields_by_name['response_code'].enum_type = _GETPLAYLISTSRESPONSE_RESPONSECODE _GETPLAYLISTSRESPONSE.fields_by_name['playlist'].message_type = _PLAYLIST _GETPLAYLISTSRESPONSE_RESPONSECODE.containing_type = _GETPLAYLISTSRESPONSE _BATCHLOOKUPREQUEST.fields_by_name['track'].message_type = _LOOKUPTRACKREQUEST _BATCHLOOKUPREQUEST.fields_by_name['playlist'].message_type = _LOOKUPPLAYLISTREQUEST _BATCHLOOKUPREQUEST.fields_by_name['metadata_type'].enum_type = _BATCHLOOKUPREQUEST_METADATATYPE _BATCHLOOKUPREQUEST.fields_by_name['playlist_entry'].message_type = _LOOKUPPLAYLISTENTRYREQUEST _BATCHLOOKUPREQUEST_METADATATYPE.containing_type = _BATCHLOOKUPREQUEST _BATCHLOOKUPRESPONSE.fields_by_name['track'].message_type = _TRACK _BATCHLOOKUPRESPONSE.fields_by_name['playlist'].message_type = _PLAYLIST _BATCHLOOKUPRESPONSE.fields_by_name['playlist_entry'].message_type = _PLAYLISTENTRY _MUTATETRACKREQUEST.fields_by_name['create_track'].message_type = _TRACK _MUTATETRACKREQUEST.fields_by_name['update_track'].message_type = _TRACK _MUTATERESPONSE.fields_by_name['response_code'].enum_type = _MUTATERESPONSE_MUTATERESPONSECODE _MUTATERESPONSE.fields_by_name['availability_status'].enum_type = _MUTATERESPONSE_AVAILABILITYSTATUS _MUTATERESPONSE_MUTATERESPONSECODE.containing_type = _MUTATERESPONSE _MUTATERESPONSE_AVAILABILITYSTATUS.containing_type = _MUTATERESPONSE _BATCHMUTATETRACKSREQUEST.fields_by_name['track_mutation'].message_type = _MUTATETRACKREQUEST _BATCHMUTATETRACKSRESPONSE.fields_by_name['response_code'].enum_type = _BATCHMUTATETRACKSRESPONSE_BATCHMUTATETRACKSRESPONSECODE _BATCHMUTATETRACKSRESPONSE.fields_by_name['mutate_response'].message_type = _MUTATERESPONSE _BATCHMUTATETRACKSRESPONSE_BATCHMUTATETRACKSRESPONSECODE.containing_type = _BATCHMUTATETRACKSRESPONSE _MUTATEPLAYLISTREQUEST.fields_by_name['create_playlist'].message_type = _PLAYLIST _MUTATEPLAYLISTREQUEST.fields_by_name['update_playlist'].message_type = _PLAYLIST _MUTATEPLAYLISTREQUEST.fields_by_name['playlist_entry'].message_type = _PLAYLISTENTRY _BATCHMUTATEPLAYLISTSREQUEST.fields_by_name['playlist_mutation'].message_type = _MUTATEPLAYLISTREQUEST _BATCHMUTATEPLAYLISTSRESPONSE.fields_by_name['response_code'].enum_type = _BATCHMUTATEPLAYLISTSRESPONSE_BATCHMUTATEPLAYLISTSRESPONSECODE _BATCHMUTATEPLAYLISTSRESPONSE.fields_by_name['mutate_response'].message_type = _MUTATERESPONSE _BATCHMUTATEPLAYLISTSRESPONSE_BATCHMUTATEPLAYLISTSRESPONSECODE.containing_type = _BATCHMUTATEPLAYLISTSRESPONSE _MUTATEPLAYLISTENTRYREQUEST.fields_by_name['create_playlist_entry'].message_type = _PLAYLISTENTRY _MUTATEPLAYLISTENTRYREQUEST.fields_by_name['update_playlist_entry'].message_type = _PLAYLISTENTRY _MUTATEPLAYLISTENTRYREQUEST.fields_by_name['delete_playlist_entry'].message_type = _PLAYLISTENTRY _BATCHMUTATEPLAYLISTENTRIESREQUEST.fields_by_name['playlist_entry_mutation'].message_type = _MUTATEPLAYLISTENTRYREQUEST _BATCHMUTATEPLAYLISTENTRIESRESPONSE.fields_by_name['response_code'].enum_type = _BATCHMUTATEPLAYLISTENTRIESRESPONSE_BATCHMUTATEPLAYLISTENTRIESRESPONSECODE _BATCHMUTATEPLAYLISTENTRIESRESPONSE.fields_by_name['mutate_response'].message_type = _MUTATERESPONSE _BATCHMUTATEPLAYLISTENTRIESRESPONSE_BATCHMUTATEPLAYLISTENTRIESRESPONSECODE.containing_type = _BATCHMUTATEPLAYLISTENTRIESRESPONSE _MAGICPLAYLISTSEED.fields_by_name['seed_type'].enum_type = _MAGICPLAYLISTSEED_SEEDTYPE _MAGICPLAYLISTSEED_SEEDTYPE.containing_type = _MAGICPLAYLISTSEED _MAGICPLAYLISTREQUEST.fields_by_name['seed'].message_type = _MAGICPLAYLISTSEED _MAGICPLAYLISTRESPONSE.fields_by_name['playlist'].message_type = _PLAYLIST _MAGICPLAYLISTRESPONSE.fields_by_name['playlist_entry'].message_type = _PLAYLISTENTRY _ALBUM.fields_by_name['album_art'].message_type = _IMAGEREF _ALBUMSORTORDER.fields_by_name['attribute'].enum_type = _ALBUMSORTORDER_ALBUMATTRIBUTE _ALBUMSORTORDER_ALBUMATTRIBUTE.containing_type = _ALBUMSORTORDER _GETALBUMSREQUEST.fields_by_name['sort_order'].message_type = _ALBUMSORTORDER _GETALBUMSRESPONSE.fields_by_name['album'].message_type = _ALBUM _ARTIST.fields_by_name['album'].message_type = _ALBUM _GETARTISTSREQUEST.fields_by_name['sort_order'].message_type = _ARTISTSORTORDER _GETARTISTSRESPONSE.fields_by_name['artist'].message_type = _ARTIST _MUSICGENRE.fields_by_name['album'].message_type = _ALBUM _GETGENRESREQUEST.fields_by_name['sort_order'].message_type = _GENRESORTORDER _GETGENRESRESPONSE.fields_by_name['genre'].message_type = _MUSICGENRE _GETDYNAMICPLAYLISTENTRIESREQUEST.fields_by_name['playlist_entries_type'].enum_type = _GETDYNAMICPLAYLISTENTRIESREQUEST_DYNAMICPLAYLISTENTRIESTYPE _GETDYNAMICPLAYLISTENTRIESREQUEST_DYNAMICPLAYLISTENTRIESTYPE.containing_type = _GETDYNAMICPLAYLISTENTRIESREQUEST _GETDYNAMICPLAYLISTENTRIESRESPONSE.fields_by_name['response_code'].enum_type = _GETDYNAMICPLAYLISTENTRIESRESPONSE_RESPONSECODE _GETDYNAMICPLAYLISTENTRIESRESPONSE.fields_by_name['playlist_entry'].message_type = _PLAYLISTENTRY _GETDYNAMICPLAYLISTENTRIESRESPONSE.fields_by_name['playlist_entries_type'].enum_type = _GETDYNAMICPLAYLISTENTRIESRESPONSE_DYNAMICPLAYLISTENTRIESTYPE _GETDYNAMICPLAYLISTENTRIESRESPONSE_DYNAMICPLAYLISTENTRIESTYPE.containing_type = _GETDYNAMICPLAYLISTENTRIESRESPONSE _GETDYNAMICPLAYLISTENTRIESRESPONSE_RESPONSECODE.containing_type = _GETDYNAMICPLAYLISTENTRIESRESPONSE _TRACKTYPEAGGREGATE.fields_by_name['track_type_value'].enum_type = _TRACKTYPEAGGREGATE_TRACKTYPE _TRACKTYPEAGGREGATE_TRACKTYPE.containing_type = _TRACKTYPEAGGREGATE _GETAGGREGATIONSBYTRACKTYPERESPONSE.fields_by_name['track_type_aggregate'].message_type = _TRACKTYPEAGGREGATE _AVAILABILITYSTATUSAGGREGATE.fields_by_name['availability_status'].enum_type = _AVAILABILITYSTATUSAGGREGATE_AVAILABILITYSTATUS _AVAILABILITYSTATUSAGGREGATE_AVAILABILITYSTATUS.containing_type = _AVAILABILITYSTATUSAGGREGATE _GETAGGREGATIONSBYAVAILABILITYSTATUSRESPONSE.fields_by_name['availability_status_aggregate'].message_type = _AVAILABILITYSTATUSAGGREGATE _ADDPROMOTRACKSRESPONSE.fields_by_name['track'].message_type = _TRACK _PLAYLISTAGGREGATE.fields_by_name['album_art'].message_type = _IMAGEREF _GETPLAYLISTAGGREGATIONSRESPONSE.fields_by_name['playlist_aggregate'].message_type = _PLAYLISTAGGREGATE _REMOTECONTROLCOMMANDRESPONSE.fields_by_name['response_code'].enum_type = _REMOTECONTROLCOMMANDRESPONSE_RESPONSECODE _REMOTECONTROLCOMMANDRESPONSE_RESPONSECODE.containing_type = _REMOTECONTROLCOMMANDRESPONSE DESCRIPTOR.message_types_by_name['AudioRef'] = _AUDIOREF DESCRIPTOR.message_types_by_name['ImageRef'] = _IMAGEREF DESCRIPTOR.message_types_by_name['UploadedUitsId3Tag'] = _UPLOADEDUITSID3TAG DESCRIPTOR.message_types_by_name['AdditionalMetadata'] = _ADDITIONALMETADATA DESCRIPTOR.message_types_by_name['TrackExtras'] = _TRACKEXTRAS DESCRIPTOR.message_types_by_name['Track'] = _TRACK DESCRIPTOR.message_types_by_name['Tracks'] = _TRACKS DESCRIPTOR.message_types_by_name['Playlist'] = _PLAYLIST DESCRIPTOR.message_types_by_name['PlaylistEntry'] = _PLAYLISTENTRY DESCRIPTOR.message_types_by_name['TrackSearchRestriction'] = _TRACKSEARCHRESTRICTION DESCRIPTOR.message_types_by_name['TrackSearchRestrictionSet'] = _TRACKSEARCHRESTRICTIONSET DESCRIPTOR.message_types_by_name['TrackSortOrder'] = _TRACKSORTORDER DESCRIPTOR.message_types_by_name['GetTracksRequest'] = _GETTRACKSREQUEST DESCRIPTOR.message_types_by_name['GetTracksResponse'] = _GETTRACKSRESPONSE DESCRIPTOR.message_types_by_name['GetPlaylistEntriesRequest'] = _GETPLAYLISTENTRIESREQUEST DESCRIPTOR.message_types_by_name['GetPlaylistEntriesResponse'] = _GETPLAYLISTENTRIESRESPONSE DESCRIPTOR.message_types_by_name['PlaylistSortOrder'] = _PLAYLISTSORTORDER DESCRIPTOR.message_types_by_name['GetPlaylistsRequest'] = _GETPLAYLISTSREQUEST DESCRIPTOR.message_types_by_name['GetPlaylistsResponse'] = _GETPLAYLISTSRESPONSE DESCRIPTOR.message_types_by_name['LookupTrackRequest'] = _LOOKUPTRACKREQUEST DESCRIPTOR.message_types_by_name['LookupPlaylistEntryRequest'] = _LOOKUPPLAYLISTENTRYREQUEST DESCRIPTOR.message_types_by_name['LookupPlaylistRequest'] = _LOOKUPPLAYLISTREQUEST DESCRIPTOR.message_types_by_name['BatchLookupRequest'] = _BATCHLOOKUPREQUEST DESCRIPTOR.message_types_by_name['BatchLookupResponse'] = _BATCHLOOKUPRESPONSE DESCRIPTOR.message_types_by_name['MutateTrackRequest'] = _MUTATETRACKREQUEST DESCRIPTOR.message_types_by_name['MutateResponse'] = _MUTATERESPONSE DESCRIPTOR.message_types_by_name['BatchMutateTracksRequest'] = _BATCHMUTATETRACKSREQUEST DESCRIPTOR.message_types_by_name['BatchMutateTracksResponse'] = _BATCHMUTATETRACKSRESPONSE DESCRIPTOR.message_types_by_name['MutatePlaylistRequest'] = _MUTATEPLAYLISTREQUEST DESCRIPTOR.message_types_by_name['BatchMutatePlaylistsRequest'] = _BATCHMUTATEPLAYLISTSREQUEST DESCRIPTOR.message_types_by_name['BatchMutatePlaylistsResponse'] = _BATCHMUTATEPLAYLISTSRESPONSE DESCRIPTOR.message_types_by_name['MutatePlaylistEntryRequest'] = _MUTATEPLAYLISTENTRYREQUEST DESCRIPTOR.message_types_by_name['BatchMutatePlaylistEntriesRequest'] = _BATCHMUTATEPLAYLISTENTRIESREQUEST DESCRIPTOR.message_types_by_name['BatchMutatePlaylistEntriesResponse'] = _BATCHMUTATEPLAYLISTENTRIESRESPONSE DESCRIPTOR.message_types_by_name['MagicPlaylistSeed'] = _MAGICPLAYLISTSEED DESCRIPTOR.message_types_by_name['MagicPlaylistRequest'] = _MAGICPLAYLISTREQUEST DESCRIPTOR.message_types_by_name['MagicPlaylistResponse'] = _MAGICPLAYLISTRESPONSE DESCRIPTOR.message_types_by_name['FlushLockerRequest'] = _FLUSHLOCKERREQUEST DESCRIPTOR.message_types_by_name['FlushLockerResponse'] = _FLUSHLOCKERRESPONSE DESCRIPTOR.message_types_by_name['LockerNotification'] = _LOCKERNOTIFICATION DESCRIPTOR.message_types_by_name['Album'] = _ALBUM DESCRIPTOR.message_types_by_name['AlbumSortOrder'] = _ALBUMSORTORDER DESCRIPTOR.message_types_by_name['GetAlbumsRequest'] = _GETALBUMSREQUEST DESCRIPTOR.message_types_by_name['GetAlbumsResponse'] = _GETALBUMSRESPONSE DESCRIPTOR.message_types_by_name['Artist'] = _ARTIST DESCRIPTOR.message_types_by_name['ArtistSortOrder'] = _ARTISTSORTORDER DESCRIPTOR.message_types_by_name['GetArtistsRequest'] = _GETARTISTSREQUEST DESCRIPTOR.message_types_by_name['GetArtistsResponse'] = _GETARTISTSRESPONSE DESCRIPTOR.message_types_by_name['MusicGenre'] = _MUSICGENRE DESCRIPTOR.message_types_by_name['GenreSortOrder'] = _GENRESORTORDER DESCRIPTOR.message_types_by_name['GetGenresRequest'] = _GETGENRESREQUEST DESCRIPTOR.message_types_by_name['GetGenresResponse'] = _GETGENRESRESPONSE DESCRIPTOR.message_types_by_name['GetDynamicPlaylistEntriesRequest'] = _GETDYNAMICPLAYLISTENTRIESREQUEST DESCRIPTOR.message_types_by_name['GetDynamicPlaylistEntriesResponse'] = _GETDYNAMICPLAYLISTENTRIESRESPONSE DESCRIPTOR.message_types_by_name['GetAggregationsByTrackTypeRequest'] = _GETAGGREGATIONSBYTRACKTYPEREQUEST DESCRIPTOR.message_types_by_name['TrackTypeAggregate'] = _TRACKTYPEAGGREGATE DESCRIPTOR.message_types_by_name['GetAggregationsByTrackTypeResponse'] = _GETAGGREGATIONSBYTRACKTYPERESPONSE DESCRIPTOR.message_types_by_name['GetAggregationsByAvailabilityStatusRequest'] = _GETAGGREGATIONSBYAVAILABILITYSTATUSREQUEST DESCRIPTOR.message_types_by_name['AvailabilityStatusAggregate'] = _AVAILABILITYSTATUSAGGREGATE DESCRIPTOR.message_types_by_name['GetAggregationsByAvailabilityStatusResponse'] = _GETAGGREGATIONSBYAVAILABILITYSTATUSRESPONSE DESCRIPTOR.message_types_by_name['AddPromoTracksRequest'] = _ADDPROMOTRACKSREQUEST DESCRIPTOR.message_types_by_name['AddPromoTracksResponse'] = _ADDPROMOTRACKSRESPONSE DESCRIPTOR.message_types_by_name['GetPlaylistAggregationsRequest'] = _GETPLAYLISTAGGREGATIONSREQUEST DESCRIPTOR.message_types_by_name['PlaylistAggregate'] = _PLAYLISTAGGREGATE DESCRIPTOR.message_types_by_name['GetPlaylistAggregationsResponse'] = _GETPLAYLISTAGGREGATIONSRESPONSE DESCRIPTOR.message_types_by_name['RemoteControlCommandRequest'] = _REMOTECONTROLCOMMANDREQUEST DESCRIPTOR.message_types_by_name['RemoteControlCommandResponse'] = _REMOTECONTROLCOMMANDRESPONSE AudioRef = _reflection.GeneratedProtocolMessageType('AudioRef', (_message.Message,), dict( DESCRIPTOR = _AUDIOREF, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.AudioRef) )) _sym_db.RegisterMessage(AudioRef) ImageRef = _reflection.GeneratedProtocolMessageType('ImageRef', (_message.Message,), dict( DESCRIPTOR = _IMAGEREF, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.ImageRef) )) _sym_db.RegisterMessage(ImageRef) UploadedUitsId3Tag = _reflection.GeneratedProtocolMessageType('UploadedUitsId3Tag', (_message.Message,), dict( DESCRIPTOR = _UPLOADEDUITSID3TAG, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UploadedUitsId3Tag) )) _sym_db.RegisterMessage(UploadedUitsId3Tag) AdditionalMetadata = _reflection.GeneratedProtocolMessageType('AdditionalMetadata', (_message.Message,), dict( DESCRIPTOR = _ADDITIONALMETADATA, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.AdditionalMetadata) )) _sym_db.RegisterMessage(AdditionalMetadata) TrackExtras = _reflection.GeneratedProtocolMessageType('TrackExtras', (_message.Message,), dict( DESCRIPTOR = _TRACKEXTRAS, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.TrackExtras) )) _sym_db.RegisterMessage(TrackExtras) Track = _reflection.GeneratedProtocolMessageType('Track', (_message.Message,), dict( DESCRIPTOR = _TRACK, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.Track) )) _sym_db.RegisterMessage(Track) Tracks = _reflection.GeneratedProtocolMessageType('Tracks', (_message.Message,), dict( DESCRIPTOR = _TRACKS, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.Tracks) )) _sym_db.RegisterMessage(Tracks) Playlist = _reflection.GeneratedProtocolMessageType('Playlist', (_message.Message,), dict( DESCRIPTOR = _PLAYLIST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.Playlist) )) _sym_db.RegisterMessage(Playlist) PlaylistEntry = _reflection.GeneratedProtocolMessageType('PlaylistEntry', (_message.Message,), dict( DESCRIPTOR = _PLAYLISTENTRY, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.PlaylistEntry) )) _sym_db.RegisterMessage(PlaylistEntry) TrackSearchRestriction = _reflection.GeneratedProtocolMessageType('TrackSearchRestriction', (_message.Message,), dict( DESCRIPTOR = _TRACKSEARCHRESTRICTION, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.TrackSearchRestriction) )) _sym_db.RegisterMessage(TrackSearchRestriction) TrackSearchRestrictionSet = _reflection.GeneratedProtocolMessageType('TrackSearchRestrictionSet', (_message.Message,), dict( DESCRIPTOR = _TRACKSEARCHRESTRICTIONSET, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.TrackSearchRestrictionSet) )) _sym_db.RegisterMessage(TrackSearchRestrictionSet) TrackSortOrder = _reflection.GeneratedProtocolMessageType('TrackSortOrder', (_message.Message,), dict( DESCRIPTOR = _TRACKSORTORDER, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.TrackSortOrder) )) _sym_db.RegisterMessage(TrackSortOrder) GetTracksRequest = _reflection.GeneratedProtocolMessageType('GetTracksRequest', (_message.Message,), dict( DESCRIPTOR = _GETTRACKSREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetTracksRequest) )) _sym_db.RegisterMessage(GetTracksRequest) GetTracksResponse = _reflection.GeneratedProtocolMessageType('GetTracksResponse', (_message.Message,), dict( DESCRIPTOR = _GETTRACKSRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetTracksResponse) )) _sym_db.RegisterMessage(GetTracksResponse) GetPlaylistEntriesRequest = _reflection.GeneratedProtocolMessageType('GetPlaylistEntriesRequest', (_message.Message,), dict( DESCRIPTOR = _GETPLAYLISTENTRIESREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetPlaylistEntriesRequest) )) _sym_db.RegisterMessage(GetPlaylistEntriesRequest) GetPlaylistEntriesResponse = _reflection.GeneratedProtocolMessageType('GetPlaylistEntriesResponse', (_message.Message,), dict( DESCRIPTOR = _GETPLAYLISTENTRIESRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetPlaylistEntriesResponse) )) _sym_db.RegisterMessage(GetPlaylistEntriesResponse) PlaylistSortOrder = _reflection.GeneratedProtocolMessageType('PlaylistSortOrder', (_message.Message,), dict( DESCRIPTOR = _PLAYLISTSORTORDER, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.PlaylistSortOrder) )) _sym_db.RegisterMessage(PlaylistSortOrder) GetPlaylistsRequest = _reflection.GeneratedProtocolMessageType('GetPlaylistsRequest', (_message.Message,), dict( DESCRIPTOR = _GETPLAYLISTSREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetPlaylistsRequest) )) _sym_db.RegisterMessage(GetPlaylistsRequest) GetPlaylistsResponse = _reflection.GeneratedProtocolMessageType('GetPlaylistsResponse', (_message.Message,), dict( DESCRIPTOR = _GETPLAYLISTSRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetPlaylistsResponse) )) _sym_db.RegisterMessage(GetPlaylistsResponse) LookupTrackRequest = _reflection.GeneratedProtocolMessageType('LookupTrackRequest', (_message.Message,), dict( DESCRIPTOR = _LOOKUPTRACKREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.LookupTrackRequest) )) _sym_db.RegisterMessage(LookupTrackRequest) LookupPlaylistEntryRequest = _reflection.GeneratedProtocolMessageType('LookupPlaylistEntryRequest', (_message.Message,), dict( DESCRIPTOR = _LOOKUPPLAYLISTENTRYREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.LookupPlaylistEntryRequest) )) _sym_db.RegisterMessage(LookupPlaylistEntryRequest) LookupPlaylistRequest = _reflection.GeneratedProtocolMessageType('LookupPlaylistRequest', (_message.Message,), dict( DESCRIPTOR = _LOOKUPPLAYLISTREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.LookupPlaylistRequest) )) _sym_db.RegisterMessage(LookupPlaylistRequest) BatchLookupRequest = _reflection.GeneratedProtocolMessageType('BatchLookupRequest', (_message.Message,), dict( DESCRIPTOR = _BATCHLOOKUPREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.BatchLookupRequest) )) _sym_db.RegisterMessage(BatchLookupRequest) BatchLookupResponse = _reflection.GeneratedProtocolMessageType('BatchLookupResponse', (_message.Message,), dict( DESCRIPTOR = _BATCHLOOKUPRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.BatchLookupResponse) )) _sym_db.RegisterMessage(BatchLookupResponse) MutateTrackRequest = _reflection.GeneratedProtocolMessageType('MutateTrackRequest', (_message.Message,), dict( DESCRIPTOR = _MUTATETRACKREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.MutateTrackRequest) )) _sym_db.RegisterMessage(MutateTrackRequest) MutateResponse = _reflection.GeneratedProtocolMessageType('MutateResponse', (_message.Message,), dict( DESCRIPTOR = _MUTATERESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.MutateResponse) )) _sym_db.RegisterMessage(MutateResponse) BatchMutateTracksRequest = _reflection.GeneratedProtocolMessageType('BatchMutateTracksRequest', (_message.Message,), dict( DESCRIPTOR = _BATCHMUTATETRACKSREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.BatchMutateTracksRequest) )) _sym_db.RegisterMessage(BatchMutateTracksRequest) BatchMutateTracksResponse = _reflection.GeneratedProtocolMessageType('BatchMutateTracksResponse', (_message.Message,), dict( DESCRIPTOR = _BATCHMUTATETRACKSRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.BatchMutateTracksResponse) )) _sym_db.RegisterMessage(BatchMutateTracksResponse) MutatePlaylistRequest = _reflection.GeneratedProtocolMessageType('MutatePlaylistRequest', (_message.Message,), dict( DESCRIPTOR = _MUTATEPLAYLISTREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.MutatePlaylistRequest) )) _sym_db.RegisterMessage(MutatePlaylistRequest) BatchMutatePlaylistsRequest = _reflection.GeneratedProtocolMessageType('BatchMutatePlaylistsRequest', (_message.Message,), dict( DESCRIPTOR = _BATCHMUTATEPLAYLISTSREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.BatchMutatePlaylistsRequest) )) _sym_db.RegisterMessage(BatchMutatePlaylistsRequest) BatchMutatePlaylistsResponse = _reflection.GeneratedProtocolMessageType('BatchMutatePlaylistsResponse', (_message.Message,), dict( DESCRIPTOR = _BATCHMUTATEPLAYLISTSRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.BatchMutatePlaylistsResponse) )) _sym_db.RegisterMessage(BatchMutatePlaylistsResponse) MutatePlaylistEntryRequest = _reflection.GeneratedProtocolMessageType('MutatePlaylistEntryRequest', (_message.Message,), dict( DESCRIPTOR = _MUTATEPLAYLISTENTRYREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.MutatePlaylistEntryRequest) )) _sym_db.RegisterMessage(MutatePlaylistEntryRequest) BatchMutatePlaylistEntriesRequest = _reflection.GeneratedProtocolMessageType('BatchMutatePlaylistEntriesRequest', (_message.Message,), dict( DESCRIPTOR = _BATCHMUTATEPLAYLISTENTRIESREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.BatchMutatePlaylistEntriesRequest) )) _sym_db.RegisterMessage(BatchMutatePlaylistEntriesRequest) BatchMutatePlaylistEntriesResponse = _reflection.GeneratedProtocolMessageType('BatchMutatePlaylistEntriesResponse', (_message.Message,), dict( DESCRIPTOR = _BATCHMUTATEPLAYLISTENTRIESRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.BatchMutatePlaylistEntriesResponse) )) _sym_db.RegisterMessage(BatchMutatePlaylistEntriesResponse) MagicPlaylistSeed = _reflection.GeneratedProtocolMessageType('MagicPlaylistSeed', (_message.Message,), dict( DESCRIPTOR = _MAGICPLAYLISTSEED, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.MagicPlaylistSeed) )) _sym_db.RegisterMessage(MagicPlaylistSeed) MagicPlaylistRequest = _reflection.GeneratedProtocolMessageType('MagicPlaylistRequest', (_message.Message,), dict( DESCRIPTOR = _MAGICPLAYLISTREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.MagicPlaylistRequest) )) _sym_db.RegisterMessage(MagicPlaylistRequest) MagicPlaylistResponse = _reflection.GeneratedProtocolMessageType('MagicPlaylistResponse', (_message.Message,), dict( DESCRIPTOR = _MAGICPLAYLISTRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.MagicPlaylistResponse) )) _sym_db.RegisterMessage(MagicPlaylistResponse) FlushLockerRequest = _reflection.GeneratedProtocolMessageType('FlushLockerRequest', (_message.Message,), dict( DESCRIPTOR = _FLUSHLOCKERREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.FlushLockerRequest) )) _sym_db.RegisterMessage(FlushLockerRequest) FlushLockerResponse = _reflection.GeneratedProtocolMessageType('FlushLockerResponse', (_message.Message,), dict( DESCRIPTOR = _FLUSHLOCKERRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.FlushLockerResponse) )) _sym_db.RegisterMessage(FlushLockerResponse) LockerNotification = _reflection.GeneratedProtocolMessageType('LockerNotification', (_message.Message,), dict( DESCRIPTOR = _LOCKERNOTIFICATION, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.LockerNotification) )) _sym_db.RegisterMessage(LockerNotification) Album = _reflection.GeneratedProtocolMessageType('Album', (_message.Message,), dict( DESCRIPTOR = _ALBUM, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.Album) )) _sym_db.RegisterMessage(Album) AlbumSortOrder = _reflection.GeneratedProtocolMessageType('AlbumSortOrder', (_message.Message,), dict( DESCRIPTOR = _ALBUMSORTORDER, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.AlbumSortOrder) )) _sym_db.RegisterMessage(AlbumSortOrder) GetAlbumsRequest = _reflection.GeneratedProtocolMessageType('GetAlbumsRequest', (_message.Message,), dict( DESCRIPTOR = _GETALBUMSREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetAlbumsRequest) )) _sym_db.RegisterMessage(GetAlbumsRequest) GetAlbumsResponse = _reflection.GeneratedProtocolMessageType('GetAlbumsResponse', (_message.Message,), dict( DESCRIPTOR = _GETALBUMSRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetAlbumsResponse) )) _sym_db.RegisterMessage(GetAlbumsResponse) Artist = _reflection.GeneratedProtocolMessageType('Artist', (_message.Message,), dict( DESCRIPTOR = _ARTIST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.Artist) )) _sym_db.RegisterMessage(Artist) ArtistSortOrder = _reflection.GeneratedProtocolMessageType('ArtistSortOrder', (_message.Message,), dict( DESCRIPTOR = _ARTISTSORTORDER, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.ArtistSortOrder) )) _sym_db.RegisterMessage(ArtistSortOrder) GetArtistsRequest = _reflection.GeneratedProtocolMessageType('GetArtistsRequest', (_message.Message,), dict( DESCRIPTOR = _GETARTISTSREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetArtistsRequest) )) _sym_db.RegisterMessage(GetArtistsRequest) GetArtistsResponse = _reflection.GeneratedProtocolMessageType('GetArtistsResponse', (_message.Message,), dict( DESCRIPTOR = _GETARTISTSRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetArtistsResponse) )) _sym_db.RegisterMessage(GetArtistsResponse) MusicGenre = _reflection.GeneratedProtocolMessageType('MusicGenre', (_message.Message,), dict( DESCRIPTOR = _MUSICGENRE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.MusicGenre) )) _sym_db.RegisterMessage(MusicGenre) GenreSortOrder = _reflection.GeneratedProtocolMessageType('GenreSortOrder', (_message.Message,), dict( DESCRIPTOR = _GENRESORTORDER, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GenreSortOrder) )) _sym_db.RegisterMessage(GenreSortOrder) GetGenresRequest = _reflection.GeneratedProtocolMessageType('GetGenresRequest', (_message.Message,), dict( DESCRIPTOR = _GETGENRESREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetGenresRequest) )) _sym_db.RegisterMessage(GetGenresRequest) GetGenresResponse = _reflection.GeneratedProtocolMessageType('GetGenresResponse', (_message.Message,), dict( DESCRIPTOR = _GETGENRESRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetGenresResponse) )) _sym_db.RegisterMessage(GetGenresResponse) GetDynamicPlaylistEntriesRequest = _reflection.GeneratedProtocolMessageType('GetDynamicPlaylistEntriesRequest', (_message.Message,), dict( DESCRIPTOR = _GETDYNAMICPLAYLISTENTRIESREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetDynamicPlaylistEntriesRequest) )) _sym_db.RegisterMessage(GetDynamicPlaylistEntriesRequest) GetDynamicPlaylistEntriesResponse = _reflection.GeneratedProtocolMessageType('GetDynamicPlaylistEntriesResponse', (_message.Message,), dict( DESCRIPTOR = _GETDYNAMICPLAYLISTENTRIESRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetDynamicPlaylistEntriesResponse) )) _sym_db.RegisterMessage(GetDynamicPlaylistEntriesResponse) GetAggregationsByTrackTypeRequest = _reflection.GeneratedProtocolMessageType('GetAggregationsByTrackTypeRequest', (_message.Message,), dict( DESCRIPTOR = _GETAGGREGATIONSBYTRACKTYPEREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetAggregationsByTrackTypeRequest) )) _sym_db.RegisterMessage(GetAggregationsByTrackTypeRequest) TrackTypeAggregate = _reflection.GeneratedProtocolMessageType('TrackTypeAggregate', (_message.Message,), dict( DESCRIPTOR = _TRACKTYPEAGGREGATE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.TrackTypeAggregate) )) _sym_db.RegisterMessage(TrackTypeAggregate) GetAggregationsByTrackTypeResponse = _reflection.GeneratedProtocolMessageType('GetAggregationsByTrackTypeResponse', (_message.Message,), dict( DESCRIPTOR = _GETAGGREGATIONSBYTRACKTYPERESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetAggregationsByTrackTypeResponse) )) _sym_db.RegisterMessage(GetAggregationsByTrackTypeResponse) GetAggregationsByAvailabilityStatusRequest = _reflection.GeneratedProtocolMessageType('GetAggregationsByAvailabilityStatusRequest', (_message.Message,), dict( DESCRIPTOR = _GETAGGREGATIONSBYAVAILABILITYSTATUSREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetAggregationsByAvailabilityStatusRequest) )) _sym_db.RegisterMessage(GetAggregationsByAvailabilityStatusRequest) AvailabilityStatusAggregate = _reflection.GeneratedProtocolMessageType('AvailabilityStatusAggregate', (_message.Message,), dict( DESCRIPTOR = _AVAILABILITYSTATUSAGGREGATE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.AvailabilityStatusAggregate) )) _sym_db.RegisterMessage(AvailabilityStatusAggregate) GetAggregationsByAvailabilityStatusResponse = _reflection.GeneratedProtocolMessageType('GetAggregationsByAvailabilityStatusResponse', (_message.Message,), dict( DESCRIPTOR = _GETAGGREGATIONSBYAVAILABILITYSTATUSRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetAggregationsByAvailabilityStatusResponse) )) _sym_db.RegisterMessage(GetAggregationsByAvailabilityStatusResponse) AddPromoTracksRequest = _reflection.GeneratedProtocolMessageType('AddPromoTracksRequest', (_message.Message,), dict( DESCRIPTOR = _ADDPROMOTRACKSREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.AddPromoTracksRequest) )) _sym_db.RegisterMessage(AddPromoTracksRequest) AddPromoTracksResponse = _reflection.GeneratedProtocolMessageType('AddPromoTracksResponse', (_message.Message,), dict( DESCRIPTOR = _ADDPROMOTRACKSRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.AddPromoTracksResponse) )) _sym_db.RegisterMessage(AddPromoTracksResponse) GetPlaylistAggregationsRequest = _reflection.GeneratedProtocolMessageType('GetPlaylistAggregationsRequest', (_message.Message,), dict( DESCRIPTOR = _GETPLAYLISTAGGREGATIONSREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetPlaylistAggregationsRequest) )) _sym_db.RegisterMessage(GetPlaylistAggregationsRequest) PlaylistAggregate = _reflection.GeneratedProtocolMessageType('PlaylistAggregate', (_message.Message,), dict( DESCRIPTOR = _PLAYLISTAGGREGATE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.PlaylistAggregate) )) _sym_db.RegisterMessage(PlaylistAggregate) GetPlaylistAggregationsResponse = _reflection.GeneratedProtocolMessageType('GetPlaylistAggregationsResponse', (_message.Message,), dict( DESCRIPTOR = _GETPLAYLISTAGGREGATIONSRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetPlaylistAggregationsResponse) )) _sym_db.RegisterMessage(GetPlaylistAggregationsResponse) RemoteControlCommandRequest = _reflection.GeneratedProtocolMessageType('RemoteControlCommandRequest', (_message.Message,), dict( DESCRIPTOR = _REMOTECONTROLCOMMANDREQUEST, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.RemoteControlCommandRequest) )) _sym_db.RegisterMessage(RemoteControlCommandRequest) RemoteControlCommandResponse = _reflection.GeneratedProtocolMessageType('RemoteControlCommandResponse', (_message.Message,), dict( DESCRIPTOR = _REMOTECONTROLCOMMANDRESPONSE, __module__ = 'locker_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.RemoteControlCommandResponse) )) _sym_db.RegisterMessage(RemoteControlCommandResponse) # @@protoc_insertion_point(module_scope) gmusicapi-12.1.1/gmusicapi/protocol/mobileclient.py0000664000175000017500000015252013400371522022717 0ustar simonsimon00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """Calls made by the mobile client.""" from __future__ import print_function, division, absolute_import, unicode_literals from six import raise_from from builtins import * # noqa import base64 import calendar import copy from datetime import datetime from hashlib import sha1 import hmac import time from uuid import uuid1 import validictory import json from gmusicapi.exceptions import ValidationException, CallFailure from gmusicapi.protocol.shared import Call, authtypes from gmusicapi.utils import utils # URL for sj service sj_url = 'https://mclients.googleapis.com/sj/v2.5/' sj_stream_url = 'https://mclients.googleapis.com/music/' # shared schemas sj_image_color_styles = { 'type': 'object', 'additionalProperties': False, 'properties': { 'primary': {'type': 'object', 'additionalProperties': False, 'properties': { 'red': {'type': 'integer'}, 'green': {'type': 'integer'}, 'blue': {'type': 'integer'} }}, 'scrim': {'type': 'object', 'additionalProperties': False, 'properties': { 'red': {'type': 'integer'}, 'green': {'type': 'integer'}, 'blue': {'type': 'integer'} }}, 'accent': {'type': 'object', 'additionalProperties': False, 'properties': { 'red': {'type': 'integer'}, 'green': {'type': 'integer'}, 'blue': {'type': 'integer'} }} } } sj_image = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'url': {'type': 'string'}, 'aspectRatio': {'type': 'string', 'required': False}, 'autogen': {'type': 'boolean', 'required': False}, 'colorStyles': sj_image_color_styles.copy() } } sj_image['properties']['colorStyles']['required'] = False sj_video = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'id': {'type': 'string'}, 'title': {'type': 'string', 'required': False}, 'thumbnails': {'type': 'array', 'required': False, 'items': {'type': 'object', 'properties': { 'url': {'type': 'string'}, 'width': {'type': 'integer'}, 'height': {'type': 'integer'}, }}}, } } sj_track = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'title': {'type': 'string'}, 'artist': {'type': 'string'}, 'album': {'type': 'string'}, 'albumArtist': {'type': 'string', 'blank': True}, 'trackNumber': {'type': 'integer'}, 'totalTrackCount': {'type': 'integer', 'required': False}, 'durationMillis': {'type': 'string'}, 'albumArtRef': {'type': 'array', 'items': {'type': 'object', 'properties': {'url': {'type': 'string'}}}, 'required': False}, 'artistArtRef': {'type': 'array', 'items': {'type': 'object', 'properties': {'url': {'type': 'string'}}}, 'required': False, }, 'discNumber': {'type': 'integer'}, 'totalDiscCount': {'type': 'integer', 'required': False}, 'estimatedSize': {'type': 'string', 'required': False}, 'trackType': {'type': 'string', 'required': False}, 'storeId': {'type': 'string', 'required': False}, 'albumId': {'type': 'string'}, 'artistId': {'type': 'array', 'items': {'type': 'string', 'blank': True}, 'required': False}, 'nid': {'type': 'string', 'required': False}, 'trackAvailableForPurchase': {'type': 'boolean', 'required': False}, 'albumAvailableForPurchase': {'type': 'boolean', 'required': False}, 'composer': {'type': 'string', 'blank': True}, 'playCount': {'type': 'integer', 'required': False}, 'year': {'type': 'integer', 'required': False}, 'rating': {'type': 'string', 'required': False}, 'genre': {'type': 'string', 'required': False}, 'trackAvailableForSubscription': {'type': 'boolean', 'required': False}, # Only available when rating differs from '0' # when using :change_song_metadata:, specifying this value will cause all clients to # properly update (web/mobile). As value int(round(time.time() * 1000000)) works quite well 'lastRatingChangeTimestamp': {'type': 'string', 'required': False}, 'primaryVideo': sj_video.copy(), 'lastModifiedTimestamp': {'type': 'string', 'required': False}, 'explicitType': {'type': 'string', 'required': False}, 'contentType': {'type': 'string', 'required': False}, 'deleted': {'type': 'boolean', 'required': False}, 'creationTimestamp': {'type': 'string', 'required': False}, 'comment': {'type': 'string', 'required': False, 'blank': True}, 'beatsPerMinute': {'type': 'integer', 'required': False}, 'recentTimestamp': {'type': 'string', 'required': False}, 'clientId': {'type': 'string', 'required': False}, 'id': {'type': 'string', 'required': False} } } sj_track['properties']['primaryVideo']['required'] = False sj_playlist = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'name': {'type': 'string'}, 'deleted': {'type': 'boolean', 'required': False}, # for public 'type': {'type': 'string', 'pattern': r'MAGIC|SHARED|USER_GENERATED', 'required': False, }, 'lastModifiedTimestamp': {'type': 'string', 'required': False}, # for public 'recentTimestamp': {'type': 'string', 'required': False}, # for public 'shareToken': {'type': 'string'}, 'ownerProfilePhotoUrl': {'type': 'string', 'required': False}, 'ownerName': {'type': 'string', 'required': False}, 'accessControlled': {'type': 'boolean', 'required': False}, # for public 'shareState': {'type': 'string', 'pattern': r'PRIVATE|PUBLIC', 'required': False}, # for public 'creationTimestamp': {'type': 'string', 'required': False}, # for public 'id': {'type': 'string', 'required': False}, # for public 'albumArtRef': {'type': 'array', 'items': {'type': 'object', 'properties': {'url': {'type': 'string'}}}, 'required': False, }, 'description': {'type': 'string', 'blank': True, 'required': False}, 'explicitType': {'type': 'string', 'required': False}, 'contentType': {'type': 'string', 'required': False} } } sj_plentry = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'id': {'type': 'string'}, 'clientId': {'type': 'string'}, 'playlistId': {'type': 'string'}, 'absolutePosition': {'type': 'string'}, 'trackId': {'type': 'string'}, 'creationTimestamp': {'type': 'string'}, 'lastModifiedTimestamp': {'type': 'string'}, 'deleted': {'type': 'boolean'}, 'source': {'type': 'string'}, 'track': sj_track.copy() }, } sj_plentry['properties']['track']['required'] = False sj_attribution = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'license_url': {'type': 'string', 'required': False}, 'license_title': {'type': 'string', 'required': False}, 'source_title': {'type': 'string', 'blank': True, 'required': False}, 'source_url': {'type': 'string', 'blank': True, 'required': False}, } } sj_album = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'name': {'type': 'string'}, 'albumArtist': {'type': 'string'}, 'albumArtRef': {'type': 'string', 'required': False}, 'albumId': {'type': 'string'}, 'artist': {'type': 'string', 'blank': True}, 'artistId': {'type': 'array', 'items': {'type': 'string', 'blank': True}}, 'year': {'type': 'integer', 'required': False}, 'tracks': {'type': 'array', 'items': sj_track, 'required': False}, 'description': {'type': 'string', 'required': False}, 'description_attribution': sj_attribution.copy(), 'explicitType': {'type': 'string', 'required': False}, 'contentType': {'type': 'string', 'required': False} } } sj_album['properties']['description_attribution']['required'] = False sj_artist = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'name': {'type': 'string'}, 'artistArtRef': {'type': 'string', 'required': False}, 'artistArtRefs': {'type': 'array', 'items': sj_image, 'required': False}, 'artistBio': {'type': 'string', 'required': False}, 'artistId': {'type': 'string', 'blank': True, 'required': False}, 'albums': {'type': 'array', 'items': sj_album, 'required': False}, 'topTracks': {'type': 'array', 'items': sj_track, 'required': False}, 'total_albums': {'type': 'integer', 'required': False}, 'artist_bio_attribution': sj_attribution.copy(), } } sj_artist['properties']['artist_bio_attribution']['required'] = False sj_artist['properties']['related_artists'] = { 'type': 'array', 'items': sj_artist, # note the recursion 'required': False } sj_genre = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'id': {'type': 'string'}, 'name': {'type': 'string'}, 'children': { 'type': 'array', 'required': False, 'items': {'type': 'string'} }, 'parentId': { 'type': 'string', 'required': False, }, 'images': { 'type': 'array', 'required': False, 'items': { 'type': 'object', 'additionalProperties': False, 'properties': { 'url': {'type': 'string'} } } } } } sj_station_metadata_seed = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, # one of these will be present 'artist': { 'type': sj_artist, 'required': False }, 'genre': { 'type': sj_genre, 'required': False }, } } sj_station_seed = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'seedType': {'type': 'string'}, # one of these will be present 'albumId': {'type': 'string', 'required': False}, 'artistId': {'type': 'string', 'required': False}, 'genreId': {'type': 'string', 'required': False}, 'trackId': {'type': 'string', 'required': False}, 'trackLockerId': {'type': 'string', 'required': False}, 'curatedStationId': {'type': 'string', 'required': False}, 'metadataSeed': {'type': sj_station_metadata_seed, 'required': False} } } sj_station_track = sj_track.copy() sj_station_track['properties']['wentryid'] = {'type': 'string', 'required': False} sj_station = { 'type': 'object', 'additionalProperties': False, 'properties': { 'imageUrl': {'type': 'string', 'required': False}, 'kind': {'type': 'string'}, 'name': {'type': 'string'}, 'deleted': {'type': 'boolean', 'required': False}, # for public 'lastModifiedTimestamp': {'type': 'string', 'required': False}, 'recentTimestamp': {'type': 'string', 'required': False}, # for public 'clientId': {'type': 'string', 'required': False}, # for public 'sessionToken': {'type': 'string', 'required': False}, # for free radios 'skipEventHistory': {'type': 'array'}, # TODO: What's in this array? 'seed': sj_station_seed, 'stationSeeds': {'type': 'array', 'items': sj_station_seed}, 'id': {'type': 'string', 'required': False}, # for public 'description': {'type': 'string', 'required': False}, 'tracks': {'type': 'array', 'required': False, 'items': sj_station_track}, 'imageUrls': {'type': 'array', 'required': False, 'items': sj_image }, 'compositeArtRefs': {'type': 'array', 'required': False, 'items': sj_image }, 'contentTypes': {'type': 'array', 'required': False, 'items': {'type': 'string'} }, 'byline': {'type': 'string', 'required': False}, 'adTargeting': { 'type': 'object', 'properties': { 'keyword': {'type': 'array', 'items': {'type': 'string'} }, }, 'required': False, }, } } sj_listen_now_album = { 'type': 'object', 'additionalProperties': False, 'properties': { 'artist_metajam_id': {'type': 'string'}, 'artist_name': {'type': 'string'}, 'artist_profile_image': { 'type': 'object', 'url': {'type': 'string'} }, 'description': { 'type': 'string', 'blank': True }, 'description_attribution': { 'type': sj_attribution, 'required': False }, 'explicitType': {'type': 'string', 'required': False}, 'id': { 'type': 'object', 'properties': { 'metajamCompactKey': {'type': 'string'}, 'artist': {'type': 'string'}, 'title': {'type': 'string'} } }, 'title': {'type': 'string'} } } sj_listen_now_station = { 'type': 'object', 'additionalProperties': False, 'properties': { 'highlight_color': { 'type': 'string', 'required': False }, 'id': { 'type': 'object', 'seeds': { 'type': 'array', 'items': {'type': sj_station_seed} } }, 'profile_image': { 'type': 'object', 'required': False, 'url': {'type': 'string'} }, 'title': {'type': 'string'} } } sj_listen_now_item = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'compositeArtRefs': { 'type': 'array', 'required': False, 'items': {'type': sj_image} }, 'images': { 'type': 'array', 'items': {'type': sj_image}, 'required': False, }, 'suggestion_reason': {'type': 'string'}, 'suggestion_text': {'type': 'string'}, 'type': {'type': 'string'}, 'album': { 'type': sj_listen_now_album, 'required': False }, 'radio_station': { 'type': sj_listen_now_station, 'required': False } } } sj_podcast_genre = { 'type': 'object', 'additionalProperties': False, 'properties': { 'id': {'type': 'string'}, 'displayName': {'type': 'string'} } } sj_podcast_genre['properties']['subgroups'] = { 'type': 'array', 'required': False, 'items': sj_podcast_genre } sj_podcast_episode = { 'type': 'object', 'additionalProperties': False, 'properties': { 'art': { 'type': 'array', 'required': False, 'items': sj_image }, 'author': { 'type': 'string', 'required': False }, 'deleted': { 'type': 'string', 'required': False }, 'description': { 'type': 'string', 'required': False }, 'durationMillis': {'type': 'string'}, 'episodeId': {'type': 'string'}, 'explicitType': {'type': 'string'}, 'fileSize': {'type': 'string'}, 'playbackPositionMillis': { 'type': 'string', 'required': False }, 'publicationTimestampMillis': { 'type': 'string', 'required': False }, 'seriesId': {'type': 'string'}, 'seriesTitle': {'type': 'string'}, 'title': {'type': 'string'} }, } sj_podcast_series = { 'type': 'object', 'additionalProperties': False, 'properties': { 'art': { 'type': 'array', 'required': False, 'items': sj_image }, 'author': {'type': 'string'}, 'continuationToken': { 'type': 'string', 'required': False, 'blank': True }, 'copyright': { 'type': 'string', 'required': False }, 'description': { 'type': 'string', 'required': False }, 'episodes': { 'type': 'array', 'required': False, 'items': sj_podcast_episode }, 'explicitType': {'type': 'string'}, 'link': { 'type': 'string', 'required': False }, 'seriesId': {'type': 'string'}, 'title': {'type': 'string'}, 'totalNumEpisodes': {'type': 'integer'}, 'userPreferences': { 'type': 'object', 'required': False, 'properties': { 'autoDownload': { 'type': 'boolean', 'required': False }, 'notifyOnNewEpisode': { 'type': 'boolean', 'required': False }, 'subscribed': {'type': 'boolean'} } } } } sj_situation = { 'type': 'object', 'additionalProperties': False, 'properties': { 'description': {'type': 'string'}, 'id': {'type': 'string'}, 'imageUrl': {'type': 'string', 'required': False}, 'title': {'type': 'string'}, 'wideImageUrl': {'type': 'string', 'required': False}, 'stations': {'type': 'array', 'required': False, 'items': sj_station } } } sj_situation['properties']['situations'] = { 'type': 'array', 'required': False, 'items': sj_situation } sj_search_result_cluster_info = { 'type': 'object', 'additionalProperties': False, 'properties': { 'category': {'type': 'string'}, 'id': {'type': 'string'}, 'type': {'type': 'string'} } } sj_search_result = { 'type': 'object', 'additionalProperties': False, 'properties': { 'score': {'type': 'number', 'required': False}, 'type': {'type': 'string'}, 'best_result': {'type': 'boolean', 'required': False}, 'navigational_result': {'type': 'boolean', 'required': False}, 'navigational_confidence': {'type': 'number', 'required': False}, 'cluster': { 'type': 'array', 'required': False, 'items': sj_search_result_cluster_info }, 'artist': sj_artist.copy(), 'album': sj_album.copy(), 'track': sj_track.copy(), 'playlist': sj_playlist.copy(), 'genre': sj_genre.copy(), 'series': sj_podcast_series.copy(), 'station': sj_station.copy(), 'situation': sj_situation.copy(), 'youtube_video': sj_video.copy() } } sj_search_result['properties']['artist']['required'] = False sj_search_result['properties']['album']['required'] = False sj_search_result['properties']['track']['required'] = False sj_search_result['properties']['playlist']['required'] = False sj_search_result['properties']['genre']['required'] = False sj_search_result['properties']['series']['required'] = False sj_search_result['properties']['station']['required'] = False sj_search_result['properties']['situation']['required'] = False sj_search_result['properties']['youtube_video']['required'] = False sj_search_result_cluster = { 'type': 'object', 'additionalProperties': False, 'properties': { 'cluster': {'type': sj_search_result_cluster_info}, 'displayName': {'type': 'string', 'required': False}, 'entries': { 'type': 'array', 'items': sj_search_result, 'required': False }, 'resultToken': {'type': 'string', 'required': False} } } class McCall(Call): """Abstract base for mobile client calls.""" required_auth = authtypes(gpsoauth=True) # validictory schema for the response _res_schema = utils.NotImplementedField @classmethod def validate(cls, response, msg): """Use validictory and a static schema (stored in cls._res_schema).""" try: return validictory.validate(msg, cls._res_schema) except ValueError as e: raise_from(ValidationException(str(e)), e) @classmethod def check_success(cls, response, msg): # TODO not sure if this is still valid for mc pass # if 'success' in msg and not msg['success']: # raise CallFailure( # "the server reported failure. This is usually" # " caused by bad arguments, but can also happen if requests" # " are made too quickly (eg creating a playlist then" # " modifying it before the server has created it)", # cls.__name__) @classmethod def parse_response(cls, response): return cls._parse_json(response.text) class McListCall(McCall): """Abc for calls that list a resource.""" # concrete classes provide: item_schema = utils.NotImplementedField filter_text = utils.NotImplementedField static_headers = {'Content-Type': 'application/json'} static_params = {'alt': 'json', 'include-tracks': 'true'} _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'nextPageToken': {'type': 'string', 'required': False}, 'data': {'type': 'object', 'items': {'type': 'array', 'items': item_schema}, 'required': False, }, }, } @classmethod def dynamic_params(cls, updated_after=None, start_token=None, max_results=None): """ :param updated_after: datetime.datetime; defaults to epoch """ if updated_after is None: microseconds = -1 else: microseconds = utils.datetime_to_microseconds(updated_after) return {'updated-min': microseconds} @classmethod def dynamic_data(cls, updated_after=None, start_token=None, max_results=None): """ :param updated_after: ignored :param start_token: nextPageToken from a previous response :param max_results: a positive int; if not provided, server defaults to 1000 """ data = {} if start_token is not None: data['start-token'] = start_token if max_results is not None: data['max-results'] = str(max_results) return json.dumps(data) @classmethod def parse_response(cls, response): # empty results don't include the data key # make sure it's always there res = cls._parse_json(response.text) if 'data' not in res: res['data'] = {'items': []} return res @classmethod def filter_response(cls, msg): filtered = copy.deepcopy(msg) filtered['data']['items'] = ["<%s %s>" % (len(filtered['data']['items']), cls.filter_text)] return filtered class McBatchMutateCall(McCall): """Abc for batch mutation calls.""" static_headers = {'Content-Type': 'application/json'} static_params = {'alt': 'json'} _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'mutate_response': { 'type': 'array', 'items': { 'type': 'object', 'properties': { 'id': {'type': 'string', 'required': False}, 'client_id': {'type': 'string', 'blank': True, 'required': False}, 'response_code': {'type': 'string'}, }, }, } }, } @staticmethod def dynamic_data(mutations): """ :param mutations: list of mutation dictionaries """ return json.dumps({'mutations': mutations}) @classmethod def check_success(cls, response, msg): if ('error' in msg or not all([d.get('response_code', None) in ('OK', 'CONFLICT') for d in msg.get('mutate_response', [])])): raise CallFailure('The server reported failure while' ' changing the requested resource.' " If this wasn't caused by invalid arguments" ' or server flakiness,' ' please open an issue.', cls.__name__) class McStreamCall(McCall): # this call will redirect to the mp3 static_allow_redirects = False _s1 = bytes(base64.b64decode('VzeC4H4h+T2f0VI180nVX8x+Mb5HiTtGnKgH52Otj8ZCGDz9jRW' 'yHb6QXK0JskSiOgzQfwTY5xgLLSdUSreaLVMsVVWfxfa8Rw==')) _s2 = bytes(base64.b64decode('ZAPnhUkYwQ6y5DdQxWThbvhJHN8msQ1rqJw0ggKdufQjelrKuiG' 'GJI30aswkgCWTDyHkTGK9ynlqTkJ5L4CiGGUabGeo8M6JTQ==')) # bitwise and of _s1 and _s2 ascii, converted to string _key = ''.join([chr(c1 ^ c2) for (c1, c2) in zip(_s1, _s2)]).encode("ascii") @classmethod def get_signature(cls, item_id, salt=None): """Return a (sig, salt) pair for url signing.""" if salt is None: salt = str(int(time.time() * 1000)) mac = hmac.new(cls._key, item_id.encode("utf-8"), sha1) mac.update(salt.encode("utf-8")) sig = base64.urlsafe_b64encode(mac.digest())[:-1] return sig, salt @staticmethod def dynamic_headers(item_id, device_id, quality): return {'X-Device-ID': device_id} @classmethod def dynamic_params(cls, item_id, device_id, quality): sig, salt = cls.get_signature(item_id) params = {'opt': quality, 'net': 'mob', 'pt': 'e', 'slt': salt, 'sig': sig, } if item_id.startswith(('T', 'D')): # Store track or podcast episode. params['mjck'] = item_id else: # Library track. params['songid'] = item_id return params @staticmethod def parse_response(response): return response.headers['location'] # ie where we were redirected @classmethod def validate(cls, response, msg): pass class Config(McCall): static_method = 'GET' static_url = sj_url + 'config' item_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'key': {'type': 'string'}, 'value': {'type': 'string'} } } _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'data': {'type': 'object', 'entries': {'type': 'array', 'items': item_schema}, } } } class Search(McCall): """Search for All Access tracks.""" static_method = 'GET' static_url = sj_url + 'query' # The result types returned are requested in the `ct` parameter. # Free account search is restricted so may not contain hits for all result types. # 1: Song, 2: Artist, 3: Album, 4: Playlist, 5: Genre, # 6: Station, 7: Situation, 8: Video, 9: Podcast Series static_params = {'ct': '1,2,3,4,5,6,7,8,9', 'ic': True} _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'clusterDetail': {'type': 'array', 'items': {'type': sj_search_result_cluster}}, 'suggestedQuery': {'type': 'string', 'required': False} }, } @staticmethod def dynamic_params(query, max_results): return {'q': query, 'max-results': max_results} class ListTracks(McListCall): item_schema = sj_track filter_text = 'tracks' static_method = 'POST' static_url = sj_url + 'trackfeed' class GetStreamUrl(McStreamCall): static_method = 'GET' static_url = sj_stream_url + 'mplay' class GetStationTrackStreamUrl(McStreamCall): static_method = 'GET' static_url = sj_stream_url + 'wplay' @staticmethod def dynamic_headers(item_id, wentry_id, session_token, quality): return {'X-Device-ID': ''} @classmethod def dynamic_params(cls, song_id, wentry_id, session_token, quality): sig, salt = cls.get_signature(song_id) params = {} if song_id[0] == 'T': # all access params['mjck'] = song_id else: params['songid'] = song_id params['sesstok'] = session_token.encode('utf-8') params['wentryid'] = wentry_id.encode('utf-8') params['tier'] = 'fr' params.update( {'audio_formats': 'mp3', 'opt': quality, 'net': 'mob', 'pt': 'a', 'slt': salt, 'sig': sig, }) return params @staticmethod def parse_response(response): res = json.loads(response.text) return res['location'] class ListPlaylists(McListCall): item_schema = sj_playlist filter_text = 'playlists' static_method = 'POST' static_url = sj_url + 'playlistfeed' class ListPlaylistEntries(McListCall): item_schema = sj_plentry filter_text = 'plentries' static_method = 'POST' static_url = sj_url + 'plentryfeed' class ListSharedPlaylistEntries(McListCall): shared_plentry = sj_plentry.copy() del shared_plentry['properties']['playlistId'] del shared_plentry['properties']['clientId'] item_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'shareToken': {'type': 'string'}, 'responseCode': {'type': 'string'}, 'playlistEntry': { 'type': 'array', 'items': shared_plentry, 'required': False, } } } _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'entries': {'type': 'array', 'items': item_schema, }, }, } filter_text = 'shared plentries' static_method = 'POST' static_url = sj_url + 'plentries/shared' # odd: this request has an additional level of nesting compared to others, # and changes the data/entry schema to entries/playlistEntry. # Those horrible naming choices make this even harder to understand. @classmethod def dynamic_params(cls, share_token, updated_after=None, start_token=None, max_results=None): return super(ListSharedPlaylistEntries, cls).dynamic_params( updated_after, start_token, max_results) @classmethod def dynamic_data(cls, share_token, updated_after=None, start_token=None, max_results=None): """ :param share_token: from a shared playlist :param updated_after: ignored :param start_token: nextPageToken from a previous response :param max_results: a positive int; if not provided, server defaults to 1000 """ data = {} data['shareToken'] = share_token if start_token is not None: data['start-token'] = start_token if max_results is not None: data['max-results'] = str(max_results) return json.dumps({'entries': [data]}) @classmethod def parse_response(cls, response): res = cls._parse_json(response.text) if 'playlistEntry' not in res['entries'][0]: res['entries'][0]['playlistEntry'] = [] return res @classmethod def filter_response(cls, msg): filtered = copy.deepcopy(msg) filtered['entries'][0]['playlistEntry'] = ["<%s %s>" % (len(filtered['entries'][0]['playlistEntry']), cls.filter_text)] return filtered class BatchMutatePlaylists(McBatchMutateCall): static_method = 'POST' static_url = sj_url + 'playlistbatch' @staticmethod def build_playlist_deletes(playlist_ids): # TODO can probably pull this up one """ :param playlist_ids: """ return [{'delete': id} for id in playlist_ids] @staticmethod def build_playlist_updates(pl_updates): """ :param pl_updates: [{'id': '', name': '', 'description': '', 'public': ''}] """ return [{'update': { 'id': pl_update['id'], 'name': pl_update['name'], 'description': pl_update['description'], 'shareState': pl_update['public'] }} for pl_update in pl_updates] @staticmethod def build_playlist_adds(pl_descriptions): """ :param pl_descriptions: [{'name': '', 'description': '','public': ''}] """ return [{'create': { 'creationTimestamp': '-1', 'deleted': False, 'lastModifiedTimestamp': '0', 'name': pl_desc['name'], 'description': pl_desc['description'], 'type': 'USER_GENERATED', 'shareState': pl_desc['public'], }} for pl_desc in pl_descriptions] class BatchMutatePlaylistEntries(McBatchMutateCall): filter_text = 'plentries' item_schema = sj_plentry static_method = 'POST' static_url = sj_url + 'plentriesbatch' @staticmethod def build_plentry_deletes(entry_ids): """ :param entry_ids: """ return [{'delete': id} for id in entry_ids] @staticmethod def build_plentry_reorder(plentry, preceding_cid, following_cid): """ :param plentry: plentry that is moving :param preceding_cid: clientid of entry that will be before the moved entry :param following_cid: "" that will be after the moved entry """ mutation = copy.deepcopy(plentry) keys_to_keep = {'clientId', 'creationTimestamp', 'deleted', 'id', 'lastModifiedTimestamp', 'playlistId', 'source', 'trackId'} for key in list(mutation.keys()): if key not in keys_to_keep: del mutation[key] # horribly misleading key names; these are _clientids_ # using entryids works sometimes, but with seemingly random results if preceding_cid: mutation['precedingEntryId'] = preceding_cid if following_cid: mutation['followingEntryId'] = following_cid return {'update': mutation} @staticmethod def build_plentry_adds(playlist_id, song_ids): """ :param playlist_id: :param song_ids: """ mutations = [] prev_id, cur_id, next_id = None, str(uuid1()), str(uuid1()) for i, song_id in enumerate(song_ids): m_details = { 'clientId': cur_id, 'creationTimestamp': '-1', 'deleted': False, 'lastModifiedTimestamp': '0', 'playlistId': playlist_id, 'source': 1, 'trackId': song_id, } if song_id.startswith('T'): m_details['source'] = 2 # AA track if i > 0: m_details['precedingEntryId'] = prev_id if i < len(song_ids) - 1: m_details['followingEntryId'] = next_id mutations.append({'create': m_details}) prev_id, cur_id, next_id = cur_id, next_id, str(uuid1()) return mutations class GetDeviceManagementInfo(McCall): """Get registered device information.""" static_method = 'GET' static_url = sj_url + "devicemanagementinfo" static_params = {'alt': 'json'} _device_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'id': {'type': 'string'}, 'friendlyName': {'type': 'string', 'blank': True}, 'type': {'type': 'string'}, 'lastAccessedTimeMs': {'type': 'integer'}, # only for mobile devices 'smartPhone': {'type': 'boolean', 'required': False}, } } _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'data': {'type': 'object', 'items': {'type': 'array', 'items': _device_schema}, 'required': False, }, }, } class DeauthDevice(McCall): """Deauthorize a device from devicemanagementinfo.""" static_method = 'DELETE' static_url = sj_url + "devicemanagementinfo" @staticmethod def dynamic_params(device_id): return {'delete-id': device_id} class ListPromotedTracks(McListCall): item_schema = sj_track filter_text = 'tracks' static_method = 'POST' static_url = sj_url + 'ephemeral/top' class ListListenNowItems(McCall): static_method = 'GET' static_url = sj_url + "listennow/getlistennowitems" static_params = {'alt': 'json'} _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'listennow_items': { 'type': 'array', 'items': {'type': sj_listen_now_item} } } } @staticmethod def filter_response(msg): filtered = copy.deepcopy(msg) if 'listennow_items' in filtered: filtered['listennow_items'] = \ ["<%s listennow_items>" % len(filtered['listennow_items'])] class ListListenNowSituations(McCall): static_method = 'POST' static_url = sj_url + 'listennow/situations' static_headers = {'Content-Type': 'application/json'} static_params = {'alt': 'json'} _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'distilledContextWrapper': {'type': 'object', 'distilledContextToken': {'type': 'string'}, 'required': False}, 'primaryHeader': {'type': 'string'}, 'subHeader': {'type': 'string'}, 'situations': {'type': 'array', 'items': sj_situation, }, }, } @classmethod def dynamic_data(cls): tz_offset = calendar.timegm(time.localtime()) - calendar.timegm(time.gmtime()) return json.dumps({ 'requestSignals': {'timeZoneOffsetSecs': tz_offset} }) @staticmethod def filter_response(msg): filtered = copy.deepcopy(msg) if 'situations' in filtered.get('data', {}): filtered['data']['situations'] = \ ["<%s situations>" % len(filtered['data']['situations'])] return filtered class GetBrowsePodcastHierarchy(McCall): static_method = 'GET' static_url = sj_url + 'podcast/browsehierarchy' static_params = {'alt': 'json'} _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'groups': { 'type': 'array', 'required': False, # Only on errors 'items': sj_podcast_genre } } } class ListBrowsePodcastSeries(McCall): static_method = 'GET' static_url = sj_url + 'podcast/browse' static_params = {'alt': 'json'} _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'series': { 'type': 'array', 'items': {'type': sj_podcast_series} } } } @classmethod def dynamic_params(cls, id=None): return {'id': id} @staticmethod def filter_response(msg): filtered = copy.deepcopy(msg) if 'series' in filtered: filtered['series'] = \ ["<%s podcasts>" % len(filtered['series'])] class BatchMutatePodcastSeries(McBatchMutateCall): static_method = 'POST' static_url = sj_url + 'podcastseries/batchmutate' @staticmethod def build_podcast_updates(updates): """ :param updates: ``{'seriesId': '', 'subscribed': '', 'userPreferences': {'notifyOnNewEpisode': '', 'subscribed': ''}}...`` """ return [{'update': update} for update in updates] # The podcastseries and podcastepisode list calls are strange in that they require a device # ID and pass updated-min, max-results, and start-token as params. # The start-token param is required, even if not given, to get a result for more than one # call in a session. class ListPodcastSeries(McListCall): item_schema = sj_podcast_series filter_text = 'podcast series' static_method = 'GET' static_url = sj_url + 'podcastseries' @staticmethod def dynamic_headers(device_id, updated_after=None, start_token=None, max_results=None): return {'X-Device-ID': device_id} @classmethod def dynamic_params(cls, device_id=None, updated_after=None, start_token=None, max_results=None): params = {} if updated_after is None: microseconds = -1 else: microseconds = utils.datetime_to_microseconds(updated_after) params['updated-min'] = microseconds params['start-token'] = start_token if max_results is not None: params['max-results'] = str(max_results) return params @classmethod def dynamic_data(cls, device_id=None, updated_after=None, start_token=None, max_results=None): pass # The podcastseries and podcastepisode list calls are strange in that they require a device # ID and pass updated-min, max-results, and start-token as params. # The start-token param is required, even if not given, to get a result for more than one # call in a session. class ListPodcastEpisodes(McListCall): item_schema = sj_podcast_episode filter_text = 'podcast episodes' static_method = 'GET' static_url = sj_url + 'podcastepisode' @staticmethod def dynamic_headers(device_id, updated_after=None, start_token=None, max_results=None): return {'X-Device-ID': device_id} @classmethod def dynamic_params(cls, device_id=None, updated_after=None, start_token=None, max_results=None): params = {} if updated_after is None: microseconds = -1 else: microseconds = utils.datetime_to_microseconds(updated_after) params['updated-min'] = microseconds params['start-token'] = start_token if max_results is not None: params['max-results'] = str(max_results) return params @classmethod def dynamic_data(cls, device_id=None, updated_after=None, start_token=None, max_results=None): pass class GetPodcastEpisodeStreamUrl(McStreamCall): static_method = 'GET' static_url = sj_stream_url + 'fplay' class ListStations(McListCall): item_schema = sj_station filter_text = 'stations' static_method = 'POST' static_url = sj_url + 'radio/station' class ListStationTracks(McCall): _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'data': {'type': 'object', 'stations': {'type': 'array', 'items': sj_station}, 'required': False, }, }, } static_headers = {'Content-Type': 'application/json'} static_params = {'alt': 'json', 'include-tracks': 'true'} static_method = 'POST' static_url = sj_url + 'radio/stationfeed' @staticmethod def dynamic_data(station_id, num_entries, recently_played): """ :param station_id: :param num_entries: maximum number of tracks to return :param recently_played: a list of...song ids? never seen an example """ # TODO # clearly, this supports more than one at a time, # but then that might introduce paging? # I'll leave it for someone else if station_id == 'IFL': return json.dumps({'contentFilter': 1, 'stations': [ { 'numEntries': num_entries, 'recentlyPlayed': recently_played, 'seed': {'seedType': 6} } ]}) return json.dumps({'contentFilter': 1, 'stations': [ { 'numEntries': num_entries, 'radioId': station_id, 'recentlyPlayed': recently_played } ]}) @staticmethod def filter_response(msg): filtered = copy.deepcopy(msg) if 'stations' in filtered.get('data', {}): filtered['data']['stations'] = \ ["<%s stations>" % len(filtered['data']['stations'])] return filtered class BatchMutateStations(McBatchMutateCall): static_method = 'POST' static_url = sj_url + 'radio/editstation' @staticmethod def build_deletes(station_ids): """ :param station_ids: """ return [{'delete': id, 'includeFeed': False, 'numEntries': 0} for id in station_ids] @staticmethod def build_add(name, seed, include_tracks, num_tracks, recent_datetime=None): """ :param name: the title :param seed: a dict {'itemId': id, 'seedType': int} :param include_tracks: if True, return `num_tracks` tracks in the response :param num_tracks: :param recent_datetime: purpose unknown. defaults to now. """ if recent_datetime is None: recent_datetime = datetime.now() recent_timestamp = utils.datetime_to_microseconds(recent_datetime) return { "createOrGet": { "clientId": str(uuid1()), "deleted": False, "imageType": 1, "lastModifiedTimestamp": "-1", "name": name, "recentTimestamp": str(recent_timestamp), "seed": seed, "tracks": [] }, "includeFeed": include_tracks, "numEntries": num_tracks, "params": { "contentFilter": 1 } } class BatchMutateTracks(McBatchMutateCall): static_method = 'POST' static_url = sj_url + 'trackbatch' # utility functions to build the mutation dicts @staticmethod def build_track_deletes(track_ids): """ :param track_ids: """ return [{'delete': id} for id in track_ids] @staticmethod def build_track_add(store_track_info): """ :param store_track_info: sj_track """ track_dict = copy.deepcopy(store_track_info) for key in ('kind', 'trackAvailableForPurchase', 'albumAvailableForPurchase', 'albumArtRef', 'artistId', ): if key in track_dict: del track_dict[key] for key, default in { 'playCount': 0, 'rating': '0', 'genre': '', 'lastModifiedTimestamp': '0', 'deleted': False, 'beatsPerMinute': -1, 'composer': '', 'creationTimestamp': '-1', 'totalDiscCount': 0, }.items(): track_dict.setdefault(key, default) # TODO unsure about this track_dict['trackType'] = 8 return {'create': track_dict} class GetPodcastSeries(McCall): static_method = 'GET' static_url = sj_url + 'podcast/fetchseries' static_headers = {'Content-Type': 'application/json'} static_params = {'alt': 'json'} _res_schema = sj_podcast_series @staticmethod def dynamic_params(podcast_series_id, num_episodes): return { 'nid': podcast_series_id, 'num': num_episodes} class GetPodcastEpisode(McCall): static_method = 'GET' static_url = sj_url + 'podcast/fetchepisode' static_headers = {'Content-Type': 'application/json'} static_params = {'alt': 'json'} _res_schema = sj_podcast_episode @staticmethod def dynamic_params(podcast_episode_id): return {'nid': podcast_episode_id} class GetStoreTrack(McCall): # TODO does this accept library ids, too? static_method = 'GET' static_url = sj_url + 'fetchtrack' static_headers = {'Content-Type': 'application/json'} static_params = {'alt': 'json'} _res_schema = sj_track @staticmethod def dynamic_params(track_id): return {'nid': track_id} class GetGenres(McCall): static_method = 'GET' static_url = sj_url + 'explore/genres' static_params = {'alt': 'json'} _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'kind': {'type': 'string'}, 'genres': { 'type': 'array', 'items': sj_genre, 'required': False, # only on errors } } } @staticmethod def dynamic_params(parent_genre_id): return {'parent-genre': parent_genre_id} class GetArtist(McCall): static_method = 'GET' static_url = sj_url + 'fetchartist' static_params = {'alt': 'json'} _res_schema = sj_artist @staticmethod def dynamic_params(artist_id, include_albums, num_top_tracks, num_rel_artist): """ :param include_albums: bool :param num_top_tracks: int :param num_rel_artist: int """ return {'nid': artist_id, 'include-albums': include_albums, 'num-top-tracks': num_top_tracks, 'num-related-artists': num_rel_artist, } class GetAlbum(McCall): static_method = 'GET' static_url = sj_url + 'fetchalbum' static_params = {'alt': 'json'} _res_schema = sj_album @staticmethod def dynamic_params(album_id, tracks): include_tracks = bool(tracks) if tracks else None return {'nid': album_id, 'include-tracks': include_tracks} class IncrementPlayCount(McCall): static_method = 'POST' static_url = sj_url + 'trackstats' static_params = {'alt': 'json'} static_headers = {'Content-Type': 'application/json'} _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'responses': { 'type': 'array', 'items': { 'type': 'object', 'additionalProperties': False, 'properties': { 'id': {'type': 'string', 'required': False}, # not provided for AA tracks? 'response_code': {'type': 'string'}, } } } } } @staticmethod def dynamic_data(sid, plays, playtime): # TODO this can support multiple tracks at a time play_timestamp = utils.datetime_to_microseconds(playtime) event = { 'context_type': 1, 'event_timestamp_micros': str(play_timestamp), 'event_type': 2, # This can also send a context_id which is the album/artist id # the song was found from. } return json.dumps({'track_stats': [{ 'id': sid, 'incremental_plays': plays, 'last_play_time_millis': str(play_timestamp // 1000), 'type': 2 if sid.startswith('T') else 1, 'track_events': [event] * plays, }]}) gmusicapi-12.1.1/gmusicapi/protocol/musicmanager.py0000664000175000017500000005513313400371522022726 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- """Calls made by the Music Manager (related to uploading).""" from __future__ import print_function, division, absolute_import, unicode_literals from six import raise_from from builtins import * # noqa import base64 import hashlib import itertools import os import shutil from tempfile import NamedTemporaryFile import dateutil.parser from decorator import decorator from google.protobuf.message import DecodeError import mutagen import json from gmusicapi.exceptions import CallFailure from gmusicapi.protocol import upload_pb2, locker_pb2, download_pb2 from gmusicapi.protocol.shared import Call, ParseException, authtypes from gmusicapi.utils import utils log = utils.DynamicClientLogger(__name__) _android_url = 'https://android.clients.google.com/upsj/' @decorator def pb(f, *args, **kwargs): """Decorator to serialize a protobuf message.""" msg = f(*args, **kwargs) return msg.SerializeToString() class MmCall(Call): """Abstract base for Music Manager calls.""" static_method = 'POST' # remember that setting this in a subclass overrides, not merges # static + dynamic does merge, though static_headers = {'User-agent': 'Music Manager (1, 0, 55, 7425 HTTPS - Windows)'} required_auth = authtypes(oauth=True) # this is a shared union class that has all specific upload types # nearly all of the proto calls return a message of this form res_msg_type = upload_pb2.UploadResponse @classmethod def parse_response(cls, response): """Parse the cls.res_msg_type proto msg.""" res_msg = cls.res_msg_type() try: res_msg.ParseFromString(response.content) except DecodeError as e: raise_from(ParseException(str(e)), e) return res_msg @classmethod def filter_response(cls, msg): return Call._filter_proto(msg) class GetClientState(MmCall): static_url = _android_url + 'clientstate' @classmethod @pb def dynamic_data(cls, uploader_id): """ :param uploader_id: MM uses host MAC address """ req_msg = upload_pb2.ClientStateRequest() req_msg.uploader_id = uploader_id return req_msg class AuthenticateUploader(MmCall): """Sent to auth, reauth, or register our upload client.""" static_url = _android_url + 'upauth' @classmethod def check_success(cls, response, msg): if msg.HasField('auth_status') and msg.auth_status != upload_pb2.UploadResponse.OK: enum_desc = upload_pb2._UPLOADRESPONSE.enum_types[1] res_name = enum_desc.values_by_number[msg.auth_status].name raise CallFailure( "Upload auth error code %s: %s." " See http://goo.gl/O6xe7 for more information. " % ( msg.auth_status, res_name ), cls.__name__ ) @classmethod @pb def dynamic_data(cls, uploader_id, uploader_friendly_name): """ :param uploader_id: MM uses host MAC address :param uploader_friendly_name: MM uses hostname """ req_msg = upload_pb2.UpAuthRequest() req_msg.uploader_id = uploader_id req_msg.friendly_name = uploader_friendly_name return req_msg class UploadMetadata(MmCall): static_url = _android_url + 'metadata' static_params = {'version': 1} @staticmethod def get_track_clientid(filepath): # The id is a 22 char hash of the file. It is found by: # stripping tags # getting an md5 sum # converting sum to base64 # removing trailing === m = hashlib.md5() try: ext = os.path.splitext(filepath)[1] # delete=False is needed because the NamedTemporaryFile # can't be opened by name a second time on Windows otherwise. with NamedTemporaryFile(suffix=ext, delete=False) as temp: shutil.copy(filepath, temp.name) audio = mutagen.File(temp.name, easy=True) audio.delete() audio.save() while True: data = temp.read(65536) if not data: break m.update(data) finally: try: os.remove(temp.name) except OSError: log.exception("Could not remove temporary file %r", temp.name) return base64.encodestring(m.digest())[:-3] # these collections define how locker_pb2.Track fields align to mutagen's. shared_fields = ('album', 'artist', 'composer', 'genre') field_map = { # mutagen: Track 'albumartist': 'album_artist', 'bpm': 'beats_per_minute', } count_fields = { # mutagen: (part, total) 'discnumber': ('disc_number', 'total_disc_count'), 'tracknumber': ('track_number', 'total_track_count'), } @classmethod def fill_track_info(cls, filepath): """Given the path and contents of a track, return a filled locker_pb2.Track. On problems, raise ValueError.""" track = locker_pb2.Track() # The track protobuf message supports an additional metadata list field. # ALBUM_ART_HASH has been observed being sent in this field so far. # Append locker_pb2.AdditionalMetadata objects to additional_metadata. # AdditionalMetadata objects consist of two fields, 'tag_name' and 'value'. additional_metadata = [] track.client_id = cls.get_track_clientid(filepath) audio = mutagen.File(filepath, easy=True) if audio is None: raise ValueError("could not open to read metadata") elif isinstance(audio, mutagen.asf.ASF): # WMA entries store more info than just the value. # Monkeypatch in a dict {key: value} to keep interface the same for all filetypes. asf_dict = dict((k, [ve.value for ve in v]) for (k, v) in audio.tags.as_dict().items()) audio.tags = asf_dict extension = os.path.splitext(filepath)[1].upper() if isinstance(extension, bytes): extension = extension.decode('utf8') if extension: # Trim leading period if it exists (ie extension not empty). extension = extension[1:] if isinstance(audio, mutagen.mp4.MP4) and ( audio.info.codec == 'alac' or audio.info.codec_description == 'ALAC'): extension = 'ALAC' elif isinstance(audio, mutagen.mp4.MP4) and audio.info.codec_description.startswith('AAC'): extension = 'AAC' if extension.upper() == 'M4B': # M4B are supported by the music manager, and transcoded like normal. extension = 'M4A' if not hasattr(locker_pb2.Track, extension): raise ValueError("unsupported filetype: {0} for file {1}".format(extension, filepath)) track.original_content_type = getattr(locker_pb2.Track, extension) track.estimated_size = os.path.getsize(filepath) track.last_modified_timestamp = int(os.path.getmtime(filepath)) # These are typically zeroed in my examples. track.play_count = 0 track.client_date_added = 0 track.recent_timestamp = 0 track.rating = locker_pb2.Track.NOT_RATED # star rating track.duration_millis = int(audio.info.length * 1000) try: bitrate = audio.info.bitrate // 1000 except AttributeError: # mutagen doesn't provide bitrate for some lossless formats (eg FLAC), so # provide an estimation instead. This shouldn't matter too much; # the bitrate will always be > 320, which is the highest scan and match quality. bitrate = (track.estimated_size * 8) // track.duration_millis track.original_bit_rate = bitrate # Populate metadata. def track_set(field_name, val, msg=track): """Returns result of utils.pb_set and logs on failures. Should be used when setting directly from metadata.""" success = utils.pb_set(msg, field_name, val) if not success: log.info("could not pb_set track.%s = %r for '%r'", field_name, val, filepath) return success # Title is required. # If it's not in the metadata, the filename will be used. if "title" in audio: title = audio['title'][0] if isinstance(title, mutagen.asf.ASFUnicodeAttribute): title = title.value track_set('title', title) else: # Assume ascii or unicode. track.title = os.path.basename(filepath) if "date" in audio: date_val = str(audio['date'][0]) try: datetime = dateutil.parser.parse(date_val, fuzzy=True) except (ValueError, TypeError) as e: # TypeError provides compatibility with: # https://bugs.launchpad.net/dateutil/+bug/1247643 log.warning("could not parse date md for '%r': (%s)", filepath, e) else: track_set('year', datetime.year) for null_field in ['artist', 'album']: # If these fields aren't provided, they'll render as "undefined" in the web interface; # see https://github.com/simon-weber/gmusicapi/issues/236. # Defaulting them to an empty string fixes this. if null_field not in audio: track_set(null_field, '') # Mass-populate the rest of the simple fields. # Merge shared and unshared fields into {mutagen: Track}. fields = dict( itertools.chain( ((shared, shared) for shared in cls.shared_fields), cls.field_map.items())) for mutagen_f, track_f in fields.items(): if mutagen_f in audio: track_set(track_f, audio[mutagen_f][0]) for mutagen_f, (track_f, track_total_f) in cls.count_fields.items(): if mutagen_f in audio: numstrs = str(audio[mutagen_f][0]).split("/") track_set(track_f, numstrs[0]) if len(numstrs) == 2 and numstrs[1]: track_set(track_total_f, numstrs[1]) if additional_metadata: track.track_extras.additional_metadata.extend(additional_metadata) return track @classmethod @pb def dynamic_data(cls, tracks, uploader_id, do_not_rematch=False): """ :param tracks: list of filled locker_pb2.Track :param uploader_id: :param do_not_rematch: seems to be ignored """ req_msg = upload_pb2.UploadMetadataRequest() req_msg.track.extend(tracks) for track in req_msg.track: track.do_not_rematch = do_not_rematch req_msg.uploader_id = uploader_id return req_msg class GetUploadJobs(MmCall): # TODO static_url = _android_url + 'getjobs' static_params = {'version': 1} @classmethod def check_success(cls, response, msg): if msg.HasField('getjobs_response') and not msg.getjobs_response.get_tracks_success: raise CallFailure('get_tracks_success == False', cls.__name__) @classmethod @pb def dynamic_data(cls, uploader_id): """ :param uploader_id: MM uses host MAC address """ req_msg = upload_pb2.GetJobsRequest() req_msg.uploader_id = uploader_id return req_msg class GetUploadSession(MmCall): """Called when we want to upload; the server returns the url to use. This is a json call, and doesn't share much with the other calls.""" static_method = 'POST' static_url = 'https://uploadsj.clients.google.com/uploadsj/scottyagent' @classmethod def parse_response(cls, response): return cls._parse_json(response.text) @staticmethod def filter_response(res): return res @staticmethod def dynamic_data(uploader_id, num_already_uploaded, track, filepath, server_id, do_not_rematch=False): """track is a locker_pb2.Track, and the server_id is from a metadata upload.""" # small info goes inline, big things get their own external PUT. # still not sure as to thresholds - I've seen big album art go inline. if isinstance(filepath, bytes): filepath = filepath.decode('utf8') inlined = { "title": "jumper-uploader-title-42", "ClientId": track.client_id, "ClientTotalSongCount": "1", # TODO think this is ie "how many will you upload" "CurrentTotalUploadedCount": str(num_already_uploaded), "CurrentUploadingTrack": track.title, "ServerId": server_id, "SyncNow": "true", "TrackBitRate": track.original_bit_rate, "TrackDoNotRematch": str(do_not_rematch).lower(), "UploaderId": uploader_id, } message = { "clientId": "Jumper Uploader", "createSessionRequest": { "fields": [ { "external": { "filename": os.path.basename(filepath), "name": os.path.abspath(filepath), "put": {}, # used to use this; don't see it in examples # "size": track.estimated_size, } } ] }, "protocolVersion": "0.8" } # Insert the inline info. for key in inlined: payload = inlined[key] if not isinstance(payload, str): payload = str(payload) message['createSessionRequest']['fields'].append( { "inlined": { "content": payload, "name": key } } ) return json.dumps(message) @staticmethod def process_session(res): """Return (got_session, error_details). error_details is (should_retry, reason, error_code) or None if got_session.""" if 'sessionStatus' in res: return (True, None) if 'errorMessage' in res: try: # This terribly nested structure is Google's doing. error_code = (res['errorMessage']['additionalInfo'] ['uploader_service.GoogleRupioAdditionalInfo']['completionInfo'] ['customerSpecificInfo']['ResponseCode']) except KeyError: # The returned nested structure is not as expected: cannot get Response Code error_code = None got_session = False if error_code == 503: should_retry = True reason = 'upload servers still syncing' # TODO unsure about these codes elif error_code == 200: should_retry = False reason = 'this song is already uploaded' elif error_code == 404: should_retry = False reason = 'the request was rejected' else: should_retry = True reason = 'the server reported an unknown error' return (got_session, (should_retry, reason, error_code)) return (False, (True, "the server's response could not be understood", None)) class UploadFile(MmCall): """Called after getting a session to actually upload a file.""" # TODO recent protocols use multipart encoding static_method = 'PUT' @classmethod def parse_response(cls, response): return cls._parse_json(response.text) @staticmethod def filter_response(res): return res @staticmethod def dynamic_headers(session_url, content_type, audio): return {'CONTENT-TYPE': content_type} @staticmethod def dynamic_url(session_url, content_type, audio): # this actually includes params, but easier to pass them straight through return session_url @staticmethod def dynamic_data(session_url, content_type, audio): return audio class ProvideSample(MmCall): """Give the server a scan and match sample. The sample is a 128k mp3 slice of the file, usually 15 seconds long.""" static_method = 'POST' static_params = {'version': 1} static_url = _android_url + 'sample' @staticmethod @pb def dynamic_data(filepath, server_challenge, track, uploader_id, mock_sample=None): """Raise IOError on transcoding problems, or ValueError for invalid input. :param mock_sample: if provided, will be sent in place of a proper sample """ msg = upload_pb2.UploadSampleRequest() msg.uploader_id = uploader_id sample_msg = upload_pb2.TrackSample() sample_msg.track.CopyFrom(track) sample_msg.signed_challenge_info.CopyFrom(server_challenge) sample_spec = server_challenge.challenge_info # convenience if mock_sample is None: # The sample is simply a small (usually 15 second) clip of the song, # transcoded into 128kbs mp3. The server dictates where the cut should be made. sample_msg.sample = utils.transcode_to_mp3( filepath, quality='128k', slice_start=sample_spec.start_millis // 1000, slice_duration=sample_spec.duration_millis // 1000 ) else: sample_msg.sample = mock_sample # You can provide multiple samples; I just provide one at a time. msg.track_sample.extend([sample_msg]) return msg class UpdateUploadState(MmCall): """Notify the server that we will be starting/stopping/pausing our upload. I believe this is used for the webclient 'currently uploading' widget, but that might also be the current_uploading information. """ static_method = 'POST' static_params = {'version': 1} static_url = _android_url + 'uploadstate' @staticmethod @pb def dynamic_data(to_state, uploader_id): """Raise ValueError on problems. :param to_state: one of 'start', 'paused', or 'stopped' """ msg = upload_pb2.UpdateUploadStateRequest() msg.uploader_id = uploader_id try: state = getattr(upload_pb2.UpdateUploadStateRequest, to_state.upper()) except AttributeError as e: raise ValueError(str(e)) msg.state = state return msg class CancelUploadJobs(MmCall): """This call will cancel any outstanding upload jobs (ie from GetJobs). The Music Manager only calls it when the user changes the location of their local collection. It doesn't actually return anything useful.""" static_method = 'POST' static_url = _android_url + 'deleteuploadrequested' @staticmethod @pb def dynamic_data(uploader_id): """ :param uploader_id: id """ msg = upload_pb2.DeleteUploadRequestedRequest() # what a mouthful! msg.uploader_id = uploader_id return msg class ListTracks(MmCall): """List all tracks. Returns a subset of all available metadata. Can optionally filter for only free/purchased tracks.""" res_msg_type = download_pb2.GetTracksToExportResponse static_method = 'POST' static_url = 'https://music.google.com/music/exportids' # example response: # download_track_info { # id: "970d9e51-b392-3857-897a-170e456cba60" # title: "Temporary Trip" # album: "Pay Attention" # album_artist: "The Mighty Mighty Bosstones" # artist: "The Mighty Mighty Bosstones" # track_number: 14 # track_size: 3577382 # } @staticmethod def dynamic_headers(client_id, *args, **kwargs): return {'X-Device-ID': client_id} @staticmethod @pb def dynamic_data(client_id, cont_token=None, export_type=1, updated_min=0): """Works similarly to the webclient method. Chunks are up to 1000 tracks. :param client_id: an authorized uploader_id :param cont_token: (optional) token to get the next library chunk. :param export_type: 1='ALL', 2='PURCHASED_AND_PROMOTIONAL' :param updated_min: likely a timestamp; never seen an example of this != 0 """ msg = download_pb2.GetTracksToExportRequest() msg.client_id = client_id msg.export_type = export_type if cont_token is not None: msg.continuation_token = cont_token msg.updated_min = updated_min return msg @classmethod def check_success(cls, response, msg): if msg.status != download_pb2.GetTracksToExportResponse.OK: enum_desc = download_pb2._GETTRACKSTOEXPORTRESPONSE.enum_types[0] res_name = enum_desc.values_by_number[msg.status].name raise CallFailure( "Track export (list) error code %s: %s." % ( msg.status, res_name ), cls.__name__ ) # TODO @staticmethod def filter_response(msg): """Only log a summary.""" cont_token = None if msg.HasField('continuation_token'): cont_token = msg.continuation_token updated_min = None if msg.HasField('updated_min'): updated_min = msg.updated_min return "<%s songs>, updated_min: %r, continuation_token: %r" % ( len(msg.download_track_info), updated_min, cont_token) class GetDownloadLink(MmCall): """Get a url where a track can be downloaded. Auth is not needed to retrieve the resulting url.""" static_method = 'GET' static_headers = {} static_params = {'version': 2} static_url = 'https://music.google.com/music/export' @staticmethod def dynamic_headers(sid, client_id): return {'X-Device-ID': client_id} @staticmethod def dynamic_params(sid, client_id): return {'songid': sid} @classmethod def parse_response(cls, response): return cls._parse_json(response.text) @staticmethod def filter_response(res): return res class DownloadTrack(MmCall): """Given a url, retrieve a track. Unlike the Webclient, this requires authentication. The entire Requests.Response is returned.""" static_method = 'GET' @staticmethod def dynamic_url(url): """ :param url: result of a call to GetDownloadLink """ return url @classmethod def parse_response(cls, response): return response @staticmethod def filter_response(res): return "code: %s; size: %s bytes; disposition: %r" % ( res.status_code, res.headers['Content-Length'], res.headers['Content-Disposition']) gmusicapi-12.1.1/gmusicapi/protocol/shared.py0000664000175000017500000002773413400371522021527 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- """Definitions shared by multiple clients.""" from __future__ import print_function, division, absolute_import, unicode_literals from builtins import * # noqa from six import raise_from from collections import namedtuple from google.protobuf.descriptor import FieldDescriptor import json from gmusicapi.exceptions import ( CallFailure, ParseException, ValidationException, ) from gmusicapi.utils import utils import requests from future.utils import with_metaclass log = utils.DynamicClientLogger(__name__) _auth_names = ('xt', 'sso', 'oauth', 'gpsoauth',) """ AuthTypes has fields for each type of auth, each of which store a bool: xt: webclient xsrf param/cookie sso: webclient Authorization header oauth: musicmanager/mobileclient Bearer header gpsoauth: mobileclient gpsoauth GoogleLogin header """ AuthTypes = namedtuple('AuthTypes', _auth_names) def authtypes(**kwargs): """Convinience factory for AuthTypes that defaults authtypes to False.""" for name in _auth_names: if name not in kwargs: kwargs[name] = False return AuthTypes(**kwargs) class BuildRequestMeta(type): """Metaclass to create build_request from static/dynamic config.""" def __new__(cls, name, bases, dct): # To not mess with mro and inheritance, build the class first. new_cls = super(BuildRequestMeta, cls).__new__(cls, name, bases, dct) merge_keys = ('headers', 'params') all_keys = ('method', 'url', 'files', 'data', 'verify', 'allow_redirects') + merge_keys config = {} # stores key: val for static or f(*args, **kwargs) -> val for dyn dyn = lambda key: 'dynamic_' + key # noqa stat = lambda key: 'static_' + key # noqa has_key = lambda key: hasattr(new_cls, key) # noqa get_key = lambda key: getattr(new_cls, key) # noqa for key in all_keys: if not has_key(dyn(key)) and not has_key(stat(key)): continue # this key will be ignored; requests will default it if has_key(dyn(key)): config[key] = get_key(dyn(key)) else: config[key] = get_key(stat(key)) for key in merge_keys: # merge case: dyn took precedence above, but stat also exists if has_key(dyn(key)) and has_key(stat(key)): def key_closure(stat_val=get_key(stat(key)), dyn_func=get_key(dyn(key))): def build_key(*args, **kwargs): dyn_val = dyn_func(*args, **kwargs) stat_val.update(dyn_val) return stat_val return build_key config[key] = key_closure() # To explain some of the funkiness wrt closures, see: # http://stackoverflow.com/questions/233673/lexical-closures-in-python # create the actual build_request method def req_closure(config=config): def build_request(cls, *args, **kwargs): req_kwargs = {} for key, val in config.items(): if hasattr(val, '__call__'): val = val(*args, **kwargs) req_kwargs[key] = val return req_kwargs return build_request new_cls.build_request = classmethod(req_closure()) return new_cls class Call(with_metaclass(BuildRequestMeta, object)): """ Clients should use Call.perform(). Calls define how to build their requests through static and dynamic data. For example, a request might always send some user-agent: this is static. Or, it might need the name of a song to modify: this is dynamic. Specially named fields define the data, and correspond with requests.Request kwargs: method: eg 'GET' or 'POST' url: string files: dictionary of {filename: fileobject} files to multipart upload. data: the body of the request If a dictionary is provided, form-encoding will take place. A string will be sent as-is. verify: if True, verify SSL certs params (m): dictionary of URL parameters to append to the URL. headers (m): dictionary Static data shold prepends static_ to a field: class SomeCall(Call): static_url = 'http://foo.com/thiscall' And dynamic data prepends dynamic_ to a method: class SomeCall(Call): #*args, **kwargs are passed from SomeCall.build_request (and Call.perform) def dynamic_url(endpoint): return 'http://foo.com/' + endpoint Dynamic data takes precedence over static if both exist, except for attributes marked with (m) above. These get merged, with dynamic overriding on key conflicts (though all this really shouldn't be relied on). Here's a contrived example that merges static and dynamic headers: class SomeCall(Call): static_headers = {'user-agent': "I'm totally a Google client!"} @classmethod def dynamic_headers(cls, keep_alive=False): return {'Connection': keep_alive} If neither a static nor dynamic member is defined, the param is not used to create the requests.Request. Calls declare the kind of auth they require with an AuthTypes object named required_auth. Calls must define parse_response. Calls can also define filter_response, validate and check_success. Calls are organized semantically, so one endpoint might have multiple calls. """ gets_logged = True fail_on_non_200 = True required_auth = authtypes() # all false by default @classmethod def parse_response(cls, response): """Parses a requests.Response to data.""" raise NotImplementedError @classmethod def validate(cls, response, msg): """Raise ValidationException on problems. :param response: a requests.Response :param msg: the result of parse_response on response """ pass @classmethod def check_success(cls, response, msg): """Raise CallFailure on problems. :param response: a requests.Response :param msg: the result of parse_response on response """ pass @classmethod def filter_response(cls, msg): """Return a version of a parsed response appropriate for logging.""" return msg # default to identity @classmethod def perform(cls, session, validate, *args, **kwargs): """Send, parse, validate and check success of this call. *args and **kwargs are passed to protocol.build_transaction. :param session: a PlaySession used to send this request. :param validate: if False, do not validate. :param required_auth: if in kwargs, overrides the static protocol required_auth. """ # TODO link up these docs call_name = cls.__name__ if cls.gets_logged: log.debug("%s(args=%s, kwargs=%s)", call_name, [utils.truncate(a) for a in args], dict((k, utils.truncate(v)) for (k, v) in kwargs.items()) ) else: log.debug("%s()", call_name) required_auth = kwargs.pop('required_auth', cls.required_auth) req_kwargs = cls.build_request(*args, **kwargs) response = session.send(req_kwargs, required_auth) # TODO trim the logged response if it's huge? safe_req_kwargs = req_kwargs.copy() if safe_req_kwargs.get('headers', {}).get('Authorization', None) is not None: safe_req_kwargs['headers']['Authorization'] = '' if cls.fail_on_non_200: try: response.raise_for_status() except requests.HTTPError as e: err_msg = str(e) if cls.gets_logged: err_msg += "\n(requests kwargs: %r)" % (safe_req_kwargs) err_msg += "\n(response was: %r)" % response.text raise CallFailure(err_msg, call_name) try: parsed_response = cls.parse_response(response) except ParseException: err_msg = ("the server's response could not be understood." " The call may still have succeeded, but it's unlikely.") if cls.gets_logged: err_msg += "\n(requests kwargs: %r)" % (safe_req_kwargs) err_msg += "\n(response was: %r)" % response.text log.exception("could not parse %s response: %r", call_name, response.text) else: log.exception("could not parse %s response: (omitted)", call_name) raise CallFailure(err_msg, call_name) if cls.gets_logged: log.debug(cls.filter_response(parsed_response)) try: # order is important; validate only has a schema for a successful response cls.check_success(response, parsed_response) if validate: cls.validate(response, parsed_response) except CallFailure as e: if not cls.gets_logged: raise # otherwise, reraise a new exception with our req/res context err_msg = ("{e_message}\n" "(requests kwargs: {req_kwargs!r})\n" "(response was: {content!r})").format( e_message=str(e), req_kwargs=safe_req_kwargs, content=response.text) raise_from(CallFailure(err_msg, e.callname), e) except ValidationException as e: # TODO shouldn't be using formatting err_msg = "the response format for %s was not recognized." % call_name err_msg += "\n\n%s\n" % e if cls.gets_logged: raw_response = response.text if len(raw_response) > 10000: raw_response = raw_response[:10000] + '...' err_msg += ("\nFirst, try the develop branch." " If you can recreate this error with the most recent code" " please [create an issue](http://goo.gl/qbAW8) that includes" " the above ValidationException" " and the following request/response:\n%r\n\n%r\n" "\nA traceback follows:\n") % (safe_req_kwargs, raw_response) log.exception(err_msg) return parsed_response @staticmethod def _parse_json(text): try: return json.loads(text) except ValueError as e: raise_from(ParseException(str(e)), e) @staticmethod def _filter_proto(msg, make_copy=True): """Filter all byte fields in the message and submessages.""" filtered = msg if make_copy: filtered = msg.__class__() filtered.CopyFrom(msg) fields = filtered.ListFields() # eg of filtering a specific field # if any(fd.name == 'field_name' for fd, val in fields): # filtered.field_name = '' # Filter all byte fields. for field_name, val in ((fd.name, val) for fd, val in fields if fd.type == FieldDescriptor.TYPE_BYTES): setattr(filtered, field_name, bytes("<%s bytes>" % len(val), 'utf8')) # Filter submessages. for field in (val for fd, val in fields if fd.type == FieldDescriptor.TYPE_MESSAGE): # protobuf repeated api is bad for reflection is_repeated = hasattr(field, '__len__') if not is_repeated: Call._filter_proto(field, make_copy=False) else: for i in range(len(field)): # repeatedComposite does not allow setting old_fields = [f for f in field] del field[:] field.extend([Call._filter_proto(f, make_copy=False) for f in old_fields]) return filtered gmusicapi-12.1.1/gmusicapi/protocol/uits_pb2.py0000664000175000017500000007155313374625453022025 0ustar simonsimon00000000000000# Generated by the protocol buffer compiler. DO NOT EDIT! # source: uits.proto import sys _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database from google.protobuf import descriptor_pb2 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor.FileDescriptor( name='uits.proto', package='wireless_android_skyjam', syntax='proto2', serialized_pb=_b('\n\nuits.proto\x12\x17wireless_android_skyjam\"\x83\x01\n\tProductId\x12\x35\n\x04type\x18\x01 \x02(\x0e\x32\'.wireless_android_skyjam.ProductId.Type\x12\x18\n\tcompleted\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\n\n\x02id\x18\x03 \x02(\t\"\x19\n\x04Type\x12\x07\n\x03UPC\x10\x01\x12\x08\n\x04GRID\x10\x02\"\\\n\x07\x41ssetId\x12\x33\n\x04type\x18\x01 \x02(\x0e\x32%.wireless_android_skyjam.AssetId.Type\x12\n\n\x02id\x18\x02 \x02(\t\"\x10\n\x04Type\x12\x08\n\x04ISRC\x10\x01\"/\n\rTransactionId\x12\x12\n\x07version\x18\x01 \x02(\t:\x01\x31\x12\n\n\x02id\x18\x02 \x02(\t\"|\n\x07MediaId\x12\x46\n\x0e\x61lgorithm_type\x18\x01 \x02(\x0e\x32..wireless_android_skyjam.MediaId.AlgorithmType\x12\x0c\n\x04hash\x18\x02 \x02(\t\"\x1b\n\rAlgorithmType\x12\n\n\x06SHA256\x10\x01\"\xa3\x01\n\x07UrlInfo\x12\x33\n\x04type\x18\x01 \x02(\x0e\x32%.wireless_android_skyjam.UrlInfo.Type\x12\x0b\n\x03url\x18\x02 \x02(\t\"V\n\x04Type\x12\x08\n\x04WCOM\x10\x01\x12\x08\n\x04WCOP\x10\x02\x12\x08\n\x04WOAF\x10\x03\x12\x08\n\x04WOAR\x10\x04\x12\x08\n\x04WOAS\x10\x05\x12\x08\n\x04WORS\x10\x06\x12\x08\n\x04WPAY\x10\x07\x12\x08\n\x04WPUB\x10\x08\"\xac\x01\n\x0f\x43opyrightStatus\x12;\n\x04type\x18\x01 \x02(\x0e\x32-.wireless_android_skyjam.CopyrightStatus.Type\x12\x11\n\tcopyright\x18\x02 \x01(\t\"I\n\x04Type\x12\x0f\n\x0bUNSPECIFIED\x10\x01\x12\x15\n\x11\x41LLRIGHTSRESERVED\x10\x02\x12\x0e\n\nPRERELEASE\x10\x03\x12\t\n\x05OTHER\x10\x04\"$\n\x05\x45xtra\x12\x0c\n\x04type\x18\x01 \x02(\t\x12\r\n\x05value\x18\x02 \x02(\t\"\xf5\x04\n\x0cUitsMetadata\x12\r\n\x05nonce\x18\x01 \x02(\t\x12\x16\n\x0e\x64istributor_id\x18\x02 \x02(\t\x12\x18\n\x10transaction_date\x18\x03 \x02(\t\x12\x36\n\nproduct_id\x18\x04 \x02(\x0b\x32\".wireless_android_skyjam.ProductId\x12\x32\n\x08\x61sset_id\x18\x05 \x02(\x0b\x32 .wireless_android_skyjam.AssetId\x12>\n\x0etransaction_id\x18\x06 \x02(\x0b\x32&.wireless_android_skyjam.TransactionId\x12\x32\n\x08media_id\x18\x07 \x02(\x0b\x32 .wireless_android_skyjam.MediaId\x12\x32\n\x08url_info\x18\x08 \x01(\x0b\x32 .wireless_android_skyjam.UrlInfo\x12Z\n\x16parental_advisory_type\x18\t \x01(\x0e\x32:.wireless_android_skyjam.UitsMetadata.ParentalAdvisoryType\x12\x42\n\x10\x63opyright_status\x18\n \x01(\x0b\x32(.wireless_android_skyjam.CopyrightStatus\x12-\n\x05\x65xtra\x18\x0b \x03(\x0b\x32\x1e.wireless_android_skyjam.Extra\"A\n\x14ParentalAdvisoryType\x12\x0f\n\x0bUNSPECIFIED\x10\x01\x12\x0c\n\x08\x45XPLICIT\x10\x02\x12\n\n\x06\x45\x44ITED\x10\x03\"\xa5\x02\n\rUitsSignature\x12L\n\x0e\x61lgorithm_type\x18\x01 \x02(\x0e\x32\x34.wireless_android_skyjam.UitsSignature.AlgorithmType\x12Z\n\x15\x63\x61nonicalization_type\x18\x02 \x02(\x0e\x32;.wireless_android_skyjam.UitsSignature.CanonicalizationType\x12\x0e\n\x06key_id\x18\x03 \x02(\t\x12\r\n\x05value\x18\x04 \x02(\t\")\n\rAlgorithmType\x12\x0b\n\x07RSA2048\x10\x01\x12\x0b\n\x07\x44SA2048\x10\x02\" \n\x14\x43\x61nonicalizationType\x12\x08\n\x04NONE\x10\x01\"z\n\x04Uits\x12\x37\n\x08metadata\x18\x01 \x02(\x0b\x32%.wireless_android_skyjam.UitsMetadata\x12\x39\n\tsignature\x18\x02 \x02(\x0b\x32&.wireless_android_skyjam.UitsSignature') ) _sym_db.RegisterFileDescriptor(DESCRIPTOR) _PRODUCTID_TYPE = _descriptor.EnumDescriptor( name='Type', full_name='wireless_android_skyjam.ProductId.Type', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='UPC', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='GRID', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=146, serialized_end=171, ) _sym_db.RegisterEnumDescriptor(_PRODUCTID_TYPE) _ASSETID_TYPE = _descriptor.EnumDescriptor( name='Type', full_name='wireless_android_skyjam.AssetId.Type', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='ISRC', index=0, number=1, options=None, type=None), ], containing_type=None, options=None, serialized_start=249, serialized_end=265, ) _sym_db.RegisterEnumDescriptor(_ASSETID_TYPE) _MEDIAID_ALGORITHMTYPE = _descriptor.EnumDescriptor( name='AlgorithmType', full_name='wireless_android_skyjam.MediaId.AlgorithmType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='SHA256', index=0, number=1, options=None, type=None), ], containing_type=None, options=None, serialized_start=413, serialized_end=440, ) _sym_db.RegisterEnumDescriptor(_MEDIAID_ALGORITHMTYPE) _URLINFO_TYPE = _descriptor.EnumDescriptor( name='Type', full_name='wireless_android_skyjam.UrlInfo.Type', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='WCOM', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='WCOP', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='WOAF', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='WOAR', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='WOAS', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='WORS', index=5, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='WPAY', index=6, number=7, options=None, type=None), _descriptor.EnumValueDescriptor( name='WPUB', index=7, number=8, options=None, type=None), ], containing_type=None, options=None, serialized_start=520, serialized_end=606, ) _sym_db.RegisterEnumDescriptor(_URLINFO_TYPE) _COPYRIGHTSTATUS_TYPE = _descriptor.EnumDescriptor( name='Type', full_name='wireless_android_skyjam.CopyrightStatus.Type', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='UNSPECIFIED', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='ALLRIGHTSRESERVED', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='PRERELEASE', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='OTHER', index=3, number=4, options=None, type=None), ], containing_type=None, options=None, serialized_start=708, serialized_end=781, ) _sym_db.RegisterEnumDescriptor(_COPYRIGHTSTATUS_TYPE) _UITSMETADATA_PARENTALADVISORYTYPE = _descriptor.EnumDescriptor( name='ParentalAdvisoryType', full_name='wireless_android_skyjam.UitsMetadata.ParentalAdvisoryType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='UNSPECIFIED', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='EXPLICIT', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='EDITED', index=2, number=3, options=None, type=None), ], containing_type=None, options=None, serialized_start=1386, serialized_end=1451, ) _sym_db.RegisterEnumDescriptor(_UITSMETADATA_PARENTALADVISORYTYPE) _UITSSIGNATURE_ALGORITHMTYPE = _descriptor.EnumDescriptor( name='AlgorithmType', full_name='wireless_android_skyjam.UitsSignature.AlgorithmType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='RSA2048', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='DSA2048', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=1672, serialized_end=1713, ) _sym_db.RegisterEnumDescriptor(_UITSSIGNATURE_ALGORITHMTYPE) _UITSSIGNATURE_CANONICALIZATIONTYPE = _descriptor.EnumDescriptor( name='CanonicalizationType', full_name='wireless_android_skyjam.UitsSignature.CanonicalizationType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='NONE', index=0, number=1, options=None, type=None), ], containing_type=None, options=None, serialized_start=1715, serialized_end=1747, ) _sym_db.RegisterEnumDescriptor(_UITSSIGNATURE_CANONICALIZATIONTYPE) _PRODUCTID = _descriptor.Descriptor( name='ProductId', full_name='wireless_android_skyjam.ProductId', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='type', full_name='wireless_android_skyjam.ProductId.type', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='completed', full_name='wireless_android_skyjam.ProductId.completed', index=1, number=2, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='id', full_name='wireless_android_skyjam.ProductId.id', index=2, number=3, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _PRODUCTID_TYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=40, serialized_end=171, ) _ASSETID = _descriptor.Descriptor( name='AssetId', full_name='wireless_android_skyjam.AssetId', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='type', full_name='wireless_android_skyjam.AssetId.type', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='id', full_name='wireless_android_skyjam.AssetId.id', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _ASSETID_TYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=173, serialized_end=265, ) _TRANSACTIONID = _descriptor.Descriptor( name='TransactionId', full_name='wireless_android_skyjam.TransactionId', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='version', full_name='wireless_android_skyjam.TransactionId.version', index=0, number=1, type=9, cpp_type=9, label=2, has_default_value=True, default_value=_b("1").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='id', full_name='wireless_android_skyjam.TransactionId.id', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=267, serialized_end=314, ) _MEDIAID = _descriptor.Descriptor( name='MediaId', full_name='wireless_android_skyjam.MediaId', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='algorithm_type', full_name='wireless_android_skyjam.MediaId.algorithm_type', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='hash', full_name='wireless_android_skyjam.MediaId.hash', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _MEDIAID_ALGORITHMTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=316, serialized_end=440, ) _URLINFO = _descriptor.Descriptor( name='UrlInfo', full_name='wireless_android_skyjam.UrlInfo', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='type', full_name='wireless_android_skyjam.UrlInfo.type', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='url', full_name='wireless_android_skyjam.UrlInfo.url', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _URLINFO_TYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=443, serialized_end=606, ) _COPYRIGHTSTATUS = _descriptor.Descriptor( name='CopyrightStatus', full_name='wireless_android_skyjam.CopyrightStatus', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='type', full_name='wireless_android_skyjam.CopyrightStatus.type', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='copyright', full_name='wireless_android_skyjam.CopyrightStatus.copyright', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _COPYRIGHTSTATUS_TYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=609, serialized_end=781, ) _EXTRA = _descriptor.Descriptor( name='Extra', full_name='wireless_android_skyjam.Extra', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='type', full_name='wireless_android_skyjam.Extra.type', index=0, number=1, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='value', full_name='wireless_android_skyjam.Extra.value', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=783, serialized_end=819, ) _UITSMETADATA = _descriptor.Descriptor( name='UitsMetadata', full_name='wireless_android_skyjam.UitsMetadata', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='nonce', full_name='wireless_android_skyjam.UitsMetadata.nonce', index=0, number=1, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='distributor_id', full_name='wireless_android_skyjam.UitsMetadata.distributor_id', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='transaction_date', full_name='wireless_android_skyjam.UitsMetadata.transaction_date', index=2, number=3, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='product_id', full_name='wireless_android_skyjam.UitsMetadata.product_id', index=3, number=4, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='asset_id', full_name='wireless_android_skyjam.UitsMetadata.asset_id', index=4, number=5, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='transaction_id', full_name='wireless_android_skyjam.UitsMetadata.transaction_id', index=5, number=6, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='media_id', full_name='wireless_android_skyjam.UitsMetadata.media_id', index=6, number=7, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='url_info', full_name='wireless_android_skyjam.UitsMetadata.url_info', index=7, number=8, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='parental_advisory_type', full_name='wireless_android_skyjam.UitsMetadata.parental_advisory_type', index=8, number=9, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='copyright_status', full_name='wireless_android_skyjam.UitsMetadata.copyright_status', index=9, number=10, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='extra', full_name='wireless_android_skyjam.UitsMetadata.extra', index=10, number=11, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _UITSMETADATA_PARENTALADVISORYTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=822, serialized_end=1451, ) _UITSSIGNATURE = _descriptor.Descriptor( name='UitsSignature', full_name='wireless_android_skyjam.UitsSignature', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='algorithm_type', full_name='wireless_android_skyjam.UitsSignature.algorithm_type', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='canonicalization_type', full_name='wireless_android_skyjam.UitsSignature.canonicalization_type', index=1, number=2, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='key_id', full_name='wireless_android_skyjam.UitsSignature.key_id', index=2, number=3, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='value', full_name='wireless_android_skyjam.UitsSignature.value', index=3, number=4, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _UITSSIGNATURE_ALGORITHMTYPE, _UITSSIGNATURE_CANONICALIZATIONTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=1454, serialized_end=1747, ) _UITS = _descriptor.Descriptor( name='Uits', full_name='wireless_android_skyjam.Uits', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='metadata', full_name='wireless_android_skyjam.Uits.metadata', index=0, number=1, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='signature', full_name='wireless_android_skyjam.Uits.signature', index=1, number=2, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=1749, serialized_end=1871, ) _PRODUCTID.fields_by_name['type'].enum_type = _PRODUCTID_TYPE _PRODUCTID_TYPE.containing_type = _PRODUCTID _ASSETID.fields_by_name['type'].enum_type = _ASSETID_TYPE _ASSETID_TYPE.containing_type = _ASSETID _MEDIAID.fields_by_name['algorithm_type'].enum_type = _MEDIAID_ALGORITHMTYPE _MEDIAID_ALGORITHMTYPE.containing_type = _MEDIAID _URLINFO.fields_by_name['type'].enum_type = _URLINFO_TYPE _URLINFO_TYPE.containing_type = _URLINFO _COPYRIGHTSTATUS.fields_by_name['type'].enum_type = _COPYRIGHTSTATUS_TYPE _COPYRIGHTSTATUS_TYPE.containing_type = _COPYRIGHTSTATUS _UITSMETADATA.fields_by_name['product_id'].message_type = _PRODUCTID _UITSMETADATA.fields_by_name['asset_id'].message_type = _ASSETID _UITSMETADATA.fields_by_name['transaction_id'].message_type = _TRANSACTIONID _UITSMETADATA.fields_by_name['media_id'].message_type = _MEDIAID _UITSMETADATA.fields_by_name['url_info'].message_type = _URLINFO _UITSMETADATA.fields_by_name['parental_advisory_type'].enum_type = _UITSMETADATA_PARENTALADVISORYTYPE _UITSMETADATA.fields_by_name['copyright_status'].message_type = _COPYRIGHTSTATUS _UITSMETADATA.fields_by_name['extra'].message_type = _EXTRA _UITSMETADATA_PARENTALADVISORYTYPE.containing_type = _UITSMETADATA _UITSSIGNATURE.fields_by_name['algorithm_type'].enum_type = _UITSSIGNATURE_ALGORITHMTYPE _UITSSIGNATURE.fields_by_name['canonicalization_type'].enum_type = _UITSSIGNATURE_CANONICALIZATIONTYPE _UITSSIGNATURE_ALGORITHMTYPE.containing_type = _UITSSIGNATURE _UITSSIGNATURE_CANONICALIZATIONTYPE.containing_type = _UITSSIGNATURE _UITS.fields_by_name['metadata'].message_type = _UITSMETADATA _UITS.fields_by_name['signature'].message_type = _UITSSIGNATURE DESCRIPTOR.message_types_by_name['ProductId'] = _PRODUCTID DESCRIPTOR.message_types_by_name['AssetId'] = _ASSETID DESCRIPTOR.message_types_by_name['TransactionId'] = _TRANSACTIONID DESCRIPTOR.message_types_by_name['MediaId'] = _MEDIAID DESCRIPTOR.message_types_by_name['UrlInfo'] = _URLINFO DESCRIPTOR.message_types_by_name['CopyrightStatus'] = _COPYRIGHTSTATUS DESCRIPTOR.message_types_by_name['Extra'] = _EXTRA DESCRIPTOR.message_types_by_name['UitsMetadata'] = _UITSMETADATA DESCRIPTOR.message_types_by_name['UitsSignature'] = _UITSSIGNATURE DESCRIPTOR.message_types_by_name['Uits'] = _UITS ProductId = _reflection.GeneratedProtocolMessageType('ProductId', (_message.Message,), dict( DESCRIPTOR = _PRODUCTID, __module__ = 'uits_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.ProductId) )) _sym_db.RegisterMessage(ProductId) AssetId = _reflection.GeneratedProtocolMessageType('AssetId', (_message.Message,), dict( DESCRIPTOR = _ASSETID, __module__ = 'uits_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.AssetId) )) _sym_db.RegisterMessage(AssetId) TransactionId = _reflection.GeneratedProtocolMessageType('TransactionId', (_message.Message,), dict( DESCRIPTOR = _TRANSACTIONID, __module__ = 'uits_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.TransactionId) )) _sym_db.RegisterMessage(TransactionId) MediaId = _reflection.GeneratedProtocolMessageType('MediaId', (_message.Message,), dict( DESCRIPTOR = _MEDIAID, __module__ = 'uits_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.MediaId) )) _sym_db.RegisterMessage(MediaId) UrlInfo = _reflection.GeneratedProtocolMessageType('UrlInfo', (_message.Message,), dict( DESCRIPTOR = _URLINFO, __module__ = 'uits_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UrlInfo) )) _sym_db.RegisterMessage(UrlInfo) CopyrightStatus = _reflection.GeneratedProtocolMessageType('CopyrightStatus', (_message.Message,), dict( DESCRIPTOR = _COPYRIGHTSTATUS, __module__ = 'uits_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.CopyrightStatus) )) _sym_db.RegisterMessage(CopyrightStatus) Extra = _reflection.GeneratedProtocolMessageType('Extra', (_message.Message,), dict( DESCRIPTOR = _EXTRA, __module__ = 'uits_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.Extra) )) _sym_db.RegisterMessage(Extra) UitsMetadata = _reflection.GeneratedProtocolMessageType('UitsMetadata', (_message.Message,), dict( DESCRIPTOR = _UITSMETADATA, __module__ = 'uits_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UitsMetadata) )) _sym_db.RegisterMessage(UitsMetadata) UitsSignature = _reflection.GeneratedProtocolMessageType('UitsSignature', (_message.Message,), dict( DESCRIPTOR = _UITSSIGNATURE, __module__ = 'uits_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UitsSignature) )) _sym_db.RegisterMessage(UitsSignature) Uits = _reflection.GeneratedProtocolMessageType('Uits', (_message.Message,), dict( DESCRIPTOR = _UITS, __module__ = 'uits_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.Uits) )) _sym_db.RegisterMessage(Uits) # @@protoc_insertion_point(module_scope) gmusicapi-12.1.1/gmusicapi/protocol/upload_pb2.py0000664000175000017500000026112313374625453022317 0ustar simonsimon00000000000000# Generated by the protocol buffer compiler. DO NOT EDIT! # source: upload.proto import sys _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database from google.protobuf import descriptor_pb2 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from . import locker_pb2 as locker__pb2 DESCRIPTOR = _descriptor.FileDescriptor( name='upload.proto', package='wireless_android_skyjam', syntax='proto2', serialized_pb=_b('\n\x0cupload.proto\x12\x17wireless_android_skyjam\x1a\x0clocker.proto\"\xb1\x01\n\x0eResponseStatus\x12K\n\rresponse_code\x18\x01 \x02(\x0e\x32\x34.wireless_android_skyjam.ResponseStatus.ResponseCode\"R\n\x0cResponseCode\x12\x06\n\x02OK\x10\x01\x12\x12\n\x0e\x41LREADY_EXISTS\x10\x02\x12\x0e\n\nSOFT_ERROR\x10\x03\x12\x16\n\x12METADATA_TOO_LARGE\x10\x04\"\xa7\x01\n\x0fUploadOperation\x12\x45\n\toperation\x18\x01 \x02(\x0e\x32\x32.wireless_android_skyjam.UploadOperation.Operation\"M\n\tOperation\x12\x14\n\x10OPERATION_CREATE\x10\x01\x12\x14\n\x10OPERATION_MODIFY\x10\x02\x12\x14\n\x10OPERATION_DELETE\x10\x03\"\xa1\x01\n\x11\x45nhancedChallenge\x12P\n\x0ehash_algorithm\x18\x01 \x01(\x0e\x32\x38.wireless_android_skyjam.EnhancedChallenge.HashAlgorithm\x12\x13\n\x0brandom_seed\x18\x02 \x01(\x0c\"%\n\rHashAlgorithm\x12\x0b\n\x07SHA_256\x10\x01\x12\x07\n\x03MD5\x10\x02\"\xac\x01\n\x11\x43lientFpChallenge\x12^\n\x15\x66ingerprint_algorithm\x18\x01 \x01(\x0e\x32?.wireless_android_skyjam.ClientFpChallenge.FingerprintAlgorithm\"7\n\x14\x46ingerprintAlgorithm\x12\x08\n\x04NONE\x10\x00\x12\x15\n\x11LIGHTMP3FP_CLIENT\x10\x01\"\xca\x02\n\rChallengeInfo\x12\x17\n\x0f\x63lient_track_id\x18\x01 \x02(\t\x12\x14\n\x0cstart_millis\x18\x02 \x02(\x05\x12\x17\n\x0f\x64uration_millis\x18\x03 \x02(\x05\x12\x19\n\x11\x63hallenge_user_id\x18\x04 \x01(\t\x12\x1b\n\x13\x63hallenge_timestamp\x18\x05 \x01(\x03\x12\x14\n\x0c\x65xpect_match\x18\x06 \x01(\x08\x12\x46\n\x12\x65nhanced_challenge\x18\x07 \x01(\x0b\x32*.wireless_android_skyjam.EnhancedChallenge\x12\x12\n\ncandidates\x18\x08 \x03(\t\x12G\n\x13\x63lient_fp_challenge\x18\t \x01(\x0b\x32*.wireless_android_skyjam.ClientFpChallenge\"5\n\x19\x45nhancedChallengeResponse\x12\x18\n\x10\x63hallenge_answer\x18\x01 \x01(\x0c\"h\n\x13SignedChallengeInfo\x12>\n\x0e\x63hallenge_info\x18\x01 \x02(\x0b\x32&.wireless_android_skyjam.ChallengeInfo\x12\x11\n\tsignature\x18\x02 \x02(\x0c\"\xbe\x02\n\x0bTrackSample\x12-\n\x05track\x18\x01 \x02(\x0b\x32\x1e.wireless_android_skyjam.Track\x12K\n\x15signed_challenge_info\x18\x02 \x02(\x0b\x32,.wireless_android_skyjam.SignedChallengeInfo\x12\x0e\n\x06sample\x18\x03 \x01(\x0c\x12\x41\n\rsample_format\x18\x04 \x01(\x0e\x32*.wireless_android_skyjam.Track.ContentType\x12;\n\x0euser_album_art\x18\x05 \x01(\x0b\x32#.wireless_android_skyjam.ImageUnion\x12#\n\x1b\x65nhanced_challenge_response\x18\x06 \x01(\x0c\"\xa5\x01\n\x15UploadPlaylistRequest\x12\x42\n\x10upload_operation\x18\x01 \x02(\x0b\x32(.wireless_android_skyjam.UploadOperation\x12\x33\n\x08playlist\x18\x02 \x03(\x0b\x32!.wireless_android_skyjam.Playlist\x12\x13\n\x0buploader_id\x18\x03 \x02(\t\"z\n\x10PlaylistResponse\x12@\n\x0fresponse_status\x18\x01 \x02(\x0b\x32\'.wireless_android_skyjam.ResponseStatus\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x11\n\tserver_id\x18\x03 \x01(\t\"^\n\x16UploadPlaylistResponse\x12\x44\n\x11playlist_response\x18\x01 \x03(\x0b\x32).wireless_android_skyjam.PlaylistResponse\"\xb5\x01\n\x1aUploadPlaylistEntryRequest\x12\x42\n\x10upload_operation\x18\x01 \x02(\x0b\x32(.wireless_android_skyjam.UploadOperation\x12>\n\x0eplaylist_entry\x18\x02 \x03(\x0b\x32&.wireless_android_skyjam.PlaylistEntry\x12\x13\n\x0buploader_id\x18\x03 \x02(\t\"\x7f\n\x15PlaylistEntryResponse\x12@\n\x0fresponse_status\x18\x01 \x02(\x0b\x32\'.wireless_android_skyjam.ResponseStatus\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x11\n\tserver_id\x18\x03 \x01(\t\"n\n\x1bUploadPlaylistEntryResponse\x12O\n\x17playlist_entry_response\x18\x01 \x03(\x0b\x32..wireless_android_skyjam.PlaylistEntryResponse\"[\n\x15UploadMetadataRequest\x12-\n\x05track\x18\x01 \x03(\x0b\x32\x1e.wireless_android_skyjam.Track\x12\x13\n\x0buploader_id\x18\x02 \x02(\t\"\xb0\x01\n\x18UpdateUploadStateRequest\x12L\n\x05state\x18\x01 \x02(\x0e\x32=.wireless_android_skyjam.UpdateUploadStateRequest.UploadState\x12\x13\n\x0buploader_id\x18\x02 \x02(\t\"1\n\x0bUploadState\x12\t\n\x05START\x10\x01\x12\n\n\x06PAUSED\x10\x02\x12\x0b\n\x07STOPPED\x10\x03\"M\n\x12\x43lientStateRequest\x12\x13\n\x0buploader_id\x18\x01 \x02(\t\x12\"\n\x1aget_purchased_tracks_since\x18\x02 \x01(\x03\"\x80\x02\n\x13\x43lientStateResponse\x12\x1a\n\x12locker_track_limit\x18\x01 \x01(\x03\x12\x1c\n\x14user_songs_in_locker\x18\x02 \x01(\x03\x12\x1e\n\x16track_size_limit_in_mb\x18\x03 \x01(\x05\x12#\n\x1buser_purchased_tracks_since\x18\x04 \x01(\x03\x12\x19\n\x11total_track_count\x18\x05 \x01(\x03\x12O\n\x0foverride_config\x18\x06 \x01(\x0b\x32\x36.wireless_android_skyjam.OverrideConfigValueCollection\"\xc5\x01\n\x16UploadMetadataResponse\x12K\n\x15signed_challenge_info\x18\x01 \x03(\x0b\x32,.wireless_android_skyjam.SignedChallengeInfo\x12\x11\n\tupload_id\x18\x02 \x03(\t\x12K\n\x15track_sample_response\x18\x03 \x03(\x0b\x32,.wireless_android_skyjam.TrackSampleResponse\"\xbd\x03\n\x13TrackSampleResponse\x12\x17\n\x0f\x63lient_track_id\x18\x01 \x02(\t\x12P\n\rresponse_code\x18\x02 \x02(\x0e\x32\x39.wireless_android_skyjam.TrackSampleResponse.ResponseCode\x12\x17\n\x0fserver_track_id\x18\x03 \x01(\t\x12\x15\n\ralbum_art_url\x18\x04 \x01(\t\x12\x14\n\x0c\x65xpect_match\x18\x05 \x01(\x08\"\xf4\x01\n\x0cResponseCode\x12\x0b\n\x07MATCHED\x10\x01\x12\x14\n\x10UPLOAD_REQUESTED\x10\x02\x12\x15\n\x11INVALID_SIGNATURE\x10\x03\x12\x12\n\x0e\x41LREADY_EXISTS\x10\x04\x12\x13\n\x0fTRANSIENT_ERROR\x10\x05\x12\x13\n\x0fPERMANENT_ERROR\x10\x06\x12\x1d\n\x19TRACK_COUNT_LIMIT_REACHED\x10\x07\x12\x16\n\x12REJECT_STORE_TRACK\x10\x08\x12\x1f\n\x1bREJECT_STORE_TRACK_BY_LABEL\x10\t\x12\x14\n\x10REJECT_DRM_TRACK\x10\n\"f\n\x13UploadSampleRequest\x12:\n\x0ctrack_sample\x18\x01 \x03(\x0b\x32$.wireless_android_skyjam.TrackSample\x12\x13\n\x0buploader_id\x18\x02 \x02(\t\"c\n\x14UploadSampleResponse\x12K\n\x15track_sample_response\x18\x01 \x03(\x0b\x32,.wireless_android_skyjam.TrackSampleResponse\";\n\nImageUnion\x12\x16\n\x0euser_album_art\x18\x01 \x01(\x0c\x12\x15\n\ralbum_art_url\x18\x02 \x01(\t\"\xe4\t\n\x0eUploadResponse\x12K\n\rresponse_type\x18\x01 \x01(\x0e\x32\x34.wireless_android_skyjam.UploadResponse.ResponseType\x12J\n\x11metadata_response\x18\x02 \x01(\x0b\x32/.wireless_android_skyjam.UploadMetadataResponse\x12J\n\x11playlist_response\x18\x03 \x01(\x0b\x32/.wireless_android_skyjam.UploadPlaylistResponse\x12U\n\x17playlist_entry_response\x18\x04 \x01(\x0b\x32\x34.wireless_android_skyjam.UploadPlaylistEntryResponse\x12\x46\n\x0fsample_response\x18\x05 \x01(\x0b\x32-.wireless_android_skyjam.UploadSampleResponse\x12\x42\n\x10getjobs_response\x18\x07 \x01(\x0b\x32(.wireless_android_skyjam.GetJobsResponse\x12J\n\x14\x63lientstate_response\x18\x08 \x01(\x0b\x32,.wireless_android_skyjam.ClientStateResponse\x12\x35\n\x06policy\x18\x06 \x01(\x0b\x32%.wireless_android_skyjam.ClientPolicy\x12G\n\x0b\x61uth_status\x18\x0b \x01(\x0e\x32\x32.wireless_android_skyjam.UploadResponse.AuthStatus\x12\x12\n\nauth_error\x18\x0c \x01(\x08\x12@\n\x0fupauth_response\x18\r \x01(\x0b\x32\'.wireless_android_skyjam.UpAuthResponse\"\xfa\x01\n\x0cResponseType\x12\x15\n\x11METADATA_RESPONSE\x10\x01\x12\x15\n\x11PLAYLIST_RESPONSE\x10\x02\x12\x1b\n\x17PLAYLIST_ENTRY_RESPONSE\x10\x03\x12\x13\n\x0fSAMPLE_RESPONSE\x10\x04\x12\x14\n\x10GETJOBS_RESPONSE\x10\x05\x12\x11\n\rAUTH_RESPONSE\x10\x06\x12\x19\n\x15\x43LIENT_STATE_RESPONSE\x10\x07\x12 \n\x1cUPDATE_UPLOAD_STATE_RESPONSE\x10\x08\x12$\n DELETE_UPLOAD_REQUESTED_RESPONSE\x10\t\"\xea\x01\n\nAuthStatus\x12\x06\n\x02OK\x10\x08\x12\x15\n\x11MAX_LIMIT_REACHED\x10\t\x12!\n\x1d\x43LIENT_BOUND_TO_OTHER_ACCOUNT\x10\n\x12\x19\n\x15\x43LIENT_NOT_AUTHORIZED\x10\x0b\x12\"\n\x1eMAX_PER_MACHINE_USERS_EXCEEDED\x10\x0c\x12\x17\n\x13\x43LIENT_PLEASE_RETRY\x10\r\x12\x12\n\x0eNOT_SUBSCRIBED\x10\x0e\x12\x13\n\x0fINVALID_REQUEST\x10\x0f\x12\x19\n\x15UPGRADE_MUSIC_MANAGER\x10\x10\"\xb4\x01\n\x0eTracksToUpload\x12\x11\n\tclient_id\x18\x01 \x02(\t\x12\x11\n\tserver_id\x18\x02 \x02(\t\x12\x43\n\x06status\x18\x05 \x02(\x0e\x32\x33.wireless_android_skyjam.TracksToUpload.TrackStatus\"7\n\x0bTrackStatus\x12\x12\n\x0e\x46ORCE_REUPLOAD\x10\x03\x12\x14\n\x10UPLOAD_REQUESTED\x10\x04\"%\n\x0eGetJobsRequest\x12\x13\n\x0buploader_id\x18\x01 \x02(\t\"p\n\x0fGetJobsResponse\x12\x41\n\x10tracks_to_upload\x18\x01 \x03(\x0b\x32\'.wireless_android_skyjam.TracksToUpload\x12\x1a\n\x12get_tracks_success\x18\x02 \x02(\x08\"l\n\rUpAuthRequest\x12\x13\n\x0buploader_id\x18\x01 \x02(\t\x12\x15\n\rfriendly_name\x18\x02 \x01(\t\x12\x16\n\x0emac_identifier\x18\x03 \x01(\t\x12\x17\n\x0fhash_identifier\x18\x04 \x01(\t\"%\n\x0eUpAuthResponse\x12\x13\n\x0buploader_id\x18\x01 \x01(\t\"3\n\x1c\x44\x65leteUploadRequestedRequest\x12\x13\n\x0buploader_id\x18\x01 \x02(\t\"\x83\x02\n\x0c\x43lientPolicy\x12\x15\n\rpause_uploads\x18\x01 \x01(\x08\x12\r\n\x05\x61\x62ort\x18\x02 \x01(\x08\x12\x18\n\x10retry_in_minutes\x18\x03 \x01(\x05\x12\x1a\n\x12\x62\x61ndwidth_cap_kbps\x18\x04 \x01(\x05\x12\x17\n\x0fpause_downloads\x18\x05 \x01(\x08\x12#\n\x1b\x64ownload_bandwidth_cap_kbps\x18\x06 \x01(\x05\x12\x16\n\x0esend_analytics\x18\x07 \x01(\x08\x12\x15\n\ranalytics_url\x18\x08 \x01(\t\x12\x16\n\x0e\x65nable_gapless\x18\t \x01(\x08\x12\x12\n\nenable_m4p\x18\n \x01(\x08\"p\n\x1dUploadMetadataInternalRequest\x12\x0e\n\x06gaiaid\x18\x01 \x02(\x04\x12?\n\x07request\x18\x02 \x02(\x0b\x32..wireless_android_skyjam.UploadMetadataRequest\"\x9f\x02\n\x13OverrideConfigValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\x12O\n\x08priority\x18\x03 \x01(\x0e\x32=.wireless_android_skyjam.OverrideConfigValue.OverridePriority\"\x9a\x01\n\x10OverridePriority\x12\x1d\n\x10\x44\x45\x46\x41ULT_PRIORITY\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01\x12\x13\n\x0fLOWEST_PRIORITY\x10\x64\x12\x11\n\x0cLOW_PRIORITY\x10\xc8\x01\x12\x14\n\x0fMEDIUM_PRIORITY\x10\xac\x02\x12\x12\n\rHIGH_PRIORITY\x10\x90\x03\x12\x15\n\x10HIGHEST_PRIORITY\x10\xf4\x03\"\\\n\x1dOverrideConfigValueCollection\x12;\n\x05value\x18\x01 \x03(\x0b\x32,.wireless_android_skyjam.OverrideConfigValue') , dependencies=[locker__pb2.DESCRIPTOR,]) _sym_db.RegisterFileDescriptor(DESCRIPTOR) _RESPONSESTATUS_RESPONSECODE = _descriptor.EnumDescriptor( name='ResponseCode', full_name='wireless_android_skyjam.ResponseStatus.ResponseCode', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='OK', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='ALREADY_EXISTS', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='SOFT_ERROR', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='METADATA_TOO_LARGE', index=3, number=4, options=None, type=None), ], containing_type=None, options=None, serialized_start=151, serialized_end=233, ) _sym_db.RegisterEnumDescriptor(_RESPONSESTATUS_RESPONSECODE) _UPLOADOPERATION_OPERATION = _descriptor.EnumDescriptor( name='Operation', full_name='wireless_android_skyjam.UploadOperation.Operation', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='OPERATION_CREATE', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='OPERATION_MODIFY', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='OPERATION_DELETE', index=2, number=3, options=None, type=None), ], containing_type=None, options=None, serialized_start=326, serialized_end=403, ) _sym_db.RegisterEnumDescriptor(_UPLOADOPERATION_OPERATION) _ENHANCEDCHALLENGE_HASHALGORITHM = _descriptor.EnumDescriptor( name='HashAlgorithm', full_name='wireless_android_skyjam.EnhancedChallenge.HashAlgorithm', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='SHA_256', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='MD5', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=530, serialized_end=567, ) _sym_db.RegisterEnumDescriptor(_ENHANCEDCHALLENGE_HASHALGORITHM) _CLIENTFPCHALLENGE_FINGERPRINTALGORITHM = _descriptor.EnumDescriptor( name='FingerprintAlgorithm', full_name='wireless_android_skyjam.ClientFpChallenge.FingerprintAlgorithm', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='NONE', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='LIGHTMP3FP_CLIENT', index=1, number=1, options=None, type=None), ], containing_type=None, options=None, serialized_start=687, serialized_end=742, ) _sym_db.RegisterEnumDescriptor(_CLIENTFPCHALLENGE_FINGERPRINTALGORITHM) _UPDATEUPLOADSTATEREQUEST_UPLOADSTATE = _descriptor.EnumDescriptor( name='UploadState', full_name='wireless_android_skyjam.UpdateUploadStateRequest.UploadState', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='START', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='PAUSED', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='STOPPED', index=2, number=3, options=None, type=None), ], containing_type=None, options=None, serialized_start=2593, serialized_end=2642, ) _sym_db.RegisterEnumDescriptor(_UPDATEUPLOADSTATEREQUEST_UPLOADSTATE) _TRACKSAMPLERESPONSE_RESPONSECODE = _descriptor.EnumDescriptor( name='ResponseCode', full_name='wireless_android_skyjam.TrackSampleResponse.ResponseCode', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='MATCHED', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='UPLOAD_REQUESTED', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='INVALID_SIGNATURE', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='ALREADY_EXISTS', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='TRANSIENT_ERROR', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='PERMANENT_ERROR', index=5, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='TRACK_COUNT_LIMIT_REACHED', index=6, number=7, options=None, type=None), _descriptor.EnumValueDescriptor( name='REJECT_STORE_TRACK', index=7, number=8, options=None, type=None), _descriptor.EnumValueDescriptor( name='REJECT_STORE_TRACK_BY_LABEL', index=8, number=9, options=None, type=None), _descriptor.EnumValueDescriptor( name='REJECT_DRM_TRACK', index=9, number=10, options=None, type=None), ], containing_type=None, options=None, serialized_start=3384, serialized_end=3628, ) _sym_db.RegisterEnumDescriptor(_TRACKSAMPLERESPONSE_RESPONSECODE) _UPLOADRESPONSE_RESPONSETYPE = _descriptor.EnumDescriptor( name='ResponseType', full_name='wireless_android_skyjam.UploadResponse.ResponseType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='METADATA_RESPONSE', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='PLAYLIST_RESPONSE', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='PLAYLIST_ENTRY_RESPONSE', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='SAMPLE_RESPONSE', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='GETJOBS_RESPONSE', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='AUTH_RESPONSE', index=5, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='CLIENT_STATE_RESPONSE', index=6, number=7, options=None, type=None), _descriptor.EnumValueDescriptor( name='UPDATE_UPLOAD_STATE_RESPONSE', index=7, number=8, options=None, type=None), _descriptor.EnumValueDescriptor( name='DELETE_UPLOAD_REQUESTED_RESPONSE', index=8, number=9, options=None, type=None), ], containing_type=None, options=None, serialized_start=4662, serialized_end=4912, ) _sym_db.RegisterEnumDescriptor(_UPLOADRESPONSE_RESPONSETYPE) _UPLOADRESPONSE_AUTHSTATUS = _descriptor.EnumDescriptor( name='AuthStatus', full_name='wireless_android_skyjam.UploadResponse.AuthStatus', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='OK', index=0, number=8, options=None, type=None), _descriptor.EnumValueDescriptor( name='MAX_LIMIT_REACHED', index=1, number=9, options=None, type=None), _descriptor.EnumValueDescriptor( name='CLIENT_BOUND_TO_OTHER_ACCOUNT', index=2, number=10, options=None, type=None), _descriptor.EnumValueDescriptor( name='CLIENT_NOT_AUTHORIZED', index=3, number=11, options=None, type=None), _descriptor.EnumValueDescriptor( name='MAX_PER_MACHINE_USERS_EXCEEDED', index=4, number=12, options=None, type=None), _descriptor.EnumValueDescriptor( name='CLIENT_PLEASE_RETRY', index=5, number=13, options=None, type=None), _descriptor.EnumValueDescriptor( name='NOT_SUBSCRIBED', index=6, number=14, options=None, type=None), _descriptor.EnumValueDescriptor( name='INVALID_REQUEST', index=7, number=15, options=None, type=None), _descriptor.EnumValueDescriptor( name='UPGRADE_MUSIC_MANAGER', index=8, number=16, options=None, type=None), ], containing_type=None, options=None, serialized_start=4915, serialized_end=5149, ) _sym_db.RegisterEnumDescriptor(_UPLOADRESPONSE_AUTHSTATUS) _TRACKSTOUPLOAD_TRACKSTATUS = _descriptor.EnumDescriptor( name='TrackStatus', full_name='wireless_android_skyjam.TracksToUpload.TrackStatus', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='FORCE_REUPLOAD', index=0, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='UPLOAD_REQUESTED', index=1, number=4, options=None, type=None), ], containing_type=None, options=None, serialized_start=5277, serialized_end=5332, ) _sym_db.RegisterEnumDescriptor(_TRACKSTOUPLOAD_TRACKSTATUS) _OVERRIDECONFIGVALUE_OVERRIDEPRIORITY = _descriptor.EnumDescriptor( name='OverridePriority', full_name='wireless_android_skyjam.OverrideConfigValue.OverridePriority', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='DEFAULT_PRIORITY', index=0, number=-1, options=None, type=None), _descriptor.EnumValueDescriptor( name='LOWEST_PRIORITY', index=1, number=100, options=None, type=None), _descriptor.EnumValueDescriptor( name='LOW_PRIORITY', index=2, number=200, options=None, type=None), _descriptor.EnumValueDescriptor( name='MEDIUM_PRIORITY', index=3, number=300, options=None, type=None), _descriptor.EnumValueDescriptor( name='HIGH_PRIORITY', index=4, number=400, options=None, type=None), _descriptor.EnumValueDescriptor( name='HIGHEST_PRIORITY', index=5, number=500, options=None, type=None), ], containing_type=None, options=None, serialized_start=6199, serialized_end=6353, ) _sym_db.RegisterEnumDescriptor(_OVERRIDECONFIGVALUE_OVERRIDEPRIORITY) _RESPONSESTATUS = _descriptor.Descriptor( name='ResponseStatus', full_name='wireless_android_skyjam.ResponseStatus', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='response_code', full_name='wireless_android_skyjam.ResponseStatus.response_code', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _RESPONSESTATUS_RESPONSECODE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=56, serialized_end=233, ) _UPLOADOPERATION = _descriptor.Descriptor( name='UploadOperation', full_name='wireless_android_skyjam.UploadOperation', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='operation', full_name='wireless_android_skyjam.UploadOperation.operation', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _UPLOADOPERATION_OPERATION, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=236, serialized_end=403, ) _ENHANCEDCHALLENGE = _descriptor.Descriptor( name='EnhancedChallenge', full_name='wireless_android_skyjam.EnhancedChallenge', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='hash_algorithm', full_name='wireless_android_skyjam.EnhancedChallenge.hash_algorithm', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='random_seed', full_name='wireless_android_skyjam.EnhancedChallenge.random_seed', index=1, number=2, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _ENHANCEDCHALLENGE_HASHALGORITHM, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=406, serialized_end=567, ) _CLIENTFPCHALLENGE = _descriptor.Descriptor( name='ClientFpChallenge', full_name='wireless_android_skyjam.ClientFpChallenge', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='fingerprint_algorithm', full_name='wireless_android_skyjam.ClientFpChallenge.fingerprint_algorithm', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _CLIENTFPCHALLENGE_FINGERPRINTALGORITHM, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=570, serialized_end=742, ) _CHALLENGEINFO = _descriptor.Descriptor( name='ChallengeInfo', full_name='wireless_android_skyjam.ChallengeInfo', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='client_track_id', full_name='wireless_android_skyjam.ChallengeInfo.client_track_id', index=0, number=1, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='start_millis', full_name='wireless_android_skyjam.ChallengeInfo.start_millis', index=1, number=2, type=5, cpp_type=1, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='duration_millis', full_name='wireless_android_skyjam.ChallengeInfo.duration_millis', index=2, number=3, type=5, cpp_type=1, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='challenge_user_id', full_name='wireless_android_skyjam.ChallengeInfo.challenge_user_id', index=3, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='challenge_timestamp', full_name='wireless_android_skyjam.ChallengeInfo.challenge_timestamp', index=4, number=5, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='expect_match', full_name='wireless_android_skyjam.ChallengeInfo.expect_match', index=5, number=6, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='enhanced_challenge', full_name='wireless_android_skyjam.ChallengeInfo.enhanced_challenge', index=6, number=7, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='candidates', full_name='wireless_android_skyjam.ChallengeInfo.candidates', index=7, number=8, type=9, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_fp_challenge', full_name='wireless_android_skyjam.ChallengeInfo.client_fp_challenge', index=8, number=9, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=745, serialized_end=1075, ) _ENHANCEDCHALLENGERESPONSE = _descriptor.Descriptor( name='EnhancedChallengeResponse', full_name='wireless_android_skyjam.EnhancedChallengeResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='challenge_answer', full_name='wireless_android_skyjam.EnhancedChallengeResponse.challenge_answer', index=0, number=1, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=1077, serialized_end=1130, ) _SIGNEDCHALLENGEINFO = _descriptor.Descriptor( name='SignedChallengeInfo', full_name='wireless_android_skyjam.SignedChallengeInfo', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='challenge_info', full_name='wireless_android_skyjam.SignedChallengeInfo.challenge_info', index=0, number=1, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='signature', full_name='wireless_android_skyjam.SignedChallengeInfo.signature', index=1, number=2, type=12, cpp_type=9, label=2, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=1132, serialized_end=1236, ) _TRACKSAMPLE = _descriptor.Descriptor( name='TrackSample', full_name='wireless_android_skyjam.TrackSample', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='track', full_name='wireless_android_skyjam.TrackSample.track', index=0, number=1, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='signed_challenge_info', full_name='wireless_android_skyjam.TrackSample.signed_challenge_info', index=1, number=2, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sample', full_name='wireless_android_skyjam.TrackSample.sample', index=2, number=3, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sample_format', full_name='wireless_android_skyjam.TrackSample.sample_format', index=3, number=4, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='user_album_art', full_name='wireless_android_skyjam.TrackSample.user_album_art', index=4, number=5, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='enhanced_challenge_response', full_name='wireless_android_skyjam.TrackSample.enhanced_challenge_response', index=5, number=6, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=1239, serialized_end=1557, ) _UPLOADPLAYLISTREQUEST = _descriptor.Descriptor( name='UploadPlaylistRequest', full_name='wireless_android_skyjam.UploadPlaylistRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='upload_operation', full_name='wireless_android_skyjam.UploadPlaylistRequest.upload_operation', index=0, number=1, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist', full_name='wireless_android_skyjam.UploadPlaylistRequest.playlist', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='uploader_id', full_name='wireless_android_skyjam.UploadPlaylistRequest.uploader_id', index=2, number=3, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=1560, serialized_end=1725, ) _PLAYLISTRESPONSE = _descriptor.Descriptor( name='PlaylistResponse', full_name='wireless_android_skyjam.PlaylistResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='response_status', full_name='wireless_android_skyjam.PlaylistResponse.response_status', index=0, number=1, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_id', full_name='wireless_android_skyjam.PlaylistResponse.client_id', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='server_id', full_name='wireless_android_skyjam.PlaylistResponse.server_id', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=1727, serialized_end=1849, ) _UPLOADPLAYLISTRESPONSE = _descriptor.Descriptor( name='UploadPlaylistResponse', full_name='wireless_android_skyjam.UploadPlaylistResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='playlist_response', full_name='wireless_android_skyjam.UploadPlaylistResponse.playlist_response', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=1851, serialized_end=1945, ) _UPLOADPLAYLISTENTRYREQUEST = _descriptor.Descriptor( name='UploadPlaylistEntryRequest', full_name='wireless_android_skyjam.UploadPlaylistEntryRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='upload_operation', full_name='wireless_android_skyjam.UploadPlaylistEntryRequest.upload_operation', index=0, number=1, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_entry', full_name='wireless_android_skyjam.UploadPlaylistEntryRequest.playlist_entry', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='uploader_id', full_name='wireless_android_skyjam.UploadPlaylistEntryRequest.uploader_id', index=2, number=3, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=1948, serialized_end=2129, ) _PLAYLISTENTRYRESPONSE = _descriptor.Descriptor( name='PlaylistEntryResponse', full_name='wireless_android_skyjam.PlaylistEntryResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='response_status', full_name='wireless_android_skyjam.PlaylistEntryResponse.response_status', index=0, number=1, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_id', full_name='wireless_android_skyjam.PlaylistEntryResponse.client_id', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='server_id', full_name='wireless_android_skyjam.PlaylistEntryResponse.server_id', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=2131, serialized_end=2258, ) _UPLOADPLAYLISTENTRYRESPONSE = _descriptor.Descriptor( name='UploadPlaylistEntryResponse', full_name='wireless_android_skyjam.UploadPlaylistEntryResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='playlist_entry_response', full_name='wireless_android_skyjam.UploadPlaylistEntryResponse.playlist_entry_response', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=2260, serialized_end=2370, ) _UPLOADMETADATAREQUEST = _descriptor.Descriptor( name='UploadMetadataRequest', full_name='wireless_android_skyjam.UploadMetadataRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='track', full_name='wireless_android_skyjam.UploadMetadataRequest.track', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='uploader_id', full_name='wireless_android_skyjam.UploadMetadataRequest.uploader_id', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=2372, serialized_end=2463, ) _UPDATEUPLOADSTATEREQUEST = _descriptor.Descriptor( name='UpdateUploadStateRequest', full_name='wireless_android_skyjam.UpdateUploadStateRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='state', full_name='wireless_android_skyjam.UpdateUploadStateRequest.state', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='uploader_id', full_name='wireless_android_skyjam.UpdateUploadStateRequest.uploader_id', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _UPDATEUPLOADSTATEREQUEST_UPLOADSTATE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=2466, serialized_end=2642, ) _CLIENTSTATEREQUEST = _descriptor.Descriptor( name='ClientStateRequest', full_name='wireless_android_skyjam.ClientStateRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='uploader_id', full_name='wireless_android_skyjam.ClientStateRequest.uploader_id', index=0, number=1, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='get_purchased_tracks_since', full_name='wireless_android_skyjam.ClientStateRequest.get_purchased_tracks_since', index=1, number=2, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=2644, serialized_end=2721, ) _CLIENTSTATERESPONSE = _descriptor.Descriptor( name='ClientStateResponse', full_name='wireless_android_skyjam.ClientStateResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='locker_track_limit', full_name='wireless_android_skyjam.ClientStateResponse.locker_track_limit', index=0, number=1, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='user_songs_in_locker', full_name='wireless_android_skyjam.ClientStateResponse.user_songs_in_locker', index=1, number=2, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track_size_limit_in_mb', full_name='wireless_android_skyjam.ClientStateResponse.track_size_limit_in_mb', index=2, number=3, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='user_purchased_tracks_since', full_name='wireless_android_skyjam.ClientStateResponse.user_purchased_tracks_since', index=3, number=4, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='total_track_count', full_name='wireless_android_skyjam.ClientStateResponse.total_track_count', index=4, number=5, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='override_config', full_name='wireless_android_skyjam.ClientStateResponse.override_config', index=5, number=6, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=2724, serialized_end=2980, ) _UPLOADMETADATARESPONSE = _descriptor.Descriptor( name='UploadMetadataResponse', full_name='wireless_android_skyjam.UploadMetadataResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='signed_challenge_info', full_name='wireless_android_skyjam.UploadMetadataResponse.signed_challenge_info', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='upload_id', full_name='wireless_android_skyjam.UploadMetadataResponse.upload_id', index=1, number=2, type=9, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='track_sample_response', full_name='wireless_android_skyjam.UploadMetadataResponse.track_sample_response', index=2, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=2983, serialized_end=3180, ) _TRACKSAMPLERESPONSE = _descriptor.Descriptor( name='TrackSampleResponse', full_name='wireless_android_skyjam.TrackSampleResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='client_track_id', full_name='wireless_android_skyjam.TrackSampleResponse.client_track_id', index=0, number=1, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='response_code', full_name='wireless_android_skyjam.TrackSampleResponse.response_code', index=1, number=2, type=14, cpp_type=8, label=2, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='server_track_id', full_name='wireless_android_skyjam.TrackSampleResponse.server_track_id', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album_art_url', full_name='wireless_android_skyjam.TrackSampleResponse.album_art_url', index=3, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='expect_match', full_name='wireless_android_skyjam.TrackSampleResponse.expect_match', index=4, number=5, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _TRACKSAMPLERESPONSE_RESPONSECODE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=3183, serialized_end=3628, ) _UPLOADSAMPLEREQUEST = _descriptor.Descriptor( name='UploadSampleRequest', full_name='wireless_android_skyjam.UploadSampleRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='track_sample', full_name='wireless_android_skyjam.UploadSampleRequest.track_sample', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='uploader_id', full_name='wireless_android_skyjam.UploadSampleRequest.uploader_id', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=3630, serialized_end=3732, ) _UPLOADSAMPLERESPONSE = _descriptor.Descriptor( name='UploadSampleResponse', full_name='wireless_android_skyjam.UploadSampleResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='track_sample_response', full_name='wireless_android_skyjam.UploadSampleResponse.track_sample_response', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=3734, serialized_end=3833, ) _IMAGEUNION = _descriptor.Descriptor( name='ImageUnion', full_name='wireless_android_skyjam.ImageUnion', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='user_album_art', full_name='wireless_android_skyjam.ImageUnion.user_album_art', index=0, number=1, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='album_art_url', full_name='wireless_android_skyjam.ImageUnion.album_art_url', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=3835, serialized_end=3894, ) _UPLOADRESPONSE = _descriptor.Descriptor( name='UploadResponse', full_name='wireless_android_skyjam.UploadResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='response_type', full_name='wireless_android_skyjam.UploadResponse.response_type', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='metadata_response', full_name='wireless_android_skyjam.UploadResponse.metadata_response', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_response', full_name='wireless_android_skyjam.UploadResponse.playlist_response', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='playlist_entry_response', full_name='wireless_android_skyjam.UploadResponse.playlist_entry_response', index=3, number=4, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sample_response', full_name='wireless_android_skyjam.UploadResponse.sample_response', index=4, number=5, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='getjobs_response', full_name='wireless_android_skyjam.UploadResponse.getjobs_response', index=5, number=7, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='clientstate_response', full_name='wireless_android_skyjam.UploadResponse.clientstate_response', index=6, number=8, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='policy', full_name='wireless_android_skyjam.UploadResponse.policy', index=7, number=6, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='auth_status', full_name='wireless_android_skyjam.UploadResponse.auth_status', index=8, number=11, type=14, cpp_type=8, label=1, has_default_value=False, default_value=8, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='auth_error', full_name='wireless_android_skyjam.UploadResponse.auth_error', index=9, number=12, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='upauth_response', full_name='wireless_android_skyjam.UploadResponse.upauth_response', index=10, number=13, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _UPLOADRESPONSE_RESPONSETYPE, _UPLOADRESPONSE_AUTHSTATUS, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=3897, serialized_end=5149, ) _TRACKSTOUPLOAD = _descriptor.Descriptor( name='TracksToUpload', full_name='wireless_android_skyjam.TracksToUpload', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='client_id', full_name='wireless_android_skyjam.TracksToUpload.client_id', index=0, number=1, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='server_id', full_name='wireless_android_skyjam.TracksToUpload.server_id', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='status', full_name='wireless_android_skyjam.TracksToUpload.status', index=2, number=5, type=14, cpp_type=8, label=2, has_default_value=False, default_value=3, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _TRACKSTOUPLOAD_TRACKSTATUS, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=5152, serialized_end=5332, ) _GETJOBSREQUEST = _descriptor.Descriptor( name='GetJobsRequest', full_name='wireless_android_skyjam.GetJobsRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='uploader_id', full_name='wireless_android_skyjam.GetJobsRequest.uploader_id', index=0, number=1, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=5334, serialized_end=5371, ) _GETJOBSRESPONSE = _descriptor.Descriptor( name='GetJobsResponse', full_name='wireless_android_skyjam.GetJobsResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='tracks_to_upload', full_name='wireless_android_skyjam.GetJobsResponse.tracks_to_upload', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='get_tracks_success', full_name='wireless_android_skyjam.GetJobsResponse.get_tracks_success', index=1, number=2, type=8, cpp_type=7, label=2, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=5373, serialized_end=5485, ) _UPAUTHREQUEST = _descriptor.Descriptor( name='UpAuthRequest', full_name='wireless_android_skyjam.UpAuthRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='uploader_id', full_name='wireless_android_skyjam.UpAuthRequest.uploader_id', index=0, number=1, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='friendly_name', full_name='wireless_android_skyjam.UpAuthRequest.friendly_name', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='mac_identifier', full_name='wireless_android_skyjam.UpAuthRequest.mac_identifier', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='hash_identifier', full_name='wireless_android_skyjam.UpAuthRequest.hash_identifier', index=3, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=5487, serialized_end=5595, ) _UPAUTHRESPONSE = _descriptor.Descriptor( name='UpAuthResponse', full_name='wireless_android_skyjam.UpAuthResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='uploader_id', full_name='wireless_android_skyjam.UpAuthResponse.uploader_id', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=5597, serialized_end=5634, ) _DELETEUPLOADREQUESTEDREQUEST = _descriptor.Descriptor( name='DeleteUploadRequestedRequest', full_name='wireless_android_skyjam.DeleteUploadRequestedRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='uploader_id', full_name='wireless_android_skyjam.DeleteUploadRequestedRequest.uploader_id', index=0, number=1, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=5636, serialized_end=5687, ) _CLIENTPOLICY = _descriptor.Descriptor( name='ClientPolicy', full_name='wireless_android_skyjam.ClientPolicy', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='pause_uploads', full_name='wireless_android_skyjam.ClientPolicy.pause_uploads', index=0, number=1, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='abort', full_name='wireless_android_skyjam.ClientPolicy.abort', index=1, number=2, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='retry_in_minutes', full_name='wireless_android_skyjam.ClientPolicy.retry_in_minutes', index=2, number=3, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='bandwidth_cap_kbps', full_name='wireless_android_skyjam.ClientPolicy.bandwidth_cap_kbps', index=3, number=4, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='pause_downloads', full_name='wireless_android_skyjam.ClientPolicy.pause_downloads', index=4, number=5, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='download_bandwidth_cap_kbps', full_name='wireless_android_skyjam.ClientPolicy.download_bandwidth_cap_kbps', index=5, number=6, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='send_analytics', full_name='wireless_android_skyjam.ClientPolicy.send_analytics', index=6, number=7, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='analytics_url', full_name='wireless_android_skyjam.ClientPolicy.analytics_url', index=7, number=8, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='enable_gapless', full_name='wireless_android_skyjam.ClientPolicy.enable_gapless', index=8, number=9, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='enable_m4p', full_name='wireless_android_skyjam.ClientPolicy.enable_m4p', index=9, number=10, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=5690, serialized_end=5949, ) _UPLOADMETADATAINTERNALREQUEST = _descriptor.Descriptor( name='UploadMetadataInternalRequest', full_name='wireless_android_skyjam.UploadMetadataInternalRequest', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='gaiaid', full_name='wireless_android_skyjam.UploadMetadataInternalRequest.gaiaid', index=0, number=1, type=4, cpp_type=4, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='request', full_name='wireless_android_skyjam.UploadMetadataInternalRequest.request', index=1, number=2, type=11, cpp_type=10, label=2, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=5951, serialized_end=6063, ) _OVERRIDECONFIGVALUE = _descriptor.Descriptor( name='OverrideConfigValue', full_name='wireless_android_skyjam.OverrideConfigValue', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='key', full_name='wireless_android_skyjam.OverrideConfigValue.key', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='value', full_name='wireless_android_skyjam.OverrideConfigValue.value', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='priority', full_name='wireless_android_skyjam.OverrideConfigValue.priority', index=2, number=3, type=14, cpp_type=8, label=1, has_default_value=False, default_value=-1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _OVERRIDECONFIGVALUE_OVERRIDEPRIORITY, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=6066, serialized_end=6353, ) _OVERRIDECONFIGVALUECOLLECTION = _descriptor.Descriptor( name='OverrideConfigValueCollection', full_name='wireless_android_skyjam.OverrideConfigValueCollection', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='value', full_name='wireless_android_skyjam.OverrideConfigValueCollection.value', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=6355, serialized_end=6447, ) _RESPONSESTATUS.fields_by_name['response_code'].enum_type = _RESPONSESTATUS_RESPONSECODE _RESPONSESTATUS_RESPONSECODE.containing_type = _RESPONSESTATUS _UPLOADOPERATION.fields_by_name['operation'].enum_type = _UPLOADOPERATION_OPERATION _UPLOADOPERATION_OPERATION.containing_type = _UPLOADOPERATION _ENHANCEDCHALLENGE.fields_by_name['hash_algorithm'].enum_type = _ENHANCEDCHALLENGE_HASHALGORITHM _ENHANCEDCHALLENGE_HASHALGORITHM.containing_type = _ENHANCEDCHALLENGE _CLIENTFPCHALLENGE.fields_by_name['fingerprint_algorithm'].enum_type = _CLIENTFPCHALLENGE_FINGERPRINTALGORITHM _CLIENTFPCHALLENGE_FINGERPRINTALGORITHM.containing_type = _CLIENTFPCHALLENGE _CHALLENGEINFO.fields_by_name['enhanced_challenge'].message_type = _ENHANCEDCHALLENGE _CHALLENGEINFO.fields_by_name['client_fp_challenge'].message_type = _CLIENTFPCHALLENGE _SIGNEDCHALLENGEINFO.fields_by_name['challenge_info'].message_type = _CHALLENGEINFO _TRACKSAMPLE.fields_by_name['track'].message_type = locker__pb2._TRACK _TRACKSAMPLE.fields_by_name['signed_challenge_info'].message_type = _SIGNEDCHALLENGEINFO _TRACKSAMPLE.fields_by_name['sample_format'].enum_type = locker__pb2._TRACK_CONTENTTYPE _TRACKSAMPLE.fields_by_name['user_album_art'].message_type = _IMAGEUNION _UPLOADPLAYLISTREQUEST.fields_by_name['upload_operation'].message_type = _UPLOADOPERATION _UPLOADPLAYLISTREQUEST.fields_by_name['playlist'].message_type = locker__pb2._PLAYLIST _PLAYLISTRESPONSE.fields_by_name['response_status'].message_type = _RESPONSESTATUS _UPLOADPLAYLISTRESPONSE.fields_by_name['playlist_response'].message_type = _PLAYLISTRESPONSE _UPLOADPLAYLISTENTRYREQUEST.fields_by_name['upload_operation'].message_type = _UPLOADOPERATION _UPLOADPLAYLISTENTRYREQUEST.fields_by_name['playlist_entry'].message_type = locker__pb2._PLAYLISTENTRY _PLAYLISTENTRYRESPONSE.fields_by_name['response_status'].message_type = _RESPONSESTATUS _UPLOADPLAYLISTENTRYRESPONSE.fields_by_name['playlist_entry_response'].message_type = _PLAYLISTENTRYRESPONSE _UPLOADMETADATAREQUEST.fields_by_name['track'].message_type = locker__pb2._TRACK _UPDATEUPLOADSTATEREQUEST.fields_by_name['state'].enum_type = _UPDATEUPLOADSTATEREQUEST_UPLOADSTATE _UPDATEUPLOADSTATEREQUEST_UPLOADSTATE.containing_type = _UPDATEUPLOADSTATEREQUEST _CLIENTSTATERESPONSE.fields_by_name['override_config'].message_type = _OVERRIDECONFIGVALUECOLLECTION _UPLOADMETADATARESPONSE.fields_by_name['signed_challenge_info'].message_type = _SIGNEDCHALLENGEINFO _UPLOADMETADATARESPONSE.fields_by_name['track_sample_response'].message_type = _TRACKSAMPLERESPONSE _TRACKSAMPLERESPONSE.fields_by_name['response_code'].enum_type = _TRACKSAMPLERESPONSE_RESPONSECODE _TRACKSAMPLERESPONSE_RESPONSECODE.containing_type = _TRACKSAMPLERESPONSE _UPLOADSAMPLEREQUEST.fields_by_name['track_sample'].message_type = _TRACKSAMPLE _UPLOADSAMPLERESPONSE.fields_by_name['track_sample_response'].message_type = _TRACKSAMPLERESPONSE _UPLOADRESPONSE.fields_by_name['response_type'].enum_type = _UPLOADRESPONSE_RESPONSETYPE _UPLOADRESPONSE.fields_by_name['metadata_response'].message_type = _UPLOADMETADATARESPONSE _UPLOADRESPONSE.fields_by_name['playlist_response'].message_type = _UPLOADPLAYLISTRESPONSE _UPLOADRESPONSE.fields_by_name['playlist_entry_response'].message_type = _UPLOADPLAYLISTENTRYRESPONSE _UPLOADRESPONSE.fields_by_name['sample_response'].message_type = _UPLOADSAMPLERESPONSE _UPLOADRESPONSE.fields_by_name['getjobs_response'].message_type = _GETJOBSRESPONSE _UPLOADRESPONSE.fields_by_name['clientstate_response'].message_type = _CLIENTSTATERESPONSE _UPLOADRESPONSE.fields_by_name['policy'].message_type = _CLIENTPOLICY _UPLOADRESPONSE.fields_by_name['auth_status'].enum_type = _UPLOADRESPONSE_AUTHSTATUS _UPLOADRESPONSE.fields_by_name['upauth_response'].message_type = _UPAUTHRESPONSE _UPLOADRESPONSE_RESPONSETYPE.containing_type = _UPLOADRESPONSE _UPLOADRESPONSE_AUTHSTATUS.containing_type = _UPLOADRESPONSE _TRACKSTOUPLOAD.fields_by_name['status'].enum_type = _TRACKSTOUPLOAD_TRACKSTATUS _TRACKSTOUPLOAD_TRACKSTATUS.containing_type = _TRACKSTOUPLOAD _GETJOBSRESPONSE.fields_by_name['tracks_to_upload'].message_type = _TRACKSTOUPLOAD _UPLOADMETADATAINTERNALREQUEST.fields_by_name['request'].message_type = _UPLOADMETADATAREQUEST _OVERRIDECONFIGVALUE.fields_by_name['priority'].enum_type = _OVERRIDECONFIGVALUE_OVERRIDEPRIORITY _OVERRIDECONFIGVALUE_OVERRIDEPRIORITY.containing_type = _OVERRIDECONFIGVALUE _OVERRIDECONFIGVALUECOLLECTION.fields_by_name['value'].message_type = _OVERRIDECONFIGVALUE DESCRIPTOR.message_types_by_name['ResponseStatus'] = _RESPONSESTATUS DESCRIPTOR.message_types_by_name['UploadOperation'] = _UPLOADOPERATION DESCRIPTOR.message_types_by_name['EnhancedChallenge'] = _ENHANCEDCHALLENGE DESCRIPTOR.message_types_by_name['ClientFpChallenge'] = _CLIENTFPCHALLENGE DESCRIPTOR.message_types_by_name['ChallengeInfo'] = _CHALLENGEINFO DESCRIPTOR.message_types_by_name['EnhancedChallengeResponse'] = _ENHANCEDCHALLENGERESPONSE DESCRIPTOR.message_types_by_name['SignedChallengeInfo'] = _SIGNEDCHALLENGEINFO DESCRIPTOR.message_types_by_name['TrackSample'] = _TRACKSAMPLE DESCRIPTOR.message_types_by_name['UploadPlaylistRequest'] = _UPLOADPLAYLISTREQUEST DESCRIPTOR.message_types_by_name['PlaylistResponse'] = _PLAYLISTRESPONSE DESCRIPTOR.message_types_by_name['UploadPlaylistResponse'] = _UPLOADPLAYLISTRESPONSE DESCRIPTOR.message_types_by_name['UploadPlaylistEntryRequest'] = _UPLOADPLAYLISTENTRYREQUEST DESCRIPTOR.message_types_by_name['PlaylistEntryResponse'] = _PLAYLISTENTRYRESPONSE DESCRIPTOR.message_types_by_name['UploadPlaylistEntryResponse'] = _UPLOADPLAYLISTENTRYRESPONSE DESCRIPTOR.message_types_by_name['UploadMetadataRequest'] = _UPLOADMETADATAREQUEST DESCRIPTOR.message_types_by_name['UpdateUploadStateRequest'] = _UPDATEUPLOADSTATEREQUEST DESCRIPTOR.message_types_by_name['ClientStateRequest'] = _CLIENTSTATEREQUEST DESCRIPTOR.message_types_by_name['ClientStateResponse'] = _CLIENTSTATERESPONSE DESCRIPTOR.message_types_by_name['UploadMetadataResponse'] = _UPLOADMETADATARESPONSE DESCRIPTOR.message_types_by_name['TrackSampleResponse'] = _TRACKSAMPLERESPONSE DESCRIPTOR.message_types_by_name['UploadSampleRequest'] = _UPLOADSAMPLEREQUEST DESCRIPTOR.message_types_by_name['UploadSampleResponse'] = _UPLOADSAMPLERESPONSE DESCRIPTOR.message_types_by_name['ImageUnion'] = _IMAGEUNION DESCRIPTOR.message_types_by_name['UploadResponse'] = _UPLOADRESPONSE DESCRIPTOR.message_types_by_name['TracksToUpload'] = _TRACKSTOUPLOAD DESCRIPTOR.message_types_by_name['GetJobsRequest'] = _GETJOBSREQUEST DESCRIPTOR.message_types_by_name['GetJobsResponse'] = _GETJOBSRESPONSE DESCRIPTOR.message_types_by_name['UpAuthRequest'] = _UPAUTHREQUEST DESCRIPTOR.message_types_by_name['UpAuthResponse'] = _UPAUTHRESPONSE DESCRIPTOR.message_types_by_name['DeleteUploadRequestedRequest'] = _DELETEUPLOADREQUESTEDREQUEST DESCRIPTOR.message_types_by_name['ClientPolicy'] = _CLIENTPOLICY DESCRIPTOR.message_types_by_name['UploadMetadataInternalRequest'] = _UPLOADMETADATAINTERNALREQUEST DESCRIPTOR.message_types_by_name['OverrideConfigValue'] = _OVERRIDECONFIGVALUE DESCRIPTOR.message_types_by_name['OverrideConfigValueCollection'] = _OVERRIDECONFIGVALUECOLLECTION ResponseStatus = _reflection.GeneratedProtocolMessageType('ResponseStatus', (_message.Message,), dict( DESCRIPTOR = _RESPONSESTATUS, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.ResponseStatus) )) _sym_db.RegisterMessage(ResponseStatus) UploadOperation = _reflection.GeneratedProtocolMessageType('UploadOperation', (_message.Message,), dict( DESCRIPTOR = _UPLOADOPERATION, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UploadOperation) )) _sym_db.RegisterMessage(UploadOperation) EnhancedChallenge = _reflection.GeneratedProtocolMessageType('EnhancedChallenge', (_message.Message,), dict( DESCRIPTOR = _ENHANCEDCHALLENGE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.EnhancedChallenge) )) _sym_db.RegisterMessage(EnhancedChallenge) ClientFpChallenge = _reflection.GeneratedProtocolMessageType('ClientFpChallenge', (_message.Message,), dict( DESCRIPTOR = _CLIENTFPCHALLENGE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.ClientFpChallenge) )) _sym_db.RegisterMessage(ClientFpChallenge) ChallengeInfo = _reflection.GeneratedProtocolMessageType('ChallengeInfo', (_message.Message,), dict( DESCRIPTOR = _CHALLENGEINFO, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.ChallengeInfo) )) _sym_db.RegisterMessage(ChallengeInfo) EnhancedChallengeResponse = _reflection.GeneratedProtocolMessageType('EnhancedChallengeResponse', (_message.Message,), dict( DESCRIPTOR = _ENHANCEDCHALLENGERESPONSE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.EnhancedChallengeResponse) )) _sym_db.RegisterMessage(EnhancedChallengeResponse) SignedChallengeInfo = _reflection.GeneratedProtocolMessageType('SignedChallengeInfo', (_message.Message,), dict( DESCRIPTOR = _SIGNEDCHALLENGEINFO, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.SignedChallengeInfo) )) _sym_db.RegisterMessage(SignedChallengeInfo) TrackSample = _reflection.GeneratedProtocolMessageType('TrackSample', (_message.Message,), dict( DESCRIPTOR = _TRACKSAMPLE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.TrackSample) )) _sym_db.RegisterMessage(TrackSample) UploadPlaylistRequest = _reflection.GeneratedProtocolMessageType('UploadPlaylistRequest', (_message.Message,), dict( DESCRIPTOR = _UPLOADPLAYLISTREQUEST, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UploadPlaylistRequest) )) _sym_db.RegisterMessage(UploadPlaylistRequest) PlaylistResponse = _reflection.GeneratedProtocolMessageType('PlaylistResponse', (_message.Message,), dict( DESCRIPTOR = _PLAYLISTRESPONSE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.PlaylistResponse) )) _sym_db.RegisterMessage(PlaylistResponse) UploadPlaylistResponse = _reflection.GeneratedProtocolMessageType('UploadPlaylistResponse', (_message.Message,), dict( DESCRIPTOR = _UPLOADPLAYLISTRESPONSE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UploadPlaylistResponse) )) _sym_db.RegisterMessage(UploadPlaylistResponse) UploadPlaylistEntryRequest = _reflection.GeneratedProtocolMessageType('UploadPlaylistEntryRequest', (_message.Message,), dict( DESCRIPTOR = _UPLOADPLAYLISTENTRYREQUEST, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UploadPlaylistEntryRequest) )) _sym_db.RegisterMessage(UploadPlaylistEntryRequest) PlaylistEntryResponse = _reflection.GeneratedProtocolMessageType('PlaylistEntryResponse', (_message.Message,), dict( DESCRIPTOR = _PLAYLISTENTRYRESPONSE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.PlaylistEntryResponse) )) _sym_db.RegisterMessage(PlaylistEntryResponse) UploadPlaylistEntryResponse = _reflection.GeneratedProtocolMessageType('UploadPlaylistEntryResponse', (_message.Message,), dict( DESCRIPTOR = _UPLOADPLAYLISTENTRYRESPONSE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UploadPlaylistEntryResponse) )) _sym_db.RegisterMessage(UploadPlaylistEntryResponse) UploadMetadataRequest = _reflection.GeneratedProtocolMessageType('UploadMetadataRequest', (_message.Message,), dict( DESCRIPTOR = _UPLOADMETADATAREQUEST, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UploadMetadataRequest) )) _sym_db.RegisterMessage(UploadMetadataRequest) UpdateUploadStateRequest = _reflection.GeneratedProtocolMessageType('UpdateUploadStateRequest', (_message.Message,), dict( DESCRIPTOR = _UPDATEUPLOADSTATEREQUEST, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UpdateUploadStateRequest) )) _sym_db.RegisterMessage(UpdateUploadStateRequest) ClientStateRequest = _reflection.GeneratedProtocolMessageType('ClientStateRequest', (_message.Message,), dict( DESCRIPTOR = _CLIENTSTATEREQUEST, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.ClientStateRequest) )) _sym_db.RegisterMessage(ClientStateRequest) ClientStateResponse = _reflection.GeneratedProtocolMessageType('ClientStateResponse', (_message.Message,), dict( DESCRIPTOR = _CLIENTSTATERESPONSE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.ClientStateResponse) )) _sym_db.RegisterMessage(ClientStateResponse) UploadMetadataResponse = _reflection.GeneratedProtocolMessageType('UploadMetadataResponse', (_message.Message,), dict( DESCRIPTOR = _UPLOADMETADATARESPONSE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UploadMetadataResponse) )) _sym_db.RegisterMessage(UploadMetadataResponse) TrackSampleResponse = _reflection.GeneratedProtocolMessageType('TrackSampleResponse', (_message.Message,), dict( DESCRIPTOR = _TRACKSAMPLERESPONSE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.TrackSampleResponse) )) _sym_db.RegisterMessage(TrackSampleResponse) UploadSampleRequest = _reflection.GeneratedProtocolMessageType('UploadSampleRequest', (_message.Message,), dict( DESCRIPTOR = _UPLOADSAMPLEREQUEST, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UploadSampleRequest) )) _sym_db.RegisterMessage(UploadSampleRequest) UploadSampleResponse = _reflection.GeneratedProtocolMessageType('UploadSampleResponse', (_message.Message,), dict( DESCRIPTOR = _UPLOADSAMPLERESPONSE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UploadSampleResponse) )) _sym_db.RegisterMessage(UploadSampleResponse) ImageUnion = _reflection.GeneratedProtocolMessageType('ImageUnion', (_message.Message,), dict( DESCRIPTOR = _IMAGEUNION, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.ImageUnion) )) _sym_db.RegisterMessage(ImageUnion) UploadResponse = _reflection.GeneratedProtocolMessageType('UploadResponse', (_message.Message,), dict( DESCRIPTOR = _UPLOADRESPONSE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UploadResponse) )) _sym_db.RegisterMessage(UploadResponse) TracksToUpload = _reflection.GeneratedProtocolMessageType('TracksToUpload', (_message.Message,), dict( DESCRIPTOR = _TRACKSTOUPLOAD, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.TracksToUpload) )) _sym_db.RegisterMessage(TracksToUpload) GetJobsRequest = _reflection.GeneratedProtocolMessageType('GetJobsRequest', (_message.Message,), dict( DESCRIPTOR = _GETJOBSREQUEST, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetJobsRequest) )) _sym_db.RegisterMessage(GetJobsRequest) GetJobsResponse = _reflection.GeneratedProtocolMessageType('GetJobsResponse', (_message.Message,), dict( DESCRIPTOR = _GETJOBSRESPONSE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.GetJobsResponse) )) _sym_db.RegisterMessage(GetJobsResponse) UpAuthRequest = _reflection.GeneratedProtocolMessageType('UpAuthRequest', (_message.Message,), dict( DESCRIPTOR = _UPAUTHREQUEST, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UpAuthRequest) )) _sym_db.RegisterMessage(UpAuthRequest) UpAuthResponse = _reflection.GeneratedProtocolMessageType('UpAuthResponse', (_message.Message,), dict( DESCRIPTOR = _UPAUTHRESPONSE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UpAuthResponse) )) _sym_db.RegisterMessage(UpAuthResponse) DeleteUploadRequestedRequest = _reflection.GeneratedProtocolMessageType('DeleteUploadRequestedRequest', (_message.Message,), dict( DESCRIPTOR = _DELETEUPLOADREQUESTEDREQUEST, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.DeleteUploadRequestedRequest) )) _sym_db.RegisterMessage(DeleteUploadRequestedRequest) ClientPolicy = _reflection.GeneratedProtocolMessageType('ClientPolicy', (_message.Message,), dict( DESCRIPTOR = _CLIENTPOLICY, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.ClientPolicy) )) _sym_db.RegisterMessage(ClientPolicy) UploadMetadataInternalRequest = _reflection.GeneratedProtocolMessageType('UploadMetadataInternalRequest', (_message.Message,), dict( DESCRIPTOR = _UPLOADMETADATAINTERNALREQUEST, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.UploadMetadataInternalRequest) )) _sym_db.RegisterMessage(UploadMetadataInternalRequest) OverrideConfigValue = _reflection.GeneratedProtocolMessageType('OverrideConfigValue', (_message.Message,), dict( DESCRIPTOR = _OVERRIDECONFIGVALUE, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.OverrideConfigValue) )) _sym_db.RegisterMessage(OverrideConfigValue) OverrideConfigValueCollection = _reflection.GeneratedProtocolMessageType('OverrideConfigValueCollection', (_message.Message,), dict( DESCRIPTOR = _OVERRIDECONFIGVALUECOLLECTION, __module__ = 'upload_pb2' # @@protoc_insertion_point(class_scope:wireless_android_skyjam.OverrideConfigValueCollection) )) _sym_db.RegisterMessage(OverrideConfigValueCollection) # @@protoc_insertion_point(module_scope) gmusicapi-12.1.1/gmusicapi/protocol/webclient.py0000664000175000017500000004174213374625453022247 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- """Calls made by the web client.""" from __future__ import print_function, division, absolute_import, unicode_literals from six import raise_from from builtins import * # noqa import base64 import copy import hmac import random import string from hashlib import sha1 import validictory import json from gmusicapi.exceptions import CallFailure, ValidationException from gmusicapi.protocol.shared import Call, authtypes from gmusicapi.utils import utils, jsarray base_url = 'https://play.google.com/music/' service_url = base_url + 'services/' class Init(Call): """Called one time per session, immediately after login. This performs one-time setup: it gathers the cookies we need (specifically `xt`), and Google uses it to create the webclient DOM. Note the use of the HEAD verb. Google uses GET, but we don't need the large response containing Google's webui. """ static_method = 'HEAD' static_url = base_url + 'listen' required_auth = authtypes(sso=True) # This call doesn't actually request/return anything useful aside from cookies. @staticmethod def parse_response(response): return response.text @classmethod def check_success(cls, response, msg): if response.status_code != 200: raise CallFailure(('status code %s != 200' % response.status_code), cls.__name__) if 'xt' not in response.cookies: raise CallFailure('did not receieve xt cookies', cls.__name__) class WcCall(Call): """Abstract base for web client calls.""" required_auth = authtypes(xt=True, sso=True) # validictory schema for the response _res_schema = utils.NotImplementedField @classmethod def validate(cls, response, msg): """Use validictory and a static schema (stored in cls._res_schema).""" try: return validictory.validate(msg, cls._res_schema) except ValueError as e: raise_from(ValidationException(str(e)), e) @classmethod def check_success(cls, response, msg): # Failed responses always have a success=False key. # Some successful responses do not have a success=True key, however. # TODO remove utils.call_succeeded if 'success' in msg and not msg['success']: raise CallFailure( "the server reported failure. This is usually" " caused by bad arguments, but can also happen if requests" " are made too quickly (eg creating a playlist then" " modifying it before the server has created it)", cls.__name__) @classmethod def parse_response(cls, response): return cls._parse_json(response.text) class CreatePlaylist(WcCall): """Adds songs to a playlist.""" static_method = 'POST' static_url = service_url + 'createplaylist' static_params = {'format': 'jsarray'} _res_schema = { "type": "array", # eg: # [[0,2] # ,["id","sharetoken",[] # ,]] } @staticmethod def dynamic_data(name, description, public, session_id=""): return json.dumps([[session_id, 1], [public, name, description, []]]) class AddToPlaylist(WcCall): """Adds songs to a playlist.""" static_method = 'POST' static_url = service_url + 'addtoplaylist' _res_schema = { "type": "object", "properties": { "playlistId": {"type": "string"}, "songIds": { "type": "array", "items": { "type": "object", "properties": { "songId": {"type": "string"}, "playlistEntryId": {"type": "string"} } } } }, "additionalProperties": False } @staticmethod def dynamic_data(playlist_id, song_ids): """ :param playlist_id: id of the playlist to add to. :param song_ids: a list of song ids """ # TODO unsure what type means here. Likely involves uploaded vs store/free. song_refs = [{'id': sid, 'type': 1} for sid in song_ids] return { 'json': json.dumps( {"playlistId": playlist_id, "songRefs": song_refs} ) } @staticmethod def filter_response(msg): filtered = copy.copy(msg) filtered['songIds'] = ["<%s songs>" % len(filtered.get('songIds', []))] return filtered class ChangePlaylistOrder(WcCall): """Reorder existing tracks in a playlist.""" static_method = 'POST' static_url = service_url + 'changeplaylistorder' _res_schema = { "type": "object", "properties": { "afterEntryId": {"type": "string", "blank": True}, "playlistId": {"type": "string"}, "movedSongIds": { "type": "array", "items": {"type": "string"} } }, "additionalProperties": False } @staticmethod def dynamic_data(playlist_id, song_ids_moving, entry_ids_moving, after_entry_id=None, before_entry_id=None): """ :param playlist_id: id of the playlist getting reordered. :param song_ids_moving: a list of consecutive song ids. Matches entry_ids_moving. :param entry_ids_moving: a list of consecutive entry ids to move. Matches song_ids_moving. :param after_entry_id: the entry id to place these songs after. Default first position. :param before_entry_id: the entry id to place these songs before. Default last position. """ # empty string means first/last position if after_entry_id is None: after_entry_id = "" if before_entry_id is None: before_entry_id = "" return { 'json': json.dumps( { "playlistId": playlist_id, "movedSongIds": song_ids_moving, "movedEntryIds": entry_ids_moving, "afterEntryId": after_entry_id, "beforeEntryId": before_entry_id } ) } @staticmethod def filter_response(msg): filtered = copy.copy(msg) filtered['movedSongIds'] = ["<%s songs>" % len(filtered.get('movedSongIds', []))] return filtered class DeletePlaylist(WcCall): """Delete a playlist.""" static_method = 'POST' static_url = service_url + 'deleteplaylist' _res_schema = { "type": "object", "properties": { "deleteId": {"type": "string"} }, "additionalProperties": False } @staticmethod def dynamic_data(playlist_id): """ :param playlist_id: id of the playlist to delete. """ return { 'json': json.dumps( {"id": playlist_id} ) } class DeleteSongs(WcCall): """Delete a song from the entire library or a single playlist.""" static_method = 'POST' static_url = service_url + 'deletesong' _res_schema = { "type": "object", "properties": { "listId": {"type": "string"}, "deleteIds": { "type": "array", "items": {"type": "string"} } }, "additionalProperties": False } @staticmethod def dynamic_data(song_ids, playlist_id='all', entry_ids=None): """ :param song_ids: a list of song ids. :param playlist_id: playlist id to delete from, or 'all' for deleting from library. :param entry_ids: when deleting from playlists, corresponding list of entry ids. """ if entry_ids is None: # this is strange, but apparently correct entry_ids = [''] * len(song_ids) return { 'json': json.dumps( {"songIds": song_ids, "entryIds": entry_ids, "listId": playlist_id} ) } @staticmethod def filter_response(msg): filtered = copy.copy(msg) filtered['deleteIds'] = ["<%s songs>" % len(filtered.get('deleteIds', []))] return filtered class ChangeSongMetadata(WcCall): """Edit the metadata of songs.""" static_method = 'POST' static_url = service_url + 'modifytracks' static_params = {'format': 'jsarray'} _res_schema = { "type": "array", # eg [[0,1],[1393706382978]] } @staticmethod def dynamic_data(songs, session_id=""): """ :param songs: a list of dicts ``{'id': '...', 'albumArtUrl': '...'}`` """ supported = {'id', 'albumArtUrl', 'title', 'artist', 'albumArtist', 'album'} for s in songs: for k in s.keys(): if k not in supported: raise ValueError("ChangeSongMetadata only supports the the following keys: " + str(supported) + ". All other keys must be removed. Key encountered:" + k) # jsarray is just wonderful jsarray = [[session_id, 1]] song_arrays = [[s['id'], s.get('title'), s.get('albumArtUrl'), s.get('artist'), s.get('album'), s.get('albumArtist')] + [None] * 33 + [[]] for s in songs] jsarray.append([song_arrays]) return json.dumps(jsarray) class GetDownloadInfo(WcCall): """Get download links and counts for songs.""" static_method = 'POST' static_url = service_url + 'multidownload' _res_schema = { "type": "object", "properties": { "downloadCounts": { "type": "object", "items": { "type": "object", "properties": { "id": {"type": "integer"} } } }, "url": {"type": "string"} }, "additionalProperties": False } @staticmethod def dynamic_data(song_ids): """ :param: (list) song_ids """ return {'json': json.dumps({'songIds': song_ids})} class GetStreamUrl(WcCall): """Used to request a streaming link of a track.""" static_method = 'GET' static_url = base_url + 'play' # note use of base_url, not service_url required_auth = authtypes(sso=True) # no xt required _res_schema = { "type": "object", "properties": { "url": {"type": "string", "required": False}, "urls": {"type": "array", "required": False}, 'now': {'type': 'integer', 'required': False}, 'tier': {'type': 'integer', 'required': False}, 'replayGain': {'type': 'integer'}, 'streamAuthId': {'type': 'string'}, 'isFreeRadioUser': {'type': 'boolean'}, }, "additionalProperties": False } @staticmethod def dynamic_params(song_id): # https://github.com/simon-weber/gmusicapi/issues/137 # there are three cases when streaming: # | track type | guid songid? | slt/sig needed? | # user-uploaded yes no # AA track in library yes yes # AA track not in library no yes # without the track['type'] field we can't tell between 1 and 2, but # include slt/sig anyway; the server ignores the extra params. key = '27f7313e-f75d-445a-ac99-56386a5fe879'.encode("ascii") salt = ''.join(random.choice(string.ascii_lowercase + string.digits) for x in range(12)) salted_id = (song_id + salt).encode("utf-8") sig = base64.urlsafe_b64encode(hmac.new(key, salted_id, sha1).digest())[:-1] params = { 'u': 0, 'pt': 'e', 'slt': salt, 'sig': sig } # TODO match guid instead, should be more robust if song_id[0] == 'T': # all access params['mjck'] = song_id else: params['songid'] = song_id return params class ReportBadSongMatch(WcCall): """Request to signal the uploader to reupload a matched track.""" static_method = 'POST' static_url = service_url + 'fixsongmatch' static_params = {'format': 'jsarray'} # This no longer holds. expected_response = [[0], []] @classmethod def validate(cls, response, msg): pass # if msg != cls.expected_response: # raise ValidationException("response != %r" % cls.expected_response) @staticmethod def dynamic_data(song_ids): return json.dumps([["", 1], [song_ids]]) class UploadImage(WcCall): """Upload an image for use as album art.""" static_method = 'POST' static_url = service_url + 'imageupload' static_params = {'zx': '', # ?? 'u': 0} _res_schema = { 'type': 'object', 'properties': { 'imageUrl': {'type': 'string', 'blank': False}, 'imageDisplayUrl': {'type': 'string', 'blank': False}, }, 'additionalProperties': False } @staticmethod def dynamic_files(image_filepath): """ :param image_filepath: path to an image """ with open(image_filepath, 'rb') as f: contents = f.read() return {'albumArt': (image_filepath, contents)} class GetSettings(WcCall): """Get data that populates the settings tab: labs and devices.""" static_method = 'POST' static_url = service_url + 'fetchsettings' _device_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'deviceType': {'type': 'integer'}, 'id': {'type': 'string'}, 'lastAccessedFormatted': {'type': 'string'}, 'lastAccessedTimeMillis': {'type': 'integer'}, 'lastEventTimeMillis': {'type': 'integer'}, 'name': {'type': 'string', 'blank': True}, # only for type == 2 (android phone?): 'model': {'type': 'string', 'blank': True, 'required': False}, 'manufacturer': {'type': 'string', 'blank': True, 'required': False}, 'carrier': {'type': 'string', 'blank': True, 'required': False}, }, } _res_schema = { 'type': 'object', 'additionalProperties': False, 'properties': { 'settings': { 'type': 'object', 'additionalProperties': False, 'properties': { 'entitlementInfo': { 'type': 'object', 'additionalProperties': False, 'properties': { 'expirationMillis': {'type': 'integer', 'required': False}, 'isCanceled': {'type': 'boolean'}, 'isSubscription': {'type': 'boolean'}, 'isTrial': {'type': 'boolean'}, }}, 'lab': { 'type': 'array', 'items': { 'type': 'object', 'additionalProperties': False, 'properties': { 'description': {'type': 'string'}, 'enabled': {'type': 'boolean'}, 'displayName': {'type': 'string'}, 'experimentName': {'type': 'string'}, }, }}, 'maxUploadedTracks': {'type': 'integer'}, 'subscriptionNewsletter': {'type': 'boolean', 'required': False}, 'uploadDevice': { 'type': 'array', 'items': _device_schema, }}, } }, } @staticmethod def dynamic_data(session_id): """ :param: session_id """ return {'json': json.dumps({'sessionId': session_id})} class DeauthDevice(WcCall): """Deauthorize a device from GetSettings.""" static_method = 'POST' static_url = service_url + 'modifysettings' @staticmethod def dynamic_data(device_id, session_id): return {'json': json.dumps({'deauth': device_id, 'sessionId': session_id})} @classmethod def validate(cls, response, msg): if msg.text != '{}': raise ValidationException("expected an empty object; received %r" % msg.text) class GetSharedPlaylist(WcCall): """Get the contents and metadata for a shared playlist.""" static_method = 'POST' static_url = service_url + 'loadsharedplaylist' static_params = {'format': 'jsarray'} _res_schema = { 'type': 'array', } @classmethod def parse_response(cls, response): return cls._parse_json(jsarray.to_json(response.text)) @staticmethod def dynamic_data(session_id, share_token): return json.dumps([ [session_id, 1], [share_token] ]) gmusicapi-12.1.1/gmusicapi/session.py0000664000175000017500000002523513400371522020075 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- """ Sessions handle the details of authentication and transporting requests. """ from __future__ import print_function, division, absolute_import, unicode_literals from builtins import * # noqa from collections import namedtuple from contextlib import closing import json import gpsoauth import httplib2 # included with oauth2client import mechanicalsoup import oauth2client from oauth2client.client import OAuth2Credentials import requests from gmusicapi.exceptions import ( AlreadyLoggedIn, NotLoggedIn, CallFailure ) from gmusicapi.protocol import webclient from gmusicapi.utils import utils log = utils.DynamicClientLogger(__name__) OAuthInfo = namedtuple('OAuthInfo', 'client_id client_secret scope redirect_uri') def credentials_from_refresh_token(token, oauth_info): # why doesn't Google provide this!? cred_json = {"_module": "oauth2client.client", "token_expiry": "2000-01-01T00:13:37Z", # to refresh now "access_token": 'bogus', "token_uri": "https://accounts.google.com/o/oauth2/token", "invalid": False, "token_response": { "access_token": 'bogus', "token_type": "Bearer", "expires_in": 3600, "refresh_token": token}, "client_id": oauth_info.client_id, "id_token": None, "client_secret": oauth_info.client_secret, "revoke_uri": "https://accounts.google.com/o/oauth2/revoke", "_class": "OAuth2Credentials", "refresh_token": token, "user_agent": None} return OAuth2Credentials.new_from_json(json.dumps(cred_json)) class _Base(object): def __init__(self, rsession_setup=None): """ :param rsession_setup: a callable that will be called with the backing requests.Session to delegate config to callers. """ self._rsession = requests.Session() if rsession_setup is None: rsession_setup = lambda x: x # noqa self._rsession_setup = rsession_setup self._rsession_setup(self._rsession) self.is_authenticated = False def _send_with_auth(self, req_kwargs, desired_auth, rsession): raise NotImplementedError def _send_without_auth(self, req_kwargs, rsession): return rsession.request(**req_kwargs) def login(self, *args, **kwargs): # subclasses extend / use super() if self.is_authenticated: raise AlreadyLoggedIn def logout(self): """ Reset the session to an unauthenticated, default state. """ self._rsession.close() self._rsession = requests.Session() self._rsession_setup(self._rsession) self.is_authenticated = False def send(self, req_kwargs, desired_auth, rsession=None): """Send a request from a Call using this session's auth. :param req_kwargs: kwargs for requests.Session.request :param desired_auth: protocol.shared.AuthTypes to attach :param rsession: (optional) a requests.Session to use (default ``self._rsession`` - this is exposed for test purposes) """ res = None if not any(desired_auth): if rsession is None: # use a throwaway session to ensure it's clean with closing(requests.Session()) as new_session: self._rsession_setup(new_session) res = self._send_without_auth(req_kwargs, new_session) else: res = self._send_without_auth(req_kwargs, rsession) else: if not self.is_authenticated: raise NotLoggedIn if rsession is None: rsession = self._rsession res = self._send_with_auth(req_kwargs, desired_auth, rsession) return res class Webclient(_Base): def login(self, email, password, *args, **kwargs): """ Perform serviceloginauth then retrieve webclient cookies. :param email: :param password: """ super(Webclient, self).login() # Google's login form has a bunch of hidden fields I'd rather not deal with manually. browser = mechanicalsoup.Browser(soup_config={"features": "html.parser"}) login_page = browser.get('https://accounts.google.com/ServiceLoginAuth', params={'service': 'sj', 'continue': 'https://play.google.com/music/listen'}) form_candidates = login_page.soup.select("form") if len(form_candidates) > 1: log.error("Google login form dom has changed; there are %s candidate forms:\n%s", len(form_candidates), form_candidates) return False form = form_candidates[0] form.select("#Email")[0]['value'] = email response = browser.submit(form, 'https://accounts.google.com/AccountLoginInfo') try: response.raise_for_status() except requests.HTTPError: log.exception("submitting login form failed") return False form_candidates = response.soup.select("form") if len(form_candidates) > 1: log.error("Google login form dom has changed; there are %s candidate forms:\n%s", len(form_candidates), form_candidates) return False form = form_candidates[0] form.select("#Passwd")[0]['value'] = password response = browser.submit(form, 'https://accounts.google.com/ServiceLoginAuth') try: response.raise_for_status() except requests.HTTPError: log.exception("submitting login form failed") return False # We can't use in without .keys(), since international users will see a # CookieConflictError. if 'SID' not in list(browser.session.cookies.keys()): # Invalid auth. return False self._rsession.cookies.update(browser.session.cookies) self.is_authenticated = True # Get webclient cookies. # They're stored automatically by requests on the webclient session. try: webclient.Init.perform(self, True) except CallFailure: log.exception("unable to initialize webclient cookies") self.logout() return self.is_authenticated def _send_with_auth(self, req_kwargs, desired_auth, rsession): if desired_auth.xt: req_kwargs.setdefault('params', {}) req_kwargs['params'].update({'u': 0, 'xt': rsession.cookies['xt']}) return rsession.request(**req_kwargs) class Musicmanager(_Base): oauth = OAuthInfo( '652850857958.apps.googleusercontent.com', 'ji1rklciNp2bfsFJnEH_i6al', 'https://www.googleapis.com/auth/musicmanager', 'urn:ietf:wg:oauth:2.0:oob' ) def __init__(self, *args, **kwargs): super(Musicmanager, self).__init__(*args, **kwargs) self._oauth_creds = None def login(self, oauth_credentials, *args, **kwargs): """Store an already-acquired oauth2client.Credentials.""" super(Musicmanager, self).login() try: # refresh the token right away to check auth validity oauth_credentials.refresh(httplib2.Http()) except oauth2client.client.Error: log.exception("error when refreshing oauth credentials") if oauth_credentials.access_token_expired: log.info("could not refresh oauth credentials") return False self._oauth_creds = oauth_credentials self.is_authenticated = True return self.is_authenticated def _send_with_auth(self, req_kwargs, desired_auth, rsession): if desired_auth.oauth: if self._oauth_creds.access_token_expired: self._oauth_creds.refresh(httplib2.Http()) req_kwargs['headers'] = req_kwargs.get('headers', {}) req_kwargs['headers']['Authorization'] = \ 'Bearer ' + self._oauth_creds.access_token return rsession.request(**req_kwargs) class Mobileclient(Musicmanager): oauth = OAuthInfo( '228293309116.apps.googleusercontent.com', 'GL1YV0XMp0RlL7ylCV3ilFz-', 'https://www.googleapis.com/auth/skyjam', 'urn:ietf:wg:oauth:2.0:oob' ) def __init__(self, *args, **kwargs): super(Mobileclient, self).__init__(*args, **kwargs) self._master_token = None self._authtoken = None self._locale = None self._is_subscribed = None def gpsoauth_login(self, email, password, android_id, *args, **kwargs): """ Get a master token, then use it to get a skyjam OAuth token. :param email: :param password: :param android_id: """ # TODO calling directly into base is weird _Base.login(self, email, password, android_id, *args, **kwargs) res = gpsoauth.perform_master_login(email, password, android_id) if 'Token' not in res: return False self._master_token = res['Token'] res = gpsoauth.perform_oauth( email, self._master_token, android_id, service='sj', app='com.google.android.music', client_sig='38918a453d07199354f8b19af05ec6562ced5788') if 'Auth' not in res: return False self._authtoken = res['Auth'] self.is_authenticated = True return True def _send_with_auth(self, req_kwargs, desired_auth, rsession): # Default to English (United States) if no locale given. if not self._locale: self._locale = 'en_US' # Set locale for all Mobileclient calls. req_kwargs.setdefault('params', {}) req_kwargs['params'].update({'hl': self._locale}) # As of API v2.5, dv is a required parameter for all calls. # The dv value is part of the Android app version number, # but setting this to 0 works fine. req_kwargs['params'].update({'dv': 0}) if self._is_subscribed: req_kwargs['params'].update({'tier': 'aa'}) else: req_kwargs['params'].update({'tier': 'fr'}) req_kwargs.setdefault('headers', {}) if desired_auth.gpsoauth: # does this expire? req_kwargs['headers']['Authorization'] = \ 'GoogleLogin auth=' + self._authtoken return rsession.request(**req_kwargs) if desired_auth.oauth: return super(Mobileclient, self)._send_with_auth(req_kwargs, desired_auth, rsession) raise ValueError("_send_with_auth got invalid desired_auth: {}".format(desired_auth)) gmusicapi-12.1.1/gmusicapi/test/0000755000175000017500000000000013512455420017012 5ustar simonsimon00000000000000gmusicapi-12.1.1/gmusicapi/test/__init__.py0000600000175000017500000000003012667715127021121 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- gmusicapi-12.1.1/gmusicapi/test/audiotest_small.mp30000600000175000017500000000407112667715127022633 0ustar simonsimon00000000000000ID3TIT2 quick.mp3TPE1gmusicapi test artistTALBalbumTSSE Lavf53.21.1ÿóÄXøGÿœÿ9»å% EÒåÿóÄÀ< @óÿ«õq>'ðÿóÄ(j¼Šú~#Ê÷§÷EG±ÿóÄð–·-xDì8éOòê,@ÿóĨ AË mLþÿE ÈÉ9c*1ìùÿóÄ(ðÄÈ?vÝ㬿ÿõUÿóÄ)0 Ș'œ¦›‘ÿûzÕ+ÿóÄ-¸ÌÀŦB@¢9± Uj+…ÿóÄ3à ÐÀ˜wC˜J¦TsZê<€Î¶ÿóÄ4Ý€€2f·úê'FaèêÿóÄ4Ð Ä ß³ÿï×7ÀËì.ŠÿóÄ5ØÀÈŒÙÈ*'ÂúoMFÿóÄ6ÀÌÀÀ€,èjª)ÀÏ£³Ó¡³5<ÿóÄ9P ØÀˆ=d<<¹ø[äB•  ÿóÄ<ø ÐȘ€  ÁAGqLAME3.9ÿóÄ=È ÌÀ˜8.4UUUUUUUUUUÿóÄ> Ä UUUUUUUUUUUUUÿóÄ>ØÀˆUUUUUUUUUUUUUÿóÄB@ à xBUUUUUUUUUUUUUÿóÄF°Ì`†UUUUUUUUUUUUUgmusicapi-12.1.1/gmusicapi/test/fetchartist.jsarray0000600000175000017500000017713112667715127022746 0ustar simonsimon00000000000000[[0] ,[[[,"Apoecs6off3y6k4h5nvqqos4b5e","Amorphis",0,[[,"Circle","Amorphis","http://lh6.ggpht.com/55Ea0FcQgrnPcw4h6kwtgEKbOS-dCyGBpk8mYBWe_pgj7dEjZc3yFR_bwVR0PYWk-f1Bqi2i",0,0,[] ,"Bfr2onjv7g7tm4rzosewnnwxxyy",[] ,2013,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"The Beginning Of Times","Amorphis","http://lh3.ggpht.com/LgN08osfWUWN6S-IAOa99h4Qr1Wj21pcHp7r0AnErxK1HuJurqklV0Ycgk2JxB1Pu8IpJleKlQ",0,0,[] ,"Bet2e22dinehb7qiksqirljvcnq",[] ,2011,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Magic And Mayhem - Tales From The Early Years","Amorphis","http://lh4.ggpht.com/epPwDFc2GCsvvaBSf_LPgu5bCh0qXpJxi1DSU5gUGhwJZ5moYPtcPRAY3yww1YTEzM1g_erBuQ",0,0,[] ,"B7dplgr5h2jzzkcyrwhifgwl2v4",[] ,2010,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Forging The Land Of Thousand Lakes","Amorphis","http://lh5.ggpht.com/RrzvFnn-8w2qwVMbc-5Tv1vIKOzPFUvIu5MfWrUnYvH0WuafnxhLLxYBkCa_wnCrf2Bj80TS7g",0,0,[] ,"Bawp2fb6emntvumfrqzrd5p3rai",[] ,2010,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Skyforger","Amorphis","http://lh4.ggpht.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44",0,0,[] ,"B5nc22xlcmdwi3zn5htkohstg44",[] ,2009,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Silver Bride","Amorphis","http://lh4.ggpht.com/ZzdZ-VPXMnCI8j-H7vdB67Fs_qkjlknrUMn41AWIu6fTNs7NrGgvbark4JctJdV17mon1UvmEA",0,0,[] ,"Bx4otoi4ljssgfxkkgay52kbgma",[] ,2009,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Silent Waters","Amorphis","http://lh3.ggpht.com/KKida9Y4-ngxEIgyHe467fxT_BsDirfoPoCkp9PDFTkZfMcBu8XqUktOyOzMusZP7KCq_dEleQ",0,0,[] ,"B2qyakjo4xjp4oqobfkebtu6v74",[] ,2007,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Silent Waters","Amorphis","http://lh4.ggpht.com/DyMbom0aNDKKeeD9JybMk8vKnAFAQeJ16B5MdaespaJE-FDfPsXSyXlxmLew2n6WTZhfSB8Q",0,0,[] ,"Binwkon2zzj4rgbvxqm3xoej6t4",[] ,2007,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Eclipse","Amorphis","http://lh5.ggpht.com/-vLUmfh_fqSJ_Xk6BenBs87OrPuUl5--_NhoJsX7SypPmGQZVABi3szLY3uvtFRCQBuguc4k",0,0,[] ,"Bvpiqruq3mw7a6kmdapua2ejafq",[] ,2006,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"House of sleep","Amorphis","http://lh5.ggpht.com/KlmvCY1TwIhbXBUVQJeozs3ptQiFVLETyNNBbDJhEnYxbJR_ens07tzmXQfgZSrbXPqAlvk7Fxk",0,0,[] ,"Bvx2opb3mvxrxfykz5iqb2v7kde",[] ,2006,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Far From The Sun","Amorphis","http://lh3.ggpht.com/oyBKVKnvr1YOGV3Xqp-TTsqWJnhIX5M_8GclclVtkPMDNr5gJ1dV7au33JSsl8snepqsgDJyBw",0,0,[] ,"B7n3fftcxef7a2qpvtujmz62mya",[] ,2004,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Chapters","Amorphis","http://lh5.ggpht.com/mIs0nP6OkfEoMyziAJzrVEjrpVOFZ5OjItAToXfWQGu1ZbCcVDOhtHL-bHK8KXXRKEZ42fgc",0,0,[] ,"Bhs6pazgseku7l6x6cpavrbhx2q",[] ,2003,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"AM Universum","Amorphis","http://lh5.ggpht.com/jIshGkD2br3vsw9qK-ppNyrSxGpvZ15GbkLTLptAlocJ2qUgKta0qVyIEWKH4Rht6u15C_A5AQ",0,0,[] ,"B5s2cinoy6roo67mmc3gcao4sfq",[] ,2001,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Tuonela","Amorphis","http://lh5.ggpht.com/k0EZxB3AaOuhcvE1D2a41qnwrt55KFh4UdouvIjYICsJCgnr85nYb-cQd270n5_7i5c9ILuvXQ",0,0,[] ,"Bd4pz2likmhw3pxjbkreij5magu",[] ,1999,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"My Kantele","Amorphis","http://lh6.ggpht.com/7t-KGIVGopoFtbIuRPH3N_I-R_N_mW1xJHxBAWsx7VMIwgy_KBcZEpeUcNT4Ccmi6nSUy4ZR",0,0,[] ,"Bb4frlhpnn4wr4hgdwfj5frm7e4",[] ,1997,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Elegy","Amorphis","http://lh3.ggpht.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c",0,0,[] ,"Bqzxfykbqcqmjjtdom7ukegaf2u",[] ,1996,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Tales From The Thousand Lakes","Amorphis","http://lh6.ggpht.com/WSNzZmI7vNC_e1npacVLljrFNR5lcjyJ9OFTqaC_N8FuVXcKPTD8qITcy-HjPDzD-bDkou0Hxg",0,0,[] ,"B2cnpu7ako4kejm6dvebsudhjti",[] ,1994,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Karelian Isthmus","Amorphis","http://lh3.ggpht.com/P09FP2voKIAo1sXzGz10w0AGq_FCtV5sa8FaNv42A0DF0u6nvYPphScJnDt2MZWpnFzU3B11",0,0,[] ,"Bx5ns5fpgkmdkzmj3eqqvg6z37m",[] ,1992,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"The Wanderer","Amorphis","http://lh5.ggpht.com/U7V_m1qPPSaPST4oL4VV8VvaqfRecplRo9OXEH2BoiA9HatKx5O41WCrHDzWnJ8YQUZPs95B",0,0,[] ,"Bdedbfio7pchh6gs63jgsql7tqe",[] ,2013,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Hopeless Days","Amorphis","http://lh3.ggpht.com/9OpHFTBMaOp8cSq_j3tE6eoLBWE5NhbpD17famj6gUu078W13Meu3CHnDiKOp-gyqCTh054xnw",0,0,[] ,"B5qlu3wbhs6jvkb2hgapy4ct5wy",[] ,2013,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"You I Need","Amorphis","http://lh6.ggpht.com/_3GbKtqdwPBKzNSq8n44xMsiqSj4g6UBCinziiuf7wiWKkJFMxMduGCojT5cN3-U6Ziwtzn0Dg",0,0,[] ,"Bovu7xmzywthocqi6kfp5huza6u",[] ,2011,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ] ,"http://lh6.ggpht.com/lXBCm8k7pS3Y99swHQdeq7qRx9XdHMnMilOmH_vtfysihVWq4PY-9XvYnD6RzmC5o5L3jYNUovg",,,[] ,[[,"Aheqc7kveljtq7rptd7cy5gvk2q","Dark Tranquillity",0,[] ,"http://lh5.ggpht.com/JvBvykusJaCAWDEBoliHHYLfohWqUnPVeskekmd28y-nVBdFc-1Bn0rHjahM9acHGxfAGT66WiI",,,[] ,[] ,,[] ] ,[,"Aohi7l6o2tmtcrwo5bluketlluu","Sentenced",0,[] ,"http://lh5.ggpht.com/WK7Oh8H05tcbtXwvHPxXZHHzws7rrfgXvPHVVx7iqmhybPFtDRf04enMfKoblVUg6XM7lW3yLg",,,[] ,[] ,,[] ] ,[,"Ax5w2efqghbbg52sglou7ad6mgq","Katatonia",0,[] ,"http://lh3.ggpht.com/Jc7TWHx0jwDZ5pY4-opo6EW4xFGjNp2z1Z3V_YGNkRHNK_NOHVcFGPFi83tFrhuDqWEdP4YU",,,[] ,[] ,,[] ] ,[,"Atxwrjty5yhy5voe25fpspjdsju","Dimmu Borgir",0,[] ,"http://lh4.ggpht.com/EjRsgRLotoEM6MLORBr-b6WymjxBjsBN1kyjKOdiXd-3EZep7clMQpTWd3uNHpx0wL4LNkoN",,,[] ,[] ,,[] ] ,[,"A4dusjgwqx4a6ewcjvnzzqvfzhq","Nevermore",0,[] ,"http://lh3.ggpht.com/O5CTTK4yya-B179xluPIFairTN1h_wa_7j8FxRm_niZTZ733lyQQ0xXs8vrLgrDGmB2wCSi8VQ",,,[] ,[] ,,[] ] ,[,"Atwhxsffpwutebtvcgskq4yvtta","Borknagar",0,[] ,"http://lh6.ggpht.com/786Ro0cCB3Kls-3Ct14Bh7uP7qewQs77YUqAP0BggPAqWZ0NxfQ9KesiP8Py7lwqo5qKbcuHgQ",,,[] ,[] ,,[] ] ,[,"A3awutw5sf2gyupyr734fxjrtbi","Swallow The Sun",0,[] ,"http://lh3.ggpht.com/UmQRlCjk_lnUx4qURKWoF9VpoIYLzlo7NwD_Wuo43TPD-HNHpOhHSKGFnIuAQcPxwwMCmFCX",,,[] ,[] ,,[] ] ,[,"Agbaaykil3tvfgchhoccsneeevu","Paradise Lost",0,[] ,"http://lh3.ggpht.com/C1T4V1esYevmFdnkfeftMIyjM9bjBs1oxuAGU0IbOpuU3RMi4t0Cuz8a6uqVm7eetwkfVD08flw",,,[] ,[] ,,[] ] ,[,"Aw3riskko57adlcx4io3cdrlu2e","Hypocrisy",0,[] ,"http://lh4.ggpht.com/8SPrCPxmJfX7Zl97OuoBk5EeS5ERW6UzL4Rh7Z2FceLx8sEfFr-qMLB-Uenwt21mORl4WHgf",,,[] ,[] ,,[] ] ,[,"Alwbi2wdkgyrsslnzsnnjlibk2q","Draconian",0,[] ,"http://lh4.ggpht.com/ACrQxSlHGRNl9OnR0_PqUhisAaKW_GqfPgEYMetbwQS_Cbg6l980tsiPGsCediou3x-cLPY4oQ",,,[] ,[] ,,[] ] ,[,"A3u6ufaugmiyl5wkif3zl6xwslm","Therion",0,[] ,"http://lh5.ggpht.com/VDmJk5Y631wFwt6bZu0ZYxvVEqlhZfTmC0X-0f_OpfLVhQwPKOWvLr1Oh8pp6Vtal6qYs3wIjA",,,[] ,[] ,,[] ] ,[,"Akzz73amggal65dufr7thhp3mri","Samael",0,[] ,"http://lh3.ggpht.com/kAc2e0EGkZwqsUimoK3K7ndn-5qh5o6QKOThO0iOd89hkCWU5fkIFVnoGqpsqNfvEqgHZ9nWqQ",,,[] ,[] ,,[] ] ,[,"Aiduslnzrfk6vr5amytx6jsdvhm","Ensiferum",0,[] ,"http://lh6.ggpht.com/-elzvxP_5NQkkS18pyd5kicC_6B2outJ3J9-06rBrwjpKRryeb3l76r-88iJ9RRE3DAcCzHmAw",,,[] ,[] ,,[] ] ,[,"A7zqo5g4bwghnhy5sfwlz6dw73m","Moonspell",0,[] ,"http://lh5.ggpht.com/iwIQlFc7ardArrzaTpLWuzD0YLwS8owqFjuMLgpxbeYeMLsGckNg0benNsddH3BfyZ9SLlQ",,,[] ,[] ,,[] ] ,[,"Aas2td72vfmamoeyjx5slcakcaq","Eternal Tears of Sorrow",0,[] ,"http://lh5.ggpht.com/HLxCBQBGyTd4ckm6WyFlZA74FWEph9-DeORnGf8wd9o-0qf9Tt5_FelM9bdyn-tHEsRtLfR0",,,[] ,[] ,,[] ] ,[,"Acw3vmnsiey6jlfmxpmj5q4zgau","Norther",0,[] ,"http://lh5.ggpht.com/odr5lrzP4A3RCDg_KudIsXjUr4tNUynXSOlDJ6WT2bsBBg3dQtpfyzzyqy2e-qS5fLqiDtWleK8",,,[] ,[] ,,[] ] ,[,"A5z3otl4atk3qxod5lpt72fsyxm","Finntroll",0,[] ,"http://lh6.ggpht.com/T2e3fVAO21CTYqbKtmbECF1XQ-R2AGMj4YUFrBzR_84VYOjsN8YyOgWOEPNEVmB5GjRstBLGQ0w",,,[] ,[] ,,[] ] ,[,"Aoyvxco6sq3kkxoriqvx6hwzrzq","Before The Dawn",0,[] ,"http://lh3.ggpht.com/AOciEXVtam5FgbuU6pjqTAgkDfXxPcrcL8l9Df0llpUGAj0T8ivJqBGTlLHpxIlFBl-apw_5yg",,,[] ,[] ,,[] ] ,[,"A2useeid7l335g6swptdcftu52i","Moonsorrow",0,[] ,"http://lh6.ggpht.com/-DdbiHFChkYLQoRBygcO7FuvANM4fOGi6qHf0n0Qc_z0_bjbQu7bH5yLS-DanyQLyCSlL8c4yMw",,,[] ,[] ,,[] ] ,[,"A3xkmyzd5nqxsf23elieww54g2q","My Dying Bride",0,[] ,"http://lh5.ggpht.com/1qMze-nn9B93AzkPW3uvY1CZ5NEYFL9yldUKAXgl3mGz0TiP1yhvGsFwtqAf-d5li_HgNcFmSQ",,,[] ,[] ,,[] ] ] ,"Lead guitarist Esa Holopainen and drummer Jan Rechberger formed the band Amorphis in Finland in 1990. To complete their lineup they recruited vocalist/guitarist Tomi Koivusaari and bassist Olli-Pekka Laine. They released a demo in 1991 called Disment of Soul. The demo was so successful in getting attention for the group that they had acquired a record deal and released an EP the same year. They next released their first full-length album, The Karelian Isthmus, in 1992. The following year, their early demo recording session was released as Privilege of Evil. 1994 saw the group sending out their next CD, Tales from the Thousand Lakes. In 1994, Kim Rantala joined the band on keyboards, being integrated as a member of a band rather than the side musician he was replacing. An EP entitled Black Winter Day was released the next year. Their next album was Elegy. This disc showcased a new six-member lineup, completed by Pasi Koskinen as their second vocalist. The lineup at this point also included new drummer Pekka Kasari. In 1997 they released My Kantele. In 1998, Rantala left the group, being replaced by Santeri Kallio. The group released Tuonela with that lineup in 1999. The following year, Niclas Etelävuori replaced Laine on bass. In 2001 the band went in a more experimental direction, including saxophones and a greater focus on keyboards for Am Universum. Jan Rechberger rejoined the band in 2003 just in time to record Amorphis' sixth studio album, Far from the Sun. Koskinen left the band in 2004 to focus on his family and was replaced by Tomi Joutsen. For their next project, Amorphis began work on a series of concept albums based on the famous Finnish epic poem \"The Kalevala\", releasing Eclipse in 2006, Silent Waters in 2007, and Skyforger in 2009. In 2010 the band decided to revisit some of its older work, recording new arrangements of earlier material for Magic \u0026 Mayhem: Tales from the Early Years.\n\nGary Hill, Rovi",[["Tn2ugrgkeinrrb2a4ji7khungoy","Silver Bride","//lh4.googleusercontent.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44","Amorphis","Skyforger","Amorphis","silver bride","amorphis","skyforger","amorphis",,,,253000,2,,1,,,,,,1,-1,0,0,,"Tn2ugrgkeinrrb2a4ji7khungoy","Tn2ugrgkeinrrb2a4ji7khungoy",5,,,"B5nc22xlcmdwi3zn5htkohstg44","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoYgskT9A3z2ndkfnBATmcxgu_tLX1MaR-kPksoRQFob0OrmhJo-zj7q8AqkqXu0n4awlOfYsRitnKY9624k3n-ZF_0b4bG-gTHNkq7t7yxwQ593dk\u003d"] ,["T4j5jxodzredqklxxhncsua5oba","Black Winter Day","//lh4.googleusercontent.com/epPwDFc2GCsvvaBSf_LPgu5bCh0qXpJxi1DSU5gUGhwJZ5moYPtcPRAY3yww1YTEzM1g_erBuQ","Amorphis","Magic And Mayhem - Tales From The Early Years","Amorphis","black winter day","amorphis","magic and mayhem - tales from the early years","amorphis",,,,235000,4,,1,,2010,,,,0,-1,0,0,,"T4j5jxodzredqklxxhncsua5oba","T4j5jxodzredqklxxhncsua5oba",5,,,"B7dplgr5h2jzzkcyrwhifgwl2v4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrk1frCiN36JHCfquXUS7MSV4ayRBDett4pfqUORvE8bNR3Ne-O0g1bs7Gi-_HESue_NwiuG02q5cIv7ezAxcWVpb3w6u4IX7ltHYijEB8Mxdi6GlU\u003d"] ,["Tid7h5ve4jislxqofzkc5as5fbe","House Of Sleep","//lh5.googleusercontent.com/9QVck4kPpVKU-JlyUo2-n1LFuCKi_hc9geeDRse0dPpqCru1AVq5Zu-Awp5TUR0X1QBlqLk2","Amorphis","Nuclear Blast Showdown 2006","Various Artists","house of sleep","amorphis","nuclear blast showdown 2006","various artists",,,,256000,2,,1,,,,,,0,-1,0,0,,"Tid7h5ve4jislxqofzkc5as5fbe","Tid7h5ve4jislxqofzkc5as5fbe",5,,,"Bcqvnirijpn6edaw3d47vxdl22y","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpkk3wkYuLNJqhYRFHhlsb7ykqAvggz2Dw9S8-bpB5-XyH7gF-_BqYDvfOwsrHmV7N1Hj7xiYD1WV6bsIWVE3ofVHFH42Sdhh_STSrCYdWC30Cfl6g\u003d"] ,["T4ggbo3d5rn6vpjk4gymj24brvi","Silent Waters","//lh3.googleusercontent.com/KKida9Y4-ngxEIgyHe467fxT_BsDirfoPoCkp9PDFTkZfMcBu8XqUktOyOzMusZP7KCq_dEleQ","Amorphis","Silent Waters","Amorphis","silent waters","amorphis","silent waters","amorphis",,,,292000,3,,1,,,,,,0,-1,0,0,,"T4ggbo3d5rn6vpjk4gymj24brvi","T4ggbo3d5rn6vpjk4gymj24brvi",5,,,"B2qyakjo4xjp4oqobfkebtu6v74","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKr62wpQ8eibrYttO5XdaT5nunXmzj64PZqcI4eAVjb7X2zQQaeIGl55CD5BESTddvnQKpWeDLdr_TcHbdSqd6oupnrX3IxC-aZzLmpf4WF6dZccZmo\u003d"] ,["Tgofa4xs6iwnjan5bnoxzmzgyhy","Black Winter's Day","//lh3.googleusercontent.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c","Amorphis","Elegy","Amorphis","black winter's day","amorphis","elegy","amorphis",,,,218000,15,,1,,,,,,0,-1,0,0,,"Tgofa4xs6iwnjan5bnoxzmzgyhy","Tgofa4xs6iwnjan5bnoxzmzgyhy",5,,,"Bqzxfykbqcqmjjtdom7ukegaf2u","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKplKQ1YyQZGpK9-joiQ_X04Yp__LTWOUORLfjgzaqflMrO-1L5OPDttMUgZTzG8SQhrrpbKXhZddc_6wN2XbAQEIk5hIb6zWpJmQJdc-qCiNSgkzUY\u003d"] ,["Tcdvc6lncrz2ftzqicfoahcm4da","You I Need","//lh3.googleusercontent.com/LgN08osfWUWN6S-IAOa99h4Qr1Wj21pcHp7r0AnErxK1HuJurqklV0Ycgk2JxB1Pu8IpJleKlQ","Amorphis","The Beginning Of Times","Amorphis","you i need","amorphis","the beginning of times","amorphis",,,,262000,4,,1,,,,,,0,-1,0,0,,"Tcdvc6lncrz2ftzqicfoahcm4da","Tcdvc6lncrz2ftzqicfoahcm4da",5,,,"Bet2e22dinehb7qiksqirljvcnq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqn4g_RqAsbn8uQ-lg4czK4gii_TTwRELMCiVFf2j6WQrZmzXo1-Iv2GTlC6aG2QrWhB67pV1FVvPjx8SaJw5b4JNecxkpKfmPrIXxCB6mKMJ9_q7c\u003d"] ,["Tu4kvc6wwmnekvsnmfg7h635yce","Alone","//lh5.googleusercontent.com/jIshGkD2br3vsw9qK-ppNyrSxGpvZ15GbkLTLptAlocJ2qUgKta0qVyIEWKH4Rht6u15C_A5AQ","Amorphis","AM Universum","Amorphis","alone","amorphis","am universum","amorphis",,,,378000,1,,1,,2001,,,,0,-1,0,0,,"Tu4kvc6wwmnekvsnmfg7h635yce","Tu4kvc6wwmnekvsnmfg7h635yce",5,,,"B5s2cinoy6roo67mmc3gcao4sfq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpTagHwHSklgHIUkz7-SMqqv8d_sr1KbQXw8p6_rvktIa8ObXh90uz7fJYIM1b7Hi0k7C4vckQ7TtQl7a5XdJHtyK9D1GE1SKw2JSC0nrdDb1k-nBM\u003d"] ,["Tygvooab57vculuyvk25tkcsjh4","Sky Is Mine","//lh4.googleusercontent.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44","Amorphis","Skyforger","Amorphis","sky is mine","amorphis","skyforger","amorphis",,,,260000,4,,1,,,,,,0,-1,0,0,,"Tygvooab57vculuyvk25tkcsjh4","Tygvooab57vculuyvk25tkcsjh4",5,,,"B5nc22xlcmdwi3zn5htkohstg44","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKr6ul_sKW-O241dFmr1ucZk85tkGriRHqoYAgM_n4m_7o70LHyR_VkIKxDY_yKgTcM0LTvLI2t8tPoJ3a6QGPVrSrFZMkWv6E45LFo4_Xg3Eg2Fa9U\u003d"] ,["Tkpqzhrdmosh4odfv2v6yzj6twe","My Kantele","//lh3.googleusercontent.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c","Amorphis","Elegy","Amorphis","my kantele","amorphis","elegy","amorphis",,,,355000,11,,1,,,,,,0,-1,0,0,,"Tkpqzhrdmosh4odfv2v6yzj6twe","Tkpqzhrdmosh4odfv2v6yzj6twe",5,,,"Bqzxfykbqcqmjjtdom7ukegaf2u","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpNdn-BjBJbVgv0bfDgfCFWgkBtdkDA-MJ-krLfiz4QRS8mty6h0JNhjUj9xPdfE2SQPfy6ujxEyH9EMCdiTCL-GDhPwFL4t0JPie_p7zbEAAopUI8\u003d"] ,["Tozeep2kvaauk2apztjppg3s6uq","Mermaid","//lh3.googleusercontent.com/LgN08osfWUWN6S-IAOa99h4Qr1Wj21pcHp7r0AnErxK1HuJurqklV0Ycgk2JxB1Pu8IpJleKlQ","Amorphis","The Beginning Of Times","Amorphis","mermaid","amorphis","the beginning of times","amorphis",,,,264000,2,,1,,,,,,0,-1,0,0,,"Tozeep2kvaauk2apztjppg3s6uq","Tozeep2kvaauk2apztjppg3s6uq",5,,,"Bet2e22dinehb7qiksqirljvcnq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrilYKoRz6jtDAa6ZvE7MM8-pYKKIecNM5R_DD5Ug-t6VeoldDKfjSIb2uE3sJmi13cN2slIqBVdRy3WXbgf6YDZutd32tHmglkbGTzsA7Le1gIL8g\u003d"] ,["T4ugyccbgxdbglt4c3fqe4jckxe","The Smoke","//lh5.googleusercontent.com/-vLUmfh_fqSJ_Xk6BenBs87OrPuUl5--_NhoJsX7SypPmGQZVABi3szLY3uvtFRCQBuguc4k","Amorphis","Eclipse","Amorphis","the smoke","amorphis","eclipse","amorphis",,,,218000,7,,1,,,,,,0,-1,0,0,,"T4ugyccbgxdbglt4c3fqe4jckxe","T4ugyccbgxdbglt4c3fqe4jckxe",5,,,"Bvpiqruq3mw7a6kmdapua2ejafq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpwH2zfItWMUn4r3g0hA7cei8Ohm0McJinXox4m97cKHsNfQvmguEVQZV8nTxl0sUvUti9RHNrId8Uxd3i-_NS7T1stPw_ScR2n2fhIPdZtiIrZHlI\u003d"] ,["Tchc3ra7dxolvvqjodib73hdawy","Skyforger","//lh4.googleusercontent.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44","Amorphis","Skyforger","Amorphis","skyforger","amorphis","skyforger","amorphis",,,,315000,8,,1,,,,,,0,-1,0,0,,"Tchc3ra7dxolvvqjodib73hdawy","Tchc3ra7dxolvvqjodib73hdawy",5,,,"B5nc22xlcmdwi3zn5htkohstg44","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqXKkRHUsnrx0wHa7t1qTPxVQDSXb7cKk3JqVvPBcrw8fY3KojuNafkC1JLPplcEIdTQ2wBCZRUgpDWt5Ki3m7LhTv6FNjhIemUKR-AMemvTNCDHJc\u003d"] ,["T2opnfk3yfi7suppu2dhn53y42m","Magic And Mayhem","//lh4.googleusercontent.com/epPwDFc2GCsvvaBSf_LPgu5bCh0qXpJxi1DSU5gUGhwJZ5moYPtcPRAY3yww1YTEzM1g_erBuQ","Amorphis","Magic And Mayhem - Tales From The Early Years","Amorphis","magic and mayhem","amorphis","magic and mayhem - tales from the early years","amorphis",,,,324000,1,,1,,2010,,,,0,-1,0,0,,"T2opnfk3yfi7suppu2dhn53y42m","T2opnfk3yfi7suppu2dhn53y42m",5,,,"B7dplgr5h2jzzkcyrwhifgwl2v4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpUvzp9AoBKT594ac807rxH-eG6Zjmsl9TdjvEA1cUG9KN_s8ah4EC_83PRrT-mZeCSF8ETTsp0t-bRzW9NUontUsKFWUyBtrk6S6-IBEwCDJQ8vHE\u003d"] ,["Tnpwezddt35tr3lwgvodru2754u","Elegy","//lh3.googleusercontent.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c","Amorphis","Elegy","Amorphis","elegy","amorphis","elegy","amorphis",,,,441000,9,,1,,,,,,0,-1,0,0,,"Tnpwezddt35tr3lwgvodru2754u","Tnpwezddt35tr3lwgvodru2754u",5,,,"Bqzxfykbqcqmjjtdom7ukegaf2u","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqYDcMNrJpbWTp9dRw22Ut2bvMB844POz9oKEm4AXwdQbLqz5hoRe5tbuCzUEiE1KePilUsXX3cCmHhi0WPBog-KFxKh7zc7fdnheFbeSw1d5gMvzc\u003d"] ,["Tp6csoq5bzpwnhquvcx3vvqwwcq","My Kantele (Acoustic Reprise)","//lh6.googleusercontent.com/7t-KGIVGopoFtbIuRPH3N_I-R_N_mW1xJHxBAWsx7VMIwgy_KBcZEpeUcNT4Ccmi6nSUy4ZR","Amorphis","My Kantele","Amorphis","my kantele (acoustic reprise)","amorphis","my kantele","amorphis",,,,357000,1,,1,,1997,,,,0,-1,0,0,,"Tp6csoq5bzpwnhquvcx3vvqwwcq","Tp6csoq5bzpwnhquvcx3vvqwwcq",5,,,"Bb4frlhpnn4wr4hgdwfj5frm7e4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKptArGH2dF4jOc5KLY9NFz6EcKfiMVC2e9eY6k4HpLaFi7y3noSQZYgMPcyePSOEvBIDLfTB6RNyEeJeGt8N5MtK1SyZGGN7jUbUGhQof0xb7fKvUA\u003d"] ,["T4vwr6hyner5klj4x4pxgwi7o2q","Battle For Light","//lh3.googleusercontent.com/LgN08osfWUWN6S-IAOa99h4Qr1Wj21pcHp7r0AnErxK1HuJurqklV0Ycgk2JxB1Pu8IpJleKlQ","Amorphis","The Beginning Of Times","Amorphis","battle for light","amorphis","the beginning of times","amorphis",,,,335000,1,,1,,,,,,0,-1,0,0,,"T4vwr6hyner5klj4x4pxgwi7o2q","T4vwr6hyner5klj4x4pxgwi7o2q",5,,,"Bet2e22dinehb7qiksqirljvcnq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpH0gZw-Aov9FW-MMpBN8-xNS2u6NSJnHympf-bjszVtlT0WU3TedgUNNx1ddpv8ANTwlHaSYS8I5D1TbFY9-HpmPTgt-8Mcei0YKnOtWzmlHQp94A\u003d"] ,["Tvlqf3zasxnnzag4mf7x4nntmz4","Tuonela","//lh5.googleusercontent.com/k0EZxB3AaOuhcvE1D2a41qnwrt55KFh4UdouvIjYICsJCgnr85nYb-cQd270n5_7i5c9ILuvXQ","Amorphis","Tuonela","Amorphis","tuonela","amorphis","tuonela","amorphis",,,,272000,4,,1,,1999,,,,0,-1,0,0,,"Tvlqf3zasxnnzag4mf7x4nntmz4","Tvlqf3zasxnnzag4mf7x4nntmz4",5,,,"Bd4pz2likmhw3pxjbkreij5magu","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpaPZF0rtbtqVdtKVlF3AsrIqYrPd75Enf1akiRcfGXXAfo5--KrupyiGkrxqJdP7XCFIktkajizmcFma2YOWKCX9qtSDpbaaPllTz915idJi9ZW90\u003d"] ,["Tolmpssrmdg2vzix5a4nr4lmcny","Divinity","//lh5.googleusercontent.com/k0EZxB3AaOuhcvE1D2a41qnwrt55KFh4UdouvIjYICsJCgnr85nYb-cQd270n5_7i5c9ILuvXQ","Amorphis","Tuonela","Amorphis","divinity","amorphis","tuonela","amorphis",,,,296000,6,,1,,1999,,,,0,-1,0,0,,"Tolmpssrmdg2vzix5a4nr4lmcny","Tolmpssrmdg2vzix5a4nr4lmcny",5,,,"Bd4pz2likmhw3pxjbkreij5magu","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoduS_OObBvQ5-iYZEfCu_Kbpsb_9uO_Ar9vJ_xjkFgkkQb-Qo3Q0M98j3APqUsrs8g-79vP5Hx6miG7qZjis5RKmIKSOjWcRq-J1nJOfYQnG6IOnA\u003d"] ,["Tnefj6mswd6twzfmze2smhrfu4y","Into Hiding","//lh4.googleusercontent.com/epPwDFc2GCsvvaBSf_LPgu5bCh0qXpJxi1DSU5gUGhwJZ5moYPtcPRAY3yww1YTEzM1g_erBuQ","Amorphis","Magic And Mayhem - Tales From The Early Years","Amorphis","into hiding","amorphis","magic and mayhem - tales from the early years","amorphis",,,,233000,3,,1,,2010,,,,0,-1,0,0,,"Tnefj6mswd6twzfmze2smhrfu4y","Tnefj6mswd6twzfmze2smhrfu4y",5,,,"B7dplgr5h2jzzkcyrwhifgwl2v4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKooYYzu-T01FlnIfv7-A0b2RUM7V79ICyj0qIzQcQdZHXfSxllzhHYDGBZcyBhrQ1nZHC3TExwthoKEUqs-Vry9tIg8tJ_udXJQV4h_-E11qoTjbs4\u003d"] ,["Twszfk3si6mgcrfxvcgqb4gisw4","From The Heaven Of My Heart","//lh4.googleusercontent.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44","Amorphis","Skyforger","Amorphis","from the heaven of my heart","amorphis","skyforger","amorphis",,,,320000,3,,1,,,,,,0,-1,0,0,,"Twszfk3si6mgcrfxvcgqb4gisw4","Twszfk3si6mgcrfxvcgqb4gisw4",5,,,"B5nc22xlcmdwi3zn5htkohstg44","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoT7SA6dku8JnykA5Qs1IVV-Gz3qJnyuefDNzbmVvRkvPJ_-b9R0lFZPVAwlZAyZeUk472aVIbM6NnKApW2CKSvUbM5kVt-uovmH4Ed0EUezKZ1G9Y\u003d"] ,["Tixppb7vxxhcgz6e6e3q257sqma","Her Alone","//lh3.googleusercontent.com/KKida9Y4-ngxEIgyHe467fxT_BsDirfoPoCkp9PDFTkZfMcBu8XqUktOyOzMusZP7KCq_dEleQ","Amorphis","Silent Waters","Amorphis","her alone","amorphis","silent waters","amorphis",,,,363000,6,,1,,,,,,0,-1,0,0,,"Tixppb7vxxhcgz6e6e3q257sqma","Tixppb7vxxhcgz6e6e3q257sqma",5,,,"B2qyakjo4xjp4oqobfkebtu6v74","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoyusGpQIlpwcDV2wjMljfLgADkRkDr6I9w_e3RIPFMuOk2uDVjKO2F3gjQoqYJOfeTubvH0b6xLzVk0G0NH29DfLyInTwGeRyD9pZac7ucP-xeQUM\u003d"] ,["Tix7fqw2ti2tz7ak2komsldr7km","Beginning Of Time","//lh3.googleusercontent.com/LgN08osfWUWN6S-IAOa99h4Qr1Wj21pcHp7r0AnErxK1HuJurqklV0Ycgk2JxB1Pu8IpJleKlQ","Amorphis","The Beginning Of Times","Amorphis","beginning of time","amorphis","the beginning of times","amorphis",,,,350000,12,,1,,,,,,0,-1,0,0,,"Tix7fqw2ti2tz7ak2komsldr7km","Tix7fqw2ti2tz7ak2komsldr7km",5,,,"Bet2e22dinehb7qiksqirljvcnq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpom-3RenZ20HF9hpcZNCbdTIbaa7lx3eoIq-se0pepLiw4M8Je_dJKSoLLdXV89fS59C3VYi1noEOsbR7nJn9BbA7t9URfKXpSjTHRXvMPzVh3bvU\u003d"] ,["Tfk7k55csfm6dyvr2jfcxcc5xmy","Light My Fire","//lh4.googleusercontent.com/epPwDFc2GCsvvaBSf_LPgu5bCh0qXpJxi1DSU5gUGhwJZ5moYPtcPRAY3yww1YTEzM1g_erBuQ","Amorphis","Magic And Mayhem - Tales From The Early Years","Amorphis","light my fire","amorphis","magic and mayhem - tales from the early years","amorphis",,,,164000,13,,1,,2010,,,,0,-1,0,0,,"Tfk7k55csfm6dyvr2jfcxcc5xmy","Tfk7k55csfm6dyvr2jfcxcc5xmy",5,,,"B7dplgr5h2jzzkcyrwhifgwl2v4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqzWLgPIxA6AyJaEVgZb0kOv1wjHtdQJYoluzkXILFSbnb5li_XQWdJP6EEU_MzmnULNGQFP7HL4Yp6RsnINnZ-0wHoQX35M5sB_4zFXV62-Zkt-bs\u003d"] ,["Tzscfkz2nqjxkvnyge2k7axenny","My Enemy","//lh3.googleusercontent.com/LgN08osfWUWN6S-IAOa99h4Qr1Wj21pcHp7r0AnErxK1HuJurqklV0Ycgk2JxB1Pu8IpJleKlQ","Amorphis","The Beginning Of Times","Amorphis","my enemy","amorphis","the beginning of times","amorphis",,,,205000,3,,1,,,,,,0,-1,0,0,,"Tzscfkz2nqjxkvnyge2k7axenny","Tzscfkz2nqjxkvnyge2k7axenny",5,,,"Bet2e22dinehb7qiksqirljvcnq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrUrhdzvBMlqB00Gch8jvwp-hk3vXFQGXzIdw7SV_V_RJ6n1Ocp2Ta1sJyOafdCsaOXI5kExZNnPd5KSDSJQcd4fvd0DzVTvxfV6uvwnJYgtRP3Lxo\u003d"] ,["Tvmdvd42v3u2iwzwwy677egcqbi","Sampo","//lh4.googleusercontent.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44","Amorphis","Skyforger","Amorphis","sampo","amorphis","skyforger","amorphis",,,,368000,1,,1,,,,,,0,-1,0,0,,"Tvmdvd42v3u2iwzwwy677egcqbi","Tvmdvd42v3u2iwzwwy677egcqbi",5,,,"B5nc22xlcmdwi3zn5htkohstg44","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKo_EAanUU4I8lnaqtCtND8pCVCc7DnGWf9VPqKNIK4zJ00tDGLQtG7EL0mgta5cc-QIQ8Yk2cqh0v-585IaMAIESIxWh1IshgSN87NRFU19yXXDtog\u003d"] ,["Tctmawqku4g6x4ju2eilrkbjjim","Weaving The Incantation","//lh3.googleusercontent.com/KKida9Y4-ngxEIgyHe467fxT_BsDirfoPoCkp9PDFTkZfMcBu8XqUktOyOzMusZP7KCq_dEleQ","Amorphis","Silent Waters","Amorphis","weaving the incantation","amorphis","silent waters","amorphis",,,,299000,1,,1,,,,,,0,-1,0,0,,"Tctmawqku4g6x4ju2eilrkbjjim","Tctmawqku4g6x4ju2eilrkbjjim",5,,,"B2qyakjo4xjp4oqobfkebtu6v74","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpx3mjJ8tC5H8FC2kM_upAWhFaWhwTlsjiWq1oqHAJ13I729rAQr0kdk6QffCCixmglafM76Q8zS-lwIS94S4HbJT47gRvgDK77O9bE1Ma2xbIqfk8\u003d"] ,["Ti5n23ai5m2664pmyykiigs55q4","Shaman","//lh3.googleusercontent.com/KKida9Y4-ngxEIgyHe467fxT_BsDirfoPoCkp9PDFTkZfMcBu8XqUktOyOzMusZP7KCq_dEleQ","Amorphis","Silent Waters","Amorphis","shaman","amorphis","silent waters","amorphis",,,,297000,8,,1,,,,,,0,-1,0,0,,"Ti5n23ai5m2664pmyykiigs55q4","Ti5n23ai5m2664pmyykiigs55q4",5,,,"B2qyakjo4xjp4oqobfkebtu6v74","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpIkdlQ7EO9mV3JvSMqHEi0M2Ihsys-dpxgGHDMz5wyTdzEJ0E0371cdoRJ71sTPTshGlRj0h6qG1aHbQyHxpx58QTg_4ssjb62cB5LtA4KO-EzilU\u003d"] ,["Txg2wxm532tw33wny5hdmfyr4ga","Enigma","//lh3.googleusercontent.com/KKida9Y4-ngxEIgyHe467fxT_BsDirfoPoCkp9PDFTkZfMcBu8XqUktOyOzMusZP7KCq_dEleQ","Amorphis","Silent Waters","Amorphis","enigma","amorphis","silent waters","amorphis",,,,216000,7,,1,,,,,,0,-1,0,0,,"Txg2wxm532tw33wny5hdmfyr4ga","Txg2wxm532tw33wny5hdmfyr4ga",5,,,"B2qyakjo4xjp4oqobfkebtu6v74","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKr36dfAzo0iu-l6LzOQXRgxeKlBv5ey9OHsywX1a3YK_rqffrpEbfUg4ha7X0JeTK4-ZUeip6pBbpTOxMwl4Qaw8Ousvl0TlASeKX9pWSnozdkpqE0\u003d"] ,["Tyklsvrslqcuptowg46aikkxwdm","Thousand Lakes","//lh6.googleusercontent.com/WSNzZmI7vNC_e1npacVLljrFNR5lcjyJ9OFTqaC_N8FuVXcKPTD8qITcy-HjPDzD-bDkou0Hxg","Amorphis","Tales From The Thousand Lakes","Amorphis","thousand lakes","amorphis","tales from the thousand lakes","amorphis",,,,124000,1,,1,,,,,,0,-1,0,0,,"Tyklsvrslqcuptowg46aikkxwdm","Tyklsvrslqcuptowg46aikkxwdm",5,,,"B2cnpu7ako4kejm6dvebsudhjti","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKr-Ki7GGtXcH-cFIx5iWcIjyOdjU4E5XA8YjvaqnubqBKmtB2Zx3_87NqEc59_M9ZC0AjnxTZ9VpV_EIwkUPTLM3IKDNi1MqtgKz67uPvTDgnkQoNY\u003d"] ,["Txathaqvbavsrcgj3kxm75opydq","Three Words","//lh3.googleusercontent.com/LgN08osfWUWN6S-IAOa99h4Qr1Wj21pcHp7r0AnErxK1HuJurqklV0Ycgk2JxB1Pu8IpJleKlQ","Amorphis","The Beginning Of Times","Amorphis","three words","amorphis","the beginning of times","amorphis",,,,235000,6,,1,,,,,,0,-1,0,0,,"Txathaqvbavsrcgj3kxm75opydq","Txathaqvbavsrcgj3kxm75opydq",5,,,"Bet2e22dinehb7qiksqirljvcnq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoauIqxPzgMHDCORHebb4yzzPIfP0h2bSepd1dPYFKPOUK1yDcjrYi6XydNyflS2sRuERmuMHIiJjs8_Bx9Pz05T8PHkL9s6uIkY6maVCpvFdK8_Kk\u003d"] ,["Tmkrownnht4mvduqsqowyb7e6ym","The White Swan","//lh3.googleusercontent.com/KKida9Y4-ngxEIgyHe467fxT_BsDirfoPoCkp9PDFTkZfMcBu8XqUktOyOzMusZP7KCq_dEleQ","Amorphis","Silent Waters","Amorphis","the white swan","amorphis","silent waters","amorphis",,,,291000,9,,1,,,,,,0,-1,0,0,,"Tmkrownnht4mvduqsqowyb7e6ym","Tmkrownnht4mvduqsqowyb7e6ym",5,,,"B2qyakjo4xjp4oqobfkebtu6v74","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqCc-WN93vyIYWYOZnULXM96sgWMVbA9AJca-iVH4CLA-0dbffVgE84Y6lAL9FoNQm1kf7sbc557PTpcDaLhAjNx8pAfr2XK9PqvJu2HLCF96gdw3c\u003d"] ,["Tijijkeg4esnepc43qje64mvkpq","Far From The Sun","//lh3.googleusercontent.com/oyBKVKnvr1YOGV3Xqp-TTsqWJnhIX5M_8GclclVtkPMDNr5gJ1dV7au33JSsl8snepqsgDJyBw","Amorphis","Far From The Sun","Amorphis","far from the sun","amorphis","far from the sun","amorphis",,,,240000,5,,1,,2004,,,,0,-1,0,0,,"Tijijkeg4esnepc43qje64mvkpq","Tijijkeg4esnepc43qje64mvkpq",5,,,"B7n3fftcxef7a2qpvtujmz62mya","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKr6c-jXZ-woVbHbQyLHqEfULUS7mvCFHuCDa0cYzDkb5hazWDX1DwHV4Z9wxmNgbK_Duow6ANdmGirtI7HgLbmYdUYCd7rRyvhhyius9fanFeWjZWw\u003d"] ,["Twwbzgdwdty6j3qa6tb4fffezee","My Sun","//lh4.googleusercontent.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44","Amorphis","Skyforger","Amorphis","my sun","amorphis","skyforger","amorphis",,,,244000,6,,1,,,,,,0,-1,0,0,,"Twwbzgdwdty6j3qa6tb4fffezee","Twwbzgdwdty6j3qa6tb4fffezee",5,,,"B5nc22xlcmdwi3zn5htkohstg44","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrLWpb9mjHkq0G9cCWt2i7MJKZG77mrEOb4CJQQrMawQ2zrPGQyKjU56gExDSwzh6jbD8SKaF4RVgaVCJS1Zciz-G3T9LdWx-Q45nRjdMcIQCMhZuw\u003d"] ,["T6npnim6h36bepvbqmvwt7q6doi","The Way","//lh5.googleusercontent.com/mIs0nP6OkfEoMyziAJzrVEjrpVOFZ5OjItAToXfWQGu1ZbCcVDOhtHL-bHK8KXXRKEZ42fgc","Amorphis","Chapters","Amorphis","the way","amorphis","chapters","amorphis",,,,275000,4,,1,,,,,,0,-1,0,0,,"T6npnim6h36bepvbqmvwt7q6doi","T6npnim6h36bepvbqmvwt7q6doi",5,,,"Bhs6pazgseku7l6x6cpavrbhx2q","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKos7o8ET7cGkGZ-Y7wkjOplRpKKbvhcjFhrU99_ghZMTht-eZ6Gj9B1-zmhe0828TCMTILy2cfHCCSHOrKMI_dhsjV48B6CAyO8FSmpdJ_H9fUvv_A\u003d"] ,["Tzdac2mmkmah2by3ei4fjzf2okm","Reformation","//lh3.googleusercontent.com/LgN08osfWUWN6S-IAOa99h4Qr1Wj21pcHp7r0AnErxK1HuJurqklV0Ycgk2JxB1Pu8IpJleKlQ","Amorphis","The Beginning Of Times","Amorphis","reformation","amorphis","the beginning of times","amorphis",,,,273000,7,,1,,,,,,0,-1,0,0,,"Tzdac2mmkmah2by3ei4fjzf2okm","Tzdac2mmkmah2by3ei4fjzf2okm",5,,,"Bet2e22dinehb7qiksqirljvcnq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrXUH1bajJLcQGpBIjSUUn1XyM2rUfxpXxjEO-RZA4Nf4wYaj_g2gBxsxF2hc-jJqmFN5KPgihfttim7vQ-stPcN8UNqeq0HNIzCNirMkDfLbbgxK0\u003d"] ,["Twl5w6cjycfekaqrgtx6hywdd64","Majestic Beast","//lh4.googleusercontent.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44","Amorphis","Skyforger","Amorphis","majestic beast","amorphis","skyforger","amorphis",,,,259000,5,,1,,,,,,0,-1,0,0,,"Twl5w6cjycfekaqrgtx6hywdd64","Twl5w6cjycfekaqrgtx6hywdd64",5,,,"B5nc22xlcmdwi3zn5htkohstg44","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpTpZvqGdghtoRk6vqhyKOyNziVUI0dVbfD25Kz_uBCRiyU5Jg0iqvxJ5YZgqC45ENJWOIRCQ9lgnKMOHOsXTXEm5AuQxgdcgLX68wINTnslCmB3hw\u003d"] ,["To4p42gfkdem7gy2vqla4fbwlvm","Greed","//lh5.googleusercontent.com/k0EZxB3AaOuhcvE1D2a41qnwrt55KFh4UdouvIjYICsJCgnr85nYb-cQd270n5_7i5c9ILuvXQ","Amorphis","Tuonela","Amorphis","greed","amorphis","tuonela","amorphis",,,,257000,5,,1,,1999,,,,0,-1,0,0,,"To4p42gfkdem7gy2vqla4fbwlvm","To4p42gfkdem7gy2vqla4fbwlvm",5,,,"Bd4pz2likmhw3pxjbkreij5magu","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpr07ODxk86keGs2mB8VVffynXgy7w7uoqtwQgZYPyOn-Ybvuk59_vmJTRHwRdw6o_d5OOGPkJiVaNh5Cq44Vp9jvfwd859z7mB5fra7xnoatw5Dow\u003d"] ,["Tegrh7d5zwsui66np4c3nbaru7y","I Of Crimson Blood","//lh3.googleusercontent.com/KKida9Y4-ngxEIgyHe467fxT_BsDirfoPoCkp9PDFTkZfMcBu8XqUktOyOzMusZP7KCq_dEleQ","Amorphis","Silent Waters","Amorphis","i of crimson blood","amorphis","silent waters","amorphis",,,,307000,5,,1,,,,,,0,-1,0,0,,"Tegrh7d5zwsui66np4c3nbaru7y","Tegrh7d5zwsui66np4c3nbaru7y",5,,,"B2qyakjo4xjp4oqobfkebtu6v74","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKowFoHIG3mQwS66azJc-G4owM9mqNN5lvxwSXpPQ9HLYruCa0iYV1zRUezukGAKG2nfw3V-ANSovKdgx3VQtLQ_5MAlPpWO-IdP2IYQA3xJ42cqpr8\u003d"] ,["Tbxtgmb6htgctsie6gwfab6vexe","Drowned Maid","//lh4.googleusercontent.com/epPwDFc2GCsvvaBSf_LPgu5bCh0qXpJxi1DSU5gUGhwJZ5moYPtcPRAY3yww1YTEzM1g_erBuQ","Amorphis","Magic And Mayhem - Tales From The Early Years","Amorphis","drowned maid","amorphis","magic and mayhem - tales from the early years","amorphis",,,,251000,10,,1,,2010,,,,0,-1,0,0,,"Tbxtgmb6htgctsie6gwfab6vexe","Tbxtgmb6htgctsie6gwfab6vexe",5,,,"B7dplgr5h2jzzkcyrwhifgwl2v4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrFLs209LEvnRZo7s9_LNGRmF4y0rpD-ZPFGiHSCr4lGkwiG4yJjX2G79zRRmI4YRKBcN9ggghHYZCLVixFhJ0iTZ6aX8C31sHDxdXo_S4MPm5hvsg\u003d"] ,["T75tw7xp3exmnlp6glhyl6yjrou","Silver Bride (Edit)","//lh3.googleusercontent.com/5XPQqenJN-A_NMbACMPbcaIkm6S4Ltkq1SffSig03cSApytrv6kXjPqnA5erSS7mLggmMGydVoM","Amorphis","Nuclear Blast Showdown Summer 2010","Various Artists","silver bride (edit)","amorphis","nuclear blast showdown summer 2010","various artists",,,,266000,10,,1,,,,,,0,-1,0,0,,"T75tw7xp3exmnlp6glhyl6yjrou","T75tw7xp3exmnlp6glhyl6yjrou",5,,,"Bg3n7keafzj7m434x7v5pufiyce","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpLbIYmIFhNvCYem9o5_Tf49RtUOzso6OxcyjKq-0i3BlsSqbyBDYj3LrtAOrHNsAJ_jqnGSK3f5AkFYQ3z8DKU_YLnciO2Z36AfTDhlTMtkaL3Nxk\u003d"] ,["Ttnotcha4ohcm3q5xfve4qjkhma","Towards And Against","//lh3.googleusercontent.com/KKida9Y4-ngxEIgyHe467fxT_BsDirfoPoCkp9PDFTkZfMcBu8XqUktOyOzMusZP7KCq_dEleQ","Amorphis","Silent Waters","Amorphis","towards and against","amorphis","silent waters","amorphis",,,,301000,4,,1,,,,,,0,-1,0,0,,"Ttnotcha4ohcm3q5xfve4qjkhma","Ttnotcha4ohcm3q5xfve4qjkhma",5,,,"B2qyakjo4xjp4oqobfkebtu6v74","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpFZnEy2F1dkBdf-qdsekip9gkso7V38bNjrerk8VkkY50AKY6gaLXNhvGEsXu6TDvxJ5aP-saewSc9vvlf9e5w6UomRKJzG1Pd_Q37xLmUm7DGD50\u003d"] ,["Tlwtfa4ouuv66d2z6ycyhdzh2da","Battle for Light","//lh5.googleusercontent.com/aiuus9dfF3VysvwcmQ2L8F4Frc46iBsAl2hZhskZNyQKzRPC8_55zuI3g44pEbU0qPD193OivQ","Amorphis","Metal Hymns Vol. 10","Various Artists","battle for light","amorphis","metal hymns vol. 10","various artists",,,,335000,4,,1,,,,,,0,-1,0,0,,"Tlwtfa4ouuv66d2z6ycyhdzh2da","Tlwtfa4ouuv66d2z6ycyhdzh2da",5,,,"Bzes7chi2dbcq3islwvqa456ghm","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoiMqiuV98mJxrM5s96jTjvYV7cdAOV5LT1j0lfMePHaLgy5cVKfszSObufGT2VSuMN-fXP405JugVGBtn7Hm2HJQyAgIOFql7edndDgisSe2IRL-E\u003d"] ,["Tqki6ipg6t7ketta7ohjosc7fh4","Crimson Wave","//lh5.googleusercontent.com/jIshGkD2br3vsw9qK-ppNyrSxGpvZ15GbkLTLptAlocJ2qUgKta0qVyIEWKH4Rht6u15C_A5AQ","Amorphis","AM Universum","Amorphis","crimson wave","amorphis","am universum","amorphis",,,,285000,5,,1,,2001,,,,0,-1,0,0,,"Tqki6ipg6t7ketta7ohjosc7fh4","Tqki6ipg6t7ketta7ohjosc7fh4",5,,,"B5s2cinoy6roo67mmc3gcao4sfq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoNACzhmvzcNpjUkjLEFReWfYnC1BB7VpNsf0tuhTceqoC0-jdGjfpupTv5E4KQR3POLRqKOjIpGMQHrqxb3ZKA7Ng23UcBbydTCDnmlOapA1gFLjg\u003d"] ,["The63l6fm3gzutn4kn22ytj7y3m","Morning Star","//lh5.googleusercontent.com/k0EZxB3AaOuhcvE1D2a41qnwrt55KFh4UdouvIjYICsJCgnr85nYb-cQd270n5_7i5c9ILuvXQ","Amorphis","Tuonela","Amorphis","morning star","amorphis","tuonela","amorphis",,,,231000,2,,1,,1999,,,,0,-1,0,0,,"The63l6fm3gzutn4kn22ytj7y3m","The63l6fm3gzutn4kn22ytj7y3m",5,,,"Bd4pz2likmhw3pxjbkreij5magu","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKo4v5g8wcSdj5e4liUDBQt7b36G6GJEPgVjv-a8GLI9eeS4JQHrm52VApnalOlIF6FncpeF7ulXgo-0HRDXlg0HcDR-OFH3J0jNP79WsiUdoBRbZVY\u003d"] ,["Tn7oav7e6bfytcx4m5wflo2gazu","Empty Opening","//lh5.googleusercontent.com/-vLUmfh_fqSJ_Xk6BenBs87OrPuUl5--_NhoJsX7SypPmGQZVABi3szLY3uvtFRCQBuguc4k","Amorphis","Eclipse","Amorphis","empty opening","amorphis","eclipse","amorphis",,,,459000,10,,1,,,,,,0,-1,0,0,,"Tn7oav7e6bfytcx4m5wflo2gazu","Tn7oav7e6bfytcx4m5wflo2gazu",5,,,"Bvpiqruq3mw7a6kmdapua2ejafq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpvM5UXvclGNYpxSlAmW-Y2nTYR0xeayBkavGcrrGVZoxjb6MQYzX93YPPJgzo1wEcOelnTBsVwwj-9dck9PYX_Db7mYShE___qblyYq6sAaDAu2VQ\u003d"] ,["Tt5t22sa3k2yvy6tbl2nvrr6giy","From Earth I Rose","//lh4.googleusercontent.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44","Amorphis","Skyforger","Amorphis","from earth i rose","amorphis","skyforger","amorphis",,,,304000,10,,1,,,,,,0,-1,0,0,,"Tt5t22sa3k2yvy6tbl2nvrr6giy","Tt5t22sa3k2yvy6tbl2nvrr6giy",5,,,"B5nc22xlcmdwi3zn5htkohstg44","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKo0QLLUp4EE3ZbzGEsDKGxTakBDr46tCDkLpvgOPJaZrUD4ABrTcX9ayg-uup3xXMNqfY7bNkynxJn_YluN1LJW34-tUwAbA8CEqCeYS2BqC1kPxcQ\u003d"] ,["Tkjzb4os7wlyfvs36wjv2dsougq","The Castaway","//lh4.googleusercontent.com/epPwDFc2GCsvvaBSf_LPgu5bCh0qXpJxi1DSU5gUGhwJZ5moYPtcPRAY3yww1YTEzM1g_erBuQ","Amorphis","Magic And Mayhem - Tales From The Early Years","Amorphis","the castaway","amorphis","magic and mayhem - tales from the early years","amorphis",,,,356000,7,,1,,2010,,,,0,-1,0,0,,"Tkjzb4os7wlyfvs36wjv2dsougq","Tkjzb4os7wlyfvs36wjv2dsougq",5,,,"B7dplgr5h2jzzkcyrwhifgwl2v4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKofIY3Mg2CCvLNkzzWqqDMiYFk__NeZJw4tL-gZdUYSqlNuE6vp-UZ_diV3uP2JwlxBhgrpzEIesTWSlaK3en_tAR7wAoSHMmdJQOWEj7glpqdsLwk\u003d"] ,["Tmwelhpeibrwropw7hiuuzt52ia","Leaves Scar","//lh5.googleusercontent.com/-vLUmfh_fqSJ_Xk6BenBs87OrPuUl5--_NhoJsX7SypPmGQZVABi3szLY3uvtFRCQBuguc4k","Amorphis","Eclipse","Amorphis","leaves scar","amorphis","eclipse","amorphis",,,,202000,3,,1,,,,,,0,-1,0,0,,"Tmwelhpeibrwropw7hiuuzt52ia","Tmwelhpeibrwropw7hiuuzt52ia",5,,,"Bvpiqruq3mw7a6kmdapua2ejafq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKq0zTUHtqMoJ32vqs4UMEAPWj-EAXoPOCOrI03YaZnSDrv8svapcCjBlWwQbRPnIMswx8g1tesZIM-WqQTtiD6KPRPh8wh3jyPQpwyTFhlT0DHfTW8\u003d"] ,["Tdpveiw34rnifiqniyns3xh3ufi","Black River","//lh3.googleusercontent.com/KKida9Y4-ngxEIgyHe467fxT_BsDirfoPoCkp9PDFTkZfMcBu8XqUktOyOzMusZP7KCq_dEleQ","Amorphis","Silent Waters","Amorphis","black river","amorphis","silent waters","amorphis",,,,229000,10,,1,,,,,,0,-1,0,0,,"Tdpveiw34rnifiqniyns3xh3ufi","Tdpveiw34rnifiqniyns3xh3ufi",5,,,"B2qyakjo4xjp4oqobfkebtu6v74","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoQXbU39d_c9exAEJLT9klrZkIuoOsYGYd1c4-qj0CD4wQInsVF_WenVib32oNwsfp-DbO0cBouy74HCNpRgttIZKRpwLOug1FODBJEelkt714UHRQ\u003d"] ,["To7ojnolzazw3whayxdtqbs4skm","Brother Moon","//lh5.googleusercontent.com/-vLUmfh_fqSJ_Xk6BenBs87OrPuUl5--_NhoJsX7SypPmGQZVABi3szLY3uvtFRCQBuguc4k","Amorphis","Eclipse","Amorphis","brother moon","amorphis","eclipse","amorphis",,,,310000,9,,1,,,,,,0,-1,0,0,,"To7ojnolzazw3whayxdtqbs4skm","To7ojnolzazw3whayxdtqbs4skm",5,,,"Bvpiqruq3mw7a6kmdapua2ejafq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKq3nbZJvlGqiSO82iqUSLU1LIe8ZoZEd22lkngsQ6RKLKua_samTtfuaOLY8Mp7WR0n1_xJXUj6EHL7sB4o1PLqmOmrediNMYrDgUp3ASIbK1DAN10\u003d"] ,["Tggcheeofzcqhqaazxhcj7pew5q","Against Widows","//lh3.googleusercontent.com/Dj34n_0W7-iKJTGIW0az6xqXc5JesMbMdcfDycQRGL2gH7ws-XUg9In4GDOkUgrGJJna24gqig","Amorphis","Death ... Is just the beginning Vol.4","Various Artists","against widows","amorphis","death ... is just the beginning vol.4","various artists",,,,244000,1,,1,,,,,,0,-1,0,0,,"Tggcheeofzcqhqaazxhcj7pew5q","Tggcheeofzcqhqaazxhcj7pew5q",5,,,"Bwg6marhxg27vetxvflfle3jtoi","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqzovIJh22W4EB9cO4Dx_lT6ganw2p7eQyFM25f91MJLTDglJXXIOLeVXu6IlOoz7JE0x5XtW9tcKSnP7e0YNg4KKxd9pt-XPfRI0KfM2G9oCsqGtk\u003d"] ,["T2fdenaeou5lem7qx267e4uidoy","Two Moons","//lh5.googleusercontent.com/PtH3wuz8KJiuaPR6Q57-5XxMJ0lFLpAImsH5yu9MDUZCeW08bek1022tXxv9pepKBJ23Ua1O3A","Amorphis","20 Years - Two Decades Of Nuclear Blast Vol.1","Various Artists","two moons","amorphis","20 years - two decades of nuclear blast vol.1","various artists",,,,206000,18,,1,,,,,,0,-1,0,0,,"T2fdenaeou5lem7qx267e4uidoy","T2fdenaeou5lem7qx267e4uidoy",5,,,"Bmj4fibk4gf6eqfn5wvi6csnaku","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqH2F5hfp0-eh73UOnNqj62abrgwGri8Qz4VQYkAXQwQ7lEAksLTTW15eh2hNWLCrEkgOdmX9Ou4hNrCOU2voef9oHgCIIHCaUpTqv9KWZ_hgaGa9w\u003d"] ,["Ta35vwg6l45ah3mvdjfrzci4whm","Cares","//lh3.googleusercontent.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c","Amorphis","Elegy","Amorphis","cares","amorphis","elegy","amorphis",,,,269000,6,,1,,,,,,0,-1,0,0,,"Ta35vwg6l45ah3mvdjfrzci4whm","Ta35vwg6l45ah3mvdjfrzci4whm",5,,,"Bqzxfykbqcqmjjtdom7ukegaf2u","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpC3Drb_JlI8uK_PtQZowtopqtNLpRFyrPbX5WZ7pzSJplrg8YXEfqKtDI22Id_r4DeZmOa_3BuebHdnm4qtl0vqzF41L9itaUpIdHr-XiKHVjfA2E\u003d"] ,["Tuyujnygjfvbrozt75gnmtyennm","In The Beginning","//lh6.googleusercontent.com/WSNzZmI7vNC_e1npacVLljrFNR5lcjyJ9OFTqaC_N8FuVXcKPTD8qITcy-HjPDzD-bDkou0Hxg","Amorphis","Tales From The Thousand Lakes","Amorphis","in the beginning","amorphis","tales from the thousand lakes","amorphis",,,,217000,7,,1,,,,,,0,-1,0,0,,"Tuyujnygjfvbrozt75gnmtyennm","Tuyujnygjfvbrozt75gnmtyennm",5,,,"B2cnpu7ako4kejm6dvebsudhjti","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrHnz2M_w-_NQ_l2IWtFG0L8LV0FgLBSQpU-MOW0Q93gqYPYOXqjwZq6AmAxOVlR681AI7D1C2YIPh_OC9q1QXG07QCTdaytaOFDM7r0VFMm--ZY3o\u003d"] ,["Tpcwuuhos3ohls5avrycmdsbe64","Privilege of Evil (From the Privilege of Evil EP)","//lh3.googleusercontent.com/P09FP2voKIAo1sXzGz10w0AGq_FCtV5sa8FaNv42A0DF0u6nvYPphScJnDt2MZWpnFzU3B11","Amorphis","Karelian Isthmus","Amorphis","privilege of evil (from the privilege of evil ep)","amorphis","karelian isthmus","amorphis",,,,228000,14,,1,,1992,,,,0,-1,0,0,,"Tpcwuuhos3ohls5avrycmdsbe64","Tpcwuuhos3ohls5avrycmdsbe64",5,,,"Bx5ns5fpgkmdkzmj3eqqvg6z37m","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKodOs98MseHQsXjoN6-UwoKPLUycpDkK_f_osDShuxyuw5hOCQJMq2pJEUfT5O0PYAz6x8XWekYWIXn8mA1VF-OeI8PdvVDvZ32910PkiBGCtVSRAs\u003d"] ,["Tr6zhpp3a5n7on5lclu4mejtmwy","Sign","//lh4.googleusercontent.com/DyMbom0aNDKKeeD9JybMk8vKnAFAQeJ16B5MdaespaJE-FDfPsXSyXlxmLew2n6WTZhfSB8Q","Amorphis","Silent Waters","Amorphis","sign","amorphis","silent waters","amorphis",,,,279000,2,,1,,,,,,0,-1,0,0,,"Tr6zhpp3a5n7on5lclu4mejtmwy","Tr6zhpp3a5n7on5lclu4mejtmwy",5,,,"Binwkon2zzj4rgbvxqm3xoej6t4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpiESV4T4mQ-znRLrxpPiu3BQ0dCkaPcxOZGQME2R-CAlMbKZCvE2Xs4QhEEX__wKmFyGpacyRVR8UdvijzZypcBvWlgDkdXHFwzjFGXsMcilOaUtQ\u003d"] ,["Tr66iyrhtmynafmmrxjdpawx32e","Alone (Live)","//lh5.googleusercontent.com/RrzvFnn-8w2qwVMbc-5Tv1vIKOzPFUvIu5MfWrUnYvH0WuafnxhLLxYBkCa_wnCrf2Bj80TS7g","Amorphis","Forging The Land Of Thousand Lakes","Amorphis","alone (live)","amorphis","forging the land of thousand lakes","amorphis",,,,363000,7,,1,,,,,,0,-1,0,0,,"Tr66iyrhtmynafmmrxjdpawx32e","Tr66iyrhtmynafmmrxjdpawx32e",5,,,"Bawp2fb6emntvumfrqzrd5p3rai","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrG55xPcMn5H50wj2bMSHghnVjbFxDOwgyEAA76P8NEsVMRv4hNCf5e2giCtf1XTNwmyHXAg5o-xPh19nv9oI_EGjCvr6PD7G1UbWpwrSD21dhu94U\u003d"] ,["Titxvaj67cl5g6d2erzehgnjbya","Song Of The Troubled One","//lh4.googleusercontent.com/epPwDFc2GCsvvaBSf_LPgu5bCh0qXpJxi1DSU5gUGhwJZ5moYPtcPRAY3yww1YTEzM1g_erBuQ","Amorphis","Magic And Mayhem - Tales From The Early Years","Amorphis","song of the troubled one","amorphis","magic and mayhem - tales from the early years","amorphis",,,,254000,8,,1,,2010,,,,0,-1,0,0,,"Titxvaj67cl5g6d2erzehgnjbya","Titxvaj67cl5g6d2erzehgnjbya",5,,,"B7dplgr5h2jzzkcyrwhifgwl2v4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqGAwn0A4f9iCI-KMwqvqIsvRyCcyw0Fh2Q5ElBaSWgZ1r_XYBVB7kBDfaqErsZzQ8iEYHWV_qT0SHgn55atJUd1x1vdHP9ixpy-pbN2VYloAen5Ww\u003d"] ,["Tt4u2ox66a575dzxkseunkkugva","Highest Star","//lh4.googleusercontent.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44","Amorphis","Skyforger","Amorphis","highest star","amorphis","skyforger","amorphis",,,,284000,7,,1,,,,,,0,-1,0,0,,"Tt4u2ox66a575dzxkseunkkugva","Tt4u2ox66a575dzxkseunkkugva",5,,,"B5nc22xlcmdwi3zn5htkohstg44","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpIgcPe49yaEJqcj_wClfs7GBXhUPd1mwh85ast17Z6KLu_bp7XkrRi1TgStbKi7UQRAzmpyrL1pzbMcBfIMWpoLoUvbizTkTPQSmxnQ2b_n8K3qdU\u003d"] ,["T4jzhog53bl6l74mva4mafhtk6a","Hopeless Days","//lh6.googleusercontent.com/55Ea0FcQgrnPcw4h6kwtgEKbOS-dCyGBpk8mYBWe_pgj7dEjZc3yFR_bwVR0PYWk-f1Bqi2i","Amorphis","Circle","Amorphis","hopeless days","amorphis","circle","amorphis",,,,307000,5,,1,,,,,,0,-1,0,0,,"T4jzhog53bl6l74mva4mafhtk6a","T4jzhog53bl6l74mva4mafhtk6a",5,,,"Bfr2onjv7g7tm4rzosewnnwxxyy","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrQ-ZTKEnaF0HcpIgXwC9U31FnYizxtizG6pESSg7FiQ4_hTEwg2KKcpTSJ6kA2LB9kjD4KlCaAaOOjnVYJLiU-gPxi-CUPXk6GQSLJt7P48A9C2D4\u003d"] ,["Tibazrvrp7z6co2jdmtwtgb6pdi","Course Of Fate","//lh4.googleusercontent.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44","Amorphis","Skyforger","Amorphis","course of fate","amorphis","skyforger","amorphis",,,,255000,9,,1,,,,,,0,-1,0,0,,"Tibazrvrp7z6co2jdmtwtgb6pdi","Tibazrvrp7z6co2jdmtwtgb6pdi",5,,,"B5nc22xlcmdwi3zn5htkohstg44","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKox_C5W752cdMxutUq0qUR4DJ4hHW-XINURfAfxu83U2Hkqe-6ssmrnex3AKRnwFKYMO_wVITurSsI90E2VV7HLqB8Z5jPoSgqr131XfyQRtbVMGpc\u003d"] ,["Tottwk7pq7ftptdfggt35yshkiu","Sign From The North Side","//lh4.googleusercontent.com/epPwDFc2GCsvvaBSf_LPgu5bCh0qXpJxi1DSU5gUGhwJZ5moYPtcPRAY3yww1YTEzM1g_erBuQ","Amorphis","Magic And Mayhem - Tales From The Early Years","Amorphis","sign from the north side","amorphis","magic and mayhem - tales from the early years","amorphis",,,,304000,9,,1,,2010,,,,0,-1,0,0,,"Tottwk7pq7ftptdfggt35yshkiu","Tottwk7pq7ftptdfggt35yshkiu",5,,,"B7dplgr5h2jzzkcyrwhifgwl2v4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoSh7Ihi5yrmfP1RUDkn0Y-q9yKZBeZsY0gMG5j11XeUB2zt-2uEW7SKiiIPZDjdL_-6uMKzG_hRSePHhK769V9HMrTYSLMy_Pfxtl3F_zWCgzJco4\u003d"] ,["Tiqukyrpsyg3zox7csenu5f2dvy","On Rich And Poor","//lh4.googleusercontent.com/epPwDFc2GCsvvaBSf_LPgu5bCh0qXpJxi1DSU5gUGhwJZ5moYPtcPRAY3yww1YTEzM1g_erBuQ","Amorphis","Magic And Mayhem - Tales From The Early Years","Amorphis","on rich and poor","amorphis","magic and mayhem - tales from the early years","amorphis",,,,324000,5,,1,,2010,,,,0,-1,0,0,,"Tiqukyrpsyg3zox7csenu5f2dvy","Tiqukyrpsyg3zox7csenu5f2dvy",5,,,"B7dplgr5h2jzzkcyrwhifgwl2v4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrcKGkPxRbmMNTm_Z4V0U8Bauqodr2n64nmNC2q0EDg-gdpxTLBLWW04sfgWL6KSHF_3G6pGjwKGEDirraHKCymtnnc096Wbb5FMWQfpG1THS1_FNE\u003d"] ,["Tl4yoal7ooebi44pacmchmkj2py","On Rich and Poor","//lh3.googleusercontent.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c","Amorphis","Elegy","Amorphis","on rich and poor","amorphis","elegy","amorphis",,,,319000,4,,1,,,,,,0,-1,0,0,,"Tl4yoal7ooebi44pacmchmkj2py","Tl4yoal7ooebi44pacmchmkj2py",5,,,"Bqzxfykbqcqmjjtdom7ukegaf2u","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKr03tb6pO4uunS2MzuyMX7_Ag7S2nwMRzEVJEoVzUecsB8zbnAEzeoFCYa3wQlU_iO2VVYfjsjucrnera_SGp9WUPRBgcbwv8EsSyN-Http5YPOCW4\u003d"] ,["Toomnb3e3nt3yte2yit64c2pdeq","The Orphan","//lh3.googleusercontent.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c","Amorphis","Elegy","Amorphis","the orphan","amorphis","elegy","amorphis",,,,317000,3,,1,,,,,,0,-1,0,0,,"Toomnb3e3nt3yte2yit64c2pdeq","Toomnb3e3nt3yte2yit64c2pdeq",5,,,"Bqzxfykbqcqmjjtdom7ukegaf2u","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqZeKypYr9Rh-D2gyR1dEKNMSlcmr8C5qxU8ziIOsMEBkcZVorGDDDmn2zlkPe9B79_yV2xXFWcgOZnIjZT_I-kk804eY4DrH2UDzPC20AwNwYOjT4\u003d"] ,["T5lshpqqu5ax63lqhxeze5wux3a","Better Unborn","//lh3.googleusercontent.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c","Amorphis","Elegy","Amorphis","better unborn","amorphis","elegy","amorphis",,,,352000,1,,1,,,,,,0,-1,0,0,,"T5lshpqqu5ax63lqhxeze5wux3a","T5lshpqqu5ax63lqhxeze5wux3a",5,,,"Bqzxfykbqcqmjjtdom7ukegaf2u","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKq0uQEJuE7UpvcDqegNe8842S1aaPwIlVmlIKvUGr0bYB8-AyFI_Qj6NU2gZb7wzGCTcroeyl2qK9OqKmFlOVboJFzSrDATdRB4l92RSZYXYSPFinQ\u003d"] ,["T2tjwc5sxr6nlahux5uqm2rglvm","Day Of Your Beliefs","//lh3.googleusercontent.com/oyBKVKnvr1YOGV3Xqp-TTsqWJnhIX5M_8GclclVtkPMDNr5gJ1dV7au33JSsl8snepqsgDJyBw","Amorphis","Far From The Sun","Amorphis","day of your beliefs","amorphis","far from the sun","amorphis",,,,304000,1,,1,,2004,,,,0,-1,0,0,,"T2tjwc5sxr6nlahux5uqm2rglvm","T2tjwc5sxr6nlahux5uqm2rglvm",5,,,"B7n3fftcxef7a2qpvtujmz62mya","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpwxxj8Uf80_KDmA25H07m9XZc5xY_ZXx349F7vXFBnvWyloky2aG3pWMgyJivYCuSpZJzPbZufe-k0vU4fW0kfM9LF2celSSiP2YRhl2BJJPtswAs\u003d"] ,["Tttpuyhyym4ivj5cjutnwebmqyi","Black Embrace (From the Privilege of Evil EP)","//lh3.googleusercontent.com/P09FP2voKIAo1sXzGz10w0AGq_FCtV5sa8FaNv42A0DF0u6nvYPphScJnDt2MZWpnFzU3B11","Amorphis","Karelian Isthmus","Amorphis","black embrace (from the privilege of evil ep)","amorphis","karelian isthmus","amorphis",,,,203000,13,,1,,1992,,,,0,-1,0,0,,"Tttpuyhyym4ivj5cjutnwebmqyi","Tttpuyhyym4ivj5cjutnwebmqyi",5,,,"Bx5ns5fpgkmdkzmj3eqqvg6z37m","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqpg4qj1hBBdRQ3_Nh0LO2HWGCiOeP3yfH7-KHHKvYrd7nfYwV1jWwreWLOebgyIRTAvMWAVnLTTsdJiyWd91-tj1OVm_iJohlfLeX-rQFFemV9qi8\u003d"] ,["T3ie4cg5cteyjvolprkolyiurzy","Black Embrace","//lh3.googleusercontent.com/P09FP2voKIAo1sXzGz10w0AGq_FCtV5sa8FaNv42A0DF0u6nvYPphScJnDt2MZWpnFzU3B11","Amorphis","Karelian Isthmus","Amorphis","black embrace","amorphis","karelian isthmus","amorphis",,,,218000,5,,1,,1992,,,,0,-1,0,0,,"T3ie4cg5cteyjvolprkolyiurzy","T3ie4cg5cteyjvolprkolyiurzy",5,,,"Bx5ns5fpgkmdkzmj3eqqvg6z37m","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqVlgJldMHMO2G98RNrMjscOEPRhpT8IlVK32iO-9xgNi3cj_PF5nl4QTXehxziun5oAJ7inYYXG2EKADzOb5QdF8kJ7IolOkSZYlAgXbe_irzhaKI\u003d"] ,["Tq2jkr6dgsjlalqymcjmptbis5e","The Gathering","//lh3.googleusercontent.com/P09FP2voKIAo1sXzGz10w0AGq_FCtV5sa8FaNv42A0DF0u6nvYPphScJnDt2MZWpnFzU3B11","Amorphis","Karelian Isthmus","Amorphis","the gathering","amorphis","karelian isthmus","amorphis",,,,252000,2,,1,,1992,,,,0,-1,0,0,,"Tq2jkr6dgsjlalqymcjmptbis5e","Tq2jkr6dgsjlalqymcjmptbis5e",5,,,"Bx5ns5fpgkmdkzmj3eqqvg6z37m","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpfae02juBdouHLxXZYBxVwSV92Ali1X83ajoT-PV6sO6mcyAuuMdYteTqDqxCpJzvv6JbRNehc8v2xRF9R1twxYcIwYABEWa_eU7UwzU62dR6P2AY\u003d"] ,["Twc5q2znjhqpf7fbzzfc3qf64xy","Forgotten Sunrise","//lh6.googleusercontent.com/WSNzZmI7vNC_e1npacVLljrFNR5lcjyJ9OFTqaC_N8FuVXcKPTD8qITcy-HjPDzD-bDkou0Hxg","Amorphis","Tales From The Thousand Lakes","Amorphis","forgotten sunrise","amorphis","tales from the thousand lakes","amorphis",,,,293000,8,,1,,,,,,0,-1,0,0,,"Twc5q2znjhqpf7fbzzfc3qf64xy","Twc5q2znjhqpf7fbzzfc3qf64xy",5,,,"B2cnpu7ako4kejm6dvebsudhjti","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpLsXQ43AlAx5EDVCCM4fvns6kblNyxQh6C7hpZcBrUhJj_Tk49RC6Piue56t_5BbmUuKgkCs9lAec4IA2-gQcjJuThN4uAUneftqEaXgkSNGx-IYU\u003d"] ,["Twoo2dsdbodrrmqz6kjhhhdcdsy","Escape","//lh3.googleusercontent.com/LgN08osfWUWN6S-IAOa99h4Qr1Wj21pcHp7r0AnErxK1HuJurqklV0Ycgk2JxB1Pu8IpJleKlQ","Amorphis","The Beginning Of Times","Amorphis","escape","amorphis","the beginning of times","amorphis",,,,231000,10,,1,,,,,,0,-1,0,0,,"Twoo2dsdbodrrmqz6kjhhhdcdsy","Twoo2dsdbodrrmqz6kjhhhdcdsy",5,,,"Bet2e22dinehb7qiksqirljvcnq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqz46rH2RSRKc_vIZp4T5DSMZdPItgUiB-bAL09Pg2bOrw8Q_Hqm1XyCss5gqsPdkaBvvdJRTtbjCJDDASyMbU2IQ4x4uoxdl7UK7jv6crpRVpolDQ\u003d"] ,["Taetmqcfrl5ptmlv3ozhucsnfbm","Follow Me Into The Fire","//lh3.googleusercontent.com/oyBKVKnvr1YOGV3Xqp-TTsqWJnhIX5M_8GclclVtkPMDNr5gJ1dV7au33JSsl8snepqsgDJyBw","Amorphis","Far From The Sun","Amorphis","follow me into the fire","amorphis","far from the sun","amorphis",,,,326000,12,,1,,2004,,,,0,-1,0,0,,"Taetmqcfrl5ptmlv3ozhucsnfbm","Taetmqcfrl5ptmlv3ozhucsnfbm",5,,,"B7n3fftcxef7a2qpvtujmz62mya","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrnvx4J5ivqRDX1txXfeJPTvilRsZILkQQPz_BCf1Y_EmGEayRejCfDZwpo57KvVAcEDRoiq4EJbVo9s_tfKq-AByJUOx9mC7CHT4MZ4CcpvO5idmQ\u003d"] ,["Thpfrrnphsjr3k6z2z4l35aepda","Northern Lights","//lh5.googleusercontent.com/mIs0nP6OkfEoMyziAJzrVEjrpVOFZ5OjItAToXfWQGu1ZbCcVDOhtHL-bHK8KXXRKEZ42fgc","Amorphis","Chapters","Amorphis","northern lights","amorphis","chapters","amorphis",,,,197000,5,,1,,,,,,0,-1,0,0,,"Thpfrrnphsjr3k6z2z4l35aepda","Thpfrrnphsjr3k6z2z4l35aepda",5,,,"Bhs6pazgseku7l6x6cpavrbhx2q","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoK_HDysFJ5bGQck4QPjI7QFXG1ITFq0TXlRObmI5RbdGdaL7IOtIiTd7BeIQiFhZWl6cm8KeWwvfScny8Op7BBsNzSv6HJGd63c5T0_Ukrqcxne1w\u003d"] ,["Tfrf5k6qolduhzxrwoi77ukhdoe","Levitation","//lh6.googleusercontent.com/7t-KGIVGopoFtbIuRPH3N_I-R_N_mW1xJHxBAWsx7VMIwgy_KBcZEpeUcNT4Ccmi6nSUy4ZR","Amorphis","My Kantele","Amorphis","levitation","amorphis","my kantele","amorphis",,,,352000,4,,1,,1997,,,,0,-1,0,0,,"Tfrf5k6qolduhzxrwoi77ukhdoe","Tfrf5k6qolduhzxrwoi77ukhdoe",5,,,"Bb4frlhpnn4wr4hgdwfj5frm7e4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKp39ZpGgU7F3bR79XlEvgoctmk50rqtsq3kVh_OT55vPw15w1NZnndd1JGaLjShDcG0RyDLV91VJec2hN_TMDHLdZUR_SOomShkD7spdzLYsTv_Mmk\u003d"] ,["Twlximk3hr35blia6hv3fchjyqe","Summer's End","//lh5.googleusercontent.com/k0EZxB3AaOuhcvE1D2a41qnwrt55KFh4UdouvIjYICsJCgnr85nYb-cQd270n5_7i5c9ILuvXQ","Amorphis","Tuonela","Amorphis","summer's end","amorphis","tuonela","amorphis",,,,337000,10,,1,,1999,,,,0,-1,0,0,,"Twlximk3hr35blia6hv3fchjyqe","Twlximk3hr35blia6hv3fchjyqe",5,,,"Bd4pz2likmhw3pxjbkreij5magu","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKooboQCpejwlwChZZFjebngvplQXZ5fp0uXXPq07VSAAbxuFneQNgaTeFOxD84gXmyUgJds_ONypeuOStB8QEk0E8c0ntRM8zP2JWZD920LxV0VJ80\u003d"] ,["Tv7hhhx4lkhmjvqasjr7ys62b4a","Goddess (Of the Sad Man)","//lh5.googleusercontent.com/jIshGkD2br3vsw9qK-ppNyrSxGpvZ15GbkLTLptAlocJ2qUgKta0qVyIEWKH4Rht6u15C_A5AQ","Amorphis","AM Universum","Amorphis","goddess (of the sad man)","amorphis","am universum","amorphis",,,,239000,2,,1,,2001,,,,0,-1,0,0,,"Tv7hhhx4lkhmjvqasjr7ys62b4a","Tv7hhhx4lkhmjvqasjr7ys62b4a",5,,,"B5s2cinoy6roo67mmc3gcao4sfq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKo1b8nh_CTOnVArcRbJKxzj8f9U4u_VBEUcUTQAgnKk2DYzUCcyLMVI5HuQACYScPvmM925VJdcoGi7leZF6Es7_Sjli-cnepIlo2URvjBfOpB00tU\u003d"] ,["Tl57xz67jx4dwfdktjdzlwa7ut4","The Brother-Slayer","//lh5.googleusercontent.com/mIs0nP6OkfEoMyziAJzrVEjrpVOFZ5OjItAToXfWQGu1ZbCcVDOhtHL-bHK8KXXRKEZ42fgc","Amorphis","Chapters","Amorphis","the brother-slayer","amorphis","chapters","amorphis",,,,217000,7,,1,,,,,,0,-1,0,0,,"Tl57xz67jx4dwfdktjdzlwa7ut4","Tl57xz67jx4dwfdktjdzlwa7ut4",5,,,"Bhs6pazgseku7l6x6cpavrbhx2q","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoZ3dHVKPNZL74wJCsYB3gGgZeoVIaiHfkGtCy3_5Ca1042iFOLjklwOSXWIeVfanuvWJRo-llkJJklqzNNd56uyNq9db2R0AhhAdEegaruzq9TJkk\u003d"] ,["T3hr2bzih7es22og5emynmfjn7a","Soothsayer","//lh3.googleusercontent.com/LgN08osfWUWN6S-IAOa99h4Qr1Wj21pcHp7r0AnErxK1HuJurqklV0Ycgk2JxB1Pu8IpJleKlQ","Amorphis","The Beginning Of Times","Amorphis","soothsayer","amorphis","the beginning of times","amorphis",,,,249000,8,,1,,,,,,0,-1,0,0,,"T3hr2bzih7es22og5emynmfjn7a","T3hr2bzih7es22og5emynmfjn7a",5,,,"Bet2e22dinehb7qiksqirljvcnq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoJAj5pInXmcd5GLQjXfGKTfIiV0Glk_7LCS9Emi7nfGV6AqwqayjLDx3_cRJHy7Pn4XSTQgTQTqGJR5wUq7GgQvllvnBhTta36LIyK0CXBGbCXZLw\u003d"] ,["Tb5uyaipvam3g2jgeshj3tyekyi","The Sign from the North Side","//lh3.googleusercontent.com/P09FP2voKIAo1sXzGz10w0AGq_FCtV5sa8FaNv42A0DF0u6nvYPphScJnDt2MZWpnFzU3B11","Amorphis","Karelian Isthmus","Amorphis","the sign from the north side","amorphis","karelian isthmus","amorphis",,,,294000,10,,1,,1992,,,,0,-1,0,0,,"Tb5uyaipvam3g2jgeshj3tyekyi","Tb5uyaipvam3g2jgeshj3tyekyi",5,,,"Bx5ns5fpgkmdkzmj3eqqvg6z37m","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoLfbP4ZY0eJ1HFZxXA9JV5czq3eDcawJ1XGEs92c3QdEm7b3mpbLr9d_z8L8HqLGZtDtswnzUODNpDoWuB8UmdrceJflc9NIxh5tld9QPZdt8WXCI\u003d"] ,["T4uuynkin5y266gjkrgrnjx64qi","Drifting Memories","//lh5.googleusercontent.com/mIs0nP6OkfEoMyziAJzrVEjrpVOFZ5OjItAToXfWQGu1ZbCcVDOhtHL-bHK8KXXRKEZ42fgc","Amorphis","Chapters","Amorphis","drifting memories","amorphis","chapters","amorphis",,,,264000,2,,1,,,,,,0,-1,0,0,,"T4uuynkin5y266gjkrgrnjx64qi","T4uuynkin5y266gjkrgrnjx64qi",5,,,"Bhs6pazgseku7l6x6cpavrbhx2q","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqi_YDSHVw7uSI8wZ_ThxUcGiaKbk7PRqpxl1H5HFF1u2jcx1g_nT_i28T8S6uX19ErrW5ocjaITSOHr5hjvMu4ZFjzGRm_7e49uGMdd5B8c9Vbnkk\u003d"] ,["T5wjnb3sdpw3pgnuwrkbkw6vknm","Forever More","//lh5.googleusercontent.com/jIshGkD2br3vsw9qK-ppNyrSxGpvZ15GbkLTLptAlocJ2qUgKta0qVyIEWKH4Rht6u15C_A5AQ","Amorphis","AM Universum","Amorphis","forever more","amorphis","am universum","amorphis",,,,273000,7,,1,,2001,,,,0,-1,0,0,,"T5wjnb3sdpw3pgnuwrkbkw6vknm","T5wjnb3sdpw3pgnuwrkbkw6vknm",5,,,"B5s2cinoy6roo67mmc3gcao4sfq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrbSZkIdLKfbkiRlYFqLLVpPPl79iOB46xENgNt8bJcDm4Xa-OBAge48hdb6dQgZT3wtfEcIpXkBC2qbWnTahPM9kG8txpp7hcyp_wxqrdETJgr4gE\u003d"] ,["Tqmwq7icj7mwtahuba4deqwij3m","Nightfall","//lh5.googleusercontent.com/k0EZxB3AaOuhcvE1D2a41qnwrt55KFh4UdouvIjYICsJCgnr85nYb-cQd270n5_7i5c9ILuvXQ","Amorphis","Tuonela","Amorphis","nightfall","amorphis","tuonela","amorphis",,,,232000,3,,1,,1999,,,,0,-1,0,0,,"Tqmwq7icj7mwtahuba4deqwij3m","Tqmwq7icj7mwtahuba4deqwij3m",5,,,"Bd4pz2likmhw3pxjbkreij5magu","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKo18R1wnZKrwnjCZ6zJ9SdTZ4OiEW28frcxJZ468c5wo3omHlU8Bv-A4xW-JxV2mHRDkBesHh1_IYUNvXyIEEisl9tznq8RRbZ9r5PTDjyJKLvibvg\u003d"] ,["Tocol2yuo43gbmwnlmfzderl4ja","Silent Waters (Edit)","//lh4.googleusercontent.com/DyMbom0aNDKKeeD9JybMk8vKnAFAQeJ16B5MdaespaJE-FDfPsXSyXlxmLew2n6WTZhfSB8Q","Amorphis","Silent Waters","Amorphis","silent waters (edit)","amorphis","silent waters","amorphis",,,,245000,1,,1,,,,,,0,-1,0,0,,"Tocol2yuo43gbmwnlmfzderl4ja","Tocol2yuo43gbmwnlmfzderl4ja",5,,,"Binwkon2zzj4rgbvxqm3xoej6t4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpQXZJmrsGhGEcetuMGQGwvt3jWxztFDpoOTB0_IeeL8cDJeo9TmrR6RC3jkeTDOZ3FsG9CFaWBFIab-NbsoCP-xbDn5wtrOepIS_MR9UpuwGoXLWI\u003d"] ,["Tnpnwmoh3kxdie5d4vds2xxqehu","Vulgar Necrolatry","//lh4.googleusercontent.com/epPwDFc2GCsvvaBSf_LPgu5bCh0qXpJxi1DSU5gUGhwJZ5moYPtcPRAY3yww1YTEzM1g_erBuQ","Amorphis","Magic And Mayhem - Tales From The Early Years","Amorphis","vulgar necrolatry","amorphis","magic and mayhem - tales from the early years","amorphis",,,,284000,2,,1,,2010,,,,0,-1,0,0,,"Tnpnwmoh3kxdie5d4vds2xxqehu","Tnpnwmoh3kxdie5d4vds2xxqehu",5,,,"B7dplgr5h2jzzkcyrwhifgwl2v4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqpqSpHFdLLS1_pa8LY9D-X4exrQpqarue9weGFjggEK5XJ4g1Ng3ODY0e1U26G4tSU2lDJjYXh10eiphbh_4PnbSVsNjoPtyoOL2n7k4IUWqbPzDs\u003d"] ,["Th7dcmxqwwn72ha2k4a76wui3wi","Weeper on the Shore","//lh3.googleusercontent.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c","Amorphis","Elegy","Amorphis","weeper on the shore","amorphis","elegy","amorphis",,,,292000,8,,1,,,,,,0,-1,0,0,,"Th7dcmxqwwn72ha2k4a76wui3wi","Th7dcmxqwwn72ha2k4a76wui3wi",5,,,"Bqzxfykbqcqmjjtdom7ukegaf2u","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKooV-1Z9-XwbqwiElWxGi-EegcMVZZRYdWsSHgSQ6J4kcAb6OVl8OzaA_nKCxczeFRKRSpTfPCJOyp-V_TiLCm3JaGl7VWfEL3o33ovqD-lrvZli_g\u003d"] ,["Tukrpehpsvced2hn74j2hosno4e","Relief","//lh3.googleusercontent.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c","Amorphis","Elegy","Amorphis","relief","amorphis","elegy","amorphis",,,,249000,10,,1,,,,,,0,-1,0,0,,"Tukrpehpsvced2hn74j2hosno4e","Tukrpehpsvced2hn74j2hosno4e",5,,,"Bqzxfykbqcqmjjtdom7ukegaf2u","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrA5h9kwTwV7QhIIUJ_YbCT1f5pdCY950RwWm4TJaH47EP6HfxnLFasBjSH6ugEnDnBSoGxz1eiB95rHjDtLIotPuwpKy4K-Mli9urEz0DgsR6k6uM\u003d"] ,["Thqkw46w5wdihzm2u7sg7ubf5ny","Vulgar Necrolatry (From the Privilege of Evil EP)","//lh3.googleusercontent.com/P09FP2voKIAo1sXzGz10w0AGq_FCtV5sa8FaNv42A0DF0u6nvYPphScJnDt2MZWpnFzU3B11","Amorphis","Karelian Isthmus","Amorphis","vulgar necrolatry (from the privilege of evil ep)","amorphis","karelian isthmus","amorphis",,,,236000,16,,1,,1992,,,,0,-1,0,0,,"Thqkw46w5wdihzm2u7sg7ubf5ny","Thqkw46w5wdihzm2u7sg7ubf5ny",5,,,"Bx5ns5fpgkmdkzmj3eqqvg6z37m","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqz51uXfHbx3TU7Rk5teJLsYpTNM6lCgHj_RGnvc3XHgLsdP9oxSXFNWcT0XEdzs-axetPJsi_5TvqEZkABr9Ppdp96uUAxbYq72oKhard68_dadrw\u003d"] ,["T6qlbwrtakcg54uamyvedqjqt4e","Born From Fire","//lh5.googleusercontent.com/-vLUmfh_fqSJ_Xk6BenBs87OrPuUl5--_NhoJsX7SypPmGQZVABi3szLY3uvtFRCQBuguc4k","Amorphis","Eclipse","Amorphis","born from fire","amorphis","eclipse","amorphis",,,,247000,4,,1,,,,,,0,-1,0,0,,"T6qlbwrtakcg54uamyvedqjqt4e","T6qlbwrtakcg54uamyvedqjqt4e",5,,,"Bvpiqruq3mw7a6kmdapua2ejafq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKocMbTTjNuMX2FyJAb3SWfJktxznn0t0l4vYgLMgKUSwOoIC6NcBZGPsn2gWxyOP6zpihwa9NnmSkptl2Y7olX83gVqdaaOl7LQGCwn88iNlO3fzGQ\u003d"] ,["T6tyrbmhdp5rl4pzyw5tvyol5zi","Veil of Sin","//lh5.googleusercontent.com/jIshGkD2br3vsw9qK-ppNyrSxGpvZ15GbkLTLptAlocJ2qUgKta0qVyIEWKH4Rht6u15C_A5AQ","Amorphis","AM Universum","Amorphis","veil of sin","amorphis","am universum","amorphis",,,,310000,8,,1,,2001,,,,0,-1,0,0,,"T6tyrbmhdp5rl4pzyw5tvyol5zi","T6tyrbmhdp5rl4pzyw5tvyol5zi",5,,,"B5s2cinoy6roo67mmc3gcao4sfq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKppZPXyWqqhMGCVxrnvBwauTBUfbmWyRj1thCkmT-I_GEsXQsl9d2McqjTzDz2CjcwrJG3CrvVKO_niMzc91gEkNYPqOnF5dU6U6-pXCabAqUTJzzo\u003d"] ,["Tkz7tjfjcrocareyluyx4rywdty","Shining","//lh5.googleusercontent.com/k0EZxB3AaOuhcvE1D2a41qnwrt55KFh4UdouvIjYICsJCgnr85nYb-cQd270n5_7i5c9ILuvXQ","Amorphis","Tuonela","Amorphis","shining","amorphis","tuonela","amorphis",,,,264000,7,,1,,1999,,,,0,-1,0,0,,"Tkz7tjfjcrocareyluyx4rywdty","Tkz7tjfjcrocareyluyx4rywdty",5,,,"Bd4pz2likmhw3pxjbkreij5magu","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrgTR3daxxr5eYgF1lMrPuMcyQm1fQsRBnT6ftmPj1GqaskNHiny2kfyEOcWqlLOilpevKuPHCmxnoNtOLV_MYCqTZ3uQr1mRDMrjClwYVnCXeq17U\u003d"] ,["Tgslklua6wijwwqnbsmwedxaszu","Withered","//lh5.googleusercontent.com/k0EZxB3AaOuhcvE1D2a41qnwrt55KFh4UdouvIjYICsJCgnr85nYb-cQd270n5_7i5c9ILuvXQ","Amorphis","Tuonela","Amorphis","withered","amorphis","tuonela","amorphis",,,,344000,8,,1,,1999,,,,0,-1,0,0,,"Tgslklua6wijwwqnbsmwedxaszu","Tgslklua6wijwwqnbsmwedxaszu",5,,,"Bd4pz2likmhw3pxjbkreij5magu","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoReahvOfLcH2UXLZkfjaxS45Ihrvhc3wuOzmnyAHP41P4JcIRCAMVvfdhvUBXPtJcVflwoGJPNiI3EMDrf4BA7Vat9vBwn6XaY9Mq-lEIl5IuDdsE\u003d"] ,["Twfa254den7j2ef4fu44yjtkjoe","The Lost Name of God","//lh3.googleusercontent.com/P09FP2voKIAo1sXzGz10w0AGq_FCtV5sa8FaNv42A0DF0u6nvYPphScJnDt2MZWpnFzU3B11","Amorphis","Karelian Isthmus","Amorphis","the lost name of god","amorphis","karelian isthmus","amorphis",,,,332000,7,,1,,1992,,,,0,-1,0,0,,"Twfa254den7j2ef4fu44yjtkjoe","Twfa254den7j2ef4fu44yjtkjoe",5,,,"Bx5ns5fpgkmdkzmj3eqqvg6z37m","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKojSSQamXSsjsyHh-2hQgsoaJ9ToRex99KX17KlAY4fTKZpOVeEntUGiUJyz-8vmkIdAVaUYBI-3DXRPmkuxGRiHJbr3RXNLUtyXfaZZzlx7RZoJEw\u003d"] ,["Tymab25esiespgza3tka73reek4","God Of Deception","//lh3.googleusercontent.com/K6gxNc8qWM9ZKqBTNM1VeGog9Sjsg5lcJzRJQnqNRXLeMW2a2APYt0y02tKOsx5yuxFlnlAc","Amorphis","Metal Hymns Vol. 4","Various Artists","god of deception","amorphis","metal hymns vol. 4","various artists",,,,218000,4,,1,,,,,,0,-1,0,0,,"Tymab25esiespgza3tka73reek4","Tymab25esiespgza3tka73reek4",5,,,"Bvuuw5lvy3p36smy27kcql7esmq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrFfTyQEzMi_UH1bd2bK5aqfJyTMdCadekh6Kee3Hg4EuYdjqfX-oWBGQD9h-x5832GGKsvWxqaPd4bGnwPe9-EWOYzRIrLsQRareoDS0Y1f5r7evQ\u003d"] ,["Tluiei2bzy4zsdm6p63jnwfqf5u","Killing Goodness","//lh3.googleusercontent.com/oyBKVKnvr1YOGV3Xqp-TTsqWJnhIX5M_8GclclVtkPMDNr5gJ1dV7au33JSsl8snepqsgDJyBw","Amorphis","Far From The Sun","Amorphis","killing goodness","amorphis","far from the sun","amorphis",,,,235000,7,,1,,2004,,,,0,-1,0,0,,"Tluiei2bzy4zsdm6p63jnwfqf5u","Tluiei2bzy4zsdm6p63jnwfqf5u",5,,,"B7n3fftcxef7a2qpvtujmz62mya","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpsOkuiebK_vkPBgSOUHQixk65g4nXQpWlcv59JzXKGvJ0Q7Es5RHkOFqLqcdp9LFLykIRKSVLXSa8eQbiqVPiOihQQoFmv50zAED2L7b4VCc9I940\u003d"] ,["Tesswexmmz3dffuutpd4owmxpcu","To Father's Cabin","//lh6.googleusercontent.com/WSNzZmI7vNC_e1npacVLljrFNR5lcjyJ9OFTqaC_N8FuVXcKPTD8qITcy-HjPDzD-bDkou0Hxg","Amorphis","Tales From The Thousand Lakes","Amorphis","to father's cabin","amorphis","tales from the thousand lakes","amorphis",,,,230000,9,,1,,,,,,0,-1,0,0,,"Tesswexmmz3dffuutpd4owmxpcu","Tesswexmmz3dffuutpd4owmxpcu",5,,,"B2cnpu7ako4kejm6dvebsudhjti","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqGxGhHlNZ1D2GDVYt3Tp54NaMu7gljM1zgO8_GMnnT99tr0Mdro5r1IhYBibHJHc6-uEhM9NFCHwPklc--8ioMpdy3uPQ6ARX9RMbJscSbolqXNU4\u003d"] ,["T3k3wr2uks4mj3k3pwoo7lpfzqe","Shatters Within","//lh5.googleusercontent.com/jIshGkD2br3vsw9qK-ppNyrSxGpvZ15GbkLTLptAlocJ2qUgKta0qVyIEWKH4Rht6u15C_A5AQ","Amorphis","AM Universum","Amorphis","shatters within","amorphis","am universum","amorphis",,,,319000,4,,1,,2001,,,,0,-1,0,0,,"T3k3wr2uks4mj3k3pwoo7lpfzqe","T3k3wr2uks4mj3k3pwoo7lpfzqe",5,,,"B5s2cinoy6roo67mmc3gcao4sfq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoYpw2nvHO1Jt17Sb6zwc4X0Q_Y56RDLKkWZDw6fp71_UWkTC0VEXzUWhwET5d6sGFo24khosunOpBXZRCcAHNslsPJ3tY0-xQOt4KJfu6bEd8lbT4\u003d"] ,["Typo34qvwgsvmiiqzk5qnlwsm4u","Silver Bride (Live)","//lh5.googleusercontent.com/RrzvFnn-8w2qwVMbc-5Tv1vIKOzPFUvIu5MfWrUnYvH0WuafnxhLLxYBkCa_wnCrf2Bj80TS7g","Amorphis","Forging The Land Of Thousand Lakes","Amorphis","silver bride (live)","amorphis","forging the land of thousand lakes","amorphis",,,,266000,1,,1,,,,,,0,-1,0,0,,"Typo34qvwgsvmiiqzk5qnlwsm4u","Typo34qvwgsvmiiqzk5qnlwsm4u",5,,,"Bawp2fb6emntvumfrqzrd5p3rai","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKpG0cOkJVBL3kY5b2zRMGLsS23NqLf-oK68ZFWQu_GDX774fmziBe5u3AcVVkHOOJgZrWAH138wSD2e3lZUdRIBHtF6j4pUqU4edEWHeugzFSmddpg\u003d"] ,["Tqj6n7dqaypyvs2uteanfnvocwe","Song of the Troubled One","//lh3.googleusercontent.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c","Amorphis","Elegy","Amorphis","song of the troubled one","amorphis","elegy","amorphis",,,,247000,7,,1,,,,,,0,-1,0,0,,"Tqj6n7dqaypyvs2uteanfnvocwe","Tqj6n7dqaypyvs2uteanfnvocwe",5,,,"Bqzxfykbqcqmjjtdom7ukegaf2u","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKoCf21bS54leX7KMIkSd1VPBb4kXjLNyXXyPVU2NunuBaDqr0kgOAnji3vuLSrA-Ta0vcuj-Sg4n9OA0JX7rqxhyy8mlXDhzJocT3jADgNPbr5kt34\u003d"] ,["T4wlaw25cna5tessemhmbdur4rq","Stonewoman","//lh5.googleusercontent.com/KlmvCY1TwIhbXBUVQJeozs3ptQiFVLETyNNBbDJhEnYxbJR_ens07tzmXQfgZSrbXPqAlvk7Fxk","Amorphis","House of sleep","Amorphis","stonewoman","amorphis","house of sleep","amorphis",,,,218000,2,,1,,2006,,,,0,-1,0,0,,"T4wlaw25cna5tessemhmbdur4rq","T4wlaw25cna5tessemhmbdur4rq",5,,,"Bvx2opb3mvxrxfykz5iqb2v7kde","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKo2FqNgl4A9F3-lXrmepCfSLFww81K6cLWp23zdk7y7shaAE4FGcL0Rm9pdpipd_xmEa5ajtgDVSRVLqFonz12e3yRom-ymW-2gVa0oiK2qW5ghUJY\u003d"] ] ] ] ] ] gmusicapi-12.1.1/gmusicapi/test/imagetest_10x10_check.png0000600000175000017500000000042012667715127023471 0ustar simonsimon00000000000000‰PNG  IHDR 2ϽtEXtSoftwareAdobe ImageReadyqÉe<²IDATxÚbøÿÿ?>œ––& ¢A.žž¤Vñ&<Š„€Ô4(·§B hbâÝ3gÎÜÊÕ͆fšJâ¿@\c FéÕhŠ'13Ïšv$ÀÄ¥@ 2aPqöb ~Ä50 …Ñ@|ˆ½x#«@åj€¦½ƒ)dr®i ~ÄP… ëf!»Ž@kµ &ò±Ѐ³È  7KÿW,ûRIEND®B`‚gmusicapi-12.1.1/gmusicapi/test/local_tests.py0000664000175000017500000001347513374625453021727 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- """ Tests that don't hit the Google Music servers. """ from __future__ import print_function, division, absolute_import, unicode_literals from builtins import * # noqa from collections import namedtuple import os import time from mock import MagicMock from proboscis.asserts import ( assert_raises, assert_true, assert_false, assert_equal, assert_is_not, Check ) from proboscis import test import gmusicapi.session from gmusicapi.clients import Mobileclient, Musicmanager from gmusicapi.exceptions import AlreadyLoggedIn from gmusicapi.protocol.shared import authtypes from gmusicapi.protocol import mobileclient from gmusicapi.utils import utils, jsarray jsarray_samples = [] jsarray_filenames = [base + '.jsarray' for base in ('searchresult', 'fetchartist')] test_file_dir = os.path.dirname(os.path.abspath(__file__)) for filepath in [os.path.join(test_file_dir, p) for p in jsarray_filenames]: with open(filepath, 'r', encoding="utf-8") as f: jsarray_samples.append(f.read()) # TODO test gather_local, transcoding # All tests end up in the local group. test = test(groups=['local']) @test def longest_increasing_sub(): lisi = utils.longest_increasing_subseq assert_equal(lisi([]), []) assert_equal(lisi(list(range(10, 0, -1))), [1]) assert_equal(lisi(list(range(10, 20))), list(range(10, 20))) assert_equal(lisi([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]), [1, 2, 3, 5, 8, 9]) # # clients # # this feels like a dumb pattern, but I can't think of a better way names = ('Mobileclient', 'Musicmanager') # Webclient removed since testing is disabled. Clients = namedtuple('Clients', [n.lower() for n in names]) def create_clients(): clients = [] for name in names: cls = getattr(gmusicapi.clients, name) c = cls() # mock out the underlying session c.session = MagicMock() clients.append(c) return Clients(*clients) @test def no_client_auth_initially(): # wc = Webclient() # assert_false(wc.is_authenticated()) mc = Mobileclient() assert_false(mc.is_authenticated()) mm = Musicmanager() assert_false(mm.is_authenticated()) @test def mm_prevents_bad_mac_format(): mm = create_clients().musicmanager with Check() as check: for bad_mac in ['bogus', '11:22:33:44:55:66:', '11:22:33:44:55:ab', '11:22:33:44:55']: check.raises( ValueError, mm._perform_upauth, uploader_id=bad_mac, uploader_name='valid') # @test # def auto_playlists_are_empty(): # # this doesn't actually hit the server at the moment. # # see issue 102 # api = Api() # assert_equal(api.get_all_playlist_ids(auto=True, user=False), # {'auto': {}}) # # sessions # Sessions = namedtuple('Sessions', [n.lower() for n in names]) def create_sessions(): sessions = [] for name in names: cls = getattr(gmusicapi.session, name) s = cls() # mock out the underlying requests.session s._rsession = MagicMock() sessions.append(s) return Sessions(*sessions) @test def no_session_auth_initially(): for s in create_sessions(): assert_false(s.is_authenticated) @test def session_raises_alreadyloggedin(): for s in create_sessions(): s.is_authenticated = True def login(): # hackish: login ignores args so we can test them all here; # this just ensures we have an acceptable amount of args s.login(*([None] * 3)) assert_raises(AlreadyLoggedIn, login) @test def session_logout(): for s in create_sessions(): s.is_authenticated = True old_session = s._rsession s.logout() assert_false(s.is_authenticated) old_session.close.assert_called_once_with() assert_is_not(s._rsession, old_session) @test def send_without_auth(): for s in create_sessions(): s.is_authenticated = True mock_session = MagicMock() mock_req_kwargs = {'fake': 'kwargs'} s.send(mock_req_kwargs, authtypes(), mock_session) # sending without auth should not use the normal session, # since that might have auth cookies automatically attached assert_false(s._rsession.called) mock_session.request.called_once_with(**mock_req_kwargs) mock_session.closed.called_once_with() # # protocol # @test def authtypes_factory_defaults(): auth = authtypes() assert_false(auth.oauth) assert_false(auth.sso) assert_false(auth.xt) @test def authtypes_factory_args(): auth = authtypes(oauth=True) assert_true(auth.oauth) assert_false(auth.sso) assert_false(auth.xt) @test def mc_url_signing(): sig, _ = mobileclient.GetStreamUrl.get_signature("Tdr6kq3xznv5kdsphyojox6dtoq", "1373247112519") assert_equal(sig, b"gua1gInBdaVo7_dSwF9y0kodua0") # # utils # @test def retry_failure_propogation(): @utils.retry(tries=1) def raise_exception(): raise AssertionError assert_raises(AssertionError, raise_exception) @test def retry_sleep_timing(): @utils.retry(tries=3, delay=.05, backoff=2) def raise_exception(): raise AssertionError pre = time.time() assert_raises(AssertionError, raise_exception) post = time.time() delta = post - pre assert_true(.15 < delta < .2, "delta: %s" % delta) @test def retry_is_dual_decorator(): @utils.retry def return_arg(arg=None): return arg assert_equal(return_arg(1), 1) @test def jsarray_parsing(): for raw in jsarray_samples: # should not raise an exception jsarray.loads(raw) @test def locate_transcoder(): utils.locate_mp3_transcoder() # should not raise gmusicapi-12.1.1/gmusicapi/test/rewrite_audiotest_tags.py0000664000175000017500000000150313374625453024160 0ustar simonsimon00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """A script that will rewrite audiotest* metadata to match their filenames.""" from __future__ import print_function, division, absolute_import, unicode_literals from builtins import * # noqa from glob import glob import os import mutagen for fname in glob(u'audiotest*'): audio = mutagen.File(fname, easy=True) if audio is None: print('could not open', fname) continue # clear existing tags for key in list(audio.tags.keys()): del audio.tags[key] # write base = os.path.basename(fname) audio['title'] = base + ' title' audio['artist'] = base + ' artist' audio.save() # read back to verify audio = mutagen.File(fname, easy=True) # assume it worked; it worked above print(fname) print(' ', audio.tags) gmusicapi-12.1.1/gmusicapi/test/run_tests.py0000664000175000017500000001071113400371522021410 0ustar simonsimon00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import, unicode_literals from future.utils import PY3, bind_method from builtins import * # noqa from collections import namedtuple import functools import logging import os import sys from proboscis import TestProgram from gmusicapi.clients import Musicmanager, Mobileclient from gmusicapi import session from gmusicapi.test import local_tests, server_tests # noqa from gmusicapi.test.utils import NoticeLogging EnvArg = namedtuple('EnvArg', 'envarg is_required kwarg description') # these names needed to be compressed to fit everything into the travisci key size. # there's also: # * GM_A: when set (to anything) states that we are testing on a subscription account. # * GM_AA_D_ID: a registered device id for use with mc streaming # wc_envargs = ( # EnvArg('GM_U', 'email', 'WC user. If not present, user will be prompted.'), # EnvArg('GM_P', 'password', 'WC password. If not present, user will be prompted.'), # ) mc_envargs = ( EnvArg('GM_AA_D_ID', True, 'device_id', 'a registered device id for use with MC streaming'), EnvArg('GM_R', False, 'oauth_credentials', 'an MC refresh token (defaults to MC.login default)'), ) mm_envargs = ( EnvArg('GM_O', False, 'oauth_credentials', 'an MM refresh token (defaults to MM.login default)'), EnvArg('GM_I', False, 'uploader_id', 'an MM uploader id (defaults to MM.login default)'), EnvArg('GM_N', False, 'uploader_name', 'an MM uploader name (default to MM.login default)'), ) # Webclient auth retreival removed while testing disabled. # # def prompt_for_wc_auth(): # """Return a valid (user, pass) tuple by continually # prompting the user.""" # # print("These tests will never delete or modify your music." # "\n\n" # "If the tests fail, you *might* end up with a test" # " song/playlist in your library, though." # "\n") # # wclient = Webclient() # valid_wc_auth = False # # while not valid_wc_auth: # print() # email = input("Email: ") # passwd = getpass() # # valid_wc_auth = wclient.login(email, passwd) # # return email, passwd def _get_kwargs(envargs): kwargs = {} for arg in envargs: if arg.is_required and arg.envarg not in os.environ: raise ValueError("%s was not exported and must be %s" % (arg.envarg, arg.description)) val = os.environ.get(arg.envarg) if arg.kwarg == 'oauth_credentials' and val is not None: oauth_info = session.Musicmanager.oauth if arg.envarg == 'GM_O' else session.Mobileclient.oauth kwargs['oauth_credentials'] = session.credentials_from_refresh_token(val, oauth_info) else: kwargs[arg.kwarg] = val return kwargs def retrieve_auth(): """Searches the env for auth. On success, return (mc_kwargs, mm_kwargs). On failure, raise ValueError.""" mc_kwargs = _get_kwargs(mc_envargs) mm_kwargs = _get_kwargs(mm_envargs) return (mc_kwargs, mm_kwargs) def freeze_method_kwargs(klass, method_name, **kwargs): method = getattr(klass, method_name) partialfunc = functools.partialmethod if PY3 else functools.partial bind_method(klass, method_name, partialfunc(method, **kwargs)) def freeze_login_details(mc_kwargs, mm_kwargs): """Set the given kwargs to be the default for client login methods.""" freeze_method_kwargs(Musicmanager, 'login', **mm_kwargs) freeze_method_kwargs(Mobileclient, 'oauth_login', **mc_kwargs) def main(): """Search env for auth envargs and run tests.""" if '--group=local' not in sys.argv: # hack: assume we're just running the proboscis local group freeze_login_details(*retrieve_auth()) # warnings typically signal a change in protocol, # so fail the build if anything >= warning are sent, noticer = NoticeLogging() noticer.setLevel(logging.WARNING) root_logger = logging.getLogger('gmusicapi') root_logger.addHandler(noticer) # proboscis does not have an exit=False equivalent, # so SystemExit must be caught instead (we need # to check the log noticer) try: TestProgram(module=sys.modules[__name__]).run_and_exit() except SystemExit as e: print() if noticer.seen_message: print('(failing build due to log warnings)') sys.exit(1) if e.code is not None: sys.exit(e.code) if __name__ == '__main__': main() gmusicapi-12.1.1/gmusicapi/test/searchresult.jsarray0000600000175000017500000001735512667715127023133 0ustar simonsimon00000000000000[[0] ,[[["Tn2ugrgkeinrrb2a4ji7khungoy","Silver Bride","//lh4.googleusercontent.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44","Amorphis","Skyforger","Amorphis","silver bride","amorphis","skyforger","amorphis",,,,253000,2,,1,,,,,,1,-1,0,0,,"Tn2ugrgkeinrrb2a4ji7khungoy","Tn2ugrgkeinrrb2a4ji7khungoy",5,,,"B5nc22xlcmdwi3zn5htkohstg44","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrmtTACdPNydA-oSQvt7tX64u1_GOk7wLTP0giF_np6am6V5f0Z63e1yMfTLc5sKl5TsyNbgZJO6G1kdiUnXkjSzUP9uJsCHCfU7EDfoW09DjC_RUk\u003d"] ,["T4j5jxodzredqklxxhncsua5oba","Black Winter Day","//lh4.googleusercontent.com/epPwDFc2GCsvvaBSf_LPgu5bCh0qXpJxi1DSU5gUGhwJZ5moYPtcPRAY3yww1YTEzM1g_erBuQ","Amorphis","Magic And Mayhem - Tales From The Early Years","Amorphis","black winter day","amorphis","magic and mayhem - tales from the early years","amorphis",,,,235000,4,,1,,2010,,,,0,-1,0,0,,"T4j5jxodzredqklxxhncsua5oba","T4j5jxodzredqklxxhncsua5oba",5,,,"B7dplgr5h2jzzkcyrwhifgwl2v4","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKp_GTx1IMg-nBWgBxu2x6xC9zgaFljJDfbZRR_BfnC_qJzLFE_gNcKbUnt59Mn9ACkvIv-GimHgY1dN2gsgQGlPHT-OBtyA75nRRpT9J4g-zLcd2Q0\u003d"] ,["T4ugyccbgxdbglt4c3fqe4jckxe","The Smoke","//lh5.googleusercontent.com/-vLUmfh_fqSJ_Xk6BenBs87OrPuUl5--_NhoJsX7SypPmGQZVABi3szLY3uvtFRCQBuguc4k","Amorphis","Eclipse","Amorphis","the smoke","amorphis","eclipse","amorphis",,,,218000,7,,1,,,,,,0,-1,0,0,,"T4ugyccbgxdbglt4c3fqe4jckxe","T4ugyccbgxdbglt4c3fqe4jckxe",5,,,"Bvpiqruq3mw7a6kmdapua2ejafq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKq0PFYLIq2R_Zgvn6mNPsDZL7D0Mu5XM4EQqc9ngT9ttlpv_LhduCgD_1BudlAwfxgDopSma8fhpAC0vn_1RGNloCMnIHa1Zg9dRFM2iH_VNQL_11M\u003d"] ,["Tid7h5ve4jislxqofzkc5as5fbe","House Of Sleep","//lh5.googleusercontent.com/9QVck4kPpVKU-JlyUo2-n1LFuCKi_hc9geeDRse0dPpqCru1AVq5Zu-Awp5TUR0X1QBlqLk2","Amorphis","Nuclear Blast Showdown 2006","Various Artists","house of sleep","amorphis","nuclear blast showdown 2006","various artists",,,,256000,2,,1,,,,,,0,-1,0,0,,"Tid7h5ve4jislxqofzkc5as5fbe","Tid7h5ve4jislxqofzkc5as5fbe",5,,,"Bcqvnirijpn6edaw3d47vxdl22y","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrg3Ynw22zd-T4GVOdyQ8WoKQ4ZY5LNoIwPvp4XF47Hpn5waindfwQlNCMRv7VvHViDikif2qKs91VkPDtbSGufsqOx-lbMrdWV-sYpFsdQlvLwFfM\u003d"] ,["T4ggbo3d5rn6vpjk4gymj24brvi","Silent Waters","//lh3.googleusercontent.com/KKida9Y4-ngxEIgyHe467fxT_BsDirfoPoCkp9PDFTkZfMcBu8XqUktOyOzMusZP7KCq_dEleQ","Amorphis","Silent Waters","Amorphis","silent waters","amorphis","silent waters","amorphis",,,,292000,3,,1,,,,,,0,-1,0,0,,"T4ggbo3d5rn6vpjk4gymj24brvi","T4ggbo3d5rn6vpjk4gymj24brvi",5,,,"B2qyakjo4xjp4oqobfkebtu6v74","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrGshKZKglyiF6j-mJcgvOh9Oc-UBvN_m0HJhKN1m-K3BhVb0aejgnPhBVh3ApKk8RFXZM2L5rkd4-Z-ndyWLxht6RhqdCDtNzGGO3SgMbvjjKEmqU\u003d"] ,["Tgofa4xs6iwnjan5bnoxzmzgyhy","Black Winter's Day","//lh3.googleusercontent.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c","Amorphis","Elegy","Amorphis","black winter's day","amorphis","elegy","amorphis",,,,218000,15,,1,,,,,,0,-1,0,0,,"Tgofa4xs6iwnjan5bnoxzmzgyhy","Tgofa4xs6iwnjan5bnoxzmzgyhy",5,,,"Bqzxfykbqcqmjjtdom7ukegaf2u","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKrmXJ0hbrJJZ-dtHwoz7YlFIHL9EKyOnFUgvnUQm4VHDhjtWaJtdPAAid4bY7JUWCOj6AQLIsTIbcHFoppDEImXLON3eReiteCK-FRsSlaqynuUEO8\u003d"] ,["Tmkrownnht4mvduqsqowyb7e6ym","The White Swan","//lh3.googleusercontent.com/KKida9Y4-ngxEIgyHe467fxT_BsDirfoPoCkp9PDFTkZfMcBu8XqUktOyOzMusZP7KCq_dEleQ","Amorphis","Silent Waters","Amorphis","the white swan","amorphis","silent waters","amorphis",,,,291000,9,,1,,,,,,0,-1,0,0,,"Tmkrownnht4mvduqsqowyb7e6ym","Tmkrownnht4mvduqsqowyb7e6ym",5,,,"B2qyakjo4xjp4oqobfkebtu6v74","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqhbVXJ17u2TdM3RQ4P3VlEsJlXOJ4VYJtYZox6YJ-qwRZME09Kp0kcUfIyAZ6Zuw_tqujctOoGgQnn6qeyc0U2mQZVLJ5nsTNYuiJ8CQVhTk-KWDg\u003d"] ,["Tu4kvc6wwmnekvsnmfg7h635yce","Alone","//lh5.googleusercontent.com/jIshGkD2br3vsw9qK-ppNyrSxGpvZ15GbkLTLptAlocJ2qUgKta0qVyIEWKH4Rht6u15C_A5AQ","Amorphis","AM Universum","Amorphis","alone","amorphis","am universum","amorphis",,,,378000,1,,1,,2001,,,,0,-1,0,0,,"Tu4kvc6wwmnekvsnmfg7h635yce","Tu4kvc6wwmnekvsnmfg7h635yce",5,,,"B5s2cinoy6roo67mmc3gcao4sfq","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqR46CxWmfC3V3Nkgj53qou41vCAjh9SjpwQ-DKx9oOpBOFhPIYd0AFcdAlBmYyoG1Ra47o0jfFnXRvQ9REZ6F8JluCW57-ugPfZhEL4Ft1Pyw9Amc\u003d"] ,["Tygvooab57vculuyvk25tkcsjh4","Sky Is Mine","//lh4.googleusercontent.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44","Amorphis","Skyforger","Amorphis","sky is mine","amorphis","skyforger","amorphis",,,,260000,4,,1,,,,,,0,-1,0,0,,"Tygvooab57vculuyvk25tkcsjh4","Tygvooab57vculuyvk25tkcsjh4",5,,,"B5nc22xlcmdwi3zn5htkohstg44","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqYmI0QK0_4bfZJ_bXW8YuQ5Vc9ZkNlYclSoBqEc4px-6CwCW41Oq9AxhK3DN-C84DpxeuZUY6OXlsDX4GLvOwqZtCKYjIBKk8z7F7nXqsJ0E3dyB8\u003d"] ,["Tnpwezddt35tr3lwgvodru2754u","Elegy","//lh3.googleusercontent.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c","Amorphis","Elegy","Amorphis","elegy","amorphis","elegy","amorphis",,,,441000,9,,1,,,,,,0,-1,0,0,,"Tnpwezddt35tr3lwgvodru2754u","Tnpwezddt35tr3lwgvodru2754u",5,,,"Bqzxfykbqcqmjjtdom7ukegaf2u","Apoecs6off3y6k4h5nvqqos4b5e",,,,,2,[] ,"AE9vGKqylt-U0uiIE7Pi7Ab4Rp0i9oACQP44wje84T94GlkNOtNaa8muR7IjXYLPKcpST37YCHy1TkWC2CBJDkRVsVv7exRjurKZVBWdLo1mpKKUFBnfozs\u003d"] ] ,[[,"Circle","Amorphis","http://lh6.ggpht.com/55Ea0FcQgrnPcw4h6kwtgEKbOS-dCyGBpk8mYBWe_pgj7dEjZc3yFR_bwVR0PYWk-f1Bqi2i",0,0,[] ,"Bfr2onjv7g7tm4rzosewnnwxxyy",[] ,2013,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Elegy","Amorphis","http://lh3.ggpht.com/33BgaRFWvACwo3fiXCbQ1vvtjzr441aKcQvbhKl59nC7jy0O8EsuPKGYRCjVe0h0m7iauLBaV4c",0,0,[] ,"Bqzxfykbqcqmjjtdom7ukegaf2u",[] ,1996,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Tales From The Thousand Lakes","Amorphis","http://lh6.ggpht.com/WSNzZmI7vNC_e1npacVLljrFNR5lcjyJ9OFTqaC_N8FuVXcKPTD8qITcy-HjPDzD-bDkou0Hxg",0,0,[] ,"B2cnpu7ako4kejm6dvebsudhjti",[] ,1994,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Silent Waters","Amorphis","http://lh3.ggpht.com/KKida9Y4-ngxEIgyHe467fxT_BsDirfoPoCkp9PDFTkZfMcBu8XqUktOyOzMusZP7KCq_dEleQ",0,0,[] ,"B2qyakjo4xjp4oqobfkebtu6v74",[] ,2007,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"The Beginning Of Times","Amorphis","http://lh3.ggpht.com/LgN08osfWUWN6S-IAOa99h4Qr1Wj21pcHp7r0AnErxK1HuJurqklV0Ycgk2JxB1Pu8IpJleKlQ",0,0,[] ,"Bet2e22dinehb7qiksqirljvcnq",[] ,2011,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Eclipse","Amorphis","http://lh5.ggpht.com/-vLUmfh_fqSJ_Xk6BenBs87OrPuUl5--_NhoJsX7SypPmGQZVABi3szLY3uvtFRCQBuguc4k",0,0,[] ,"Bvpiqruq3mw7a6kmdapua2ejafq",[] ,2006,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Skyforger","Amorphis","http://lh4.ggpht.com/Jn8CyZyMX4SGGsypqZYZN3Eoho-rdZmyctpGzRSYHUpWKJw_n_-U9M63On60T7xZSFI0ZKH9S44",0,0,[] ,"B5nc22xlcmdwi3zn5htkohstg44",[] ,2009,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Forging The Land Of Thousand Lakes","Amorphis","http://lh5.ggpht.com/RrzvFnn-8w2qwVMbc-5Tv1vIKOzPFUvIu5MfWrUnYvH0WuafnxhLLxYBkCa_wnCrf2Bj80TS7g",0,0,[] ,"Bawp2fb6emntvumfrqzrd5p3rai",[] ,2010,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Far From The Sun","Amorphis","http://lh3.ggpht.com/oyBKVKnvr1YOGV3Xqp-TTsqWJnhIX5M_8GclclVtkPMDNr5gJ1dV7au33JSsl8snepqsgDJyBw",0,0,[] ,"B7n3fftcxef7a2qpvtujmz62mya",[] ,2004,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ,[,"Karelian Isthmus","Amorphis","http://lh3.ggpht.com/P09FP2voKIAo1sXzGz10w0AGq_FCtV5sa8FaNv42A0DF0u6nvYPphScJnDt2MZWpnFzU3B11",0,0,[] ,"Bx5ns5fpgkmdkzmj3eqqvg6z37m",[] ,1992,"Apoecs6off3y6k4h5nvqqos4b5e",[] ,,2] ] ,[] ,[,,[,"Apoecs6off3y6k4h5nvqqos4b5e","Amorphis",0,[] ,"http://lh6.ggpht.com/lXBCm8k7pS3Y99swHQdeq7qRx9XdHMnMilOmH_vtfysihVWq4PY-9XvYnD6RzmC5o5L3jYNUovg",,,[] ,[] ,,[] ] ] ,,,[,"Apoecs6off3y6k4h5nvqqos4b5e","Amorphis",0,[] ,"http://lh6.ggpht.com/lXBCm8k7pS3Y99swHQdeq7qRx9XdHMnMilOmH_vtfysihVWq4PY-9XvYnD6RzmC5o5L3jYNUovg",,,[] ,[] ,,[] ] ] ] gmusicapi-12.1.1/gmusicapi/test/server_tests.py0000664000175000017500000010720313456665572022143 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- """ These tests all run against an actual Google Music account. Destructive modifications are not made, but if things go terrible wrong, an extra test playlist or song may result. """ from __future__ import print_function, division, absolute_import, unicode_literals from builtins import * # noqa from collections import namedtuple import datetime from hashlib import md5 import itertools import os import re import types import warnings from decorator import decorator from proboscis.asserts import ( assert_true, assert_equal, assert_is_not_none, assert_raises, assert_not_equal, Check, ) from proboscis import test, before_class, after_class, SkipTest import requests from requests.exceptions import SSLError from requests.packages.urllib3.exceptions import InsecureRequestWarning from gmusicapi import Musicmanager, Mobileclient # from gmusicapi.protocol import mobileclient from gmusicapi.protocol.shared import authtypes from gmusicapi.utils.utils import retry, id_or_nid import gmusicapi.test.utils as test_utils TEST_PLAYLIST_NAME = 'gmusicapi_test_playlist' TEST_PLAYLIST_DESCRIPTION = 'gmusicapi test playlist' TEST_STATION_NAME = 'gmusicapi_test_station' TEST_STORE_GENRE_ID = 'METAL' # that dumb little intro track on Conspiracy of One, # picked since it's only a few seconds long TEST_STORE_SONG_ID = 'Tf3pxtcrp2tw7i6kdxzueoz7uia' # used for testing streaming. # differences between clients are presumably from stream quality. TEST_STORE_SONG_WC_HASH = 'c3302fe6bd54ce9b310f92da1904f3b9' TEST_STORE_SONG_MC_HASH = 'ccce8dbec7b8939531db8eeaf877696f' # The Nerdist. TEST_PODCAST_SERIES_ID = 'Iliyrhelw74vdqrro77kq2vrdhy' # An episode of Note to Self. # Picked because it's very short (~4 minutes). TEST_PODCAST_EPISODE_ID = 'Diksw5cywxflebfs3dbiiabfphu' TEST_PODCAST_EPISODE_HASH = 'e8ff4efd6a3a6a1017b35e0ef564d840' # Amorphis TEST_STORE_ARTIST_ID = 'Apoecs6off3y6k4h5nvqqos4b5e' # Holographic Universe TEST_STORE_ALBUM_ID = 'B4cao5ms5jjn36notfgnhjtguwa' # this is owned by my test account, so it shouldn't disappear TEST_PLAYLIST_SHARETOKEN = ('AMaBXymHAkflgs5lvFAUyyQLYelqqMZNAB4v7Y_-' 'v9vmrctLOeW64GScAScoFHEnrLgOP5DSRpl9FYIH' 'b84HRBvyIMsxc7Zlrg==') TEST_CURATED_STATION_ID = 'L75iymnapfmeiklef5rhaqxqiry' # this is a little data class for the songs we upload TestSong = namedtuple('TestSong', 'sid title artist album full_data') def sids(test_songs): """Given [TestSong], return ['sid'].""" return [s.sid for s in test_songs] def test_subscription_features(): return 'GM_A' in os.environ @decorator def subscription(f, *args, **kwargs): """Declare a test to only be run if subscription testing is enabled.""" if test_subscription_features(): return f(*args, **kwargs) else: raise SkipTest('Subscription testing disabled') @test(groups=['server-other']) class SslVerificationTests(object): test_url = 'https://wrong.host.badssl.com/' @test def site_has_invalid_cert(self): assert_raises(SSLError, requests.head, self.test_url) def request_invalid_site(self, client): req_kwargs = {'url': self.test_url, 'method': 'HEAD'} no_auth = authtypes() client.session.send(req_kwargs, no_auth) @test def clients_verify_by_default(self): # Webclient removed since testing is disabled. for client_cls in (Mobileclient, Musicmanager): assert_raises(SSLError, self.request_invalid_site, client_cls()) @test def disable_client_verify(self): with warnings.catch_warnings(): warnings.simplefilter("ignore", category=InsecureRequestWarning) # Webclient removed since testing is disabled. for client_cls in (Mobileclient, Musicmanager): self.request_invalid_site(client_cls(verify_ssl=False)) # should not raise SSLError @test(groups=['server']) class ClientTests(object): # set on the instance in login # wc = None # webclient mm = None # musicmanager mc = None # mobileclient # These are set on the instance in eg create_song. # both are [TestSong] user_songs = None store_songs = None playlist_ids = None plentry_ids = None station_ids = None podcast_ids = None delete_podcast = True # Set to false if user already subscribed to test podcast. @property def all_songs(self): return (self.user_songs or []) + (self.store_songs or []) def mc_get_playlist_songs(self, plid): """For convenience, since mc can only get all playlists at once.""" all_contents = self.mc.get_all_user_playlist_contents() found = [p for p in all_contents if p['id'] == plid] assert_true(len(found), 1) return found[0]['tracks'] @before_class def login(self): # self.wc = test_utils.new_test_client(Webclient) # assert_true(self.wc.is_authenticated()) self.mm = test_utils.new_test_client(Musicmanager) assert_true(self.mm.is_authenticated()) self.mc = test_utils.new_test_client(Mobileclient) assert_true(self.mc.is_authenticated()) @after_class(always_run=True) def logout(self): # if self.wc is None: # raise SkipTest('did not create wc') # assert_true(self.wc.logout()) if self.mm is None: raise SkipTest('did not create mm') assert_true(self.mm.logout()) if self.mc is None: raise SkipTest('did not create mc') assert_true(self.mc.logout()) # This next section is a bit odd: it orders tests that create # required resources. # The intuitition: starting from an empty library, you need to create # a song before you can eg add it to a playlist. # The dependencies end up with an ordering that might look like: # # with song # with playlist # with plentry # with station # # # Suggestions to improve any of this are welcome! @staticmethod @retry def assert_songs_state(method, sids, present): """ Assert presence/absence of sids and return a list of TestSongs found. :param method: eg self.mc.get_all_songs :param sids: list of song ids :param present: if True verify songs are present; False the opposite """ library = method() found = [s for s in library if s['id'] in sids] expected_len = len(sids) if not present: expected_len = 0 assert_equal(len(found), expected_len) return [TestSong(s['id'], s['title'], s['artist'], s['album'], s) for s in found] @staticmethod @retry def assert_list_inc_equivalence(method, **kwargs): """ Assert that some listing method returns the same contents for incremental=True/False. :param method: eg self.mc.get_all_songs, must support `incremental` kwarg :param **kwargs: passed to method """ lib_chunk_gen = method(incremental=True, **kwargs) assert_true(isinstance(lib_chunk_gen, types.GeneratorType)) assert_equal([e for chunk in lib_chunk_gen for e in chunk], method(incremental=False, **kwargs)) @test def song_create(self): # This can create more than one song: one through uploading, one through # adding a store track to the library. user_sids = [] store_sids = [] fname = test_utils.small_mp3 uploaded, matched, not_uploaded = self.mm.upload(fname) if len(not_uploaded) == 1 and 'ALREADY_EXISTS' in not_uploaded[fname]: # delete the song if it exists already because a previous test failed self.mc.delete_songs(re.search(r'\(.*\)', not_uploaded[fname]).group().strip('()')) # and retry the upload uploaded, matched, not_uploaded = self.mm.upload(fname) # Otherwise, it should have been uploaded normally. assert_equal(not_uploaded, {}) assert_equal(matched, {}) assert_equal(list(uploaded.keys()), [fname]) user_sids.append(uploaded[fname]) if test_subscription_features(): store_sids.append(self.mc.add_store_tracks(TEST_STORE_SONG_ID)[0]) # we test get_all_songs here so that we can assume the existance # of the song for future tests (the servers take time to sync an upload) self.user_songs = self.assert_songs_state(self.mc.get_all_songs, user_sids, present=True) self.store_songs = self.assert_songs_state(self.mc.get_all_songs, store_sids, present=True) @test def playlist_create(self): mc_id = self.mc.create_playlist(TEST_PLAYLIST_NAME, "", public=True) # wc_id = self.wc.create_playlist(TEST_PLAYLIST_NAME, "", public=True) # like song_create, retry until the playlist appears @retry def assert_playlist_exists(plids): found = [p for p in self.mc.get_all_playlists() if p['id'] in plids] assert_equal(len(found), 1) assert_playlist_exists([mc_id]) self.playlist_ids = [mc_id] @test(depends_on=[playlist_create, song_create], runs_after_groups=['playlist.exists', 'song.exists']) def plentry_create(self): song_ids = [self.user_songs[0].sid] # create 3 entries total # 3 songs is the minimum to fully test reordering, and also includes the # duplicate song_id case double_id = self.user_songs[0].sid if test_subscription_features(): double_id = TEST_STORE_SONG_ID song_ids += [double_id] * 2 plentry_ids = self.mc.add_songs_to_playlist(self.playlist_ids[0], song_ids) @retry def assert_plentries_exist(plid, plentry_ids): songs = self.mc_get_playlist_songs(plid) found = [e for e in songs if e['id'] in plentry_ids] assert_equal(len(found), len(plentry_ids)) assert_plentries_exist(self.playlist_ids[0], plentry_ids) self.plentry_ids = plentry_ids @test(groups=['plentry'], depends_on=[plentry_create], runs_after_groups=['plentry.exists'], always_run=True) def plentry_delete(self): if self.plentry_ids is None: raise SkipTest('did not store self.plentry_ids') res = self.mc.remove_entries_from_playlist(self.plentry_ids) assert_equal(res, self.plentry_ids) @retry def assert_plentries_removed(plid, entry_ids): found = [e for e in self.mc_get_playlist_songs(plid) if e['id'] in entry_ids] assert_equal(len(found), 0) assert_plentries_removed(self.playlist_ids[0], self.plentry_ids) @test(groups=['playlist'], depends_on=[playlist_create], runs_after=[plentry_delete], runs_after_groups=['playlist.exists'], always_run=True) def playlist_delete(self): if self.playlist_ids is None: raise SkipTest('did not store self.playlist_ids') for plid in self.playlist_ids: res = self.mc.delete_playlist(plid) assert_equal(res, plid) @retry def assert_playlist_does_not_exist(plid): found = [p for p in self.mc.get_all_playlists() if p['id'] == plid] assert_equal(len(found), 0) for plid in self.playlist_ids: assert_playlist_does_not_exist(plid) @test @subscription def station_create(self): station_ids = [] for prefix, kwargs in ( ('Store song', {'track_id': TEST_STORE_SONG_ID}), ('Store-added song', {'track_id': self.store_songs[0].sid}), ('up song', {'track_id': self.user_songs[0].sid}), ('artist', {'artist_id': TEST_STORE_ARTIST_ID}), ('album', {'album_id': TEST_STORE_ALBUM_ID}), ('genre', {'genre_id': TEST_STORE_GENRE_ID}), ('playlist', {'playlist_token': TEST_PLAYLIST_SHARETOKEN}), ('curated station', {'curated_station_id': TEST_CURATED_STATION_ID})): station_ids.append( self.mc.create_station(prefix + ' ' + TEST_STATION_NAME, **kwargs)) @retry def assert_station_exists(station_id): stations = self.mc.get_all_stations() found = [s for s in stations if s['id'] == station_id] assert_equal(len(found), 1) for station_id in station_ids: assert_station_exists(station_id) self.station_ids = station_ids @test(groups=['station'], depends_on=[station_create, song_create], runs_after_groups=['station.exists', 'song.exists'], always_run=True) def station_delete(self): if self.station_ids is None: raise SkipTest('did not store self.station_ids') res = self.mc.delete_stations(self.station_ids) assert_equal(res, self.station_ids) @retry def assert_station_deleted(station_id): stations = self.mc.get_all_stations() found = [s for s in stations if s['id'] == station_id] assert_equal(len(found), 0) for station_id in self.station_ids: assert_station_deleted(station_id) @test(groups=['song'], depends_on=[song_create], runs_after=[plentry_delete, station_delete], runs_after_groups=["song.exists"], always_run=True) def song_delete(self): # split deletion between wc and mc # mc is only to run if subscription testing not enabled with Check() as check: for i, testsong in enumerate(self.all_songs): if True: res = self.mc.delete_songs(testsong.sid) else: with warnings.catch_warnings(): warnings.simplefilter("ignore") res = self.wc.delete_songs(testsong.sid) check.equal(res, [testsong.sid]) self.assert_songs_state(self.mc.get_all_songs, sids(self.all_songs), present=False) @test def podcast_create(self): # Check to make sure podcast doesn't already exist to prevent deletion. already_exists = [pc for pc in self.mc.get_all_podcast_series() if pc['seriesId'] == TEST_PODCAST_SERIES_ID] if already_exists: self.delete_podcast = False # like song_create, retry until the podcast appears @retry def assert_podcast_exists(pcids): found = [pc for pc in self.mc.get_all_podcast_series() if pc['seriesId'] in pcids] assert_equal(len(found), 1) pc_id = self.mc.add_podcast_series(TEST_PODCAST_SERIES_ID) assert_podcast_exists([pc_id]) self.podcast_ids = [pc_id] @test(groups=['podcast'], depends_on=[podcast_create], runs_after_groups=['podcast.exists'], always_run=True) def podcast_delete(self): if self.podcast_ids is None: raise SkipTest('did not store self.podcast_ids') if not self.delete_podcast: raise SkipTest('not deleting already existing podcast') for pcid in self.podcast_ids: res = self.mc.delete_podcast_series(pcid) assert_equal(res, pcid) @retry def assert_podcast_does_not_exist(pcid): found = [pc for pc in self.mc.get_all_podcast_series() if pc['seriesId'] == pcid] assert_equal(len(found), 0) for pcid in self.podcast_ids: assert_podcast_does_not_exist(pcid) # These decorators just prevent setting groups and depends_on over and over. # They won't work right with additional settings; if that's needed this # pattern should be factored out. # TODO it'd be nice to have per-client test groups song_test = test(groups=['song', 'song.exists'], depends_on=[song_create]) playlist_test = test(groups=['playlist', 'playlist.exists'], depends_on=[playlist_create]) plentry_test = test(groups=['plentry', 'plentry.exists'], depends_on=[plentry_create]) station_test = test(groups=['station', 'station.exists'], depends_on=[station_create]) podcast_test = test(groups=['podcast', 'podcast.exists'], depends_on=[podcast_create]) # Non-wonky tests resume down here. # --------- # MM tests # --------- def mm_get_quota(self): # just testing the call is successful self.mm.get_quota() @song_test def mm_list_new_songs(self): # mm only includes user-uploaded songs self.assert_songs_state(self.mm.get_uploaded_songs, sids(self.user_songs), present=True) self.assert_songs_state(self.mm.get_uploaded_songs, sids(self.store_songs), present=False) @test def mm_list_songs_inc_equal(self): self.assert_list_inc_equivalence(self.mm.get_uploaded_songs) @song_test def mm_download_song(self): @retry def assert_download(sid): filename, audio = self.mm.download_song(sid) # TODO could use original filename to verify this # but, when manually checking, got modified title occasionally assert_true(filename.endswith('.mp3')) assert_is_not_none(audio) assert_download(self.user_songs[0].sid) # --------- # WC tests # --------- # @test # def wc_get_registered_devices(self): # # no logic; just checking schema # self.wc.get_registered_devices() # @test # def wc_get_shared_playlist_info(self): # expected = { # u'author': u'gmusic api', # u'description': u'description here', # u'title': u'public title here', # u'num_tracks': 2 # } # assert_equal( # self.wc.get_shared_playlist_info(TEST_PLAYLIST_SHARETOKEN), # expected # ) # @test # @subscription # def wc_get_store_stream_urls(self): # urls = self.wc.get_stream_urls(TEST_STORE_SONG_ID) # assert_true(len(urls) > 1) # @test # @subscription # def wc_stream_store_track_with_header(self): # audio = self.wc.get_stream_audio(TEST_STORE_SONG_ID, use_range_header=True) # assert_equal(md5(audio).hexdigest(), TEST_STORE_SONG_WC_HASH) # @test # @subscription # def wc_stream_store_track_without_header(self): # audio = self.wc.get_stream_audio(TEST_STORE_SONG_ID, use_range_header=False) # assert_equal(md5(audio).hexdigest(), TEST_STORE_SONG_WC_HASH) # @song_test # def wc_get_download_info(self): # url, download_count = self.wc.get_song_download_info(self.user_songs[0].sid) # assert_is_not_none(url) # @song_test # def wc_get_uploaded_stream_urls(self): # urls = self.wc.get_stream_urls(self.user_songs[0].sid) # assert_equal(len(urls), 1) # url = urls[0] # assert_is_not_none(url) # assert_equal(url.split(':')[0], 'https') # @song_test # def wc_upload_album_art(self): # url = self.wc.upload_album_art(self.user_songs[0].sid, test_utils.image_filename) # assert_equal(url[:4], 'http') # # TODO download the track and verify the metadata changed # --------- # MC tests # --------- @test def mc_get_registered_devices(self): # no logic; just checking schema self.mc.get_registered_devices() @test def mc_get_browse_podcast_hierarchy(self): # no logic; just checking schema self.mc.get_browse_podcast_hierarchy() @test def mc_get_browse_podcast_series(self): # no logic; just checking schema self.mc.get_browse_podcast_series() @test def mc_get_listen_now_items(self): # no logic; just checking schema self.mc.get_listen_now_items() @test def mc_get_listen_now_situations(self): # no logic; just checking schema self.mc.get_listen_now_situations() @test def mc_list_stations_inc_equal(self): self.assert_list_inc_equivalence(self.mc.get_all_stations) @test def mc_list_shared_playlist_entries(self): entries = self.mc.get_shared_playlist_contents(TEST_PLAYLIST_SHARETOKEN) assert_true(len(entries) > 0) @test def mc_stream_podcast_episode(self): raise SkipTest('podcast ids keep changing') # uses frozen device_id # url = self.mc.get_podcast_episode_stream_url(TEST_PODCAST_EPISODE_ID) # audio = self.mc.session._rsession.get(url).content # assert_equal(md5(audio).hexdigest(), TEST_PODCAST_EPISODE_HASH) @test @subscription def mc_stream_store_track(self): url = self.mc.get_stream_url(TEST_STORE_SONG_ID) # uses frozen device_id audio = self.mc.session._rsession.get(url).content assert_equal(md5(audio).hexdigest(), TEST_STORE_SONG_MC_HASH) @song_test def mc_get_uploaded_track_stream_url(self): url = self.mc.get_stream_url(self.user_songs[0].sid) assert_is_not_none(url) assert_equal(url[:4], 'http') @staticmethod @retry def _assert_song_key_equal_to(method, sid, key, value): """ :param method: eg self.mc.get_all_songs :param sid: song id :param key: eg 'rating' :param value: eg '1' """ songs = method() if not isinstance(songs, list): # kind of a hack to support get_track_info as well songs = [songs] found = [s for s in songs if id_or_nid(s) == sid] assert_equal(len(found), 1) assert_equal(found[0][key], value) return found[0] # how can I get the rating key to show up for store tracks? # it works in Google's clients! # @test # @subscription # def mc_change_store_song_rating(self): # song = self.mc.get_track_info(TEST_STORE_SONG_ID) # # increment by one but keep in rating range # rating = int(song.get('rating', '0')) + 1 # rating = str(rating % 6) # self.mc.rate_songs(song, rating) # self._assert_song_key_equal_to(lambda: self.mc.get_track_info(TEST_STORE_SONG_ID), # id_or_nid(song), # song['rating']) @song_test def mc_change_uploaded_song_rating(self): song = self._assert_song_key_equal_to( self.mc.get_all_songs, self.all_songs[0].sid, 'rating', '0') # initially unrated self.mc.rate_songs(song, 1) self._assert_song_key_equal_to(self.mc.get_all_songs, song['id'], 'rating', '1') self.mc.rate_songs(song, 0) @song_test @retry def mc_get_promoted_songs(self): song = self.mc.get_track_info(TEST_STORE_SONG_ID) self.mc.rate_songs(song, 5) promoted = self.mc.get_promoted_songs() assert_true(len(promoted)) self.mc.rate_songs(song, 0) def _test_increment_playcount(self, sid): matching = [t for t in self.mc.get_all_songs() if t['id'] == sid] assert_equal(len(matching), 1) # playCount is an optional field. initial_playcount = matching[0].get('playCount', 0) self.mc.increment_song_playcount(sid, 2) self._assert_song_key_equal_to( self.mc.get_all_songs, sid, 'playCount', initial_playcount + 2) @song_test def mc_increment_uploaded_song_playcount(self): self._test_increment_playcount(self.all_songs[0].sid) # Fails silently. See https://github.com/simon-weber/gmusicapi/issues/349. # @song_test # @subscription # def mc_increment_store_song_playcount(self): # self._test_increment_playcount(self.all_songs[1].sid) @song_test def mc_change_uploaded_song_title_fails(self): # this used to work, but now only ratings can be changed. # this test is here so I can tell if this starts working again. song = self.assert_songs_state(self.mc.get_all_songs, [self.all_songs[0].sid], present=True)[0] old_title = song.title new_title = old_title + '_mod' # Mobileclient.change_song_metadata is deprecated, so # ignore its deprecation warning. with warnings.catch_warnings(): warnings.simplefilter('ignore') self.mc.change_song_metadata({'id': song.sid, 'title': new_title}) self._assert_song_key_equal_to(self.mc.get_all_songs, song.sid, 'title', old_title) @song_test def mc_list_songs_inc_equal(self): self.assert_list_inc_equivalence(self.mc.get_all_songs) @song_test def mc_list_songs_updated_after(self): songs_last_minute = self.mc.get_all_songs( updated_after=datetime.datetime.now() - datetime.timedelta(minutes=1)) assert_not_equal(len(songs_last_minute), 0) all_songs = self.mc.get_all_songs() assert_not_equal(len(songs_last_minute), len(all_songs)) @podcast_test def mc_list_podcast_series_inc_equal(self): self.assert_list_inc_equivalence(self.mc.get_all_podcast_series) @playlist_test def mc_list_playlists_inc_equal(self): self.assert_list_inc_equivalence(self.mc.get_all_playlists) @playlist_test def mc_edit_playlist_name(self): new_name = TEST_PLAYLIST_NAME + '_mod' plid = self.mc.edit_playlist(self.playlist_ids[0], new_name=new_name) assert_equal(self.playlist_ids[0], plid) @retry # change takes time to propogate def assert_name_equal(plid, name): playlists = self.mc.get_all_playlists() found = [p for p in playlists if p['id'] == plid] assert_equal(len(found), 1) assert_equal(found[0]['name'], name) assert_name_equal(self.playlist_ids[0], new_name) # revert self.mc.edit_playlist(self.playlist_ids[0], new_name=TEST_PLAYLIST_NAME) assert_name_equal(self.playlist_ids[0], TEST_PLAYLIST_NAME) @playlist_test def mc_edit_playlist_description(self): new_description = TEST_PLAYLIST_DESCRIPTION + '_mod' plid = self.mc.edit_playlist(self.playlist_ids[0], new_description=new_description) assert_equal(self.playlist_ids[0], plid) @retry # change takes time to propogate def assert_description_equal(plid, description): playlists = self.mc.get_all_playlists() found = [p for p in playlists if p['id'] == plid] assert_equal(len(found), 1) assert_equal(found[0]['description'], description) assert_description_equal(self.playlist_ids[0], new_description) # revert self.mc.edit_playlist(self.playlist_ids[0], new_description=TEST_PLAYLIST_DESCRIPTION) assert_description_equal(self.playlist_ids[0], TEST_PLAYLIST_DESCRIPTION) @playlist_test def mc_edit_playlist_public(self): new_public = False plid = self.mc.edit_playlist(self.playlist_ids[0], public=new_public) assert_equal(self.playlist_ids[0], plid) @retry # change takes time to propogate def assert_public_equal(plid, public): playlists = self.mc.get_all_playlists() found = [p for p in playlists if p['id'] == plid] assert_equal(len(found), 1) assert_equal(found[0]['accessControlled'], public) assert_public_equal(self.playlist_ids[0], new_public) # revert self.mc.edit_playlist(self.playlist_ids[0], public=True) assert_public_equal(self.playlist_ids[0], True) @playlist_test def mc_list_playlists_updated_after(self): pls_last_minute = self.mc.get_all_playlists( updated_after=datetime.datetime.now() - datetime.timedelta(minutes=1)) assert_not_equal(len(pls_last_minute), 0) print(pls_last_minute) all_pls = self.mc.get_all_playlists() assert_not_equal(len(pls_last_minute), len(all_pls)) @retry(tries=3) def _mc_assert_ple_position(self, entry, pos): """ :param entry: entry dict :pos: 0-based position to assert """ pl = self.mc_get_playlist_songs(entry['playlistId']) indices = [i for (i, e) in enumerate(pl) if e['id'] == entry['id']] assert_equal(len(indices), 1) assert_equal(indices[0], pos) @retry def _mc_test_ple_reodering(self, from_pos, to_pos): if from_pos == to_pos: raise ValueError('Will not test no-op reordering.') pl = self.mc_get_playlist_songs(self.playlist_ids[0]) from_e = pl[from_pos] e_before_new_pos, e_after_new_pos = None, None if from_pos < to_pos: adj = 0 else: adj = -1 if to_pos - 1 >= 0: e_before_new_pos = pl[to_pos + adj] if to_pos + 1 < len(self.plentry_ids): e_after_new_pos = pl[to_pos + adj + 1] self.mc.reorder_playlist_entry(from_e, to_follow_entry=e_before_new_pos, to_precede_entry=e_after_new_pos) self._mc_assert_ple_position(from_e, to_pos) if e_before_new_pos: self._mc_assert_ple_position(e_before_new_pos, to_pos - 1) if e_after_new_pos: self._mc_assert_ple_position(e_after_new_pos, to_pos + 1) @plentry_test def mc_reorder_ple_forwards(self): for from_pos, to_pos in [pair for pair in itertools.product(range(len(self.plentry_ids)), repeat=2) if pair[0] < pair[1]]: self._mc_test_ple_reodering(from_pos, to_pos) @plentry_test def mc_reorder_ple_backwards(self): playlist_len = len(self.plentry_ids) for from_pos, to_pos in [pair for pair in itertools.product(range(playlist_len), repeat=2) if pair[0] > pair[1]]: self._mc_test_ple_reodering(from_pos, to_pos) # This fails, unfortunately, which means n reorderings mean n # separate calls in the general case. # @plentry_test # def mc_reorder_ples_forwards(self): # pl = self.mc_get_playlist_songs(self.playlist_ids[0]) # # rot2, eg 0123 -> 2301 # pl.append(pl.pop(0)) # pl.append(pl.pop(0)) # mutate_call = mobileclient.BatchMutatePlaylistEntries # mutations = [ # mutate_call.build_plentry_reorder( # pl[-1], pl[-2]['clientId'], None), # mutate_call.build_plentry_reorder( # pl[-2], pl[-3]['clientId'], pl[-1]['clientId']) # ] # self.mc._make_call(mutate_call, [mutations]) # self._mc_assert_ple_position(pl[-1], len(pl) - 1) # self._mc_assert_ple_position(pl[-2], len(pl) - 2) @station_test @retry # sometimes this comes back with no data key @subscription def mc_list_station_tracks(self): for station_id in self.station_ids: self.mc.get_station_tracks(station_id, num_tracks=1) # used to assert that at least 1 track came back, but # our dummy uploaded track won't match anything self.mc.get_station_tracks(station_id, num_tracks=1, recently_played_ids=[TEST_STORE_SONG_ID]) self.mc.get_station_tracks(station_id, num_tracks=1, recently_played_ids=[self.user_songs[0].sid]) def mc_list_IFL_station_tracks(self): assert_equal(len(self.mc.get_station_tracks('IFL', num_tracks=1)), 1) @test(groups=['search']) def mc_search_store_no_playlists(self): res = self.mc.search('morning', max_results=100) res.pop('genre_hits') # Genre cluster is returned but without results in the new response. # TODO playlist and situation results are not returned consistently. res.pop('playlist_hits') res.pop('situation_hits') with Check() as check: for type_, hits in res.items(): if ((not test_subscription_features() and type_ in ('artist_hits', 'song_hits', 'album_hits'))): # These results aren't returned for non-sub accounts. check.true(len(hits) == 0, "%s had %s hits, expected 0" % (type_, len(hits))) else: check.true(len(hits) > 0, "%s had %s hits, expected > 0" % (type_, len(hits))) @test def mc_artist_info(self): aid = 'Apoecs6off3y6k4h5nvqqos4b5e' # amorphis optional_keys = set(('albums', 'topTracks', 'related_artists')) include_all_res = self.mc.get_artist_info(aid, include_albums=True, max_top_tracks=1, max_rel_artist=1) no_albums_res = self.mc.get_artist_info(aid, include_albums=False) no_rel_res = self.mc.get_artist_info(aid, max_rel_artist=0) no_tracks_res = self.mc.get_artist_info(aid, max_top_tracks=0) with Check() as check: check.true(set(include_all_res.keys()) & optional_keys == optional_keys) check.true(set(no_albums_res.keys()) & optional_keys == optional_keys - {'albums'}) check.true(set(no_rel_res.keys()) & optional_keys == optional_keys - {'related_artists'}) check.true(set(no_tracks_res.keys()) & optional_keys == optional_keys - {'topTracks'}) @test @retry def mc_album_info(self): include_tracks = self.mc.get_album_info(TEST_STORE_ALBUM_ID, include_tracks=True) no_tracks = self.mc.get_album_info(TEST_STORE_ALBUM_ID, include_tracks=False) with Check() as check: check.true('tracks' in include_tracks) check.true('tracks' not in no_tracks) del include_tracks['tracks'] check.equal(include_tracks, no_tracks) @test def mc_track_info(self): self.mc.get_track_info(TEST_STORE_SONG_ID) # just for the schema @test def mc_podcast_series_info(self): optional_keys = {'episodes'} include_episodes = self.mc.get_podcast_series_info(TEST_PODCAST_SERIES_ID, max_episodes=1) no_episodes = self.mc.get_podcast_series_info(TEST_PODCAST_SERIES_ID, max_episodes=0) with Check() as check: check.true(set(include_episodes.keys()) & optional_keys == optional_keys) check.true(set(no_episodes.keys()) & optional_keys == optional_keys - {'episodes'}) @test(groups=['genres']) def mc_all_genres(self): expected_genres = {u'COMEDY_SPOKEN_WORD_OTHER', u'COUNTRY', u'HOLIDAY', u'R_B_SOUL', u'FOLK', u'LATIN', u'CHRISTIAN_GOSPEL', u'ALTERNATIVE_INDIE', u'POP', u'ROCK', u'WORLD', u'VOCAL_EASY_LISTENING', u'HIP_HOP_RAP', u'JAZZ', u'METAL', u'REGGAE_SKA', u'SOUNDTRACKS_CAST_ALBUMS', u'DANCE_ELECTRONIC', u'CLASSICAL', u'NEW_AGE', u'BLUES', u'CHILDREN_MUSIC'} res = self.mc.get_genres() assert_equal(set([e['id'] for e in res]), expected_genres) @test(groups=['genres']) def mc_specific_genre(self): expected_genres = {u'PROGRESSIVE_METAL', u'CLASSIC_METAL', u'HAIR_METAL', u'INDUSTRIAL', u'ALT_METAL', u'THRASH', u'METALCORE', u'BLACK_DEATH_METAL', u'DOOM_METAL'} res = self.mc.get_genres('METAL') assert_equal(set([e['id'] for e in res]), expected_genres) @test(groups=['genres']) def mc_leaf_parent_genre(self): assert_equal(self.mc.get_genres('AFRICA'), []) @test(groups=['genres']) def mc_invalid_parent_genre(self): assert_equal(self.mc.get_genres('bogus genre'), []) gmusicapi-12.1.1/gmusicapi/test/utils.py0000664000175000017500000000477513400371522020537 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- """Utilities used in testing.""" from __future__ import print_function, division, absolute_import, unicode_literals from builtins import * # noqa import logging import os import re from gmusicapi.utils import utils from gmusicapi import Mobileclient log = utils.DynamicClientLogger(__name__) # A regex for the gm id format, eg: # c293dd5a-9aa9-33c4-8b09-0c865b56ce46 hex_set = "[0-9a-f]" gm_id_regex = re.compile(("{h}{{8}}-" + ("{h}{{4}}-" * 3) + "{h}{{12}}").format(h=hex_set)) # Get the absolute paths of the test files, which are located in the same # directory as this file. test_file_dir = os.path.dirname(os.path.abspath(__file__)) small_mp3 = os.path.join(test_file_dir, u'audiotest_small.mp3') image_filename = os.path.join(test_file_dir, u'imagetest_10x10_check.png') # that dumb intro track on conspiracy of one aa_song_id = 'Tqqufr34tuqojlvkolsrwdwx7pe' class NoticeLogging(logging.Handler): """A log handler that, if asked to emit, will set ``self.seen_message`` to True. """ def __init__(self): logging.Handler.__init__(self) # cannot use super in py 2.6; logging is still old-style self.seen_message = False def emit(self, record): self.seen_message = True def new_test_client(cls, **kwargs): """Make an instance of a client, login, and return it. kwargs are passed through to cls.login(). """ client = cls(debug_logging=True) if isinstance(client, Mobileclient): client.oauth_login(**kwargs) else: client.login(**kwargs) return client def md_entry_same(entry_name, s1, s2): """Returns (s1 and s2 have the same value for entry_name?, message).""" s1_val = s1[entry_name] s2_val = s2[entry_name] return (s1_val == s2_val, "(" + entry_name + ") " + repr(s1_val) + ", " + repr(s2_val)) def is_gm_id(s): """Returns True if the given string is in Google Music id form.""" return re.match(gm_id_regex, s) is not None def is_song(d): """Returns True is the given dict is a GM song dict.""" # Not really precise, but should be good enough. return is_gm_id(d["id"]) def is_song_list(lst): return all(map(is_song, lst)) def is_id_list(lst): """Returns True if the given list is made up of all strings in GM id form.""" return all(map(is_gm_id, lst)) def is_id_pair_list(lst): """Returns True if the given list is made up of all (id, id) pairs.""" a, b = list(zip(*lst)) return is_id_list(a + b) gmusicapi-12.1.1/gmusicapi/utils/0000755000175000017500000000000013512455420017173 5ustar simonsimon00000000000000gmusicapi-12.1.1/gmusicapi/utils/__init__.py0000600000175000017500000000003012667715127021302 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- gmusicapi-12.1.1/gmusicapi/utils/jsarray.py0000664000175000017500000000140613374625453021236 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- """ Tools to handle Google's ridiculous interchange format. """ from __future__ import print_function, division, absolute_import, unicode_literals from builtins import * # noqa from io import StringIO from tokenize import generate_tokens import json def to_json(s): """Return a valid json string, given a jsarray string. :param s: string of jsarray data """ out = [] for t in generate_tokens(StringIO(s).readline): if out and any(((',' == t[1] == out[-1]), # double comma (out[-1] == '[' and t[1] == ','), # comma opening array )): out.append('null') out.append(t[1]) return ''.join(out) def loads(s): return json.loads(to_json(s)) gmusicapi-12.1.1/gmusicapi/utils/utils.py0000644000175000017500000005207513512455274020725 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- """Utility functions used across api code.""" from __future__ import print_function, division, absolute_import, unicode_literals from past.builtins import basestring from builtins import * # noqa import ast from bisect import bisect_left from distutils import spawn import errno import functools import inspect import itertools import logging import os import re import subprocess import time import traceback import warnings from decorator import decorator from google.protobuf.descriptor import FieldDescriptor from gmusicapi import __version__ from gmusicapi.appdirs import my_appdirs from gmusicapi.exceptions import CallFailure, GmusicapiWarning, NotSubscribed # this controls the crazy logging setup that checks the callstack; # it should be monkey-patched to False after importing to disable it. # when False, static code will simply log in the standard way under the root. per_client_logging = True # Map descriptor.CPPTYPE -> python type. _python_to_cpp_types = { int: ('int32', 'int64', 'uint32', 'uint64'), float: ('double', 'float'), bool: ('bool',), str: ('string',), } cpp_type_to_python = dict( (getattr(FieldDescriptor, 'CPPTYPE_' + cpp.upper()), python) for (python, cpplist) in _python_to_cpp_types.items() for cpp in cpplist ) log_filepath = os.path.join(my_appdirs.user_log_dir, 'gmusicapi.log') printed_log_start_message = False # global, set in config_debug_logging # matches a mac address in GM form, eg # 00:11:22:33:AA:BB _mac_pattern = re.compile("^({pair}:){{5}}{pair}$".format(pair='[0-9A-F]' * 2)) class DynamicClientLogger(object): """Dynamically proxies to the logger of a Client higher in the call stack. This is a ridiculous hack needed because logging is, in the eyes of a user, per-client. So, logging from static code (eg protocol, utils) needs to log using the config of the calling client's logger. There can be multiple clients, so we can't just use a globally-available logger. Instead of refactoring every function to receieve a logger, we introspect the callstack at runtime to figure out who's calling us, then use their logger. This probably won't work on non-CPython implementations. """ def __init__(self, caller_name): self.caller_name = caller_name def __getattr__(self, name): # this isn't a totally foolproof way to proxy, but it's fine for # the usual logger.debug, etc methods. logger = logging.getLogger(self.caller_name) if per_client_logging: # search upwards for a client instance for frame_rec in inspect.getouterframes(inspect.currentframe()): frame = frame_rec[0] try: if 'self' in frame.f_locals: f_self = frame.f_locals['self'] # can't import and check against classes; that causes an import cycle if ((f_self is not None and f_self.__module__.startswith('gmusicapi.clients') and f_self.__class__.__name__ in ('Musicmanager', 'Webclient', 'Mobileclient'))): logger = f_self.logger break finally: del frame # avoid circular references else: # log to root logger. # should this be stronger? There's no default root logger set up. stack = traceback.extract_stack() logger.info('could not locate client caller in stack:\n%s', '\n'.join(traceback.format_list(stack))) return getattr(logger, name) log = DynamicClientLogger(__name__) def deprecated(instructions): """Flags a method as deprecated. :param instructions: human-readable note to assist migration. """ @decorator def wrapper(func, *args, **kwargs): message = "{0} is deprecated and may break unexpectedly; {1}".format( func.__name__, instructions) warnings.warn(message, GmusicapiWarning, stacklevel=2) return func(*args, **kwargs) return wrapper def longest_increasing_subseq(seq): """Returns the longest (non-contiguous) subsequence of seq that is strictly increasing. """ # adapted from http://goo.gl/lddm3c if not seq: return [] # head[j] = index in 'seq' of the final member of the best subsequence # of length 'j + 1' yet found head = [0] # predecessor[j] = linked list of indices of best subsequence ending # at seq[j], in reverse order predecessor = [-1] for i in range(1, len(seq)): # Find j such that: seq[head[j - 1]] < seq[i] <= seq[head[j]] # seq[head[j]] is increasing, so use binary search. j = bisect_left([seq[head[idx]] for idx in range(len(head))], seq[i]) if j == len(head): head.append(i) if seq[i] < seq[head[j]]: head[j] = i predecessor.append(head[j - 1] if j > 0 else -1) # trace subsequence back to output result = [] trace_idx = head[-1] while (trace_idx >= 0): result.append(seq[trace_idx]) trace_idx = predecessor[trace_idx] return result[::-1] def id_or_nid(song_dict): """Equivalent to ``d.get('id') or d['nid']``. Uploaded songs have an id key, while AA tracks have a nid key, which can often be used interchangably. """ return song_dict.get('id') or song_dict['nid'] def datetime_to_microseconds(dt): """Return microseconds since epoch, as an int. :param dt: a datetime.datetime """ return int(time.mktime(dt.timetuple()) * 1000000) + dt.microsecond def is_valid_mac(mac_string): """Return True if mac_string is of form eg '00:11:22:33:AA:BB'. """ if not _mac_pattern.match(mac_string): return False return True def create_mac_string(num, splitter=':'): """Return the mac address interpretation of num, in the form eg '00:11:22:33:AA:BB'. :param num: a 48-bit integer (eg from uuid.getnode) :param spliiter: a string to join the hex pairs with """ mac = hex(num)[2:] # trim trailing L for long consts if mac[-1] == 'L': mac = mac[:-1] pad = max(12 - len(mac), 0) mac = '0' * pad + mac mac = splitter.join([mac[x:x + 2] for x in range(0, 12, 2)]) mac = mac.upper() return mac # from http://stackoverflow.com/a/5032238/1231454 def make_sure_path_exists(path, mode=None): try: if mode is not None: os.makedirs(path, mode) else: os.makedirs(path) except OSError as exception: if exception.errno != errno.EEXIST: raise # from http://stackoverflow.com/a/8101118/1231454 class DocstringInheritMeta(type): """A variation on http://groups.google.com/group/comp.lang.python/msg/26f7b4fcb4d66c95 by Paul McGuire """ def __new__(meta, name, bases, clsdict): if not('__doc__' in clsdict and clsdict['__doc__']): for mro_cls in (mro_cls for base in bases for mro_cls in base.mro()): doc = mro_cls.__doc__ if doc: clsdict['__doc__'] = doc break for attr, attribute in clsdict.items(): if not attribute.__doc__: for mro_cls in (mro_cls for base in bases for mro_cls in base.mro() if hasattr(mro_cls, attr)): doc = getattr(getattr(mro_cls, attr), '__doc__') if doc: attribute.__doc__ = doc break return type.__new__(meta, name, bases, clsdict) def dual_decorator(func): """This is a decorator that converts a paramaterized decorator for no-param use. source: http://stackoverflow.com/questions/3888158. """ @functools.wraps(func) def inner(*args, **kw): if ((len(args) == 1 and not kw and callable(args[0]) and not (type(args[0]) == type and issubclass(args[0], BaseException)))): return func()(args[0]) else: return func(*args, **kw) return inner @dual_decorator def enforce_id_param(position=1): """Verifies that the caller is passing a single song id, and not a song dictionary. :param position: (optional) the position of the expected id - defaults to 1. """ @decorator def wrapper(function, *args, **kw): if not isinstance(args[position], basestring): raise ValueError("Invalid param type in position %s;" " expected an id (did you pass a dictionary?)" % position) return function(*args, **kw) return wrapper @dual_decorator def enforce_ids_param(position=1): """Verifies that the caller is passing a list of song ids, and not a list of song dictionaries. :param position: (optional) the position of the expected list - defaults to 1. """ @decorator def wrapper(function, *args, **kw): if ((not isinstance(args[position], (list, tuple)) or not all([isinstance(e, basestring) for e in args[position]]))): raise ValueError("Invalid param type in position %s;" " expected ids (did you pass dictionaries?)" % position) return function(*args, **kw) return wrapper def configure_debug_log_handlers(logger): """Warnings and above to stderr, below to gmusicapi.log when possible. Output includes line number.""" global printed_log_start_message logger.setLevel(logging.DEBUG) logging_to_file = True try: make_sure_path_exists(os.path.dirname(log_filepath), 0o700) debug_handler = logging.FileHandler(log_filepath, encoding='utf-8') except (OSError, IOError): logging_to_file = False debug_handler = logging.StreamHandler() debug_handler.setLevel(logging.DEBUG) important_handler = logging.StreamHandler() important_handler.setLevel(logging.WARNING) logger.addHandler(debug_handler) logger.addHandler(important_handler) if not printed_log_start_message: # print out startup message without verbose formatting logger.info("!-- begin debug log --!") logger.info("version: " + __version__) if logging_to_file: logger.info("logging to: " + log_filepath) printed_log_start_message = True formatter = logging.Formatter( '%(asctime)s - %(name)s (%(module)s:%(lineno)s) [%(levelname)s]: %(message)s' ) debug_handler.setFormatter(formatter) important_handler.setFormatter(formatter) @dual_decorator def retry(retry_exception=None, tries=5, delay=2, backoff=2, logger=None): """Retry calling the decorated function using an exponential backoff. An exception from a final attempt will propogate. :param retry_exception: exception (or tuple of exceptions) to check for and retry on. If None, use (AssertionError, CallFailure). :param tries: number of times to try (not retry) before giving up :param delay: initial delay between retries in seconds :param backoff: backoff multiplier :param logger: logger to use. If None, use 'gmusicapi.utils' logger Modified from http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python. """ if logger is None: logger = logging.getLogger('gmusicapi.utils') if retry_exception is None: retry_exception = (AssertionError, CallFailure) @decorator def retry_wrapper(f, *args, **kwargs): mtries, mdelay = tries, delay # make our own mutable copies while mtries > 1: try: return f(*args, **kwargs) except retry_exception as e: logger.info("%s, retrying in %s seconds...", e, mdelay) time.sleep(mdelay) mtries -= 1 mdelay *= backoff return f(*args, **kwargs) return retry_wrapper def pb_set(msg, field_name, val): """Return True and set val to field_name in msg if the assignment is type-compatible, else return False. val will be coerced to a proper type if needed. :param msg: an instance of a protobuf.message :param field_name: :param val """ # Find the proper type. field_desc = msg.DESCRIPTOR.fields_by_name[field_name] proper_type = cpp_type_to_python[field_desc.cpp_type] # Try with the given type first. # Their set hooks will automatically coerce. try_types = (type(val), proper_type) for t in try_types: log.debug("attempt %s.%s = %s(%r)", msg.__class__.__name__, field_name, t, val) try: setattr(msg, field_name, t(val)) log.debug("! success") break except (TypeError, ValueError): log.debug("X failure") else: return False # no assignments stuck return True def locate_mp3_transcoder(): """Return the path to a transcoder (ffmpeg or avconv) with mp3 support. Raise ValueError if none are suitable.""" transcoders = ['ffmpeg', 'avconv'] transcoder_details = {} for transcoder in transcoders: cmd_path = spawn.find_executable(transcoder) if cmd_path is None: transcoder_details[transcoder] = 'not installed' continue with open(os.devnull, "w") as null: stdout = subprocess.check_output([cmd_path, '-codecs'], stderr=null).decode("ascii") mp3_encoding_support = ('libmp3lame' in stdout and 'disable-libmp3lame' not in stdout) if mp3_encoding_support: transcoder_details[transcoder] = "mp3 encoding support" break # mp3 decoding/encoding supported else: transcoder_details[transcoder] = 'no mp3 encoding support' else: raise ValueError('ffmpeg or avconv must be in the path and support mp3 encoding' "\ndetails: %r" % transcoder_details) return cmd_path def transcode_to_mp3(filepath, quality='320k', slice_start=None, slice_duration=None): """Return the bytestring result of transcoding the file at *filepath* to mp3. An ID3 header is not included in the result. :param filepath: location of file :param quality: if int, pass to -q:a. if string, pass to -b:a -q:a roughly corresponds to libmp3lame -V0, -V1... :param slice_start: (optional) transcode a slice, starting at this many seconds :param slice_duration: (optional) when used with slice_start, the number of seconds in the slice Raise: * IOError: problems during transcoding * ValueError: invalid params, transcoder not found """ err_output = None cmd_path = locate_mp3_transcoder() cmd = [cmd_path, '-i', filepath] if slice_duration is not None: cmd.extend(['-t', str(slice_duration)]) if slice_start is not None: cmd.extend(['-ss', str(slice_start)]) if isinstance(quality, int): cmd.extend(['-q:a', str(quality)]) elif isinstance(quality, basestring): cmd.extend(['-b:a', quality]) else: raise ValueError("quality must be int or string, but received %r" % quality) cmd.extend(['-f', 's16le', # don't output id3 headers '-c', 'libmp3lame', 'pipe:1']) log.debug('running transcode command %r', cmd) try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) audio_out, err_output = proc.communicate() if proc.returncode != 0: err_output = ("(return code: %r)\n" % proc.returncode) + err_output.decode("ascii") raise IOError # handle errors in except except (OSError, IOError) as e: err_msg = "transcoding command (%r) failed: %s. " % (' '.join(cmd), e) if 'No such file or directory' in str(e): err_msg += '\nffmpeg or avconv must be installed and in the system path.' if err_output is not None: err_msg += "\nstderr: '%s'" % err_output log.exception('transcoding failure:\n%s', err_msg) raise IOError(err_msg) else: return audio_out def truncate(x, max_els=100, recurse_levels=0): """Return a 'shorter' truncated x of the same type, useful for logging. recurse_levels is only valid for homogeneous lists/tuples. max_els ignored for song dictionaries.""" # Coerce tuple to list to ease truncation. is_tuple = False if isinstance(x, tuple): is_tuple = True x = list(x) try: if len(x) > max_els: if isinstance(x, str): return x[:max_els] + '...' elif isinstance(x, basestring): return x[:max_els] + b'...' if isinstance(x, dict): if 'id' in x and 'titleNorm' in x: # assume to be a song dict trunc = dict((k, x.get(k)) for k in ['title', 'artist', 'album']) trunc['...'] = '...' return trunc else: return dict( itertools.chain( itertools.islice(x.items(), 0, max_els), [('...', '...')])) if isinstance(x, list): trunc = x[:max_els] + ['...'] if recurse_levels > 0: trunc = [truncate(e, recurse_levels - 1) for e in trunc[:-1]] if is_tuple: trunc = tuple(trunc) return trunc except TypeError: # does not have len pass return x @dual_decorator def empty_arg_shortcircuit(return_code='[]', position=1): """Decorate a function to shortcircuit and return something immediately if the length of a positional arg is 0. :param return_code: (optional) simple expression to eval as the return value - default is a list :param position: (optional) the position of the expected list - default is 1. """ # The normal pattern when making a collection an optional arg is to use # a sentinel (like None). Otherwise, you run the risk of the collection # being mutated - there's only one, not a new one on each call. # Here we've got multiple things we'd like to # return, so we can't do that. Rather than make some kind of enum for # 'accepted return values' I'm just allowing freedom to return basic values. # ast.literal_eval only can evaluate most literal expressions (e.g. [] and {}) @decorator def wrapper(function, *args, **kw): if len(args[position]) == 0: return ast.literal_eval(return_code) else: return function(*args, **kw) return wrapper def accept_singleton(expected_type, position=1): """Allows a function expecting a list to accept a single item as well. The item will be wrapped in a list. Will not work for nested lists. :param expected_type: the type of the items in the list :param position: (optional) the position of the expected list - defaults to 1. """ @decorator def wrapper(function, *args, **kw): if isinstance(args[position], expected_type): # args are a tuple, can't assign into them args = list(args) args[position] = [args[position]] args = tuple(args) return function(*args, **kw) return wrapper @decorator def require_subscription(function, *args, **kwargs): self = args[0] if not self.is_subscribed: raise NotSubscribed("%s requires a subscription." % function.__name__) return function(*args, **kwargs) # Modification of recipe found at # https://wiki.python.org/moin/PythonDecoratorLibrary#Cached_Properties. class cached_property(object): """Version of @property decorator that caches the result with a TTL. Tracks the property's value and last refresh time in a dict attribute of a class instance (``self._cache``) using the property name as the key. """ def __init__(self, ttl=0): self.ttl = ttl def __call__(self, fget, doc=None): self.fget = fget self.__doc__ = doc or fget.__doc__ self.__name__ = fget.__name__ self.__module__ = fget.__module__ return self def __get__(self, inst, owner): now = time.time() try: value, last_update = inst._cache[self.__name__] if (self.ttl > 0) and (now - last_update > self.ttl): raise AttributeError except (KeyError, AttributeError): value = self.fget(inst) try: cache = inst._cache except AttributeError: cache = inst._cache = {} cache[self.__name__] = (value, now) return value def __set__(self, inst, value): raise AttributeError("Can't set cached properties") def __delete__(self, inst): try: del inst._cache[self.__name__] except (KeyError, AttributeError): if not inst._cache: inst._cache = {} # Used to mark a field as unimplemented. @property def NotImplementedField(self): raise NotImplementedError gmusicapi-12.1.1/gmusicapi.egg-info/0000755000175000017500000000000013512455420017525 5ustar simonsimon00000000000000gmusicapi-12.1.1/gmusicapi.egg-info/PKG-INFO0000600000175000017500000006041113512455420020614 0ustar simonsimon00000000000000Metadata-Version: 1.1 Name: gmusicapi Version: 12.1.1 Summary: An unofficial api for Google Play Music. Home-page: http://pypi.python.org/pypi/gmusicapi/ Author: Simon Weber Author-email: simon@simonmweber.com License: Copyright (c) 2018, Simon Weber All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of the contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Description: gmusicapi: an unofficial API for Google Play Music ================================================== gmusicapi allows control of `Google Music `__ with Python. .. code-block:: python from gmusicapi import Mobileclient api = Mobileclient() # after running api.perform_oauth() once: api.oauth_login('
') # => True library = api.get_all_songs() sweet_track_ids = [track['id'] for track in library if track['artist'] == 'The Cat Empire'] playlist_id = api.create_playlist('Rad muzak') api.add_songs_to_playlist(playlist_id, sweet_track_ids) **gmusicapi is not supported nor endorsed by Google.** That said, it's actively maintained, and powers a bunch of cool projects: - alternate clients, including `one designed for the visually impaired `__, `a web-based jukebox which ships with its own server `__, `command line `__ `clients `__, `a FUSE filesystem `__, and `an Alexa skill `__ - library management tools for `syncing tracks `__, `syncing playlists `__, and `migrating to a different account `__ - proxies for media players, such as `gmusicproxy `__ and `gmusicprocurator `__, as well as plugins for `Mopidy `__, `Squeezebox `__ and `Tizonia `__. - enhancements like `autoplaylists / smart playlists `__ Getting started --------------- Start with `the usage docs `__, which will guide you through installation and the available apis. Once you're up and running, you can explore the rest of the docs at http://unofficial-google-music-api.readthedocs.io. If the documentation doesn't answer your questions, or you just want to get in touch, either `drop by #gmusicapi on Freenode `__ or shoot me an email. Status and updates ------------------ |build_status| |repominder_status| .. |build_status| image:: https://travis-ci.org/simon-weber/gmusicapi.png?branch=develop :target: https://travis-ci.org/simon-weber/gmusicapi .. |repominder_status| image:: https://img.shields.io/badge/dynamic/json.svg?label=release&query=%24.status&maxAge=43200&uri=https%3A%2F%2Fwww.repominder.com%2Fbadge%2FeyJyZXBvX2lkIjogMTEsICJ1c2VyX2lkIjogMn0%3D%2F&link=https%3A%2F%2Fwww.repominder.com%2F :target: https://www.repominder.com * November 2018: proper OAuth support for the mobileclient. * February 2016: Python 3 support! * September 2015: Google switched to a new music uploading endpoint, breaking uploading for outdated versions of gmusicapi. * June 2015: Full mobileclient and webclient functionality was restored. * May 2015: Limited mobileclient functionality was restored. * April 2015: Google deprecated clientlogin, breaking both the webclient and mobileclient. * November 2013: I started working fulltime at Venmo, meaning this project is back to night and weekend development. For fine-grained development updates, follow me on Twitter: `@simonmweber `__. .. :changelog: History ------- As of 1.0.0, `semantic versioning `__ is used. 12.1.1 ++++++ released 2019-07-13 - open default log file as utf-8 to avoid windows encoding problems 12.1.0 ++++++ released 2019-04-20 - deprecate Mobileclient.get_promoted_songs in favor of Mobileclient.get_top_songs to clarify its behavior. Functionality remains the same. 12.0.0 ++++++ released 2019-01-08 - breaking: remove gmusicapi.clients.OAUTH_FILEPATH. Use Musicmanager.OAUTH_FILEPATH instead. 11.1.1 ++++++ released 2018-12-04 - add back gmusicapi.clients.OAUTH_FILEPATH after it was removed in 11.1.0 (a breaking change) - deprecate gmusicapi.clients.OAUTH_FILEPATH in favor of Musicmanager.OAUTH_FILEPATH 11.1.0 ++++++ released 2018-11-30 - add Mobileclient OAuth support (``perform_oauth`` and ``oauth_login``) and deprecate email/password auth, which Google now often rejects. It works the same way as the Musicmanager; see `the docs `__ for an example. - update Mobileclient.search schema 11.0.4 ++++++ released 2018-11-19 - fix a number of bugs with Mobileclient.search 11.0.3 ++++++ released 2018-09-19 - fix an "__init__() takes at most 4..." warning coming from oauth2client 11.0.2 ++++++ released 2018-09-09 - fix validation of "ios:..." format device ids - add inLibrary field to station docs 11.0.1 ++++++ released 2018-03-18 - update schemas 11.0.0 ++++++ released 2017-12-09 - breaking: list calls now default to max_results=None, increasing the default number of results from 100 to 999 - add updated_after param to song/playlist listing to support differential updates - add support for free radio stations - add filepath+extension to unsupported file exception message - fix "I'm Feeling Lucky" station never refreshing its seed - fix crashes caused by some 503s during uploading - fix gmtools for https://github.com/simon-weber/Google-Music-Playlist-Importer - fix AAC and ALAC content type upload detection - blacklist requests 2.8.2 - improve id documentation - update schemas 10.1.2 ++++++ released 2017-04-03 - validate device ids to prevent 403s during streaming - fix LocalUnboundError during login for some environments - update schemas 10.1.1 ++++++ released 2017-02-10 - deprecate include_deleted param to greatly speed up responses for Mobileclient.get_all_* - Mobileclient.search now works on non-subscription accounts - fix logging IOError on read-only filesystems - fix problems caused by broken requests IDNA support 10.1.0 ++++++ released 2016-10-31 - deprecate the Webclient - add podcast support to Mobileclient: - get_all_podcast_series - get_all_podcast_episodes - add_podcast_series - delete_podcast_series - edit_podcast_series - get_podcast_episode_stream_url - get_podcast_episode_info - get_podcast_series_info - get_browse_podcast_hierarchy - get_browse_podcast_series - add Mobileclient.add_store_tracks - add Mobileclient.rate_songs - add Musicmanager.get_quota - fix get_all_user_playlist_contents hanging for large playlists - fix is_authenticated status after uploader_id exceptions - fix upload progress tracker remaining after upload - various internal improvements and schema updates 10.0.1 ++++++ released 2016-06-04 - switch to pycryptodomex - minor schema adjustments 10.0.0 ++++++ released 2016-05-01 - breaking: Mobileclient.search_all_access is now Mobileclient.search - breaking: Mobileclient.add_aa_track is now Mobileclient.add_store_track - add situation_hits and video_hits to Mobclient.search - add methods Mobileclient.deauthorize_device, .get_listen_now_items, and .get_listen_now_situations - add property Mobileclient.is_subscribed - add playlists and curated stations as station seeds - add params locale and subscription to Mobileclient.login - add param enable_transcoding to Musicmanager.upload - update to newer Google apis, returning more data in responses - reduce memory usage during uploading - fix a variety of bugs, mostly python2/3 type errors 9.0.0 +++++ released 2016-03-05 - breaking: attempting to reupload a file after changing only its tags will result in a rejection as a duplicate upload (it used to upload successfully) - fix webclient login after Google changes - fix ``'str' object has no attribute 'refresh'`` - prevent upstream protobufs TypeError by locking version - a 'matched' value may be returned even if matching is not enabled if we were unable to disallow matching 8.0.0 +++++ released 2016-02-08 - breaking: drop support for python < 2.7.9 - add (experimental) python 3 support! - add Musicmanager.get_purchased_songs - add station_hits to search_all_access results - add disc_number and total_disc_count to Musicmanager.get_uploaded_songs - add a prompt for device id in tests - upgrade gpsoauth, removing dependency on pycrypto - deprecate Webclient.create_playlist and Webclient.get_registered_devices - fix various packaging problems - fix KeyError in Mobileclient.get_station_tracks - fix a TypeError from requests - fix various bits of the docs 7.0.0 +++++ released 2015-09-19 - breaking: python 2.6 is no longer supported - breaking: webclient.get_registered_devices has a slightly different schema - fix Webclient authentication and get_stream_urls - fix MusicManager uploading: Google shut down the rupio endpoint - fix certificate validation - fix album artist metadata not being upload 6.0.0 +++++ released 2015-06-20 - fix creation of multiple android devices from android_id=None; support creating device ids from mac address. - android_id is now optional for mobileclient.get_stream_url, defaulting to android_id from login() 5.0.0 +++++ released 2015-06-02 - breaking: Webclient.login temporarily broken after clientlogin deprecation - breaking: Mobileclient.get_thumbs_up_songs renamed to mobileclient.get_promoted_songs - breaking: Mobileclient.change_playlist_name is now edit_playlist - fix Mobileclient.login breakage due to clientlogin deprecation - fix Mobileclient.get_genres: return a list and handle invalid parent genres - add support for filtering out recently played station tracks to Mobileclient.get_station_tracks - add public playlist results to Mobileclient.search_all_access - add Mobileclient.get_registered_devices - add quality option to Mobileclient.get_stream_url - add support for public playlist creation to Mobileclient.create_playlist - make optional description param for Webclient.create_playlist - better handle locating mp3 transcoder 4.0.0 +++++ released 2014-06-08 - breaking: remove webclient.change_song_metadata; use mobileclient.change_song_metadata instead - breaking: remove webclient.get_all_songs; use mobileclient.get_all_songs instead - breaking: remove webclient.get_playlist_songs; use mobileclient.get_all_user_playlist_contents instead - breaking: remove webclient.get_all_playlist_ids; use mobileclient.get_all_user_playlists instead - breaking: webclient.upload_album_art now returns a url to the uploaded image - breaking: due to backend changes, mobileclient.change_song_metadata can only change ratings - add mobileclient.get_thumbs_up_songs - add mobileclient.increment_song_playcount - add webclient.create_playlist, which is capable of creating public playlists - add webclient.get_shared_playlist_info 3.1.0 +++++ released 2014-01-20 - add verify_ssl option to client init - greatly loosen dependency version requirements 3.0.1 +++++ released 2013-12-11 - remove extraneous logging introduced in 3.0.0 -- this could have logged auth details, so it's recommended to delete old logs 3.0.0 +++++ released 2013-11-03 - Musicmanager.get_all_songs is now Musicmanager.get_uploaded_songs - Mobileclient.get_all_playlist_contents is now Mobileclient.get_all_user_playlist_contents, and will no longer return results for subscribed playlists - add Mobileclient.get_shared_playlist_contents - add Mobileclient.reorder_playlist_entry - add Mobileclient.change_song_metadata - add Mobileclient.get_album_info - add Mobileclient.get_track_info - add Mobileclient.get_genres - compatibility fixes 2.0.0 +++++ released 2013-08-01 - remove broken Webclient.{create_playlist, change_playlist, copy_playlist, search, change_playlist_name} - add Mobileclient; this will slowly replace most of the Webclient, so prefer it when possible - add support for streaming All Access songs - add Webclient.get_registered_devices - add a toggle to turn off validation per client - raise an exception when a song dictionary is passed instead of an id 1.2.0 +++++ released 2013-05-16 - add support for listing/downloading songs with the Musicmanager. When possible, this should be preferred to the Webclient's method, since it does not have a download quota. - fix a bug where the string representing a machine's mac was not properly formed for use as an uploader_id. This will cause another machine to be registered for some users; the old device can be identified from its lack of a version number. - verify user-provided uploader_ids 1.1.0 +++++ released 2013-04-19 - get_all_songs can optionally return a generator - compatibility updates for AddPlaylist call - log to appdirs.user_log_dir by default - add open_browser param to perform_oauth 1.0.0 +++++ released 2013-04-02 - breaking: Api has been split into Webclient and Musicmanager - breaking: semantic versioning (previous versions removed from PyPi) - Music Manager OAuth support - faster uploading when matching is disabled - faster login 2013.03.04 ++++++++++ - add artistMatchedId to metadata - tests are no longer a mess 2013.02.27 ++++++++++ - add support for uploading album art (`docs `__) - add support for .m4b files - add CancelUploadJobs call (not exposed in api yet) - Python 2.6 compatibility - reduced peak memory usage when uploading - logging improvements - improved error messages when uploading 2013.02.15 ++++++++++ - user now controls logging (`docs `__) - documentation overhaul 2013.02.14 ++++++++++ - fix international logins 2013.02.12 ++++++++++ - fix packaging issues 2013.02.11 ++++++++++ - improve handling of strange metadata when uploading - add a dependency on `dateutil `__ 2013.02.09 ++++++++++ - breaking: upload returns a 3-tuple (`docs `__) - breaking: get_all_playlist_ids always returns lists of ids; remove always_id_lists option (`docs `__) - breaking: remove suppress_failure option in Api.__init__ - breaking: copy_playlist ``orig_id`` argument renamed to ``playlist_id`` (`docs `__) - new: report_incorrect_match (only useful for Music Manager uploads) (`docs `__) - uploading fixed - avconv replaces ffmpeg - scan and match is supported - huge code improvements 2013.01.05 ++++++++++ - compatibility update for playlist mutation - various metadata compatibility updates 2012.11.09 ++++++++++ - bugfix: support for uploading uppercase filenames (Tom Graham) - bugfix: fix typo in multidownload validation, and add test 2012.08.31 ++++++++++ - metadata compatibility updates (storeId, lastPlayed) - fix uploading of unicode filenames without tags 2012.05.04 ++++++++++ - update allowed rating values to 1-5 (David Dooling) - update metajamId to matchedId (David Dooling) - fix broken expectation about disc/track numbering metadata 2012.04.03 ++++++++++ - change to the 3-clause BSD license - add Kevin Kwok to AUTHORS 2012.04.01 ++++++++++ - improve code in example.py - support uploading of all Google-supported formats: m4a, ogg, flac, wma, mp3. Non-mp3 are transcoded to 320kbs abr mp3 using ffmpeg - introduce dependency on ffmpeg. for non-mp3 uploading, it needs to be in path and have the needed transcoders available - get_playlists is now get_all_playlist_ids, and is faster - add an exception CallFailure. Api functions raise it if the server says their request failed - add suppress_failure (default False) option to Api.__init__() - change_playlist now returns the changed playlistId (pid) - change_song_metadata now returns a list of changed songIds (sids) - create_playlist now returns the new pid - delete_playlist now returns the deleted pid - delete_songs now returns a list of deleted sids - change_playlist now returns the pid of the playlist - which may differ from the one passed in - add_songs_to_playlist now returns a list of (sid, new playlistEntryId aka eid) tuples of added songs - remove_songs_from_playlist now returns a list of removed (sid, eid) pairs - search dictionary is now flattened, without the "results" key. see documentation for example 2012.03.27 ++++++++++ - package for pip/pypi - add AUTHORS file - remove session.py; the sessions are now just api.PlaySession (Darryl Pogue) - protocol.Metadata_Expectations.get_expectation will return UnknownExpectation when queried for unknown keys; this should prevent future problems - add immutable 'subjectToCuration' and 'metajamId' fields - use unknown 2012.03.16 ++++++++++ - add change_playlist for playlist modifications - get_playlists supports multiple playlists of the same name by returning lists of playlist ids. By default, it will return a single string (the id) for unique playlist names; see the always_id_lists parameter. - api.login now attempts to bump Music Manager authentication first, bypassing browser emulation. This allows for much faster authentication. - urls updated for the change to Google Play Music - remove_songs_from_playlist now takes (playlist_id, song_ids), for consistency with other playlist mutations 2012.03.04 ++++++++++ - change name to gmusicapi to avoid ambiguity - change delete_song and remove_song_from_playlist to delete_songs and remove_songs_from_playlist, for consistency with other functions - add verification of WC json responses - setup a sane branch model. see http://nvie.com/posts/a-successful-git-branching-model/ - improve logging Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Multimedia :: Sound/Audio Classifier: Topic :: Software Development :: Libraries :: Python Modules gmusicapi-12.1.1/gmusicapi.egg-info/SOURCES.txt0000600000175000017500000000310213512455420021375 0ustar simonsimon00000000000000HISTORY.rst LICENSE MANIFEST.in README.rst example.py setup.cfg setup.py docs/Makefile docs/source/conf.py docs/source/contributing.rst docs/source/index.rst docs/source/ports.rst docs/source/usage.rst docs/source/reference/api.rst docs/source/reference/mobileclient.rst docs/source/reference/musicmanager.rst docs/source/reference/protocol.rst docs/source/reference/webclient.rst gmusicapi/__init__.py gmusicapi/_version.py gmusicapi/appdirs.py gmusicapi/exceptions.py gmusicapi/session.py gmusicapi.egg-info/PKG-INFO gmusicapi.egg-info/SOURCES.txt gmusicapi.egg-info/dependency_links.txt gmusicapi.egg-info/not-zip-safe gmusicapi.egg-info/requires.txt gmusicapi.egg-info/top_level.txt gmusicapi/clients/__init__.py gmusicapi/clients/mobileclient.py gmusicapi/clients/musicmanager.py gmusicapi/clients/shared.py gmusicapi/clients/webclient.py gmusicapi/gmtools/__init__.py gmusicapi/gmtools/tools.py gmusicapi/protocol/__init__.py gmusicapi/protocol/download_pb2.py gmusicapi/protocol/locker_pb2.py gmusicapi/protocol/mobileclient.py gmusicapi/protocol/musicmanager.py gmusicapi/protocol/shared.py gmusicapi/protocol/uits_pb2.py gmusicapi/protocol/upload_pb2.py gmusicapi/protocol/webclient.py gmusicapi/test/__init__.py gmusicapi/test/audiotest_small.mp3 gmusicapi/test/fetchartist.jsarray gmusicapi/test/imagetest_10x10_check.png gmusicapi/test/local_tests.py gmusicapi/test/rewrite_audiotest_tags.py gmusicapi/test/run_tests.py gmusicapi/test/searchresult.jsarray gmusicapi/test/server_tests.py gmusicapi/test/utils.py gmusicapi/utils/__init__.py gmusicapi/utils/jsarray.py gmusicapi/utils/utils.pygmusicapi-12.1.1/gmusicapi.egg-info/dependency_links.txt0000600000175000017500000000000113512455420023563 0ustar simonsimon00000000000000 gmusicapi-12.1.1/gmusicapi.egg-info/not-zip-safe0000600000175000017500000000000112724712727021755 0ustar simonsimon00000000000000 gmusicapi-12.1.1/gmusicapi.egg-info/requires.txt0000600000175000017500000000046113512455420022116 0ustar simonsimon00000000000000validictory!=0.9.2,>=0.8.0 decorator>=3.3.1 mutagen>=1.34 requests!=1.2.0,!=2.12.0,!=2.12.1,!=2.12.2,!=2.18.2,!=2.2.1,!=2.8.0,!=2.8.1,>=1.1.0 python-dateutil!=2.0,>=1.3 proboscis>=1.2.5.1 protobuf>=3.0.0 oauth2client>=1.1 mock>=0.7.0 appdirs>=1.1.0 gpsoauth>=0.2.0 MechanicalSoup>=0.4.0 six>=1.9.0 future gmusicapi-12.1.1/gmusicapi.egg-info/top_level.txt0000600000175000017500000000001213512455420022240 0ustar simonsimon00000000000000gmusicapi gmusicapi-12.1.1/setup.cfg0000664000175000017500000000014713512455420015677 0ustar simonsimon00000000000000[flake8] exclude = *_pb2.py ignore = E402 max-line-length = 110 [egg_info] tag_build = tag_date = 0 gmusicapi-12.1.1/setup.py0000664000175000017500000000627013374625453015606 0ustar simonsimon00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import re from setuptools import setup, find_packages import sys import warnings dynamic_requires = [] # Python 2.7 is supported. Python 3 support is experimental if sys.version_info[0] > 2: warnings.warn("gmusicapi Python 3 support is experimental", RuntimeWarning) else: if sys.version_info[:3] < (2, 7, 9): warnings.warn("gmusicapi does not officially support versions below " "Python 2.7.9", RuntimeWarning) # try to continue anyway # This hack is from http://stackoverflow.com/a/7071358/1231454; # the version is kept in a seperate file and gets parsed - this # way, setup.py doesn't have to import the package. VERSIONFILE = 'gmusicapi/_version.py' version_line = open(VERSIONFILE).read() version_re = r"^__version__ = u['\"]([^'\"]*)['\"]" match = re.search(version_re, version_line, re.M) if match: version = match.group(1) else: raise RuntimeError("Could not find version in '%s'" % VERSIONFILE) setup( name='gmusicapi', version=version, author='Simon Weber', author_email='simon@simonmweber.com', url='http://pypi.python.org/pypi/gmusicapi/', packages=find_packages(), scripts=[], license=open('LICENSE').read(), description='An unofficial api for Google Play Music.', long_description=(open('README.rst').read() + '\n\n' + open('HISTORY.rst').read()), install_requires=[ 'validictory >= 0.8.0, != 0.9.2', # error messages 'decorator >= 3.3.1', # > 3.0 likely work, but not on pypi 'mutagen >= 1.34', # EasyID3 TPE2 mapping to albumartist ('requests >= 1.1.0, != 1.2.0,' # session.close, memory view TypeError '!= 2.2.1, != 2.8.0, != 2.8.1,' '!= 2.12.0, != 2.12.1, != 2.12.2,' # idna regression broke streaming urls '!= 2.18.2'), # SSLError became ConnectionError 'python-dateutil >= 1.3, != 2.0', # 2.0 is python3-only 'proboscis >= 1.2.5.1', # runs_after 'protobuf >= 3.0.0', 'oauth2client >= 1.1', # TokenRevokeError 'mock >= 0.7.0', # MagicMock 'appdirs >= 1.1.0', # user_log_dir 'gpsoauth >= 0.2.0', # mac -> android_id, validation, pycryptodome 'MechanicalSoup >= 0.4.0', 'six >= 1.9.0', # raise_from on Python 3 'future', ] + dynamic_requires, classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Multimedia :: Sound/Audio', 'Topic :: Software Development :: Libraries :: Python Modules', ], include_package_data=True, zip_safe=False, )