Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bunch of usability improvements, some bug fixes and functional enhancements #56

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
12 changes: 6 additions & 6 deletions ExportLists.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
# Author: John Elkins <[email protected]>
# License: MIT <LICENSE>

import os
from common import *

if len(sys.argv) < 2:
log('ERROR output directory is required')
time.sleep(3)
exit()
args = parse_args('Export playlists from Google Play Music',
'ExportLists', 'output_directory',
'Directory into which to export playlists')

# setup the output directory, create it if needed
output_dir = sys.argv[1]
output_dir = args.output_directory
if not os.path.exists(output_dir):
os.makedirs(output_dir)

# log in and load personal library
api = open_api()
api = open_api(args.username, args.android_id, password=args.password)
library = load_personal_library()

def playlist_handler(playlist_name, playlist_description, playlist_tracks):
Expand Down
64 changes: 56 additions & 8 deletions ImportList.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Author: John Elkins <[email protected]>
# License: MIT <LICENSE>

import os
import re
import datetime
import math
Expand Down Expand Up @@ -56,6 +57,34 @@ def log_unmatched(track):
csvfile.write(os.linesep)
no_matches += 1

def best_match(details, results):
# Two heuristics for choosing the best match:
#
# 1. If some of the results are already in song_ids, then choose
# one that isn't.
# 2. Prefer an exact match on title, album, artist (in that order)
# over a substring match.
if len(results) == 1:
return results[0]

# create_result_details determines the songid
pairs = ((r, create_result_details(r['track'])) for r in results)

new_pairs = [p for p in pairs if p[1]['songid'] not in song_ids]
if len(new_pairs) == 1:
# If exactly one track isn't already selected, use it.
return new_pairs[0][0]
elif len(new_pairs) > 0:
# Only use the prefer-new heuristic if at least one track passes it.
pairs = new_pairs

for field in ('title', 'album', 'artist'):
for pair in pairs:
if details[field] and details[field] == pair[1][field]:
return pair[0]

return pairs[0][0] if pairs else None

# search for the song with the given details
def search_for_track(details):
search_results = []
Expand Down Expand Up @@ -93,7 +122,7 @@ def search_for_track(details):
if not len(search_results):
return None

top_result = search_results[0]
top_result = best_match(details, search_results)
# if we have detailed info, perform a detailed search
if details['artist'] and details['title']:
search_results = [item for item in search_results if
Expand All @@ -104,7 +133,7 @@ def search_for_track(details):
s_in_s(details['album'],item['track']['album'])]
dlog('detail search results: '+str(len(search_results)))
if len(search_results) != 0:
top_result = search_results[0]
top_result = best_match(details, search_results)

return top_result

Expand Down Expand Up @@ -162,13 +191,17 @@ def score_track(details,result_details,top_score = 200):

return (result_score,score_reason)

# check to make sure a filename was given
if len(sys.argv) < 2:
delayed_exit(u'ERROR input filename is required')

args = parse_args('Import playlist into Google Play Music',
'ImportList', 'playlist_filename',
'Playlist CSV file to import',
extra_args=(
(('--replace',),
{'action': 'store_true',
'help': 'Replace playlist(s) with the same name'}),
))

# setup the input and output filenames and derive the playlist name
input_filename = sys.argv[1].decode('utf-8')
input_filename = args.playlist_filename.decode('utf-8')
output_filename = os.path.splitext(input_filename)[0]
output_filename = re.compile('_\d{14}$').sub(u'',output_filename)
playlist_name = os.path.basename(output_filename)
Expand All @@ -190,7 +223,7 @@ def score_track(details,result_details,top_score = 200):
log('done. '+str(len(tracks))+' lines loaded.')

# log in and load personal library
api = open_api()
api = open_api(args.username, args.android_id, password=args.password)
library = load_personal_library()

# begin searching for the tracks
Expand Down Expand Up @@ -295,6 +328,21 @@ def score_track(details,result_details,top_score = 200):
log(u'Adding '+unicode(len(song_ids))+' found songs to: '+playlist_name)
log('===============================================================')

# delete old playlists if requested
if args.replace:
for playlist in api.get_all_playlists():
if playlist['kind'] != 'sj#playlist':
continue
if playlist['type'] != 'USER_GENERATED':
continue
if (playlist['name'] != playlist_name and
not playlist['name'].startswith(u'{} Part '.format(
playlist_name))):
continue
api.delete_playlist(playlist['id'])
log(u'Deleted playlist {} named {}'.format(
playlist['id'], playlist['name']))

# add the songs to the playlist(s)
max_playlist_size = 1000
current_playlist = 1
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ playlist scripts for gmusic
- python 2.7 - https://www.python.org
- gmusicapi - https://github.com/simon-weber/Unofficial-Google-Music-API

Before using the scripts, open up the preferences.py file and change the username.

When the scripts are run they will prompt for your password. If you use two factor authentication you will need to create and use an application password.

## ExportLists.py
Expand Down
98 changes: 92 additions & 6 deletions common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

__required_gmusicapi_version__ = '10.0.0'

import argparse
from ConfigParser import SafeConfigParser
from collections import Counter
from gmusicapi import __version__ as gmusicapi_version
from gmusicapi import Mobileclient
from gmusicapi.exceptions import CallFailure
from preferences import *
import random
import re
import time
import getpass
Expand All @@ -33,6 +36,80 @@
if '-dDEBUG' in sys.argv:
debug = True

def parse_args(description, program_name, path_target, path_help,
extra_args=()):
default_cf = os.path.expanduser("~/.gmusic-playlist.ini")
parser = argparse.ArgumentParser(description=description)
parser.add_argument("--config-file", "-cf", action="store",
help="INI file to read settings from (default {})".
format(default_cf), default=default_cf)
parser.add_argument("--username", action="store", help="Google username")
group = parser.add_mutually_exclusive_group()
group.add_argument("--android-id", action="store", help="Android ID to "
"connect with instead of using MAC address")
group.add_argument("--create-android-id", action="store_true",
help="Generate a random android ID and store it in the "
"configuration file for future use")
parser.add_argument(path_target, action="store",
metavar=path_target.replace('_', '-'),
help=path_help)
for arg in extra_args:
parser.add_argument(*arg[0], **arg[1])

args = parser.parse_args()

if not args.username:
args.username = get_config_username(program_name, args.config_file)
args.password = get_config_password(program_name, args.config_file)
if not args.android_id or args.create_android_id:
args.android_id = get_config_android_id(program_name, args.config_file,
args.create_android_id)
return args

def read_config_file(filename):
parser = SafeConfigParser()
parser.read(filename)
return parser

def get_config_setting(program_name, config_file, setting):
parser = read_config_file(config_file)
try:
value = parser.get(program_name, setting)
return value
except:
try:
value = parser.get('defaults', setting)
return value
except:
return None

def get_config_username(program_name, config_file):
username = get_config_setting(program_name, config_file, 'username')
if not username:
sys.exit('Specify username on command line or put it in "{}" or '
'"defaults" section of {}'.format(program_name, config_file))
return username

def get_config_password(program_name, config_file):
return get_config_setting(program_name, config_file, 'password')

def get_config_android_id(program_name, config_file, do_create):
if do_create:
parser = read_config_file(config_file)
android_id = ''.join(hex(int(random.random() * 16))[-1] for
i in range(16))
if not parser.has_section('defaults'):
parser.add_section('defaults')
parser.set('defaults', 'android_id', android_id)
parser.write(open(config_file, 'w'))
return android_id
android_id = get_config_setting(program_name, config_file, 'android_id')
if not android_id:
sys.exit('Specify android ID or --create-android-id on command line '
'or put it in "{}" or "defaults" section of {}'.format(
program_name, config_file))
return android_id

# check versions
def assert_prerequisites():

Expand Down Expand Up @@ -107,8 +184,16 @@ def create_result_details(track):
result_details = {}
for key, value in track.iteritems():
result_details[key] = value
result_details['songid'] = (track.get('storeId')
if track.get('storeId') else track.get('id'))
# Previously, the code did this:
#
# result_details['songid'] = (track.get('storeId')
# if track.get('storeId') else track.get('id'))
#
# But when the code was doing that, songs which had a storeId were not
# being added to my playlists. I do not understand why storeId was
# being used instead of id; all I know is that removing that logic
# fixes my playlists.
result_details['songid'] = track.get('id')
return result_details

# creates details dictionary based off the given details list
Expand Down Expand Up @@ -171,14 +256,15 @@ def create_details_string(details_dict, skip_id = False):
return out_string

# logs into google music api
def open_api():
def open_api(username, android_id=None, password=None):
global api
log('Logging into google music...')
# get the password each time so that it isn't stored in plain text
password = getpass.getpass(username + '\'s password: ')
if not password:
password = getpass.getpass(username + '\'s password: ')

api = Mobileclient()
if not api.login(username, password, Mobileclient.FROM_MAC_ADDRESS):
if not api.login(username, password,
android_id or Mobileclient.FROM_MAC_ADDRESS):
log('ERROR unable to login')
time.sleep(3)
exit()
Expand Down
4 changes: 0 additions & 4 deletions preferences.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@

# the username to use
username = '[email protected]'

# the separator to use for detailed track information
track_info_separator = u','
#track_info_separator = u'\\'
Expand Down