Skip to content

Commit ffa22d6

Browse files
committed
Merge pull request #2360 from diomekes/importer-play
play: Add prompt choice to importer
2 parents 1801663 + 8d61342 commit ffa22d6

File tree

2 files changed

+117
-49
lines changed

2 files changed

+117
-49
lines changed

beetsplug/play.py

Lines changed: 103 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,41 @@
1919

2020
from beets.plugins import BeetsPlugin
2121
from beets.ui import Subcommand
22+
from beets.ui.commands import PromptChoice
2223
from beets import config
2324
from beets import ui
2425
from beets import util
2526
from os.path import relpath
2627
from tempfile import NamedTemporaryFile
28+
import subprocess
2729

2830
# Indicate where arguments should be inserted into the command string.
2931
# If this is missing, they're placed at the end.
3032
ARGS_MARKER = '$args'
3133

3234

35+
def play(command_str, selection, paths, open_args, log, item_type='track',
36+
keep_open=False):
37+
"""Play items in paths with command_str and optional arguments. If
38+
keep_open, return to beets, otherwise exit once command runs.
39+
"""
40+
# Print number of tracks or albums to be played, log command to be run.
41+
item_type += 's' if len(selection) > 1 else ''
42+
ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type))
43+
log.debug(u'executing command: {} {!r}', command_str, open_args)
44+
45+
try:
46+
if keep_open:
47+
command = util.shlex_split(command_str)
48+
command = command + open_args
49+
subprocess.call(command)
50+
else:
51+
util.interactive_open(open_args, command_str)
52+
except OSError as exc:
53+
raise ui.UserError(
54+
"Could not play the query: {0}".format(exc))
55+
56+
3357
class PlayPlugin(BeetsPlugin):
3458

3559
def __init__(self):
@@ -40,11 +64,14 @@ def __init__(self):
4064
'use_folders': False,
4165
'relative_to': None,
4266
'raw': False,
43-
# Backwards compatibility. See #1803 and line 74
67+
# Backwards compatibility. See #1803 and line 155
4468
'warning_threshold': -2,
4569
'warning_treshold': 100,
4670
})
4771

72+
self.register_listener('before_choose_candidate',
73+
self.before_choose_candidate_listener)
74+
4875
def commands(self):
4976
play_command = Subcommand(
5077
'play',
@@ -56,44 +83,17 @@ def commands(self):
5683
action='store',
5784
help=u'add additional arguments to the command',
5885
)
59-
play_command.func = self.play_music
86+
play_command.func = self._play_command
6087
return [play_command]
6188

62-
def play_music(self, lib, opts, args):
63-
"""Execute query, create temporary playlist and execute player
64-
command passing that playlist, at request insert optional arguments.
89+
def _play_command(self, lib, opts, args):
90+
"""The CLI command function for `beet play`. Create a list of paths
91+
from query, determine if tracks or albums are to be played.
6592
"""
66-
command_str = config['play']['command'].get()
67-
if not command_str:
68-
command_str = util.open_anything()
6993
use_folders = config['play']['use_folders'].get(bool)
7094
relative_to = config['play']['relative_to'].get()
71-
raw = config['play']['raw'].get(bool)
72-
warning_threshold = config['play']['warning_threshold'].get(int)
73-
# We use -2 as a default value for warning_threshold to detect if it is
74-
# set or not. We can't use a falsey value because it would have an
75-
# actual meaning in the configuration of this plugin, and we do not use
76-
# -1 because some people might use it as a value to obtain no warning,
77-
# which wouldn't be that bad of a practice.
78-
if warning_threshold == -2:
79-
# if warning_threshold has not been set by user, look for
80-
# warning_treshold, to preserve backwards compatibility. See #1803.
81-
# warning_treshold has the correct default value of 100.
82-
warning_threshold = config['play']['warning_treshold'].get(int)
83-
8495
if relative_to:
8596
relative_to = util.normpath(relative_to)
86-
87-
# Add optional arguments to the player command.
88-
if opts.args:
89-
if ARGS_MARKER in command_str:
90-
command_str = command_str.replace(ARGS_MARKER, opts.args)
91-
else:
92-
command_str = u"{} {}".format(command_str, opts.args)
93-
else:
94-
# Don't include the marker in the command.
95-
command_str = command_str.replace(" " + ARGS_MARKER, "")
96-
9797
# Perform search by album and add folders rather than tracks to
9898
# playlist.
9999
if opts.album:
@@ -117,13 +117,62 @@ def play_music(self, lib, opts, args):
117117
paths = [relpath(path, relative_to) for path in paths]
118118
item_type = 'track'
119119

120-
item_type += 's' if len(selection) > 1 else ''
121-
122120
if not selection:
123121
ui.print_(ui.colorize('text_warning',
124122
u'No {0} to play.'.format(item_type)))
125123
return
126124

125+
open_args = self._playlist_or_paths(paths)
126+
command_str = self._command_str(opts.args)
127+
128+
# Check if the selection exceeds configured threshold. If True,
129+
# cancel, otherwise proceed with play command.
130+
if not self._exceeds_threshold(selection, command_str, open_args,
131+
item_type):
132+
play(command_str, selection, paths, open_args, self._log,
133+
item_type)
134+
135+
def _command_str(self, args=None):
136+
"""Create a command string from the config command and optional args.
137+
"""
138+
command_str = config['play']['command'].get()
139+
if not command_str:
140+
return util.open_anything()
141+
# Add optional arguments to the player command.
142+
if args:
143+
if ARGS_MARKER in command_str:
144+
return command_str.replace(ARGS_MARKER, args)
145+
else:
146+
return u"{} {}".format(command_str, args)
147+
else:
148+
# Don't include the marker in the command.
149+
return command_str.replace(" " + ARGS_MARKER, "")
150+
151+
def _playlist_or_paths(self, paths):
152+
"""Return either the raw paths of items or a playlist of the items.
153+
"""
154+
if config['play']['raw']:
155+
return paths
156+
else:
157+
return [self._create_tmp_playlist(paths)]
158+
159+
def _exceeds_threshold(self, selection, command_str, open_args,
160+
item_type='track'):
161+
"""Prompt user whether to abort if playlist exceeds threshold. If
162+
True, cancel playback. If False, execute play command.
163+
"""
164+
warning_threshold = config['play']['warning_threshold'].get(int)
165+
# We use -2 as a default value for warning_threshold to detect if it is
166+
# set or not. We can't use a falsey value because it would have an
167+
# actual meaning in the configuration of this plugin, and we do not use
168+
# -1 because some people might use it as a value to obtain no warning,
169+
# which wouldn't be that bad of a practice.
170+
if warning_threshold == -2:
171+
# if warning_threshold has not been set by user, look for
172+
# warning_treshold, to preserve backwards compatibility. See #1803.
173+
# warning_treshold has the correct default value of 100.
174+
warning_threshold = config['play']['warning_treshold'].get(int)
175+
127176
# Warn user before playing any huge playlists.
128177
if warning_threshold and len(selection) > warning_threshold:
129178
ui.print_(ui.colorize(
@@ -132,20 +181,9 @@ def play_music(self, lib, opts, args):
132181
len(selection), item_type)))
133182

134183
if ui.input_options((u'Continue', u'Abort')) == 'a':
135-
return
184+
return True
136185

137-
ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type))
138-
if raw:
139-
open_args = paths
140-
else:
141-
open_args = [self._create_tmp_playlist(paths)]
142-
143-
self._log.debug(u'executing command: {} {!r}', command_str, open_args)
144-
try:
145-
util.interactive_open(open_args, command_str)
146-
except OSError as exc:
147-
raise ui.UserError(
148-
"Could not play the query: {0}".format(exc))
186+
return False
149187

150188
def _create_tmp_playlist(self, paths_list):
151189
"""Create a temporary .m3u file. Return the filename.
@@ -155,3 +193,21 @@ def _create_tmp_playlist(self, paths_list):
155193
m3u.write(item + b'\n')
156194
m3u.close()
157195
return m3u.name
196+
197+
def before_choose_candidate_listener(self, session, task):
198+
"""Append a "Play" choice to the interactive importer prompt.
199+
"""
200+
return [PromptChoice('y', 'plaY', self.importer_play)]
201+
202+
def importer_play(self, session, task):
203+
"""Get items from current import task and send to play function.
204+
"""
205+
selection = task.items
206+
paths = [item.path for item in selection]
207+
208+
open_args = self._playlist_or_paths(paths)
209+
command_str = self._command_str()
210+
211+
if not self._exceeds_threshold(selection, command_str, open_args):
212+
play(command_str, selection, paths, open_args, self._log,
213+
keep_open=True)

docs/plugins/play.rst

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ Play Plugin
44
The ``play`` plugin allows you to pass the results of a query to a music
55
player in the form of an m3u playlist or paths on the command line.
66

7-
Usage
8-
-----
7+
Command Line Usage
8+
------------------
99

1010
To use the ``play`` plugin, enable it in your configuration (see
1111
:ref:`using-plugins`). Then use it by invoking the ``beet play`` command with
@@ -29,6 +29,18 @@ would on the command-line)::
2929
While playing you'll be able to interact with the player if it is a
3030
command-line oriented, and you'll get its output in real time.
3131

32+
Interactive Usage
33+
-----------------
34+
35+
The `play` plugin can also be invoked during an import. If enabled, the plugin
36+
adds a `plaY` option to the prompt, so pressing `y` will execute the configured
37+
command and play the items currently being imported.
38+
39+
Once the configured command exits, you will be returned to the import
40+
decision prompt. If your player is configured to run in the background (in a
41+
client/server setup), the music will play until you choose to stop it, and the
42+
import operation continues immediately.
43+
3244
Configuration
3345
-------------
3446

0 commit comments

Comments
 (0)