mirror of
https://github.com/beetbox/beets.git
synced 2025-12-27 11:02:43 +01:00
Add beets.util.interactive_open() find cmd + execute
interactive_open() takes a target and an optional command, if it does not receive a command then it uses open_anything(). It parses command and lexes it with shlex.split(), revieling the client from that task. "config -e" command uses it, and gives a better error message in case of problem. "play" plugin uses it as well, as side-effect being that the command is now interactive, as requested in issue #1321. Fix issue #1321.
This commit is contained in:
parent
489b6b2e7e
commit
c47221555f
7 changed files with 58 additions and 39 deletions
|
|
@ -21,7 +21,6 @@ from __future__ import (division, absolute_import, print_function,
|
|||
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
|
||||
import beets
|
||||
from beets import ui
|
||||
|
|
@ -1494,24 +1493,14 @@ def config_edit():
|
|||
"""
|
||||
path = config.user_config_path()
|
||||
|
||||
if 'EDITOR' in os.environ:
|
||||
editor = os.environ['EDITOR'].encode('utf8')
|
||||
try:
|
||||
editor = [e.decode('utf8') for e in shlex.split(editor)]
|
||||
except ValueError: # Malformed shell tokens.
|
||||
editor = [editor]
|
||||
args = editor + [path]
|
||||
args.insert(1, args[0])
|
||||
else:
|
||||
base = util.open_anything()
|
||||
args = [base, base, path]
|
||||
|
||||
editor = os.environ.get('EDITOR')
|
||||
try:
|
||||
os.execlp(*args)
|
||||
except OSError:
|
||||
raise ui.UserError("Could not edit configuration. Please "
|
||||
"set the EDITOR environment variable.")
|
||||
|
||||
util.interactive_open(path, editor)
|
||||
except OSError as exc:
|
||||
message = "Could not edit configuration: {0}".format(exc)
|
||||
if not editor:
|
||||
message += ". Please set the EDITOR environment variable"
|
||||
raise ui.UserError(message)
|
||||
|
||||
config_cmd = ui.Subcommand('config',
|
||||
help='show or edit the user configuration')
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from collections import defaultdict
|
|||
import traceback
|
||||
import subprocess
|
||||
import platform
|
||||
import shlex
|
||||
|
||||
|
||||
MAX_FILENAME_LENGTH = 200
|
||||
|
|
@ -697,3 +698,26 @@ def open_anything():
|
|||
else: # Assume Unix
|
||||
base_cmd = 'xdg-open'
|
||||
return base_cmd
|
||||
|
||||
|
||||
def interactive_open(target, command=None):
|
||||
"""Open `target` file with `command` or, in not available, ask the OS to
|
||||
deal with it.
|
||||
|
||||
The executed program will have stdin, stdout and stderr.
|
||||
OSError may be raised, it is left to the caller to catch them.
|
||||
"""
|
||||
if command:
|
||||
command = command.encode('utf8')
|
||||
try:
|
||||
command = [c.decode('utf8')
|
||||
for c in shlex.split(command)]
|
||||
except ValueError: # Malformed shell tokens.
|
||||
command = [command]
|
||||
command.insert(0, command[0]) # for argv[0]
|
||||
else:
|
||||
base_cmd = open_anything()
|
||||
command = [base_cmd, base_cmd]
|
||||
|
||||
command.append(target)
|
||||
return os.execlp(*command)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ from beets import config
|
|||
from beets import ui
|
||||
from beets import util
|
||||
from os.path import relpath
|
||||
import shlex
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
|
||||
|
|
@ -60,10 +59,6 @@ class PlayPlugin(BeetsPlugin):
|
|||
relative_to = config['play']['relative_to'].get()
|
||||
if relative_to:
|
||||
relative_to = util.normpath(relative_to)
|
||||
if command_str:
|
||||
command = shlex.split(command_str)
|
||||
else:
|
||||
command = [util.open_anything()]
|
||||
|
||||
# Preform search by album and add folders rather than tracks to
|
||||
# playlist.
|
||||
|
|
@ -114,17 +109,12 @@ class PlayPlugin(BeetsPlugin):
|
|||
m3u.write(item + b'\n')
|
||||
m3u.close()
|
||||
|
||||
command.append(m3u.name)
|
||||
|
||||
# Invoke the command and log the output.
|
||||
output = util.command_output(command)
|
||||
if output:
|
||||
self._log.debug(u'Output of {0}: {1}',
|
||||
util.displayable_path(command[0]),
|
||||
output.decode('utf8', 'ignore'))
|
||||
else:
|
||||
self._log.debug(u'no output')
|
||||
|
||||
ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type))
|
||||
|
||||
util.remove(m3u.name)
|
||||
try:
|
||||
util.interactive_open(m3u.name, command_str)
|
||||
except OSError as exc:
|
||||
raise ui.UserError("Could not play the music playlist: "
|
||||
"{0}".format(exc))
|
||||
finally:
|
||||
util.remove(m3u.name)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ Changelog
|
|||
|
||||
Features:
|
||||
|
||||
* :doc:`/plugins/play` gives full interaction with the command invoked.
|
||||
:bug:`1321`
|
||||
* The summary shown to compare duplicate albums during import now displays
|
||||
the old and new filesizes. :bug:`1291`
|
||||
* The colors used are now configurable via the new config option ``colors``,
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ would on the command-line)::
|
|||
play:
|
||||
command: /usr/bin/command --option1 --option2 some_other_option
|
||||
|
||||
Enable beets' verbose logging to see the command's output if you need to
|
||||
debug.
|
||||
While playing you'll be able to interact with the player if it is a
|
||||
command-line oriented, and you'll get its output in real time.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
|
|
|||
|
|
@ -92,10 +92,11 @@ class ConfigCommandTest(unittest.TestCase, TestHelper):
|
|||
def test_config_editor_not_found(self):
|
||||
with self.assertRaises(ui.UserError) as user_error:
|
||||
with patch('os.execlp') as execlp:
|
||||
execlp.side_effect = OSError()
|
||||
execlp.side_effect = OSError('here is problem')
|
||||
self.run_command('config', '-e')
|
||||
self.assertIn('Could not edit configuration',
|
||||
unicode(user_error.exception.args[0]))
|
||||
unicode(user_error.exception))
|
||||
self.assertIn('here is problem', unicode(user_error.exception))
|
||||
|
||||
def test_edit_invalid_config_file(self):
|
||||
self.lib = Library(':memory:')
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import sys
|
|||
import re
|
||||
import os
|
||||
|
||||
from mock import patch
|
||||
|
||||
from test._common import unittest
|
||||
from test import _common
|
||||
from beets import util
|
||||
|
|
@ -36,6 +38,17 @@ class UtilTest(unittest.TestCase):
|
|||
with _common.system_mock('Tagada'):
|
||||
self.assertEqual(util.open_anything(), 'xdg-open')
|
||||
|
||||
@patch('os.execlp')
|
||||
@patch('beets.util.open_anything')
|
||||
def test_interactive_open(self, mock_open, mock_execlp):
|
||||
mock_open.return_value = 'tagada'
|
||||
util.interactive_open('foo')
|
||||
mock_execlp.assert_called_once_with('tagada', 'tagada', 'foo')
|
||||
mock_execlp.reset_mock()
|
||||
|
||||
util.interactive_open('foo', 'bar')
|
||||
mock_execlp.assert_called_once_with('bar', 'bar', 'foo')
|
||||
|
||||
def test_sanitize_unix_replaces_leading_dot(self):
|
||||
with _common.platform_posix():
|
||||
p = util.sanitize_path(u'one/.two/three')
|
||||
|
|
|
|||
Loading…
Reference in a new issue