├── MANIFEST.in ├── .gitignore ├── setup.py ├── LICENSE ├── gm_playlist_importer.py └── README /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README gm_playlist_importer.py LICENSE 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Thumbs.db 2 | #~ 3 | .DS_Store 4 | \#*\# 5 | .\#* 6 | .*.swp 7 | *.un~ 8 | .*.swo 9 | *.tmproj 10 | 11 | _local_*.py 12 | *.pyc 13 | 14 | docs/build 15 | dist 16 | __pycache__ 17 | 18 | bin 19 | lib 20 | include 21 | share 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name='Google-Music-Playlist-Importer', 5 | version='2012.03.28', 6 | author='Simon Weber', 7 | author_email='simon@simonmweber.com', 8 | url='https://github.com/simon-weber/Google-Music-Playlist-Importer', 9 | packages=[], 10 | scripts=['gm_playlist_importer.py'], 11 | license='GPLv3', 12 | description='A Python script to import local playlists to Google Music.', 13 | long_description="""\ 14 | A local list of song metadata will be matched against already-uploaded songs in Google Music. This is most useful for people who keep their music library organized. 15 | """, 16 | install_requires = [ 17 | "gmusicapi == 2012.03.27", 18 | "chardet"], 19 | classifiers = [ 20 | "Programming Language :: Python", 21 | "License :: OSI Approved :: GNU General Public License (GPL)", 22 | "Operating System :: OS Independent", 23 | "Topic :: Multimedia :: Sound/Audio", 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Simon Weber 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the copyright holder nor the 12 | names of the contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /gm_playlist_importer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Python script to import local playlists to Google Music.""" 4 | 5 | 6 | import re 7 | import sys 8 | import codecs 9 | from getpass import getpass 10 | 11 | import chardet 12 | import gmusicapi.gmtools.tools as gm_tools 13 | from gmusicapi import Mobileclient 14 | 15 | 16 | def init(max_attempts=3): 17 | """Makes an instance of the api and attempts to login with it. 18 | Returns the api after at most max_attempts. 19 | 20 | :param max_attempts: 21 | """ 22 | 23 | api = Mobileclient() 24 | 25 | logged_in = False 26 | attempts = 0 27 | 28 | print "Log in to Google Music." 29 | 30 | while not logged_in and attempts < max_attempts: 31 | email = raw_input("Email: ") 32 | password = getpass() 33 | 34 | logged_in = api.login(email, password) 35 | attempts += 1 36 | 37 | return api 38 | 39 | 40 | def guess_encoding(filename): 41 | """Returns a tuple of (guessed encoding, confidence). 42 | 43 | :param filename: 44 | """ 45 | 46 | res = chardet.detect(open(filename).read()) 47 | return (res['encoding'], res['confidence']) 48 | 49 | 50 | def main(): 51 | 52 | if not len(sys.argv) == 2: 53 | print "usage:", sys.argv[0], "" 54 | sys.exit(0) 55 | 56 | #The three md_ lists define the format of the playlist and how matching should be done against the library. 57 | #They must all have the same number of elements. 58 | 59 | #Where this pattern matches, a query will be formed from the captures. 60 | #My example matches against a playlist file with lines like: 61 | # /home/simon/music/library/The Cat Empire/Live on Earth/The Car Song.mp3 62 | #Make sure it won't match lines that don't contain song info! 63 | md_pattern = r"^/home/simon/music/library/(.*)/(.*)/(.*)\..*$" 64 | 65 | #Identify what metadata each capture represents. 66 | #These need to be valid fields in the GM json - see protocol_info in the api repo. 67 | md_cap_types = ('artist', 'album', 'title') 68 | 69 | #The lower-better priority of the capture types above. 70 | #In this example, I order title first, then artist, then album. 71 | md_cap_pr = (2, 3, 1) 72 | 73 | 74 | #Build queries from the playlist. 75 | playlist_fname = sys.argv[1] 76 | pl_encoding, confidence = guess_encoding(playlist_fname) 77 | 78 | queries = None 79 | with codecs.open(playlist_fname, encoding=pl_encoding, mode='r') as f: 80 | queries = gm_tools.build_queries_from(f, 81 | re.compile(md_pattern), 82 | md_cap_types, 83 | md_cap_pr, 84 | pl_encoding) 85 | 86 | api = init() 87 | 88 | if not api.is_authenticated(): 89 | print "Failed to log in." 90 | sys.exit(0) 91 | 92 | print "Loading library from Google Music..." 93 | library = api.get_all_songs() 94 | 95 | print "Matching songs..." 96 | 97 | matcher = gm_tools.SongMatcher(library) 98 | 99 | matched_songs = matcher.match(queries) 100 | 101 | res = raw_input("Output matches to file or terminal? (f/t): ") 102 | 103 | if res == "t": 104 | print matcher.build_log() 105 | elif res == "f": 106 | res = raw_input("Filename to write to: ") 107 | with open(res, mode='w') as f: 108 | f.write(matcher.build_log()) 109 | print "File written." 110 | 111 | 112 | 113 | go = raw_input("Create playlist from these matches? (y/n): ") 114 | if go == "y": 115 | name = raw_input("playlist name: ") 116 | p_id = api.create_playlist(name) 117 | 118 | print "Made playlist", name 119 | 120 | 121 | res = api.add_songs_to_playlist(p_id, 122 | map(gm_tools.filter_song_md, matched_songs)) 123 | print "Added songs." 124 | 125 | if __name__ == '__main__': 126 | main() 127 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | A Python script to import local playlists to Google Music. 2 | Relies on my Google Music api: https://github.com/simon-weber/Unofficial-Google-Music-API 3 | 4 | Songs are matched by metadata, not file hash. So, this is most useful for people who keep their music library organized. 5 | 6 | This is a work in progress and has only been tested on my library/playlists. That said, my music matched perfectly. Here's a quick FAQ: 7 | 8 | 9 | How do I use it? 10 | 11 | Get the source and the api. There's no ui right now; to set it up you'll need to modify some code at the top of main(). 12 | 13 | First, define the format of your playlists by modifying the regular expression md_pattern on line 63. You want this regex to match only song lines, and capture the metadata you want to match with. See my example in the code for guidance. 14 | 15 | Next, declare what kind of metadata you're capturing by modifying md_cap_types. You can find all the choices in protocol_info in the api repo. 16 | 17 | Lastly, declare the priority of the metadata when searching. More information on this below. 18 | 19 | Now you're ready to go! Run the file and pass in the filename of a playlist (or something similar that has your song metadata). It'll ask you to log in to Google Music (use your full email). Then it'll pull down your entire library and try to make matches. You might be asked to break ties now. 20 | 21 | After matching is done, you'll have the option to review matches before making a playlist. If you have a big playlist, when prompted, press f to print the log to a file; otherwise the terminal should be fine. The log has the following format for each attempted match: 22 | 23 | alert query 24 | matches 25 | 26 | The alert is intended to be an easily greppable status indicator. == is one match, !! is no match, and ?? is multiple. The query is what was pulled from the playlist to search with, and the matches are songs from your GM library. 27 | 28 | If you're happy, press y to make a new playlist. You should see it in Google Music after refreshing. 29 | 30 | 31 | How are matches made? 32 | 33 | The metadata you pick will filter the library one at a time. This is intended to allow for some mismatching metadata, so prioritize the metadata you think will match and narrow the search most. Here's an example, searching for 'The Car Song' by 'The Cat Empire' on 'Live on Earth': 34 | 35 | songs with title == 'The Car Song': 36 | The Car Song - The Cat Empire - Two Shoes 37 | The Car Song - The Cat Empire - Live at The Metro 38 | The Car Song - The Cat Empire - Live on Earth 39 | 40 | of those results, songs with artist == 'The Cat Empire': 41 | (no change) 42 | 43 | of those results, songs with album == 'Live on Earth': 44 | The Car Song - The Cat Empire - Live on Earth 45 | 46 | 47 | If the last step hadn't produced a single result, a tie breaker would run. By default, it will ask you to pick the song that matches. I've only implemented the manual tiebreaker and one that does nothing and returns many matches. I imagine an edit-distance tiebreaker would be useful. 48 | 49 | 50 | How are matches really made? 51 | 52 | Since even my metadata doesn't match all the time, it's a bit of a heuristic process. Long story short, if you keep the defaults, searches will retry insensitive to capitalization, and then with all punctuation (and non-english characters) turned into wildcards (.*). This matched my songs perfectly. If you don't want to automatically fall back to other kinds of searches, change the call to matcher.match() in main(). 53 | 54 | 55 | What do I do if my songs aren't matching? 56 | 57 | I can't test against any playlists and libraries except my own, so it's tough to account for this. I tried to make the matching process flexible, though. Look in gm_tools at SongMatcher; SearchModifiers and tie breakers can control just about every part of the search. Be sure to change the call to matcher.match() if you want to use a custom search style. 58 | 59 | If things still aren't working, feel free to give me a shout on g+ or via email. Bug reports and contributions welcome! 60 | 61 | 62 | 63 | 64 | Copyright (c) 2013 Simon Weber 65 | Licensed under the BSD 3-clause. See LICENSE. 66 | --------------------------------------------------------------------------------