[
  {
    "path": ".gitignore",
    "content": "*.csv\n*.log\n*.pyc\n\n!ExamplePlaylist.csv\n"
  },
  {
    "path": "ExamplePlaylist.csv",
    "content": ",test comment (and blank track)\n\n,test fuzzy artist title search\nstray cats stray cat strut\n\n,test fuzzy title artist search\njust what i needed the cars\n\n,test fuzzy search for song that should return a low match\ninstant karma! we all shine on john lennon\n\n,\"test detailed library search (this song isn't in aa, but it's in my library)\"\nclassical gas,vanessa-mae\n\n,test detailed all access search (the top fuzzy result is incorrect)\nam/fm,!!!,strange weather\n\n,this should return a low result\nback in black,ac/dc,back in black\n\n,test detailed search which should return song from library if you have it\norion,metallica,master of puppets\n\n,test album distinction and slight artist mismatch\nMoments in Love,The Art of Noise,And What Have You Done with My Body God?\nMoments in Love,The Art of Noise,Daft\n\n,test slight title mismatch\nMaking Love Out of Nothing at All,Air Supply,Ultimate Air Supply\n\n,\"test low score, mismatched title, and mistmatched artist, and comma in entry\"\nBlame It on the Rain,Milli Vanilli,Greatest Hits\n\n,\"test low score, mismatched title, mismatched artist, mismatched song, and entry comma\"\n1o1,Chris Duarte Groop,Ronp\n\n,test useless info in brackets and duplicate checks\n1o1 (Live!) [In Concert] {World Tour},Chris Duarte Groop,Ronp\n\n,test title only search\nBe Thou My Vision,Dallan Forgaill,\n\n,test initial unmatched fuzzy with info in brackets\nstray cats (asdfDoNotMatchMe1234) stray cat strut\n\n,\n,expected results\n,13/15 tracks imported\n,2 duplicate tracks\n,\n\n"
  },
  {
    "path": "ExportLists.py",
    "content": "# Author: John Elkins <john.elkins@yahoo.com>\n# License: MIT <LICENSE>\n\nfrom common import *\n\nif len(sys.argv) < 2:\n    log('ERROR output directory is required')\n    time.sleep(3)\n    exit()\n\n# setup the output directory, create it if needed\noutput_dir = sys.argv[1]\nif not os.path.exists(output_dir):\n    os.makedirs(output_dir)\n\n# log in and load personal library\napi = open_api()\nlibrary = load_personal_library()\n\ndef playlist_handler(playlist_name, playlist_description, playlist_tracks):\n    # skip empty and no-name playlists\n    if not playlist_name: return\n    if len(playlist_tracks) == 0: return\n\n    # setup output files\n    playlist_name = playlist_name.replace('/', '')\n    open_log(os.path.join(output_dir,playlist_name+u'.log'))\n    outfile = codecs.open(os.path.join(output_dir,playlist_name+u'.csv'),\n        encoding='utf-8',mode='w')\n\n    # keep track of stats\n    stats = create_stats()\n    export_skipped = 0\n    # keep track of songids incase we need to skip duplicates\n    song_ids = []\n\n    log('')\n    log('============================================================')\n    log(u'Exporting '+ unicode(len(playlist_tracks)) +u' tracks from '\n        +playlist_name)\n    log('============================================================')\n\n    # add the playlist description as a \"comment\"\n    if playlist_description:\n        outfile.write(tsep)\n        outfile.write(playlist_description)\n        outfile.write(os.linesep)\n\n    for tnum, pl_track in enumerate(playlist_tracks):\n        track = pl_track.get('track')\n\n        # we need to look up these track in the library\n        if not track:\n            library_track = [\n                item for item in library if item.get('id')\n                in pl_track.get('trackId')]\n            if len(library_track) == 0:\n                log(u'!! '+str(tnum+1)+repr(pl_track))\n                export_skipped += 1\n                continue\n            track = library_track[0]\n\n        result_details = create_result_details(track)\n\n        if not allow_duplicates and result_details['songid'] in song_ids:\n            log('{D} '+str(tnum+1)+'. '+create_details_string(result_details,True))\n            export_skipped += 1\n            continue\n\n        # update the stats\n        update_stats(track,stats)\n\n        # export the track\n        song_ids.append(result_details['songid'])\n        outfile.write(create_details_string(result_details))\n        outfile.write(os.linesep)\n\n    # calculate the stats\n    stats_results = calculate_stats_results(stats,len(playlist_tracks))\n\n    # output the stats to the log\n    log('')\n    log_stats(stats_results)\n    log(u'export skipped: '+unicode(export_skipped))\n\n    # close the files\n    close_log()\n    outfile.close()\n\n# the personal library is used so we can lookup tracks that fail to return\n# info from the ...playlist_contents() call\n\nplaylist_contents = api.get_all_user_playlist_contents()\n\nfor playlist in playlist_contents:\n    playlist_name = playlist.get('name')\n    playlist_description = playlist.get('description')\n    playlist_tracks = playlist.get('tracks')\n\n    playlist_handler(playlist_name, playlist_description, playlist_tracks)\n\nif export_thumbs_up:\n    # get thumbs up playlist\n    thumbs_up_tracks = []\n    for track in library:\n        if track.get('rating') is not None and int(track.get('rating')) > 1:\n            thumbs_up_tracks.append(track)\n\n\n    # modify format of each dictionary to match the data type\n    # of the other playlists\n    thumbs_up_tracks_formatted = []\n    for t in thumbs_up_tracks:\n        thumbs_up_tracks_formatted.append({'track': t})\n\n    playlist_handler('Thumbs up', 'Thumbs up tracks', thumbs_up_tracks_formatted)\n\nif export_all:\n    all_tracks_formatted = []\n    for t in library:\n        all_tracks_formatted.append({'track': t})\n\n    playlist_handler('All', 'All tracks', all_tracks_formatted)\n\nclose_api()\n    \n"
  },
  {
    "path": "ImportList.py",
    "content": "# Author: John Elkins <john.elkins@yahoo.com>\n# License: MIT <LICENSE>\n\nimport re\nimport datetime\nimport math\nimport time\nfrom common import *\n\n# the file for outputing the information google has one each song\ncsvfile = None\n\n# cleans up any open resources\ndef cleanup():\n    if csvfile:\n        csvfile.close()\n    close_log()\n    close_api()\n\n# compares two strings based only on their characters\ndef s_in_s(string1,string2):\n    if not string1 or not string2:\n        return False\n    s1 = re.compile('[\\W_]+', re.UNICODE).sub(u'',string1.lower())\n    s2 = re.compile('[\\W_]+', re.UNICODE).sub(u'',string2.lower())\n\n    return s1 in s2 or s2 in s1\n\n# sleeps a little bit after printing message before exiting\ndef delayed_exit(message):\n    log(message)\n    time.sleep(5)\n    cleanup()\n    exit()\n\n# add the song\ndef add_song(details,score):\n    (result_score,score_reason) = score\n\n    if ('+' in result_score and log_high_matches) or '-' in result_score:\n        log(result_score+track+score_reason+u' #'+str(len(song_ids)))\n        log (u'   ' + create_details_string(details, True))\n\n    if not allow_duplicates and details['songid'] in song_ids:\n        return\n\n    song_ids.append(details['songid'])\n    csvfile.write(create_details_string(details))\n    csvfile.write(os.linesep)\n\n# log an unmatched track\ndef log_unmatched(track):\n    global no_matches\n    log(u'!! '+track)\n    csvfile.write(track)\n    csvfile.write(os.linesep)\n    no_matches += 1\n\n# search for the song with the given details\ndef search_for_track(details):\n    search_results = []\n    dlog('search details: '+str(details))\n\n    # search the personal library for the track\n    lib_album_match = False\n    if details['artist'] and details['title'] and search_personal_library:\n        lib_results = [item for item in library if\n            s_in_s(details['artist'],item.get('artist'))\n            and s_in_s(details['title'],item.get('title'))]\n        dlog('lib search results: '+str(len(lib_results)))\n        for result in lib_results:\n            if s_in_s(result['album'],details['album']):\n                lib_album_match = True\n            item = {}\n            item[u'track'] = result\n            item[u'score'] = 200\n            search_results.append(item)\n\n    # search all access for the track\n    if not lib_album_match:\n        query = u''\n        if details['artist']:\n            query = details['artist']\n        if details['title']:\n            query += u' ' + details['title']\n        if not len(query):\n            query = track\n        dlog('aa search query:'+query)\n        aa_results = aa_search(query,7)\n        dlog('aa search results: '+str(len(aa_results)))\n        search_results.extend(aa_results)\n\n    if not len(search_results):\n        return None\n\n    top_result = search_results[0]\n    # if we have detailed info, perform a detailed search\n    if details['artist'] and details['title']:\n        search_results = [item for item in search_results if\n            s_in_s(details['title'],item['track']['title'])\n            and s_in_s(details['artist'],item['track']['artist'])]\n        if details['album']:\n            search_results = [item for item in search_results if\n                    s_in_s(details['album'],item['track']['album'])]\n        dlog('detail search results: '+str(len(search_results)))\n        if len(search_results) != 0:\n            top_result = search_results[0]\n\n    return top_result\n\n# match score stats\nno_matches = 0\nlow_scores = 0\nlow_titles = 0\nlow_artists = 0\ntrack_count = 0\nduplicates = 0\n\n# score the match against the query\ndef score_track(details,result_details,top_score = 200):\n    global low_scores\n    global low_titles\n    global low_artists\n    global duplicates\n\n    # check for low quality matches\n    result_score = u' + '\n    score_reason = u' '\n    is_low_result = False\n    if top_score < 120:\n        score_reason += u'{s}'\n        #low scores alone don't seem to me a good indication of an issue\n        #is_low_result = True\n    # wrong song\n    if ((details['title']\n        and not s_in_s(details['title'],result_details['title']))\n        or (not details['title']\n        and not s_in_s(track,result_details['title']))):\n        score_reason += u'{T}'\n        low_titles += 1\n        is_low_result = True\n    # wrong album\n    if (details['album'] and not ignore_album_mismatch\n        and not s_in_s(details['album'],result_details['album'])):\n        score_reason += u'{a}'\n        is_low_result = True\n    # wrong artist\n    if (details['artist']\n        and not s_in_s(details['artist'],result_details['artist'])):\n        score_reason += u'{A}'\n        low_artists += 1\n        is_low_result = True\n    # duplicate song\n    if not allow_duplicates and result_details['songid'] in song_ids:\n        score_reason += u'{D}'\n        duplicates += 1\n        is_low_result = True\n\n    if is_low_result:\n        result_score = u' - '\n        low_scores += 1\n\n    return (result_score,score_reason)\n\n# check to make sure a filename was given\nif len(sys.argv) < 2:\n    delayed_exit(u'ERROR input filename is required')\n\n\n# setup the input and output filenames and derive the playlist name\ninput_filename = sys.argv[1].decode('utf-8')\noutput_filename = os.path.splitext(input_filename)[0]\noutput_filename = re.compile('_\\d{14}$').sub(u'',output_filename)\nplaylist_name = os.path.basename(output_filename)\n\noutput_filename += u'_' + unicode(datetime.datetime.now().strftime(\n    '%Y%m%d%H%M%S'))\nlog_filename = output_filename + u'.log'\ncsv_filename = output_filename + u'.csv'\n\n#open the log and output csv files\ncsvfile = codecs.open(csv_filename, encoding='utf-8', mode='w', buffering=1)\nopen_log(log_filename)\n\n# read the playlist file into the tracks variable\ntracks = []\nplog('Reading playlist... ')\nwith codecs.open(input_filename, encoding='utf-8', mode='r', errors='ignore') as f:\n    tracks = f.read().splitlines()\nlog('done. '+str(len(tracks))+' lines loaded.')\n\n# log in and load personal library\napi = open_api()\nlibrary = load_personal_library()\n\n# begin searching for the tracks\nlog('===============================================================')\nlog(u'Searching for songs from: '+playlist_name)\nlog('===============================================================')\n\n\n# gather up the song_ids and submit as a batch\nsong_ids = []\n\n# collect some stats on the songs\nstats = create_stats()\n\n# time how long it takes\nstart_time = time.time()\n\n# loop over the tracks that were read from the input file\nfor track in tracks:\n    \n    # skip empty lines\n    if not track:\n        continue\n\n    # parse the track info if the line is in detail format\n    details_list = get_csv_fields(track)\n    details = create_details(details_list)\n\n    # skip comment lines\n    if len(details_list) == 2 and not details_list[0]:\n        log(details_list[1])\n        csvfile.write(tsep)\n        csvfile.write(details_list[1])\n        csvfile.write(os.linesep)\n        continue\n\n    # skip empty details records\n    if (len(details_list) >= 3 and not details['artist']\n        and not details['album'] and not details['title']):\n        continue\n\n    # at this point we should have a valid track\n    track_count += 1\n\n    # don't search if we already have a track id\n    if details['songid']:\n        add_song(details,score_track(details,details))\n        continue\n\n    # search for the song\n    search_result = search_for_track(details)\n\n    # a details dictionary we can use for 'smart' searching\n    smart_details = {}\n    smart_details['title'] = details['title']\n    smart_details['artist'] = details['artist']\n    smart_details['album'] = details['album']\n\n    if not details['title']:\n        smart_details['title'] = track\n\n    # if we didn't find anything strip out any (),{},[],<> from title\n    match_string = '\\[.*?\\]|{.*?}|\\(.*?\\)|<.*?>'\n    if not search_result and re.search(match_string,smart_details['title']):\n        dlog('No results found, attempting search again with modified title.')\n        smart_details['title'] = re.sub(match_string,'',smart_details['title'])\n        search_result = search_for_track(smart_details)\n\n    # if there isn't a result, try searching for the title only\n    if not search_result and search_title_only:\n        dlog('Attempting to search for title only')\n        smart_details['artist'] = None\n        smart_details['album'] = None\n        smart_details['title_only_search'] = True\n        search_result = search_for_track(smart_details)\n\n    # check for a result\n    if not search_result:\n        log_unmatched(track)\n        continue\n\n    # gather up info about result\n    result = search_result.get('track')\n    result_details = create_result_details(result)\n    result_score = score_track(details,result_details,\n        search_result.get('score'))\n\n    # if the song title doesn't match after a title only search, skip it\n    (score,reason) = result_score\n    if '{T}' in reason and 'title_only_search' in smart_details:\n        log_unmatched(track)\n        continue\n\n    update_stats(result,stats)\n    \n    # add the song to the id list\n    add_song(result_details,result_score)\n\ntotal_time = time.time() - start_time\n\nlog('===============================================================')\nlog(u'Adding '+unicode(len(song_ids))+' found songs to: '+playlist_name)\nlog('===============================================================')\n\n# add the songs to the playlist(s)\nmax_playlist_size = 1000\ncurrent_playlist = 1\ntotal_playlists_needed = int(math.ceil(len(song_ids)/float(max_playlist_size)))\nwhile current_playlist <= total_playlists_needed:\n    # build the playlist name, add part number if needed\n    current_playlist_name = playlist_name\n    if total_playlists_needed > 1:\n        current_playlist_name += u' Part ' + unicode(current_playlist)\n\n    # create the playlist and add the songs\n    playlist_id = api.create_playlist(current_playlist_name)\n    current_playlist_index = ( current_playlist - 1 ) * max_playlist_size\n    current_songs = song_ids[current_playlist_index :\n                             current_playlist_index + max_playlist_size]\n\n    added_songs = api.add_songs_to_playlist(playlist_id,current_songs)\n\n    log(u' + '+current_playlist_name+u' - '+unicode(len(added_songs))+\n        u'/'+unicode(len(current_songs))+' songs')\n\n    # go to the next playlist section\n    current_playlist += 1\n\n# log a final status\nno_match_ratio = float(no_matches) / track_count if track_count else 0\nlow_score_ratio = float(low_scores) / track_count if track_count else 0\nlow_artists_ratio = float(low_artists) / low_scores if low_scores else 0\nlow_titles_ratio = float(low_titles) / low_scores if low_scores else 0\nfound_ratio = 1 - no_match_ratio - low_score_ratio\n\nlog('===============================================================')\nlog('   ' + str(len(song_ids)) + '/' + str(track_count) + ' tracks imported')\nlog(' ! ' + str(no_match_ratio*100) + '% of tracks could not be matched')\nlog(' - ' + str(low_score_ratio*100) + '% of tracks had low match scores')\nlog('  {T} ' + str(low_titles)\n    + ' low matches were due to a song title mismatch')\nlog('  {A} ' + str(low_artists)\n    + ' low matches were due to song artist mismatch')\nif not allow_duplicates:\n    log ('  {D} ' + str(duplicates)\n        + ' duplicates were found and skipped')\nlog(' + ' + str(found_ratio*100) + '% of tracks had high match scores')\nlog('')\nstats_results = calculate_stats_results(stats,len(song_ids))\nlog_stats(stats_results)\n\nlog('\\nsearch time: '+str(total_time))\n\ncleanup()\n\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014 John Elkins\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "gmusic-playlist\n===============\n\nplaylist scripts for gmusic\n\n## Prerequisites\n\n- python 2.7 - https://www.python.org\n- gmusicapi - https://github.com/simon-weber/Unofficial-Google-Music-API\n\nBefore using the scripts, open up the preferences.py file and change the username.\n\nWhen 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.\n\n## ExportLists.py\n\nThis script will export all playlists to a given directory as csv files.  For the purpose of these scripts CSV stands for character seperated value.  The default separator charator is ','  The separator character is configurable in the preferences file.  Versions of the code previous to Aug 16 2015 used a '\\' separator character as the default.  Most spreadsheet apps can open csv files.\n\nThe order in which the artist, album, and title information appears as well as the separating character between each piece of information is configured in the preference.py file.  The default order and separator character will output song info as: \"title\",\"artist\",\"album\",\"songid\"\n\nThe csv files can be re-imported using the ImportList.py script.\n\nCommand Line Usage: python ExportLists.py OutputDir\n\nOutputDir is a directory you would like the playlists to be output to.\n\nThe export progress will be output to the console and to a log file.  At the completion of the export a status of the overal makeup of the playlist will be output.\n\n## ImportList.py\n\nThis script will import a given csv file into google music as a playlist. The title of the playlist will be the name of the text file and each track will be matched to each line in the text file.\n\nCommand Line Usage: python ImportList.py ExamplePlaylist.csv\n\nThe progress of the playlist creation will be output to the console and to a log file.  Tracks that could not be found are prefixed with !! and tracks that were found but may not be a good match are prefixed with -.  One or more of the following will appear after a track with a low match: {A}{a}{T}{s}  These markings indicate why the match was low,  {A} means the artist didn't match, {T} means the title didn't match, {a} means the album didn't match, and {s} means it had a low result score.  In addition to a log file, a csv file is created which contains all tracks found and their associated google music song id.\n\nThe csv file output from the ImportList.py script can be used to fix any song that didn't import correctly.  Open the csv file, look for the songs without any song id and see if there is something that you can change in the track info to get google to find the song.  Save the file and then re-run it through the ImportList.py script.  Since the csv file will contain the song id's for songs it already found it won't need to look those up again and will just focus on finding the songs that don't have id's yet.\n\nYou can also look up the song you want via google music's web interface and get the song id by clicking share > get link.  The song id is given in the link.\n\n## Playlist files\n\nThe format of each track in a playlist file can either be fuzzy or detailed info.  Comments are also supported.\n\nA fuzzy track is a track that has no separating characters and simply lists a song title, song title and author, or song author and title.  See the ExamplePlaylist.csv file for a few examples of fuzzy tracks.  Fuzzy tracks will only be matched to all access tracks.  If you have a song in a playlist that isn't in all access, but is in your personal library you will need to use a detailed track.\n\nA detailed track lists title,artist,and album information separated by the separator character and in the order defined in the preferences.py file.  The songId is optional, and will be added by the scripts when outputting a csv file.  See the ExamplePlaylist.csv file for a few examples of detailed track lists.  The album can be left out if not required.\n\nA comment in a playlist file follows the form of Ccomment where C is the separator character and comment is the comment.  See the ExamplePlaylist.csv file.\n\n## see also \n\n[a javascript version](https://github.com/soulfx/gmusic-playlist.js) for doing import / export directly within google music.\n"
  },
  {
    "path": "common.py",
    "content": "# Author: John Elkins <john.elkins@yahoo.com>\n# License: MIT <LICENSE>\n\n__version__ = '0.160530'\n\n__required_gmusicapi_version__ = '10.0.0'\n\nfrom collections import Counter\nfrom gmusicapi import __version__ as gmusicapi_version\nfrom gmusicapi import Mobileclient\nfrom gmusicapi.exceptions import CallFailure\nfrom preferences import *\nimport re\nimport time\nimport getpass\nimport sys\nimport os\nimport codecs\n\n# the api to use for accessing google music\napi = None\n\n# the logfile for keeping track of things\nlogfile = None\n\n# provide a shortcut for track_info_separator\ntsep = track_info_separator\n\n# flag indicating if account is all access capable\nallaccess = True\n\n# check for debug set via cmd line\nif '-dDEBUG' in sys.argv:\n    debug = True\n\n# check versions\ndef assert_prerequisites():\n\n    required = __required_gmusicapi_version__\n    actual = gmusicapi_version\n    \n    def version(ver):\n        return int(re.sub(r'\\D','',ver))\n\n    if ( version(actual) < version(required) ):\n        log(\"ERROR gmusicapi version of at least \"+required+\" is required. \")\n        exit()\n\n# loads the personal library\ndef load_personal_library():\n    plog('Loading personal library... ')\n    plib = api.get_all_songs()\n    log('done. '+str(len(plib))+' personal tracks loaded.')\n    return plib\n\n# opens the log for writing\ndef open_log(filename):\n    global logfile\n    logfile = codecs.open(filename, encoding='utf-8', mode='w', buffering=1)\n    return logfile\n\n# closes the log\ndef close_log():\n    if logfile:\n        logfile.close()\n\n# logs to both the console and log file if it exists\ndef log(message, nl = True):\n    if nl:\n        message += os.linesep\n    sys.stdout.write(message.encode(sys.stdout.encoding, errors='replace'))\n    if logfile:\n        logfile.write(message)\n\n# logs a message if debug is true\ndef dlog(message):\n    if debug:\n        log(message)\n\n# logs a progress message (a message without a line return)\ndef plog(message):\n    log(message, nl = False)\n\n# search all access\ndef aa_search(search_string,max_results):\n    global allaccess\n    results = []\n    if allaccess:\n        try:\n            results = api.search(search_string,\n                    max_results=max_results).get('song_hits')\n        except CallFailure:\n            allaccess = False\n            log('WARNING no all access subscription detected. '+\n                ' all access search disabled.')\n    return results\n\n# gets the track details available for google tracks\ndef get_google_track_details(sample_song = 'one u2'):\n    results = aa_search(sample_song,1)\n    if len(results):\n        return (results[0].get('track').keys())\n    return \"['title','artist','album']\"\n\n# creates result details from the given track\ndef create_result_details(track):\n    result_details = {}\n    for key, value in track.iteritems():\n        result_details[key] = value\n    result_details['songid'] = (track.get('storeId')\n        if track.get('storeId') else track.get('id'))\n    return result_details\n\n# creates details dictionary based off the given details list\ndef create_details(details_list):\n    details = {}\n    details['artist'] = None\n    details['album'] = None\n    details['title'] = None\n    details['songid'] = None\n    if len(details_list) < 2:\n        return details\n    for pos, nfo in enumerate(details_list):\n        if len(track_info_order) <= pos:\n            continue\n        details[track_info_order[pos]] = nfo.strip()\n    return details\n\n# split a csv line into it's separate fields\ndef get_csv_fields(csvString,sepChar=tsep):\n    fields = []\n    fieldValue = u''\n    ignoreTsep = False\n    for c in csvString:\n        if c == sepChar and not ignoreTsep:\n            fields.append(handle_quote_input(fieldValue))\n            fieldValue = u''\n            continue\n        elif c == '\"':\n            ignoreTsep = (not ignoreTsep)\n        fieldValue += c\n    fields.append(handle_quote_input(fieldValue))\n    return fields\n\n# add quotes around a csv field and return the quoted field\ndef handle_quote_output(aString):\n  \"\"\" See: https://en.wikipedia.org/wiki/Comma-separated_values#Basic_rules_and_examples \"\"\"\n  if aString.find('\"') > -1 or aString.find(tsep) > -1:\n    return '\"%s\"' % aString.replace('\"', '\"\"')\n  else:\n    return aString\n\n# remove the quotes from around a csv field, and return the unquoted field\ndef handle_quote_input(aString):\n  if len(aString) > 0 and aString[0] == '\"' and aString[-1] == '\"':\n      return aString[1:-1].replace('\"\"', '\"')\n  else:\n      return aString\n\n# creates details string based off the given details dictionary\ndef create_details_string(details_dict, skip_id = False):\n    out_string = u''\n    for nfo in track_info_order:\n        if skip_id and nfo == 'songid':\n            continue\n        if len(out_string) != 0:\n            out_string += track_info_separator\n        try:\n            out_string += handle_quote_output(unicode(details_dict[nfo]))\n        except KeyError:\n            # some songs don't have info like year, genre, etc\n            pass\n    return out_string\n\n# logs into google music api\ndef open_api():\n    global api\n    log('Logging into google music...')\n    # get the password each time so that it isn't stored in plain text\n    password = getpass.getpass(username + '\\'s password: ')\n    \n    api = Mobileclient()\n    if not api.login(username, password, Mobileclient.FROM_MAC_ADDRESS):\n        log('ERROR unable to login')\n        time.sleep(3)\n        exit()\n        \n    password = None\n    log('Login Successful.')\n    dlog(u'Available track details: '+str(get_google_track_details()))\n    return api\n\n# logs out of the google music api\ndef close_api():\n    if api:\n        api.logout()\n\n# creates a stats dictionary\ndef create_stats():\n    stats = {}\n    stats['genres'] = []\n    stats['artists'] = []\n    stats['years'] = []\n    stats['total_playcount'] = 0\n    return stats\n\n# updates the stats dictionary with info from the track\ndef update_stats(track,stats):\n    stats['artists'].append(track.get('artist'))\n    if track.get('genre'): stats['genres'].append(track.get('genre'))\n    if track.get('year'): stats['years'].append(track.get('year'))\n    if track.get('playCount'): stats['total_playcount'] += track.get(\n        'playCount')\n\n# calculates stats\ndef calculate_stats_results(stats,total_tracks):\n    results = {}\n    results['genres'] = Counter(stats['genres'])\n    results['artists'] = Counter(stats['artists'])\n    results['years'] = Counter(stats['years'])\n    results['playback_ratio'] = stats['total_playcount']/float(total_tracks)\n    return results    \n\n# logs the stats results\ndef log_stats(results):\n    log(u'top 3 genres: '+repr(results['genres'].most_common(3)))\n    log(u'top 3 artists: '+repr(results['artists'].most_common(3)))\n    log(u'top 3 years: '+repr(results['years'].most_common(3)))\n    log(u'playlist playback ratio: '+unicode(results['playback_ratio']))\n\n# display version and check prerequisites\nlog(\"gmusic-playlist: \"+__version__)\nlog(\"gmusicapi: \"+gmusicapi_version)\nassert_prerequisites();\n"
  },
  {
    "path": "preferences.py",
    "content": "\n# the username to use\nusername = 'john.elkins@gmail.com'\n\n# the separator to use for detailed track information\ntrack_info_separator = u','\n#track_info_separator = u'\\\\'\n#track_info_separator = u'|'\n\n# the order of the track details\ntrack_info_order = ['title','artist','album','songid']\n#track_info_order = ['title','artist','album','genre','year','durationMillis','playCount','rating','songid']\n\n# output debug information to the log\ndebug = False\n\n# don't import or export the same song twice\nallow_duplicates = False\n\n# == ImportList.py preferences ==============================================\n\n# ignore mismatched albums.  An album mismatch often doesn't mean the song is\n# wrong.  This is set to true so that mismatched albums don't scew the results\n# and flag too many songs with low scores\nignore_album_mismatch = True\n\n# search for tracks in the personal library, tracks found there will work\n# for you, but if you share your playlist others may not be able to play\n# some tracks.  Set to false if you want to make sure that your playlist doesn't\n# contain any tracks that are not shareable.\nsearch_personal_library = True\n\n# when unable to locate a track using full details (title,artist,album); perform\n# a search using only the song title.  this will hopefully find something to\n# at least put into the track spot.  this is handy for playlists that list the\n# composer or songwriter for a song instead of a singer.\nsearch_title_only = True\n\n# log high matches in addition to the songs that couldn't be found or had\n# low matches.\nlog_high_matches = False\n\n# export \"Thumbs Up\" playlist\nexport_thumbs_up = True\n\n# export \"ALL\" playlist\nexport_all = True\n"
  },
  {
    "path": "test/atestframe.py",
    "content": "# put the parent directory onto the path\nfrom os import sys, path\nsys.path.append(path.dirname(path.dirname(path.abspath(__file__))))\nimport unittest\n\ndef run_test():\n    unittest.main(verbosity=2)\n"
  },
  {
    "path": "test/test-common.py",
    "content": "from atestframe import *\nfrom common import *\n\nclass TestCommon(unittest.TestCase):\n\n    def test_get_csv_fields(self):\n        \"\"\" test that quoted and unquoted fields are being recognized \"\"\"\n        fields = get_csv_fields(u'something,\"good\",to \"eat\",\"like a \"\"hot\"\"\",dog',u',')\n        self.assertEqual(fields[0],u'something')\n        self.assertEqual(fields[1],u'good')\n        self.assertEqual(fields[2],u'to \"eat\"')\n        self.assertEqual(fields[3],u'like a \"hot\"')\n        self.assertEqual(fields[4],u'dog')\n        fields = get_csv_fields(u',hello',u',')\n        self.assertEqual(fields[0],u'')\n        self.assertEqual(fields[1],u'hello')\n        fields = get_csv_fields(u'test,\"commas, in, the, field\"',u',')\n        self.assertEqual(len(fields),2)\n        self.assertEqual(fields[0],u'test')\n        self.assertEqual(fields[1],u'commas, in, the, field')\n\n    def test_handle_quote_input(self):\n        \"\"\" test that quotes are being removed as expected \"\"\"\n        self.assertEqual(handle_quote_input(u''),u'')\n        self.assertEqual(handle_quote_input(u'a'),u'a')\n        self.assertEqual(handle_quote_input(u'\"\"'),u'')\n        self.assertEqual(handle_quote_input(u'\"\"asdf\"\"'),u'\"asdf\"')\n        self.assertEqual(handle_quote_input(u'\"asdf\"'),u'asdf')\n\n    def test_handle_quote_output(self):\n        \"\"\" test that quotes are applied only when needed \"\"\"\n        self.assertEqual(handle_quote_output(\"nothing to quote\"),\"nothing to quote\")\n        self.assertEqual(handle_quote_output('this \"needs\" quoting'),'\"this \"\"needs\"\" quoting\"')\n        self.assertEqual(handle_quote_output('tsep, in field'),'\"tsep, in field\"')\n\n    def test_quote_unquote(self):\n        \"\"\" test for verifying the quoting and unquoting that occurs in track values \"\"\"\n        test_values = ((\"\", \"\"),\n                       (\"bog\", \"bog\"),\n                       (\"\\\"bog\", \"\\\"\\\"\\\"bog\\\"\"),\n                       (\"\\\"bog\\\"\", \"\\\"\\\"\\\"bog\\\"\\\"\\\"\"),\n                       (\"b\\\"o\\\"g\", \"\\\"b\\\"\\\"o\\\"\\\"g\\\"\"),\n                       (\"\\\"\", \"\\\"\\\"\\\"\\\"\"))\n        for (invalue, expected) in test_values:\n            actual_out = handle_quote_output(invalue)\n            self.assertEqual(actual_out, expected) \n\n            actual_in = handle_quote_input(actual_out)\n            self.assertEqual(actual_in, invalue) \n\nrun_test()\n"
  },
  {
    "path": "test/z-README.txt",
    "content": "run tests as regular python executables like so:\npython test*\n\nif you have the coverage.py script installed\nthe tests can be run with coverage info like so:\npython -m coverage run --branch test*\npython -m coverage html"
  }
]