Skip to content
This repository has been archived by the owner on Jan 22, 2022. It is now read-only.

Add Python 3 compatibility #396

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -21,3 +21,4 @@ bin
lib
include
share
.tox
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Expand Up @@ -46,3 +46,11 @@ or together with the local tests:
* `$ python -m gmusicapi.test.run_tests`

Many of the server tests require a subscription to Google Music All-Access. If you have a subscription, set the environment variable `GM_A` (to anything).


As there is experimental support for Python 3, it would be ideal to run the tests against all supported versions of Python. You can do this by using [tox](http://tox.testrun.org/):
* Install all supported versions of Python that you'd like to test (Python 2.7, 3.4 and 3.5 are currently tested with tox)
* **Outside** of a virtualenv, `$ pip install tox`
* Set the environment variables (as above)
* Run the tests: `$ tox` (this will create virtualenvs for you)
* If you don't want to test every single version in the compatibility matrix, you can run tox with `--skip-missing-interpreters`, which will test just the python versions that you have installed
50 changes: 26 additions & 24 deletions example.py
@@ -1,6 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function, division, absolute_import, unicode_literals
from future import standard_library
standard_library.install_aliases()
from builtins import * # noqa
from getpass import getpass

from gmusicapi import Mobileclient
Expand All @@ -18,7 +21,7 @@ def ask_for_credentials():
attempts = 0

while not logged_in and attempts < 3:
email = raw_input('Email: ')
email = input('Email: ')
password = getpass()

logged_in = api.login(email, password, Mobileclient.FROM_MAC_ADDRESS)
Expand All @@ -33,59 +36,58 @@ def demonstrate():
api = ask_for_credentials()

if not api.is_authenticated():
print "Sorry, those credentials weren't accepted."
print("Sorry, those credentials weren't accepted.")
return

print 'Successfully logged in.'
print
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...',
print('Loading library...', end=' ')
library = api.get_all_songs()
print 'done.'
print('done.')

print len(library), 'tracks detected.'
print
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'].encode('utf-8'),
first_song['artist'].encode('utf-8'))
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 = raw_input('Enter a name for the playlist: ')
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
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
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.
raw_input('You can now check on Google Music that the playlist exists.\n'
'When done, press enter to delete the playlist:')
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.'
print('Deleted the playlist.')

# It's good practice to logout when finished.
api.logout()
print 'All done!'
print('All done!')

if __name__ == '__main__':
demonstrate()
3 changes: 1 addition & 2 deletions gmusicapi/_version.py
@@ -1,2 +1 @@
from __future__ import unicode_literals
__version__ = "7.0.1-dev"
__version__ = u"7.0.1-dev"
9 changes: 6 additions & 3 deletions gmusicapi/appdirs.py
Expand Up @@ -4,6 +4,9 @@
Mock version of appdirs for use in cases without the real version
"""
from __future__ import print_function, division, absolute_import, unicode_literals
from future import standard_library
standard_library.install_aliases()
from builtins import * # noqa

try:
from appdirs import AppDirs
Expand All @@ -12,9 +15,9 @@
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')])
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:
Expand Down
11 changes: 7 additions & 4 deletions gmusicapi/clients/mobileclient.py
@@ -1,4 +1,7 @@
from __future__ import print_function, division, absolute_import, unicode_literals
from future import standard_library
standard_library.install_aliases()
from builtins import * # noqa
from collections import defaultdict
import datetime
from operator import itemgetter
Expand Down Expand Up @@ -204,7 +207,7 @@ def add_aa_track(self, aa_song_id):

return res['mutate_response'][0]['id']

@utils.accept_singleton(basestring)
@utils.accept_singleton(str)
@utils.enforce_ids_param
@utils.empty_arg_shortcircuit
def delete_songs(self, library_song_ids):
Expand Down Expand Up @@ -449,7 +452,7 @@ def get_shared_playlist_contents(self, share_token):

return entries

@utils.accept_singleton(basestring, 2)
@utils.accept_singleton(str, 2)
@utils.enforce_id_param
@utils.enforce_ids_param(position=2)
@utils.empty_arg_shortcircuit(position=2)
Expand All @@ -471,7 +474,7 @@ def add_songs_to_playlist(self, playlist_id, song_ids):

return [e['id'] for e in res['mutate_response']]

@utils.accept_singleton(basestring, 1)
@utils.accept_singleton(str, 1)
@utils.enforce_ids_param(position=1)
@utils.empty_arg_shortcircuit(position=1)
def remove_entries_from_playlist(self, entry_ids):
Expand Down Expand Up @@ -706,7 +709,7 @@ def create_station(self, name,

return res['mutate_response'][0]['id']

@utils.accept_singleton(basestring)
@utils.accept_singleton(str)
@utils.enforce_ids_param
@utils.empty_arg_shortcircuit
def delete_stations(self, station_ids):
Expand Down
17 changes: 10 additions & 7 deletions gmusicapi/clients/musicmanager.py
@@ -1,8 +1,13 @@
from __future__ import print_function, division, absolute_import, unicode_literals
from future import standard_library
standard_library.install_aliases()
from builtins import * # noqa
import os
from socket import gethostname
import time
import urllib
import urllib.request
import urllib.parse
import urllib.error
from uuid import getnode as getmac
import webbrowser

Expand Down Expand Up @@ -86,8 +91,7 @@ def perform_oauth(storage_filepath=OAUTH_FILEPATH, open_browser=False):
print("If you don't see your browser, you can just copy and paste the url.")
print()

code = raw_input("Follow the prompts,"
" then paste the auth code here and hit enter: ")
code = input("Follow the prompts, then paste the auth code here and hit enter: ")

credentials = flow.step2_exchange(code)

Expand Down Expand Up @@ -166,7 +170,7 @@ def _oauth_login(self, oauth_credentials):
Return True on success; see :py:func:`login` for params.
"""

if isinstance(oauth_credentials, basestring):
if isinstance(oauth_credentials, str):
oauth_file = oauth_credentials
if oauth_file == OAUTH_FILEPATH:
utils.make_sure_path_exists(os.path.dirname(OAUTH_FILEPATH), 0o700)
Expand Down Expand Up @@ -346,8 +350,7 @@ def download_song(self, song_id):

cd_header = response.headers['content-disposition']

filename = urllib.unquote(cd_header.split("filename*=UTF-8''")[-1])
filename = filename.decode('utf-8')
filename = urllib.parse.unquote(cd_header.split("filename*=UTF-8''")[-1])

return (filename, response.content)

Expand All @@ -357,7 +360,7 @@ def download_song(self, song_id):
# #protocol incorrect here...
# return (quota.maximumTracks, quota.totalTracks, quota.availableTracks)

@utils.accept_singleton(basestring)
@utils.accept_singleton(str)
@utils.empty_arg_shortcircuit(return_code='{}')
def upload(self, filepaths, transcode_quality='320k', enable_matching=False):
"""Uploads the given filepaths.
Expand Down
8 changes: 5 additions & 3 deletions gmusicapi/clients/shared.py
@@ -1,13 +1,15 @@
from __future__ import print_function, division, absolute_import, unicode_literals
from future import standard_library
standard_library.install_aliases()
from builtins import * # noqa
import logging

from gmusicapi.utils import utils
from future.utils import with_metaclass


class _Base(object):
class _Base(with_metaclass(utils.DocstringInheritMeta, object)):
"""Factors out common client setup."""

__metaclass__ = utils.DocstringInheritMeta
_session_class = utils.NotImplementedField

num_clients = 0 # used to disambiguate loggers
Expand Down
26 changes: 14 additions & 12 deletions gmusicapi/clients/webclient.py
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-

from __future__ import print_function, division, absolute_import, unicode_literals
from urlparse import urlparse, parse_qsl
from future import standard_library
standard_library.install_aliases()
from builtins import * # noqa
from urllib.parse import urlparse, parse_qsl

import gmusicapi
from gmusicapi.clients.shared import _Base
Expand Down Expand Up @@ -238,7 +240,7 @@ def get_stream_audio(self, song_id, use_range_header=None):
for key, val in parse_qsl(urlparse(url)[4])
if key == 'range']

stream_pieces = []
stream_pieces = bytearray()
prev_end = 0
headers = None

Expand All @@ -260,13 +262,13 @@ def get_stream_audio(self, song_id, use_range_header=None):
# trim to the proper range
audio = audio[prev_end - start:]

stream_pieces.append(audio)
stream_pieces.extend(audio)

prev_end = end + 1

return b''.join(stream_pieces)
return bytes(stream_pieces)

@utils.accept_singleton(basestring)
@utils.accept_singleton(str)
@utils.enforce_ids_param
@utils.empty_arg_shortcircuit
def report_incorrect_match(self, song_ids):
Expand All @@ -286,7 +288,7 @@ def report_incorrect_match(self, song_ids):

return song_ids

@utils.accept_singleton(basestring)
@utils.accept_singleton(str)
@utils.enforce_ids_param
@utils.empty_arg_shortcircuit
def upload_album_art(self, song_ids, image_filepath):
Expand All @@ -312,7 +314,7 @@ def upload_album_art(self, song_ids, image_filepath):

# deprecated methods follow:

@utils.accept_singleton(basestring)
@utils.accept_singleton(str)
@utils.enforce_ids_param
@utils.empty_arg_shortcircuit
@utils.deprecated('prefer Mobileclient.delete_songs')
Expand All @@ -328,7 +330,7 @@ def delete_songs(self, song_ids):

return res['deleteIds']

@utils.accept_singleton(basestring, 2)
@utils.accept_singleton(str, 2)
@utils.enforce_ids_param(2)
@utils.enforce_id_param
@utils.empty_arg_shortcircuit(position=2)
Expand All @@ -350,7 +352,7 @@ def add_songs_to_playlist(self, playlist_id, song_ids):

return [(e['songId'], e['playlistEntryId']) for e in new_entries]

@utils.accept_singleton(basestring, 2)
@utils.accept_singleton(str, 2)
@utils.enforce_ids_param(2)
@utils.enforce_id_param
@utils.empty_arg_shortcircuit(position=2)
Expand Down Expand Up @@ -383,7 +385,7 @@ def remove_songs_from_playlist(self, playlist_id, sids_to_match):
else:
return []

@utils.accept_singleton(basestring, 2)
@utils.accept_singleton(str, 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.
Expand All @@ -406,7 +408,7 @@ def _remove_entries_from_playlist(self, playlist_id, entry_ids_to_remove):
num_not_found, playlist_id)

# Unzip the pairs.
sids, eids = zip(*e_s_id_pairs)
sids, eids = list(zip(*e_s_id_pairs))

res = self._make_call(webclient.DeleteSongs, sids, playlist_id, eids)

Expand Down
6 changes: 6 additions & 0 deletions gmusicapi/exceptions.py
Expand Up @@ -2,8 +2,14 @@

"""Custom exceptions used across the project."""
from __future__ import print_function, division, absolute_import, unicode_literals
from future import standard_library
from future.utils import python_2_unicode_compatible

standard_library.install_aliases()
from builtins import * # noqa


@python_2_unicode_compatible
class CallFailure(Exception):
"""Exception raised when a Google Music server responds that a call failed.

Expand Down