diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 8cf6cbbf6..5e0de77e2 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -757,15 +757,21 @@ def show_path_changes(path_changes): if max_width > col_width: # Print every change over two lines for source, dest in zip(sources, destinations): - log.info('{0} \n -> {1}', source, dest) + color_source, color_dest = colordiff(source, dest) + print_('{0} \n -> {1}'.format(color_source, color_dest)) else: # Print every change on a single line, and add a header title_pad = max_width - len('Source ') + len(' -> ') - log.info('Source {0} Destination', ' ' * title_pad) + print_('Source {0} Destination'.format(' ' * title_pad)) for source, dest in zip(sources, destinations): pad = max_width - len(source) - log.info('{0} {1} -> {2}', source, ' ' * pad, dest) + color_source, color_dest = colordiff(source, dest) + print_('{0} {1} -> {2}'.format( + color_source, + ' ' * pad, + color_dest, + )) # Helper functions for option parsing. diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 7ae71164e..d58bb28e4 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -19,6 +19,7 @@ import sys import errno import locale import re +import tempfile import shutil import fnmatch import functools @@ -478,6 +479,11 @@ def move(path, dest, replace=False): instead, in which case metadata will *not* be preserved. Paths are translated to system paths. """ + if os.path.isdir(path): + raise FilesystemError(u'source is directory', 'move', (path, dest)) + if os.path.isdir(dest): + raise FilesystemError(u'destination is directory', 'move', + (path, dest)) if samefile(path, dest): return path = syspath(path) @@ -487,15 +493,23 @@ def move(path, dest, replace=False): # First, try renaming the file. try: - os.rename(path, dest) + os.replace(path, dest) except OSError: - # Otherwise, copy and delete the original. + tmp = tempfile.mktemp(suffix='.beets', + prefix=py3_path(b'.' + os.path.basename(dest)), + dir=py3_path(os.path.dirname(dest))) + tmp = syspath(tmp) try: - shutil.copyfile(path, dest) + shutil.copyfile(path, tmp) + os.replace(tmp, dest) + tmp = None os.remove(path) except OSError as exc: raise FilesystemError(exc, 'move', (path, dest), traceback.format_exc()) + finally: + if tmp is not None: + os.remove(tmp) def link(path, dest, replace=False): diff --git a/beetsplug/gmusic.py b/beetsplug/gmusic.py index 1761dbb13..844234f94 100644 --- a/beetsplug/gmusic.py +++ b/beetsplug/gmusic.py @@ -1,5 +1,4 @@ # This file is part of beets. -# Copyright 2017, Tigran Kostandyan. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -12,124 +11,15 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Upload files to Google Play Music and list songs in its library.""" - -import os.path +"""Deprecation warning for the removed gmusic plugin.""" from beets.plugins import BeetsPlugin -from beets import ui -from beets import config -from beets.ui import Subcommand -from gmusicapi import Musicmanager, Mobileclient -from gmusicapi.exceptions import NotLoggedIn -import gmusicapi.clients class Gmusic(BeetsPlugin): def __init__(self): super().__init__() - self.m = Musicmanager() - # OAUTH_FILEPATH was moved in gmusicapi 12.0.0. - if hasattr(Musicmanager, 'OAUTH_FILEPATH'): - oauth_file = Musicmanager.OAUTH_FILEPATH - else: - oauth_file = gmusicapi.clients.OAUTH_FILEPATH - - self.config.add({ - 'auto': False, - 'uploader_id': '', - 'uploader_name': '', - 'device_id': '', - 'oauth_file': oauth_file, - }) - if self.config['auto']: - self.import_stages = [self.autoupload] - - def commands(self): - gupload = Subcommand('gmusic-upload', - help='upload your tracks to Google Play Music') - gupload.func = self.upload - - search = Subcommand('gmusic-songs', - help='list of songs in Google Play Music library') - search.parser.add_option('-t', '--track', dest='track', - action='store_true', - help='Search by track name') - search.parser.add_option('-a', '--artist', dest='artist', - action='store_true', - help='Search by artist') - search.func = self.search - return [gupload, search] - - def authenticate(self): - if self.m.is_authenticated(): - return - # Checks for OAuth2 credentials, - # if they don't exist - performs authorization - oauth_file = self.config['oauth_file'].as_filename() - if os.path.isfile(oauth_file): - uploader_id = self.config['uploader_id'] - uploader_name = self.config['uploader_name'] - self.m.login(oauth_credentials=oauth_file, - uploader_id=uploader_id.as_str().upper() or None, - uploader_name=uploader_name.as_str() or None) - else: - self.m.perform_oauth(oauth_file) - - def upload(self, lib, opts, args): - items = lib.items(ui.decargs(args)) - files = self.getpaths(items) - self.authenticate() - ui.print_('Uploading your files...') - self.m.upload(filepaths=files) - ui.print_('Your files were successfully added to library') - - def autoupload(self, session, task): - items = task.imported_items() - files = self.getpaths(items) - self.authenticate() - self._log.info('Uploading files to Google Play Music...', files) - self.m.upload(filepaths=files) - self._log.info('Your files were successfully added to your ' - + 'Google Play Music library') - - def getpaths(self, items): - return [x.path for x in items] - - def search(self, lib, opts, args): - password = config['gmusic']['password'] - email = config['gmusic']['email'] - uploader_id = config['gmusic']['uploader_id'] - device_id = config['gmusic']['device_id'] - password.redact = True - email.redact = True - # Since Musicmanager doesn't support library management - # we need to use mobileclient interface - mobile = Mobileclient() - try: - new_device_id = (device_id.as_str() - or uploader_id.as_str().replace(':', '') - or Mobileclient.FROM_MAC_ADDRESS).upper() - mobile.login(email.as_str(), password.as_str(), new_device_id) - files = mobile.get_all_songs() - except NotLoggedIn: - ui.print_( - 'Authentication error. Please check your email and password.' - ) - return - if not args: - for i, file in enumerate(files, start=1): - print(i, ui.colorize('blue', file['artist']), - file['title'], ui.colorize('red', file['album'])) - else: - if opts.track: - self.match(files, args, 'title') - else: - self.match(files, args, 'artist') - - @staticmethod - def match(files, args, search_by): - for file in files: - if ' '.join(ui.decargs(args)) in file[search_by]: - print(file['artist'], file['title'], file['album']) + self._log.warning("The 'gmusic' plugin has been removed following the" + " shutdown of Google Play Music. Remove the plugin" + " from your configuration to silence this warning.") diff --git a/docs/changelog.rst b/docs/changelog.rst index e55bdaf45..7a6e1a4b6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,9 @@ For packagers: :bug:`4037` :bug:`4038` * This version of beets no longer depends on the `six`_ library. :bug:`4030` +* The `gmusic` plugin was removed since Google Play Music has been shut down. + Thus, the optional dependency on `gmusicapi` does not exist anymore. + :bug:`4089` Major new features: @@ -37,6 +40,12 @@ Other new things: subdirectories in library. * :doc:`/plugins/info`: Support ``--album`` flag. * :doc:`/plugins/export`: Support ``--album`` flag. +* ``beet move`` path differences are now highlighted in color (when enabled). +* When moving files and a direct rename of a file is not possible, beets now + copies to a temporary file in the target folder first instead of directly + using the target path. This gets us closer to always updating files + atomically. Thanks to :user:`catap`. + :bug:`4060` * :doc:`/plugins/fetchart`: A new option to store cover art as non-progressive image. Useful for DAPs that support progressive images. Set ``deinterlace: yes`` in your configuration to enable. diff --git a/docs/plugins/gmusic.rst b/docs/plugins/gmusic.rst index 94ee2dae4..412978bd6 100644 --- a/docs/plugins/gmusic.rst +++ b/docs/plugins/gmusic.rst @@ -1,87 +1,5 @@ Gmusic Plugin ============= -The ``gmusic`` plugin lets you upload songs to Google Play Music and query -songs in your library. - - -Installation ------------- - -The plugin requires :pypi:`gmusicapi`. You can install it using ``pip``:: - - pip install gmusicapi - -.. _gmusicapi: https://github.com/simon-weber/gmusicapi/ - -Then, you can enable the ``gmusic`` plugin in your configuration (see -:ref:`using-plugins`). - - -Usage ------ -Configuration is required before use. Below is an example configuration:: - - gmusic: - email: user@example.com - password: seekrit - auto: yes - uploader_id: 00:11:22:33:AA:BB - device_id: 00112233AABB - oauth_file: ~/.config/beets/oauth.cred - - -To upload tracks to Google Play Music, use the ``gmusic-upload`` command:: - - beet gmusic-upload [QUERY] - -If you don't include a query, the plugin will upload your entire collection. - -To list your music collection, use the ``gmusic-songs`` command:: - - beet gmusic-songs [-at] [ARGS] - -Use the ``-a`` option to search by artist and ``-t`` to search by track. For -example:: - - beet gmusic-songs -a John Frusciante - beet gmusic-songs -t Black Hole Sun - -For a list of all songs in your library, run ``beet gmusic-songs`` without any -arguments. - - -Configuration -------------- -To configure the plugin, make a ``gmusic:`` section in your configuration file. -The available options are: - -- **email**: Your Google account email address. - Default: none. -- **password**: Password to your Google account. Required to query songs in - your collection. - For accounts with 2-step-verification, an - `app password `__ - will need to be generated. An app password for an account without - 2-step-verification is not required but is recommended. - Default: none. -- **auto**: Set to ``yes`` to automatically upload new imports to Google Play - Music. - Default: ``no`` -- **uploader_id**: Unique id as a MAC address, eg ``00:11:22:33:AA:BB``. - This option should be set before the maximum number of authorized devices is - reached. - If provided, use the same id for all future runs on this, and other, beets - installations as to not reach the maximum number of authorized devices. - Default: device's MAC address. -- **device_id**: Unique device ID for authorized devices. It is usually - the same as your MAC address with the colons removed, eg ``00112233AABB``. - This option only needs to be set if you receive an `InvalidDeviceId` - exception. Below the exception will be a list of valid device IDs. - Default: none. -- **oauth_file**: Filepath for oauth credentials file. - Default: `{user_data_dir} `__/gmusicapi/oauth.cred - -Refer to the `Google Play Music Help -`__ -page for more details on authorized devices. +The ``gmusic`` plugin interfaced beets to Google Play Music. It has been +removed after the shutdown of this service. diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 9c628951a..5ca8794fd 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -231,7 +231,6 @@ Miscellaneous * :doc:`filefilter`: Automatically skip files during the import process based on regular expressions. * :doc:`fuzzy`: Search albums and tracks with fuzzy string matching. -* :doc:`gmusic`: Search and upload files to Google Play Music. * :doc:`hook`: Run a command when an event is emitted by beets. * :doc:`ihate`: Automatically skip albums and tracks during the import process. * :doc:`info`: Print music files' tags to the console. diff --git a/setup.py b/setup.py index 48aede251..e6ff6a592 100755 --- a/setup.py +++ b/setup.py @@ -126,7 +126,6 @@ setup( 'embedart': ['Pillow'], 'embyupdate': ['requests'], 'chroma': ['pyacoustid'], - 'gmusic': ['gmusicapi'], 'discogs': ['python3-discogs-client>=2.3.10'], 'beatport': ['requests-oauthlib>=0.6.1'], 'kodiupdate': ['requests'],