Skip to content
This repository was archived by the owner on Feb 10, 2023. It is now read-only.

Commit b08a435

Browse files
committed
Initial commit
0 parents  commit b08a435

10 files changed

+761
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.gmusic.credentials
2+
.cache-*
3+
*.pyc

README.md

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# TuneZinc
2+
3+
Synchronize your Google Music Playlists with Spotify
4+
5+
## Installation
6+
7+
1. git clone [email protected]:brentc/tunezinc.git
8+
1. mkvirtualenv TuneZinc
9+
1. pip install requirements.txt
10+
11+
## Configuration
12+
13+
Set the following environment variables:
14+
15+
* `GMUSIC_USERNAME` Your google username
16+
* `GMUSIC_PASSWORD` Your google password
17+
* `GMUSIC_PLAYLISTS` a '`;`' separated list of playlist names you want to sync
18+
19+
You will need to register a Spotify application to authenticate with spotify. Go to
20+
[My Applications](https://developer.spotify.com/my-applications/#!/applications) and "Create An
21+
App":
22+
23+
* Application Name: TuneZinc
24+
* Description: _Some Description_
25+
* Redirect URIs: `http://example.com/tunezinc/`
26+
27+
Save the app and configure the following environment variables:
28+
29+
* `SPOTIFY_USERNAME` Your spotify username
30+
* `SPOTIFY_CLIENT_ID` Your new app's Client ID
31+
* `SPOTIFY_CLIENT_SECRET` Your new app's Client Secret
32+
33+
## Usage
34+
35+
```bash
36+
python tunezinc.py
37+
```
38+
39+
The first time you run it, it will prompt you to oAuth authenticate with Spotify by open a browser,
40+
granting access and pasting the redirected URL back to the console.
41+
42+
You may also need to do a similar process with Google Music if it needs to get details from any
43+
tracks you uploaded via All Access (even though you must still provide your username and password as
44+
an environment variable)
45+
46+
## TODO
47+
48+
See [docs/TODO](docs/TODO.md) for thoughts on what's missing/needed.
49+
50+
## Contributing
51+
52+
1. Fork it!
53+
2. Create your feature branch: `git checkout -b my-new-feature`
54+
3. Commit your changes: `git commit -am 'Add some feature'`
55+
4. Push to the branch: `git push origin my-new-feature`
56+
5. Submit a pull request :D
57+
58+
## History
59+
60+
* Feb 7, 2016 - First working version
61+
62+
## Credits
63+
64+
Brent Charbonneau
65+
66+
## License
67+
68+
The MIT License (MIT)
69+
70+
Copyright (c) 2016 Brent Charbonneau
71+
72+
Permission is hereby granted, free of charge, to any person obtaining a copy
73+
of this software and associated documentation files (the "Software"), to deal
74+
in the Software without restriction, including without limitation the rights
75+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
76+
copies of the Software, and to permit persons to whom the Software is
77+
furnished to do so, subject to the following conditions:
78+
79+
The above copyright notice and this permission notice shall be included in all
80+
copies or substantial portions of the Software.
81+
82+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
83+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
84+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
85+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
86+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
87+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
88+
SOFTWARE.

app/__init__.py

Whitespace-only changes.

app/gmusic.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import logging
2+
from datetime import datetime, time
3+
import os
4+
5+
import pytz
6+
from cached_property import cached_property
7+
from gmusicapi import Mobileclient, Musicmanager
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class Gmusic(object):
13+
_client = Mobileclient()
14+
_manager = Musicmanager()
15+
16+
def __init__(self, username, password, playlists_to_sync, credentials_storage_location):
17+
self.username = username
18+
self.password = password
19+
self.playlists_to_sync = playlists_to_sync
20+
self.credentials_storage_location = credentials_storage_location
21+
22+
def client_login(self):
23+
if not self._client.login(self.username, self.password, Mobileclient.FROM_MAC_ADDRESS):
24+
logger.error(u"Gmusic mobile client authentication failed")
25+
return False
26+
27+
logger.info(u"Gmusic mobile client authentication succeeded.")
28+
return True
29+
30+
def manager_login(self):
31+
credentials = self.credentials_storage_location
32+
33+
if not os.path.isfile(credentials):
34+
credentials = self._manager.perform_oauth(
35+
storage_filepath=self.credentials_storage_location,
36+
open_browser=True
37+
)
38+
39+
if not self._manager.login(
40+
oauth_credentials=credentials,
41+
):
42+
logger.error(u"Gmusic music manager authentication failed")
43+
return False
44+
45+
logger.info(u"Gmusic music manager authentication succeeded.")
46+
return True
47+
48+
@property
49+
def client(self):
50+
if not self._client.is_authenticated():
51+
self.client_login()
52+
return self._client
53+
54+
@property
55+
def manager(self):
56+
if not self._manager.is_authenticated():
57+
self.manager_login()
58+
return self._manager
59+
60+
@cached_property
61+
def uploaded_songs(self):
62+
return {song['id']: song for song in self.manager.get_uploaded_songs()}
63+
64+
@property
65+
def _playlists(self):
66+
playlists = self.client.get_all_user_playlist_contents()
67+
logger.debug(u"Loaded {} playlists".format(len(playlists)))
68+
return playlists
69+
70+
@cached_property
71+
def playlists(self):
72+
playlists_to_sync = []
73+
for playlist in self._playlists:
74+
if playlist['name'] in self.playlists_to_sync:
75+
playlists_to_sync.append(playlist)
76+
return playlists_to_sync
77+
78+
def get_latest_addition_date(self, playlist):
79+
lastModified = playlist.get('lastModifiedTimestamp')
80+
if lastModified:
81+
return datetime.fromtimestamp(int(lastModified) / 10 ** 6).replace(tzinfo=pytz.utc)
82+
83+
return None

app/spotify.py

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import logging
2+
import urllib
3+
from datetime import datetime
4+
5+
import dateutil
6+
import spotipy
7+
from spotipy import util as spotipy_util
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class Spotify(object):
13+
_client = None
14+
_playlists = None
15+
SCOPES = ' '.join([
16+
'playlist-read-private',
17+
'playlist-modify-public',
18+
'playlist-modify-private',
19+
'user-read-private'
20+
])
21+
22+
def __init__(self, username, client_id, client_secret, create_public=False):
23+
self.username = username
24+
self.client_id = client_id
25+
self.client_secret = client_secret
26+
self.create_public = create_public
27+
28+
def _get_auth(self):
29+
return spotipy_util.prompt_for_user_token(
30+
username=self.username,
31+
client_id=self.client_id,
32+
client_secret=self.client_secret,
33+
redirect_uri='http://example.com/tunezinc/',
34+
scope=self.SCOPES,
35+
)
36+
37+
@property
38+
def client(self):
39+
if not self._client:
40+
self._client = spotipy.Spotify(auth=self._get_auth())
41+
return self._client
42+
43+
def search(self, q, market=None, limit=10, offset=0, type='track'):
44+
"""
45+
Spotipy 2.0 doesn't support the market parameter for search results, but
46+
we can add it ourselves
47+
"""
48+
if not market:
49+
return self.client.search(q, limit, offset, type)
50+
51+
return self.client._get('search', q=q, market=market, limit=limit, offset=offset, type=type)
52+
53+
def _fetch_playlists(self):
54+
playlists = {}
55+
for playlist in self.client.user_playlists(self.username)['items']:
56+
if playlist['owner']['id'] != self.username:
57+
logger.debug(u"Skipping playlist owned by a different user, {} ".format(playlist['name']))
58+
continue
59+
60+
name = playlist['name']
61+
if playlists.get(name):
62+
raise Exception("Duplicate playlist named '{}' found.".format(playlist['name']))
63+
playlists[name] = playlist
64+
65+
logger.debug(u"Loaded {} playlists".format(len(playlists)))
66+
return playlists
67+
68+
@property
69+
def playlists(self):
70+
if not self._playlists:
71+
self._playlists = self._fetch_playlists()
72+
return self._playlists
73+
74+
def _create_playlist(self, name):
75+
playlist = self.client.user_playlist_create(self.username, name)
76+
logger.info(u"Playlist named '{}' created.".format(name))
77+
self._playlists[name] = playlist
78+
return playlist
79+
80+
def get_playlist(self, name):
81+
return self.playlists[name]
82+
83+
def get_playlist_tracks(self, playlist_uri):
84+
return self.client.user_playlist_tracks(self.username, playlist_uri)
85+
86+
def get_latest_addition_date(self, playlist_tracks):
87+
max_added_at = None
88+
items = playlist_tracks.get('items')
89+
if not items:
90+
return None
91+
92+
for item in items:
93+
added_at = item.get('added_at')
94+
if not added_at:
95+
continue
96+
if max_added_at < added_at:
97+
max_added_at = added_at
98+
99+
if max_added_at:
100+
return dateutil.parser.parse(max_added_at)
101+
return None
102+
103+
def get_or_create_playlist(self, name):
104+
try:
105+
return self.get_playlist(name)
106+
except KeyError:
107+
logger.info(u"Playlist named '{}' doesn't exist".format(name))
108+
return self._create_playlist(name)
109+
110+
def find_track(self, track):
111+
logger.debug(u"Searching for spotify track matching: {}".format(track))
112+
parts = [
113+
u'track:"{}"'.format(track.search_title),
114+
u'artist:"{}"'.format(track.artist),
115+
u'album:"{}"'.format(track.search_album),
116+
]
117+
118+
results = self.search(
119+
q=u' '.join(parts),
120+
type='track',
121+
market='from_token'
122+
)
123+
124+
items = results.get('tracks', {}).get('items')
125+
if not items:
126+
logger.info(u"No match found for {}".format(track))
127+
return None
128+
129+
for track_info in items:
130+
if track.matches_spotify_track_info(track_info):
131+
track.spotify_uri = track_info.get('uri')
132+
logger.debug(u"Match found: {}, {}".format(track, track_info))
133+
return track
134+
135+
def add_tracks_to_playlist(self, playlist, tracks):
136+
if not tracks:
137+
return 0
138+
139+
result = self.client.user_playlist_add_tracks(
140+
self.username,
141+
playlist['uri'],
142+
[track.spotify_uri for track in tracks],
143+
)

0 commit comments

Comments
 (0)