├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── gmusicapi_scripts ├── __init__.py ├── gmdelete.py ├── gmdownload.py ├── gmsearch.py ├── gmsync.py └── gmupload.py ├── requirements-dev.txt ├── setup.cfg └── setup.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # Ipython Notebook 63 | .ipynb_checkpoints 64 | 65 | # pyenv 66 | .python-version 67 | 68 | # MkDocs 69 | site/ 70 | 71 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Notable changes for the [gmusicapi-scripts](https://github.com/thebigmunch/gmusicapi-scripts) project. This project uses [Semantic Versioning](http://semver.org/) principles. 4 | 5 | 6 | ## [0.5.0](https://github.com/thebigmunch/gmusicapi-scripts/releases/tag/0.5.0) (2016-07-18) 7 | 8 | [Commits](https://github.com/thebigmunch/gmusicapi-scripts/compare/0.4.0...0.5.0) 9 | 10 | ### Fixed 11 | 12 | * Update template_to_base_path for gmusicapi-wrapper changes. Fixes issue with using incorrect tag field. 13 | 14 | 15 | ## [0.4.0](https://github.com/thebigmunch/gmusicapi-scripts/releases/tag/0.4.0) (2016-06-03) 16 | 17 | [Commits](https://github.com/thebigmunch/gmusicapi-scripts/compare/0.3.0...0.4.0) 18 | 19 | ### Changed 20 | 21 | * Update dependency versions. 22 | * Exit scripts on authentication failures. 23 | 24 | 25 | ## [0.3.0](https://github.com/thebigmunch/gmusicapi-scripts/releases/tag/0.3.0) (2016-02-29) 26 | 27 | [Commits](https://github.com/thebigmunch/gmusicapi-scripts/compare/0.2.1...0.3.0) 28 | 29 | ### Added 30 | 31 | * Output songs to be filtered in dry-run. 32 | 33 | ### Changed 34 | 35 | * Change --include-all to --all-includes to match parameter change in gmusicapi-wrapper. 36 | * Change --exclude-all to --all-excludes to match parameter change in gmusicapi-wrapper. 37 | * Change behavior of --max-depth=0; it now limits to the current directory level instead of being infinite recursion. 38 | 39 | 40 | ## [0.2.1](https://github.com/thebigmunch/gmusicapi-scripts/releases/tag/0.2.1) (2016-02-15) 41 | 42 | [Commits](https://github.com/thebigmunch/gmusicapi-scripts/compare/0.2.0...0.2.1) 43 | 44 | ### Fixed 45 | 46 | * Use correct track number metadata key for sorting. 47 | * Fix delete on success check. 48 | 49 | ### Changed 50 | 51 | * Update supported gmusicapi-wrapper versions. 52 | 53 | 54 | ## [0.2.0](https://github.com/thebigmunch/gmusicapi-scripts/releases/tag/0.2.0) (2016-02-13) 55 | 56 | [Commits](https://github.com/thebigmunch/gmusicapi-scripts/compare/0.1.0...0.2.0) 57 | 58 | ### Added 59 | 60 | * Python 3 support. 61 | 62 | ### Remove 63 | 64 | * Python 2 support. 65 | 66 | ### Changed 67 | 68 | * Port to Python 3. Python 2 is no longer supported. 69 | 70 | 71 | ## [0.1.0](https://github.com/thebigmunch/gmusicapi-scripts/releases/tag/0.1.0) (2015-12-02) 72 | 73 | [Commits](https://github.com/thebigmunch/gmusicapi-scripts/compare/b66da631025f5074df0e290aa515b7f18d14fde8...0.1.0) 74 | 75 | * First package release for PyPi. 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2017 thebigmunch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTING.md LICENSE README.md USAGE.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **gmusicapi-scripts is deprecated in favor of [google-music-scripts](https://github.com/thebigmunch/google-music-scripts).** 2 | -------------------------------------------------------------------------------- /gmusicapi_scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | __title__ = 'gmusicapi_scripts' 4 | __version__ = "0.5.0" 5 | __license__ = 'MIT' 6 | __copyright__ = 'Copyright 2016 thebigmunch ' 7 | -------------------------------------------------------------------------------- /gmusicapi_scripts/gmdelete.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | """ 5 | A script to delete songs from your Google Music library using https://github.com/simon-weber/gmusicapi. 6 | More information at https://github.com/thebigmunch/gmusicapi-scripts. 7 | 8 | Usage: 9 | gmdelete (-h | --help) 10 | gmdelete [options] [-f FILTER]... [-F FILTER]... 11 | 12 | Options: 13 | -h, --help Display help message. 14 | -u USERNAME, --user USERNAME Your Google username or e-mail address. 15 | -p PASSWORD, --pass PASSWORD Your Google or app-specific password. 16 | -I ID --android-id ID An Android device id. 17 | -l, --log Enable gmusicapi logging. 18 | -d, --dry-run Output list of songs that would be deleted. 19 | -q, --quiet Don't output status messages. 20 | With -l,--log will display gmusicapi warnings. 21 | With -d,--dry-run will display song list. 22 | -f FILTER, --include-filter FILTER Include Google songs by field:pattern filter (e.g. "artist:Muse"). 23 | Songs can match any filter criteria. 24 | This option can be set multiple times. 25 | -F FILTER, --exclude-filter FILTER Exclude Google songs by field:pattern filter (e.g. "artist:Muse"). 26 | Songs can match any filter criteria. 27 | This option can be set multiple times. 28 | -a, --all-includes Songs must match all include filter criteria to be included. 29 | -A, --all-excludes Songs must match all exclude filter criteria to be excluded. 30 | -y, --yes Delete songs without asking for confirmation. 31 | 32 | Patterns can be any valid Python regex patterns. 33 | """ 34 | 35 | import logging 36 | import sys 37 | 38 | from docopt import docopt 39 | 40 | from gmusicapi_wrapper import MobileClientWrapper 41 | 42 | QUIET = 25 43 | logging.addLevelName(25, "QUIET") 44 | 45 | logger = logging.getLogger('gmusicapi_wrapper') 46 | sh = logging.StreamHandler() 47 | logger.addHandler(sh) 48 | 49 | 50 | def main(): 51 | cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) 52 | 53 | if cli['quiet']: 54 | logger.setLevel(QUIET) 55 | else: 56 | logger.setLevel(logging.INFO) 57 | 58 | mcw = MobileClientWrapper(enable_logging=cli['log']) 59 | mcw.login(username=cli['user'], password=cli['pass'], android_id=cli['android-id']) 60 | 61 | if not mcw.is_authenticated: 62 | sys.exit() 63 | 64 | include_filters = [tuple(filt.split(':', 1)) for filt in cli['include-filter']] 65 | exclude_filters = [tuple(filt.split(':', 1)) for filt in cli['exclude-filter']] 66 | 67 | songs_to_delete, _ = mcw.get_google_songs( 68 | include_filters=include_filters, exclude_filters=exclude_filters, 69 | all_includes=cli['all-includes'], all_excludes=cli['all-excludes'] 70 | ) 71 | 72 | if cli['dry-run']: 73 | logger.info("Found {0} songs to delete".format(len(songs_to_delete))) 74 | 75 | if songs_to_delete: 76 | logger.info("\nSongs to delete:\n") 77 | 78 | for song in songs_to_delete: 79 | title = song.get('title', "") 80 | artist = song.get('artist', "") 81 | album = song.get('album', "") 82 | song_id = song['id'] 83 | 84 | logger.log(QUIET, "{0} -- {1} -- {2} ({3})".format(title, artist, album, song_id)) 85 | else: 86 | logger.info("\nNo songs to delete") 87 | else: 88 | if songs_to_delete: 89 | confirm = cli['yes'] or cli['quiet'] 90 | logger.info("") 91 | 92 | if confirm or input("Are you sure you want to delete {0} song(s) from Google Music? (y/n) ".format(len(songs_to_delete))) in ("y", "Y"): 93 | logger.info("\nDeleting {0} songs from Google Music\n".format(len(songs_to_delete))) 94 | 95 | songnum = 0 96 | total = len(songs_to_delete) 97 | pad = len(str(total)) 98 | 99 | for song in songs_to_delete: 100 | mcw.api.delete_songs(song['id']) 101 | songnum += 1 102 | 103 | title = song.get('title', "") 104 | artist = song.get('artist', "") 105 | album = song.get('album', "") 106 | song_id = song['id'] 107 | 108 | logger.debug("Deleting {0} -- {1} -- {2} ({3})".format(title, artist, album, song_id)) 109 | logger.info("Deleted {num:>{pad}}/{total} song(s) from Google Music".format(num=songnum, pad=pad, total=total)) 110 | else: 111 | logger.info("\nNo songs deleted.") 112 | else: 113 | logger.info("\nNo songs to delete") 114 | 115 | mcw.logout() 116 | logger.info("\nAll done!") 117 | 118 | 119 | if __name__ == '__main__': 120 | main() 121 | -------------------------------------------------------------------------------- /gmusicapi_scripts/gmdownload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | """ 5 | A download script for Google Music using https://github.com/simon-weber/gmusicapi. 6 | More information at https://github.com/thebigmunch/gmusicapi-scripts. 7 | 8 | Usage: 9 | gmdownload (-h | --help) 10 | gmdownload [-f FILTER]... [-F FILTER]... [options] [] 11 | 12 | Arguments: 13 | output Output file or directory name which can include a template pattern. 14 | Defaults to name suggested by Google Music in your current directory. 15 | 16 | Options: 17 | -h, --help Display help message. 18 | -c CRED, --cred CRED Specify oauth credential file name to use/create. [Default: oauth] 19 | -U ID --uploader-id ID A unique id given as a MAC address (e.g. '00:11:22:33:AA:BB'). 20 | This should only be provided when the default does not work. 21 | -l, --log Enable gmusicapi logging. 22 | -d, --dry-run Output list of songs that would be downloaded. 23 | -q, --quiet Don't output status messages. 24 | With -l,--log will display gmusicapi warnings. 25 | With -d,--dry-run will display song list. 26 | -f FILTER, --include-filter FILTER Include Google songs by field:pattern filter (e.g. "artist:Muse"). 27 | Songs can match any filter criteria. 28 | This option can be set multiple times. 29 | -F FILTER, --exclude-filter FILTER Exclude Google songs by field:pattern filter (e.g. "artist:Muse"). 30 | Songs can match any filter criteria. 31 | This option can be set multiple times. 32 | -a, --all-includes Songs must match all include filter criteria to be included. 33 | -A, --all-excludes Songs must match all exclude filter criteria to be excluded. 34 | 35 | Patterns can be any valid Python regex patterns. 36 | """ 37 | 38 | import logging 39 | import os 40 | import sys 41 | 42 | from docopt import docopt 43 | 44 | from gmusicapi_wrapper import MusicManagerWrapper 45 | 46 | QUIET = 25 47 | logging.addLevelName(25, "QUIET") 48 | 49 | logger = logging.getLogger('gmusicapi_wrapper') 50 | sh = logging.StreamHandler() 51 | logger.addHandler(sh) 52 | 53 | 54 | def main(): 55 | cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) 56 | 57 | if cli['quiet']: 58 | logger.setLevel(QUIET) 59 | else: 60 | logger.setLevel(logging.INFO) 61 | 62 | if not cli['output']: 63 | cli['output'] = os.getcwd() 64 | 65 | mmw = MusicManagerWrapper(enable_logging=cli['log']) 66 | mmw.login(oauth_filename=cli['cred'], uploader_id=cli['uploader-id']) 67 | 68 | if not mmw.is_authenticated: 69 | sys.exit() 70 | 71 | include_filters = [tuple(filt.split(':', 1)) for filt in cli['include-filter']] 72 | exclude_filters = [tuple(filt.split(':', 1)) for filt in cli['exclude-filter']] 73 | 74 | songs_to_download, songs_to_filter = mmw.get_google_songs( 75 | include_filters=include_filters, exclude_filters=exclude_filters, 76 | all_includes=cli['all-includes'], all_excludes=cli['all-excludes'] 77 | ) 78 | 79 | songs_to_download.sort(key=lambda song: (song.get('artist'), song.get('album'), song.get('track_number'))) 80 | 81 | if cli['dry-run']: 82 | logger.info("\nFound {0} song(s) to download".format(len(songs_to_download))) 83 | 84 | if songs_to_download: 85 | logger.info("\nSongs to download:\n") 86 | 87 | for song in songs_to_download: 88 | title = song.get('title', "") 89 | artist = song.get('artist', "<artist>") 90 | album = song.get('album', "<album>") 91 | song_id = song['id'] 92 | 93 | logger.log(QUIET, "{0} -- {1} -- {2} ({3})".format(title, artist, album, song_id)) 94 | else: 95 | logger.info("\nNo songs to download") 96 | 97 | if songs_to_filter: 98 | logger.info("\nSongs to filter:\n") 99 | 100 | for song in songs_to_filter: 101 | logger.log(QUIET, song) 102 | else: 103 | logger.info("\nNo songs to filter") 104 | else: 105 | if songs_to_download: 106 | logger.info("\nDownloading {0} song(s) from Google Music\n".format(len(songs_to_download))) 107 | mmw.download(songs_to_download, template=cli['output']) 108 | else: 109 | logger.info("\nNo songs to download") 110 | 111 | mmw.logout() 112 | logger.info("\nAll done!") 113 | 114 | 115 | if __name__ == '__main__': 116 | main() 117 | -------------------------------------------------------------------------------- /gmusicapi_scripts/gmsearch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | """ 5 | A script to search your Google Music library using https://github.com/simon-weber/gmusicapi. 6 | More information at https://github.com/thebigmunch/gmusicapi-scripts. 7 | 8 | Usage: 9 | gmsearch (-h | --help) 10 | gmsearch [options] [-f FILTER]... [-F FILTER]... 11 | 12 | Options: 13 | -h, --help Display help message. 14 | -u USERNAME, --user USERNAME Your Google username or e-mail address. 15 | -p PASSWORD, --pass PASSWORD Your Google or app-specific password. 16 | -I ID --android-id ID An Android device id. 17 | -l, --log Enable gmusicapi logging. 18 | -q, --quiet Don't output status messages. 19 | With -l,--log will display gmusicapi warnings. 20 | -f FILTER, --include-filter FILTER Include Google songs by field:pattern filter (e.g. "artist:Muse"). 21 | Songs can match any filter criteria. 22 | This option can be set multiple times. 23 | -F FILTER, --exclude-filter FILTER Exclude Google songs by field:pattern filter (e.g. "artist:Muse"). 24 | Songs can match any filter criteria. 25 | This option can be set multiple times. 26 | -a, --all-includes Songs must match all include filter criteria to be included. 27 | -A, --all-excludes Songs must match all exclude filter criteria to be excluded. 28 | -y, --yes Display results without asking for confirmation. 29 | 30 | Patterns can be any valid Python regex patterns. 31 | """ 32 | 33 | import logging 34 | import sys 35 | 36 | from docopt import docopt 37 | 38 | from gmusicapi_wrapper import MobileClientWrapper 39 | 40 | QUIET = 25 41 | logging.addLevelName(25, "QUIET") 42 | 43 | logger = logging.getLogger('gmusicapi_wrapper') 44 | sh = logging.StreamHandler() 45 | logger.addHandler(sh) 46 | 47 | 48 | def main(): 49 | cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) 50 | 51 | if cli['quiet']: 52 | logger.setLevel(QUIET) 53 | else: 54 | logger.setLevel(logging.INFO) 55 | 56 | mcw = MobileClientWrapper(enable_logging=cli['log']) 57 | mcw.login(username=cli['user'], password=cli['pass'], android_id=cli['android-id']) 58 | 59 | if not mcw.is_authenticated: 60 | sys.exit() 61 | 62 | include_filters = [tuple(filt.split(':', 1)) for filt in cli['include-filter']] 63 | exclude_filters = [tuple(filt.split(':', 1)) for filt in cli['exclude-filter']] 64 | 65 | logger.info("Scanning for songs...\n") 66 | search_results, _ = mcw.get_google_songs( 67 | include_filters=include_filters, exclude_filters=exclude_filters, 68 | all_includes=cli['all-includes'], all_excludes=cli['all-excludes'] 69 | ) 70 | 71 | search_results.sort(key=lambda song: (song.get('artist'), song.get('album'), song.get('trackNumber'))) 72 | 73 | if search_results: 74 | confirm = cli['yes'] or cli['quiet'] 75 | logger.info("") 76 | 77 | if confirm or input("Display {} results? (y/n) ".format(len(search_results))) in ("y", "Y"): 78 | logger.log(QUIET, "") 79 | 80 | for song in search_results: 81 | title = song.get('title', "<empty>") 82 | artist = song.get('artist', "<empty>") 83 | album = song.get('album', "<empty>") 84 | song_id = song['id'] 85 | 86 | logger.log(QUIET, "{0} -- {1} -- {2} ({3})".format(title, artist, album, song_id)) 87 | else: 88 | logger.info("\nNo songs found matching query") 89 | 90 | mcw.logout() 91 | logger.info("\nAll done!") 92 | 93 | 94 | if __name__ == '__main__': 95 | main() 96 | -------------------------------------------------------------------------------- /gmusicapi_scripts/gmsync.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | """ 5 | A sync script for Google Music using https://github.com/simon-weber/gmusicapi. 6 | More information at https://github.com/thebigmunch/gmusicapi-scripts. 7 | 8 | Usage: 9 | gmsync (-h | --help) 10 | gmsync up [-e PATTERN]... [-f FILTER]... [-F FILTER]... [options] [<input>]... 11 | gmsync down [-e PATTERN]... [-f FILTER]... [-F FILTER]... [options] [<output>] 12 | gmsync [-e PATTERN]... [-f FILTER]... [-F FILTER]... [options] [<input>]... 13 | 14 | Commands: 15 | up Sync local songs to Google Music. Default behavior. 16 | down Sync Google Music songs to local computer. 17 | 18 | Arguments: 19 | input Files, directories, or glob patterns to upload. 20 | Defaults to current directory. 21 | output Output file or directory name which can include a template pattern. 22 | Defaults to name suggested by Google Music in your current directory. 23 | 24 | Options: 25 | -h, --help Display help message. 26 | -c CRED, --cred CRED Specify oauth credential file name to use/create. [Default: oauth] 27 | -U ID --uploader-id ID A unique id given as a MAC address (e.g. '00:11:22:33:AA:BB'). 28 | This should only be provided when the default does not work. 29 | -l, --log Enable gmusicapi logging. 30 | -m, --match Enable scan and match. 31 | -d, --dry-run Output list of songs that would be uploaded. 32 | -q, --quiet Don't output status messages. 33 | With -l,--log will display gmusicapi warnings. 34 | With -d,--dry-run will display song list. 35 | --delete-on-success Delete successfully uploaded local files. 36 | -R, --no-recursion Disable recursion when scanning for local files. 37 | This is equivalent to setting --max-depth to 0. 38 | --max-depth DEPTH Set maximum depth of recursion when scanning for local files. 39 | Default is infinite recursion. 40 | Has no effect when -R, --no-recursion set. 41 | -e PATTERN, --exclude PATTERN Exclude file paths matching pattern. 42 | This option can be set multiple times. 43 | -f FILTER, --include-filter FILTER Include Google songs (download) or local songs (upload) 44 | by field:pattern filter (e.g. "artist:Muse"). 45 | Songs can match any filter criteria. 46 | This option can be set multiple times. 47 | -F FILTER, --exclude-filter FILTER Exclude Google songs (download) or local songs (upload) 48 | by field:pattern filter (e.g. "artist:Muse"). 49 | Songs can match any filter criteria. 50 | This option can be set multiple times. 51 | -a, --all-includes Songs must match all include filter criteria to be included. 52 | -A, --all-excludes Songs must match all exclude filter criteria to be excluded. 53 | 54 | Patterns can be any valid Python regex patterns. 55 | """ 56 | 57 | import logging 58 | import os 59 | import sys 60 | 61 | from docopt import docopt 62 | 63 | from gmusicapi_wrapper import MusicManagerWrapper 64 | from gmusicapi_wrapper.utils import compare_song_collections, template_to_filepath 65 | 66 | QUIET = 25 67 | logging.addLevelName(25, "QUIET") 68 | 69 | logger = logging.getLogger('gmusicapi_wrapper') 70 | sh = logging.StreamHandler() 71 | logger.addHandler(sh) 72 | 73 | 74 | def template_to_base_path(template, google_songs): 75 | """Get base output path for a list of songs for download.""" 76 | 77 | if template == os.getcwd() or template == '%suggested%': 78 | base_path = os.getcwd() 79 | else: 80 | template = os.path.abspath(template) 81 | song_paths = [template_to_filepath(template, song) for song in google_songs] 82 | base_path = os.path.dirname(os.path.commonprefix(song_paths)) 83 | 84 | return base_path 85 | 86 | 87 | def main(): 88 | cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) 89 | 90 | if cli['no-recursion']: 91 | cli['max-depth'] = 0 92 | else: 93 | cli['max-depth'] = int(cli['max-depth']) if cli['max-depth'] else float('inf') 94 | 95 | if cli['quiet']: 96 | logger.setLevel(QUIET) 97 | else: 98 | logger.setLevel(logging.INFO) 99 | 100 | if not cli['input']: 101 | cli['input'] = [os.getcwd()] 102 | 103 | if not cli['output']: 104 | cli['output'] = os.getcwd() 105 | 106 | include_filters = [tuple(filt.split(':', 1)) for filt in cli['include-filter']] 107 | exclude_filters = [tuple(filt.split(':', 1)) for filt in cli['exclude-filter']] 108 | 109 | mmw = MusicManagerWrapper(enable_logging=cli['log']) 110 | mmw.login(oauth_filename=cli['cred'], uploader_id=cli['uploader-id']) 111 | 112 | if not mmw.is_authenticated: 113 | sys.exit() 114 | 115 | if cli['down']: 116 | matched_google_songs, _ = mmw.get_google_songs( 117 | include_filters=include_filters, exclude_filters=exclude_filters, 118 | all_includes=cli['all-includes'], all_excludes=cli['all-excludes'] 119 | ) 120 | 121 | logger.info("") 122 | 123 | cli['input'] = [template_to_base_path(cli['output'], matched_google_songs)] 124 | 125 | matched_local_songs, __, __ = mmw.get_local_songs(cli['input'], exclude_patterns=cli['exclude']) 126 | 127 | logger.info("\nFinding missing songs...") 128 | songs_to_download = compare_song_collections(matched_google_songs, matched_local_songs) 129 | 130 | songs_to_download.sort(key=lambda song: (song.get('artist'), song.get('album'), song.get('track_number'))) 131 | 132 | if cli['dry-run']: 133 | logger.info("\nFound {0} song(s) to download".format(len(songs_to_download))) 134 | 135 | if songs_to_download: 136 | logger.info("\nSongs to download:\n") 137 | 138 | for song in songs_to_download: 139 | title = song.get('title', "<title>") 140 | artist = song.get('artist', "<artist>") 141 | album = song.get('album', "<album>") 142 | song_id = song['id'] 143 | 144 | logger.log(QUIET, "{0} -- {1} -- {2} ({3})".format(title, artist, album, song_id)) 145 | else: 146 | logger.info("\nNo songs to download") 147 | else: 148 | if songs_to_download: 149 | logger.info("\nDownloading {0} song(s) from Google Music\n".format(len(songs_to_download))) 150 | mmw.download(songs_to_download, template=cli['output']) 151 | else: 152 | logger.info("\nNo songs to download") 153 | else: 154 | matched_google_songs, _ = mmw.get_google_songs() 155 | 156 | logger.info("") 157 | 158 | matched_local_songs, songs_to_filter, songs_to_exclude = mmw.get_local_songs( 159 | cli['input'], include_filters=include_filters, exclude_filters=exclude_filters, 160 | all_includes=cli['all-includes'], all_excludes=cli['all-excludes'], 161 | exclude_patterns=cli['exclude'], max_depth=cli['max-depth'] 162 | ) 163 | 164 | logger.info("\nFinding missing songs...") 165 | 166 | songs_to_upload = compare_song_collections(matched_local_songs, matched_google_songs) 167 | 168 | # Sort lists for sensible output. 169 | songs_to_upload.sort() 170 | songs_to_exclude.sort() 171 | 172 | if cli['dry-run']: 173 | logger.info("\nFound {0} song(s) to upload".format(len(songs_to_upload))) 174 | 175 | if songs_to_upload: 176 | logger.info("\nSongs to upload:\n") 177 | 178 | for song in songs_to_upload: 179 | logger.log(QUIET, song) 180 | else: 181 | logger.info("\nNo songs to upload") 182 | 183 | if songs_to_filter: 184 | logger.info("\nSongs to filter:\n") 185 | 186 | for song in songs_to_filter: 187 | logger.log(QUIET, song) 188 | else: 189 | logger.info("\nNo songs to filter") 190 | 191 | if songs_to_exclude: 192 | logger.info("\nSongs to exclude:\n") 193 | 194 | for song in songs_to_exclude: 195 | logger.log(QUIET, song) 196 | else: 197 | logger.info("\nNo songs to exclude") 198 | else: 199 | if songs_to_upload: 200 | logger.info("\nUploading {0} song(s) to Google Music\n".format(len(songs_to_upload))) 201 | 202 | mmw.upload(songs_to_upload, enable_matching=cli['match'], delete_on_success=cli['delete-on-success']) 203 | else: 204 | logger.info("\nNo songs to upload") 205 | 206 | # Delete local files if they already exist on Google Music. 207 | if cli['delete-on-success']: 208 | for song in matched_local_songs: 209 | try: 210 | os.remove(song) 211 | except: 212 | logger.warning("Failed to remove {} after successful upload".format(song)) 213 | 214 | mmw.logout() 215 | logger.info("\nAll done!") 216 | 217 | 218 | if __name__ == '__main__': 219 | main() 220 | -------------------------------------------------------------------------------- /gmusicapi_scripts/gmupload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | """ 5 | An upload script for Google Music using https://github.com/simon-weber/gmusicapi. 6 | More information at https://github.com/thebigmunch/gmusicapi-scripts. 7 | 8 | Usage: 9 | gmupload (-h | --help) 10 | gmupload [-e PATTERN]... [-f FILTER]... [-F FILTER]... [options] [<input>]... 11 | 12 | Arguments: 13 | input Files, directories, or glob patterns to upload. 14 | Defaults to current directory. 15 | 16 | Options: 17 | -h, --help Display help message. 18 | -c CRED, --cred CRED Specify oauth credential file name to use/create. [Default: oauth] 19 | -U ID --uploader-id ID A unique id given as a MAC address (e.g. '00:11:22:33:AA:BB'). 20 | This should only be provided when the default does not work. 21 | -l, --log Enable gmusicapi logging. 22 | -m, --match Enable scan and match. 23 | -d, --dry-run Output list of songs that would be uploaded. 24 | -q, --quiet Don't output status messages. 25 | With -l,--log will display gmusicapi warnings. 26 | With -d,--dry-run will display song list. 27 | --delete-on-success Delete successfully uploaded local files. 28 | -R, --no-recursion Disable recursion when scanning for local files. 29 | This is equivalent to setting --max-depth to 0. 30 | --max-depth DEPTH Set maximum depth of recursion when scanning for local files. 31 | Default is infinite recursion. 32 | Has no effect when -R, --no-recursion set. 33 | -e PATTERN, --exclude PATTERN Exclude file paths matching a Python regex pattern. 34 | -f FILTER, --include-filter FILTER Include local songs by field:pattern filter (e.g. "artist:Muse"). 35 | Songs can match any filter criteria. 36 | This option can be set multiple times. 37 | -F FILTER, --exclude-filter FILTER Exclude local songs by field:pattern filter (e.g. "artist:Muse"). 38 | Songs can match any filter criteria. 39 | This option can be set multiple times. 40 | -a, --all-includes Songs must match all include filter criteria to be included. 41 | -A, --all-excludes Songs must match all exclude filter criteria to be excluded. 42 | 43 | Patterns can be any valid Python regex patterns. 44 | """ 45 | 46 | import logging 47 | import os 48 | import sys 49 | 50 | from docopt import docopt 51 | 52 | from gmusicapi_wrapper import MusicManagerWrapper 53 | 54 | QUIET = 25 55 | logging.addLevelName(25, "QUIET") 56 | 57 | logger = logging.getLogger('gmusicapi_wrapper') 58 | sh = logging.StreamHandler() 59 | logger.addHandler(sh) 60 | 61 | 62 | def main(): 63 | cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) 64 | 65 | if cli['no-recursion']: 66 | cli['max-depth'] = 0 67 | else: 68 | cli['max-depth'] = int(cli['max-depth']) if cli['max-depth'] else float('inf') 69 | 70 | if cli['quiet']: 71 | logger.setLevel(QUIET) 72 | else: 73 | logger.setLevel(logging.INFO) 74 | 75 | if not cli['input']: 76 | cli['input'] = [os.getcwd()] 77 | 78 | mmw = MusicManagerWrapper(enable_logging=cli['log']) 79 | mmw.login(oauth_filename=cli['cred'], uploader_id=cli['uploader-id']) 80 | 81 | if not mmw.is_authenticated: 82 | sys.exit() 83 | 84 | include_filters = [tuple(filt.split(':', 1)) for filt in cli['include-filter']] 85 | exclude_filters = [tuple(filt.split(':', 1)) for filt in cli['exclude-filter']] 86 | 87 | songs_to_upload, songs_to_filter, songs_to_exclude = mmw.get_local_songs( 88 | cli['input'], include_filters=include_filters, exclude_filters=exclude_filters, 89 | all_includes=cli['all-includes'], all_excludes=cli['all-excludes'], 90 | exclude_patterns=cli['exclude'], max_depth=cli['max-depth'] 91 | ) 92 | 93 | songs_to_upload.sort() 94 | songs_to_exclude.sort() 95 | 96 | if cli['dry-run']: 97 | logger.info("\nFound {0} song(s) to upload".format(len(songs_to_upload))) 98 | 99 | if songs_to_upload: 100 | logger.info("\nSongs to upload:\n") 101 | 102 | for song in songs_to_upload: 103 | logger.log(QUIET, song) 104 | else: 105 | logger.info("\nNo songs to upload") 106 | 107 | if songs_to_filter: 108 | logger.info("\nSongs to filter:\n") 109 | 110 | for song in songs_to_filter: 111 | logger.log(QUIET, song) 112 | else: 113 | logger.info("\nNo songs to filter") 114 | 115 | if songs_to_exclude: 116 | logger.info("\nSongs to exclude:\n") 117 | 118 | for song in songs_to_exclude: 119 | logger.log(QUIET, song) 120 | else: 121 | logger.info("\nNo songs to exclude") 122 | else: 123 | if songs_to_upload: 124 | logger.info("\nUploading {0} song(s) to Google Music\n".format(len(songs_to_upload))) 125 | 126 | mmw.upload(songs_to_upload, enable_matching=cli['match'], delete_on_success=cli['delete-on-success']) 127 | else: 128 | logger.info("\nNo songs to upload") 129 | 130 | mmw.logout() 131 | logger.info("\nAll done!") 132 | 133 | 134 | if __name__ == '__main__': 135 | main() 136 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | invoke 2 | wheel 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [pep8] 5 | ignore = W191, E101, E126, E402 6 | max-line-length = 200 7 | 8 | [flake8] 9 | ignore = W191, E101, E126, E309, E402 10 | max-line-length = 200 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | import re 5 | import sys 6 | 7 | from setuptools import find_packages, setup 8 | 9 | if sys.version_info[:3] < (3, 4): 10 | sys.exit("gmusicapi-scripts does not support this version of Python.") 11 | 12 | # From http://stackoverflow.com/a/7071358/1231454 13 | version_file = "gmusicapi_scripts/__init__.py" 14 | version_re = r"^__version__ = ['\"]([^'\"]*)['\"]" 15 | 16 | version_file_contents = open(version_file).read() 17 | match = re.search(version_re, version_file_contents, re.M) 18 | 19 | if match: 20 | version = match.group(1) 21 | else: 22 | raise RuntimeError("Could not find version in '{}'".format(version_file)) 23 | 24 | setup( 25 | name='gmusicapi_scripts', 26 | version=version, 27 | description='A collection of scripts using gmusicapi-wrapper and gmusicapi.', 28 | url='https://github.com/thebigmunch/gmusicapi-scripts', 29 | license='MIT', 30 | author='thebigmunch', 31 | author_email='mail@thebigmunch.me', 32 | 33 | keywords=[], 34 | classifiers=[ 35 | 'License :: OSI Approved :: MIT License', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.4', 38 | 'Programming Language :: Python :: 3.5', 39 | ], 40 | 41 | install_requires=[ 42 | 'gmusicapi-wrapper >= 0.5.0', 43 | 'docopt-unicode' 44 | ], 45 | 46 | packages=find_packages(), 47 | entry_points={ 48 | 'console_scripts': [ 49 | 'gmdelete=gmusicapi_scripts.gmdelete:main', 50 | 'gmdownload=gmusicapi_scripts.gmdownload:main', 51 | 'gmsearch=gmusicapi_scripts.gmsearch:main', 52 | 'gmsync=gmusicapi_scripts.gmsync:main', 53 | 'gmupload=gmusicapi_scripts.gmupload:main' 54 | ] 55 | }, 56 | ) 57 | --------------------------------------------------------------------------------