mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 16:42:42 +01:00
Merge branch 'master' of github.com:beetbox/beets into deinterlace
This commit is contained in:
commit
4d94bf8fad
7 changed files with 41 additions and 206 deletions
|
|
@ -757,15 +757,21 @@ def show_path_changes(path_changes):
|
||||||
if max_width > col_width:
|
if max_width > col_width:
|
||||||
# Print every change over two lines
|
# Print every change over two lines
|
||||||
for source, dest in zip(sources, destinations):
|
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:
|
else:
|
||||||
# Print every change on a single line, and add a header
|
# Print every change on a single line, and add a header
|
||||||
title_pad = max_width - len('Source ') + len(' -> ')
|
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):
|
for source, dest in zip(sources, destinations):
|
||||||
pad = max_width - len(source)
|
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.
|
# Helper functions for option parsing.
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import sys
|
||||||
import errno
|
import errno
|
||||||
import locale
|
import locale
|
||||||
import re
|
import re
|
||||||
|
import tempfile
|
||||||
import shutil
|
import shutil
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import functools
|
import functools
|
||||||
|
|
@ -478,6 +479,11 @@ def move(path, dest, replace=False):
|
||||||
instead, in which case metadata will *not* be preserved. Paths are
|
instead, in which case metadata will *not* be preserved. Paths are
|
||||||
translated to system paths.
|
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):
|
if samefile(path, dest):
|
||||||
return
|
return
|
||||||
path = syspath(path)
|
path = syspath(path)
|
||||||
|
|
@ -487,15 +493,23 @@ def move(path, dest, replace=False):
|
||||||
|
|
||||||
# First, try renaming the file.
|
# First, try renaming the file.
|
||||||
try:
|
try:
|
||||||
os.rename(path, dest)
|
os.replace(path, dest)
|
||||||
except OSError:
|
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:
|
try:
|
||||||
shutil.copyfile(path, dest)
|
shutil.copyfile(path, tmp)
|
||||||
|
os.replace(tmp, dest)
|
||||||
|
tmp = None
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
raise FilesystemError(exc, 'move', (path, dest),
|
raise FilesystemError(exc, 'move', (path, dest),
|
||||||
traceback.format_exc())
|
traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
if tmp is not None:
|
||||||
|
os.remove(tmp)
|
||||||
|
|
||||||
|
|
||||||
def link(path, dest, replace=False):
|
def link(path, dest, replace=False):
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
# This file is part of beets.
|
# This file is part of beets.
|
||||||
# Copyright 2017, Tigran Kostandyan.
|
|
||||||
#
|
#
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
# a copy of this software and associated documentation files (the
|
# a copy of this software and associated documentation files (the
|
||||||
|
|
@ -12,124 +11,15 @@
|
||||||
# The above copyright notice and this permission notice shall be
|
# The above copyright notice and this permission notice shall be
|
||||||
# included in all copies or substantial portions of the Software.
|
# included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
"""Upload files to Google Play Music and list songs in its library."""
|
"""Deprecation warning for the removed gmusic plugin."""
|
||||||
|
|
||||||
import os.path
|
|
||||||
|
|
||||||
from beets.plugins import BeetsPlugin
|
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):
|
class Gmusic(BeetsPlugin):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.m = Musicmanager()
|
|
||||||
|
|
||||||
# OAUTH_FILEPATH was moved in gmusicapi 12.0.0.
|
self._log.warning("The 'gmusic' plugin has been removed following the"
|
||||||
if hasattr(Musicmanager, 'OAUTH_FILEPATH'):
|
" shutdown of Google Play Music. Remove the plugin"
|
||||||
oauth_file = Musicmanager.OAUTH_FILEPATH
|
" from your configuration to silence this warning.")
|
||||||
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'])
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ For packagers:
|
||||||
:bug:`4037` :bug:`4038`
|
:bug:`4037` :bug:`4038`
|
||||||
* This version of beets no longer depends on the `six`_ library.
|
* This version of beets no longer depends on the `six`_ library.
|
||||||
:bug:`4030`
|
: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:
|
Major new features:
|
||||||
|
|
||||||
|
|
@ -37,6 +40,12 @@ Other new things:
|
||||||
subdirectories in library.
|
subdirectories in library.
|
||||||
* :doc:`/plugins/info`: Support ``--album`` flag.
|
* :doc:`/plugins/info`: Support ``--album`` flag.
|
||||||
* :doc:`/plugins/export`: 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
|
* :doc:`/plugins/fetchart`: A new option to store cover art as non-progressive
|
||||||
image. Useful for DAPs that support progressive images. Set ``deinterlace:
|
image. Useful for DAPs that support progressive images. Set ``deinterlace:
|
||||||
yes`` in your configuration to enable.
|
yes`` in your configuration to enable.
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,5 @@
|
||||||
Gmusic Plugin
|
Gmusic Plugin
|
||||||
=============
|
=============
|
||||||
|
|
||||||
The ``gmusic`` plugin lets you upload songs to Google Play Music and query
|
The ``gmusic`` plugin interfaced beets to Google Play Music. It has been
|
||||||
songs in your library.
|
removed after the shutdown of this service.
|
||||||
|
|
||||||
|
|
||||||
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 <https://support.google.com/accounts/answer/185833?hl=en>`__
|
|
||||||
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} <https://pypi.org/project/appdirs/>`__/gmusicapi/oauth.cred
|
|
||||||
|
|
||||||
Refer to the `Google Play Music Help
|
|
||||||
<https://support.google.com/googleplaymusic/answer/3139562?hl=en>`__
|
|
||||||
page for more details on authorized devices.
|
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,6 @@ Miscellaneous
|
||||||
* :doc:`filefilter`: Automatically skip files during the import process based
|
* :doc:`filefilter`: Automatically skip files during the import process based
|
||||||
on regular expressions.
|
on regular expressions.
|
||||||
* :doc:`fuzzy`: Search albums and tracks with fuzzy string matching.
|
* :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:`hook`: Run a command when an event is emitted by beets.
|
||||||
* :doc:`ihate`: Automatically skip albums and tracks during the import process.
|
* :doc:`ihate`: Automatically skip albums and tracks during the import process.
|
||||||
* :doc:`info`: Print music files' tags to the console.
|
* :doc:`info`: Print music files' tags to the console.
|
||||||
|
|
|
||||||
1
setup.py
1
setup.py
|
|
@ -126,7 +126,6 @@ setup(
|
||||||
'embedart': ['Pillow'],
|
'embedart': ['Pillow'],
|
||||||
'embyupdate': ['requests'],
|
'embyupdate': ['requests'],
|
||||||
'chroma': ['pyacoustid'],
|
'chroma': ['pyacoustid'],
|
||||||
'gmusic': ['gmusicapi'],
|
|
||||||
'discogs': ['python3-discogs-client>=2.3.10'],
|
'discogs': ['python3-discogs-client>=2.3.10'],
|
||||||
'beatport': ['requests-oauthlib>=0.6.1'],
|
'beatport': ['requests-oauthlib>=0.6.1'],
|
||||||
'kodiupdate': ['requests'],
|
'kodiupdate': ['requests'],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue