backpack-backtrack 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/PKG-INFO +17 -9
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/README.md +15 -7
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backpack_backtrack.egg-info/PKG-INFO +17 -9
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backpack_backtrack.egg-info/SOURCES.txt +1 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backpack_backtrack.egg-info/requires.txt +1 -1
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/bulk_pattern.py +1 -1
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_art.py +9 -9
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_assign.py +4 -4
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_menu.py +10 -10
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_names.py +6 -6
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_sort.py +1 -1
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/tag_handler.py +5 -5
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/editor_keys.py +2 -2
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/editor_view.py +12 -10
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/main.py +4 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/__init__.py +3 -2
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/activity.py +1 -1
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/browse.py +4 -4
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/command_line.py +15 -8
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/common.py +1 -38
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/settings.py +33 -91
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/sorting.py +4 -4
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/music_library.py +1 -1
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/now_playing_box.py +1 -1
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/player.py +119 -9
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/player_art.py +3 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/player_geom.py +1 -1
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/player_ui.py +102 -49
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/queue_pane.py +13 -3
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/trim/bulk.py +6 -10
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/trim/editor.py +17 -16
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/pyproject.toml +2 -2
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_appearance.py +1 -1
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_panels.py +25 -0
- backpack_backtrack-0.3.2/tests/test_player_details.py +28 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_player_layout.py +2 -2
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_player_panes.py +7 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_player_tiny.py +13 -1
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_queue_edit.py +20 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/LICENSE +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backpack_backtrack.egg-info/dependency_links.txt +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backpack_backtrack.egg-info/entry_points.txt +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backpack_backtrack.egg-info/top_level.txt +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/__init__.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/__main__.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/accents.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/album_art.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/books.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/chapter_edit.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/cli.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/cli_commands.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/config.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/deps.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/feed.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/history.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/__init__.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/browser.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_common.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_ops.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/cover_matcher.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/file_namer.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/filename_parser.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/languages.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/tag_formats.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/tag_registry.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/tag_writer.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/__init__.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/editor.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/formats.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/lyric_pane.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/md_overlay.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/sync_doc.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/text.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/time_fields.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/verify.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/browse_columns.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/chapters.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/directory.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/history.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/play.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/search.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/__init__.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/ipc.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/libvlc.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/session.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/search.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/trim/__init__.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/trim/engine.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/tuning.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/setup.cfg +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_accents.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_album_credit.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_anchor_times.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_art_fit.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_audiobooks.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_browse_categories.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_browse_columns.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_bulk_ops.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_bulk_preview.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_chapter_edit.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_cli.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_command_line.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_config_setting.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_data_safety.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_deps.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_directory.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_feed.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_idle_player.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_inline_art.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_kv_editor.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_list_result.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_lyric_pane_layout.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_lyric_timing.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_lyrics_editor_keys.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_md_overlay.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_moved_files.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_player_seek_keys.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_player_sizes.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_progress_flicker.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_random_album.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_rebound_keys.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_regressions.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_search.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_search_disc.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_session_equalizer.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_sorting.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_summarize_tag_value.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_tag_formats.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_tag_id_input.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_tag_writer.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_track_title.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_transport_icons.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_backup.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_bulk.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_chapters.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_editor.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_loudness.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_silence.py +0 -0
- {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_sting.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backpack-backtrack
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Terminal music player with VLC playback, lyrics sync, and metadata editing.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/RedEraRrow/backtrack
|
|
@@ -21,7 +21,7 @@ Requires-Dist: numpy>=1.24
|
|
|
21
21
|
Requires-Dist: pyperclip>=1.11.0
|
|
22
22
|
Requires-Dist: python-vlc>=3.0.21203
|
|
23
23
|
Requires-Dist: colorama>=0.4.6
|
|
24
|
-
Requires-Dist: backpack-backbone<0.4,>=0.3
|
|
24
|
+
Requires-Dist: backpack-backbone<0.4,>=0.3.2
|
|
25
25
|
Dynamic: license-file
|
|
26
26
|
|
|
27
27
|
# Backtrack
|
|
@@ -87,9 +87,11 @@ and has a tag editor (ID3, MP4 and Vorbis comments) with bulk operations for who
|
|
|
87
87
|
|
|
88
88
|
macOS, starting from nothing:
|
|
89
89
|
```bash
|
|
90
|
-
# Homebrew, if you don't have it yet (it asks for your password
|
|
91
|
-
# Silicon prints two lines to run afterwards: run them)
|
|
90
|
+
# Homebrew, if you don't have it yet (it asks for your password)
|
|
92
91
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
92
|
+
# put brew on your PATH, now and in new windows (Apple Silicon; does nothing on an Intel Mac)
|
|
93
|
+
echo 'eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"' >> ~/.zprofile
|
|
94
|
+
eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"
|
|
93
95
|
|
|
94
96
|
brew install pipx
|
|
95
97
|
brew install --cask vlc
|
|
@@ -155,13 +157,19 @@ last run's queue. In the player, `f` hides or shows the tab bar (also Settings
|
|
|
155
157
|
The player sits in boxes while the tab bar or a panel shows, at any window size, and edge to edge
|
|
156
158
|
only with neither (`f` hides the bar). The
|
|
157
159
|
boxes fill the window with no gaps between them: the art and the details in a box as wide as the art
|
|
158
|
-
with the panels (`w`) in columns beside it
|
|
160
|
+
with the panels (`w`) in columns beside it wherever they fit without leaving the art much smaller
|
|
161
|
+
(the player's box gives up width for them; a column on each side can differ in width, as can the
|
|
162
|
+
panels stacked in one), else under it, as in a tall, narrow window; and along the
|
|
159
163
|
bottom the controls, the progress bar, the times and the speed or sleep timer. In a short, wide
|
|
160
164
|
window the art sits beside the details instead of over them, so it stays as big as it can be.
|
|
161
165
|
In a tiny window it's just the transport: boxed in 3 to 5 rows, bare in 1 or 2 (and the tab bar and
|
|
162
166
|
status line step aside under 6 rows). As it narrows the row gives things up in turn, never cutting
|
|
163
167
|
any short: the bar, then the length (the position alone), then the time, then previous, then next,
|
|
164
|
-
leaving play/pause, and last the box.
|
|
168
|
+
leaving play/pause, and last the box. Screens never show without their boxes: one in a window
|
|
169
|
+
too small for them (under 10 rows, or 9 columns) gives way to that same transport, alone, until the
|
|
170
|
+
window grows and it comes back as you left it. Meanwhile the transport's own keys work (play/pause,
|
|
171
|
+
next, previous, seeking, volume) and `:` opens the command line; nothing else reaches the screen.
|
|
172
|
+
The art is a square
|
|
165
173
|
that fills its space (a cover that isn't square is cropped about its centre), in the player,
|
|
166
174
|
Browse's preview and the tag editor's picture screens alike; an audiobook's booklet picture too.
|
|
167
175
|
The people panel
|
|
@@ -260,11 +268,11 @@ by screen, with more than one key per action if you like), and every hint bar sh
|
|
|
260
268
|
| `←` / `→` | Seek ∓5 s |
|
|
261
269
|
| `j` / `l` | Seek ∓1 s |
|
|
262
270
|
| `,` / `.` | Seek ∓30 s |
|
|
263
|
-
| `+` / `-` | Volume up / down |
|
|
271
|
+
| `+` / `-` | Volume up / down, here and on any list (the level shows for a moment over the middle of the screen); in a text field they're typed |
|
|
264
272
|
| `m` | Show or hide the track details line (year · genre · disc/track …); remembered. Also Settings → Track details in player |
|
|
265
273
|
| `w` | Choose the panels: a box over the player to tick Lyrics, People, Queue and Chapters (one this track has nothing for says so). Its **Arrange panels…** moves them: `Tab` from panel to panel (the one moving is outlined in the accent), `↑` / `↓` up or down its column, `←` / `→` to the other side of the player, `↵` done. A wide window and a tall one each keep their own arrangement: wide, the panels are columns beside the player (either side, or both); tall, they're under it, side by side when there's room. Remembered |
|
|
266
|
-
| `↑` / `↓` | With the queue panel showing: a cursor through the queue. `↵` plays the track at it, `J` / `K` move it up / down, `d` removes it, `x` shuffles what's coming, `c` clears what's coming, `u` undoes the last queue change |
|
|
267
|
-
| `?` | Show or hide the key hints, on every screen: they start hidden. Each screen's top line ends in `[?] help`; in a text field, where `?` is typed, it says `[^/] help` and the key is Ctrl+/ (
|
|
274
|
+
| `↑` / `↓` | With the queue panel showing: a cursor through the queue (the mouse wheel or a trackpad over the panel moves it too). `↵` plays the track at it, `J` / `K` move it up / down, `d` removes it, `x` shuffles what's coming, `c` clears what's coming, `u` undoes the last queue change |
|
|
275
|
+
| `?` | Show or hide the key hints, on every screen: they start hidden. Each screen's top line ends in `[?] help`; in a text field, where `?` is typed, it says `[^/] help` and the key is Ctrl+/ (macOS's Terminal sends nothing for that, so there it says `[^g] help` and the key is Ctrl+G; either works anywhere it arrives). Clicking the key in the corner works too. In the player it stays in the hint bar. Settings → [?] help toggle hides `[?] help` everywhere; `?` still works |
|
|
268
276
|
| `[` / `]` | Previous / next track |
|
|
269
277
|
| `e` | Edit the playing track's tags |
|
|
270
278
|
| `a` / `A` | Go to the playing track's album / artist in Browse (`Esc` from there goes up through Browse) |
|
|
@@ -61,9 +61,11 @@ and has a tag editor (ID3, MP4 and Vorbis comments) with bulk operations for who
|
|
|
61
61
|
|
|
62
62
|
macOS, starting from nothing:
|
|
63
63
|
```bash
|
|
64
|
-
# Homebrew, if you don't have it yet (it asks for your password
|
|
65
|
-
# Silicon prints two lines to run afterwards: run them)
|
|
64
|
+
# Homebrew, if you don't have it yet (it asks for your password)
|
|
66
65
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
66
|
+
# put brew on your PATH, now and in new windows (Apple Silicon; does nothing on an Intel Mac)
|
|
67
|
+
echo 'eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"' >> ~/.zprofile
|
|
68
|
+
eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"
|
|
67
69
|
|
|
68
70
|
brew install pipx
|
|
69
71
|
brew install --cask vlc
|
|
@@ -129,13 +131,19 @@ last run's queue. In the player, `f` hides or shows the tab bar (also Settings
|
|
|
129
131
|
The player sits in boxes while the tab bar or a panel shows, at any window size, and edge to edge
|
|
130
132
|
only with neither (`f` hides the bar). The
|
|
131
133
|
boxes fill the window with no gaps between them: the art and the details in a box as wide as the art
|
|
132
|
-
with the panels (`w`) in columns beside it
|
|
134
|
+
with the panels (`w`) in columns beside it wherever they fit without leaving the art much smaller
|
|
135
|
+
(the player's box gives up width for them; a column on each side can differ in width, as can the
|
|
136
|
+
panels stacked in one), else under it, as in a tall, narrow window; and along the
|
|
133
137
|
bottom the controls, the progress bar, the times and the speed or sleep timer. In a short, wide
|
|
134
138
|
window the art sits beside the details instead of over them, so it stays as big as it can be.
|
|
135
139
|
In a tiny window it's just the transport: boxed in 3 to 5 rows, bare in 1 or 2 (and the tab bar and
|
|
136
140
|
status line step aside under 6 rows). As it narrows the row gives things up in turn, never cutting
|
|
137
141
|
any short: the bar, then the length (the position alone), then the time, then previous, then next,
|
|
138
|
-
leaving play/pause, and last the box.
|
|
142
|
+
leaving play/pause, and last the box. Screens never show without their boxes: one in a window
|
|
143
|
+
too small for them (under 10 rows, or 9 columns) gives way to that same transport, alone, until the
|
|
144
|
+
window grows and it comes back as you left it. Meanwhile the transport's own keys work (play/pause,
|
|
145
|
+
next, previous, seeking, volume) and `:` opens the command line; nothing else reaches the screen.
|
|
146
|
+
The art is a square
|
|
139
147
|
that fills its space (a cover that isn't square is cropped about its centre), in the player,
|
|
140
148
|
Browse's preview and the tag editor's picture screens alike; an audiobook's booklet picture too.
|
|
141
149
|
The people panel
|
|
@@ -234,11 +242,11 @@ by screen, with more than one key per action if you like), and every hint bar sh
|
|
|
234
242
|
| `←` / `→` | Seek ∓5 s |
|
|
235
243
|
| `j` / `l` | Seek ∓1 s |
|
|
236
244
|
| `,` / `.` | Seek ∓30 s |
|
|
237
|
-
| `+` / `-` | Volume up / down |
|
|
245
|
+
| `+` / `-` | Volume up / down, here and on any list (the level shows for a moment over the middle of the screen); in a text field they're typed |
|
|
238
246
|
| `m` | Show or hide the track details line (year · genre · disc/track …); remembered. Also Settings → Track details in player |
|
|
239
247
|
| `w` | Choose the panels: a box over the player to tick Lyrics, People, Queue and Chapters (one this track has nothing for says so). Its **Arrange panels…** moves them: `Tab` from panel to panel (the one moving is outlined in the accent), `↑` / `↓` up or down its column, `←` / `→` to the other side of the player, `↵` done. A wide window and a tall one each keep their own arrangement: wide, the panels are columns beside the player (either side, or both); tall, they're under it, side by side when there's room. Remembered |
|
|
240
|
-
| `↑` / `↓` | With the queue panel showing: a cursor through the queue. `↵` plays the track at it, `J` / `K` move it up / down, `d` removes it, `x` shuffles what's coming, `c` clears what's coming, `u` undoes the last queue change |
|
|
241
|
-
| `?` | Show or hide the key hints, on every screen: they start hidden. Each screen's top line ends in `[?] help`; in a text field, where `?` is typed, it says `[^/] help` and the key is Ctrl+/ (
|
|
248
|
+
| `↑` / `↓` | With the queue panel showing: a cursor through the queue (the mouse wheel or a trackpad over the panel moves it too). `↵` plays the track at it, `J` / `K` move it up / down, `d` removes it, `x` shuffles what's coming, `c` clears what's coming, `u` undoes the last queue change |
|
|
249
|
+
| `?` | Show or hide the key hints, on every screen: they start hidden. Each screen's top line ends in `[?] help`; in a text field, where `?` is typed, it says `[^/] help` and the key is Ctrl+/ (macOS's Terminal sends nothing for that, so there it says `[^g] help` and the key is Ctrl+G; either works anywhere it arrives). Clicking the key in the corner works too. In the player it stays in the hint bar. Settings → [?] help toggle hides `[?] help` everywhere; `?` still works |
|
|
242
250
|
| `[` / `]` | Previous / next track |
|
|
243
251
|
| `e` | Edit the playing track's tags |
|
|
244
252
|
| `a` / `A` | Go to the playing track's album / artist in Browse (`Esc` from there goes up through Browse) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backpack-backtrack
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Terminal music player with VLC playback, lyrics sync, and metadata editing.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/RedEraRrow/backtrack
|
|
@@ -21,7 +21,7 @@ Requires-Dist: numpy>=1.24
|
|
|
21
21
|
Requires-Dist: pyperclip>=1.11.0
|
|
22
22
|
Requires-Dist: python-vlc>=3.0.21203
|
|
23
23
|
Requires-Dist: colorama>=0.4.6
|
|
24
|
-
Requires-Dist: backpack-backbone<0.4,>=0.3
|
|
24
|
+
Requires-Dist: backpack-backbone<0.4,>=0.3.2
|
|
25
25
|
Dynamic: license-file
|
|
26
26
|
|
|
27
27
|
# Backtrack
|
|
@@ -87,9 +87,11 @@ and has a tag editor (ID3, MP4 and Vorbis comments) with bulk operations for who
|
|
|
87
87
|
|
|
88
88
|
macOS, starting from nothing:
|
|
89
89
|
```bash
|
|
90
|
-
# Homebrew, if you don't have it yet (it asks for your password
|
|
91
|
-
# Silicon prints two lines to run afterwards: run them)
|
|
90
|
+
# Homebrew, if you don't have it yet (it asks for your password)
|
|
92
91
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
92
|
+
# put brew on your PATH, now and in new windows (Apple Silicon; does nothing on an Intel Mac)
|
|
93
|
+
echo 'eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"' >> ~/.zprofile
|
|
94
|
+
eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"
|
|
93
95
|
|
|
94
96
|
brew install pipx
|
|
95
97
|
brew install --cask vlc
|
|
@@ -155,13 +157,19 @@ last run's queue. In the player, `f` hides or shows the tab bar (also Settings
|
|
|
155
157
|
The player sits in boxes while the tab bar or a panel shows, at any window size, and edge to edge
|
|
156
158
|
only with neither (`f` hides the bar). The
|
|
157
159
|
boxes fill the window with no gaps between them: the art and the details in a box as wide as the art
|
|
158
|
-
with the panels (`w`) in columns beside it
|
|
160
|
+
with the panels (`w`) in columns beside it wherever they fit without leaving the art much smaller
|
|
161
|
+
(the player's box gives up width for them; a column on each side can differ in width, as can the
|
|
162
|
+
panels stacked in one), else under it, as in a tall, narrow window; and along the
|
|
159
163
|
bottom the controls, the progress bar, the times and the speed or sleep timer. In a short, wide
|
|
160
164
|
window the art sits beside the details instead of over them, so it stays as big as it can be.
|
|
161
165
|
In a tiny window it's just the transport: boxed in 3 to 5 rows, bare in 1 or 2 (and the tab bar and
|
|
162
166
|
status line step aside under 6 rows). As it narrows the row gives things up in turn, never cutting
|
|
163
167
|
any short: the bar, then the length (the position alone), then the time, then previous, then next,
|
|
164
|
-
leaving play/pause, and last the box.
|
|
168
|
+
leaving play/pause, and last the box. Screens never show without their boxes: one in a window
|
|
169
|
+
too small for them (under 10 rows, or 9 columns) gives way to that same transport, alone, until the
|
|
170
|
+
window grows and it comes back as you left it. Meanwhile the transport's own keys work (play/pause,
|
|
171
|
+
next, previous, seeking, volume) and `:` opens the command line; nothing else reaches the screen.
|
|
172
|
+
The art is a square
|
|
165
173
|
that fills its space (a cover that isn't square is cropped about its centre), in the player,
|
|
166
174
|
Browse's preview and the tag editor's picture screens alike; an audiobook's booklet picture too.
|
|
167
175
|
The people panel
|
|
@@ -260,11 +268,11 @@ by screen, with more than one key per action if you like), and every hint bar sh
|
|
|
260
268
|
| `←` / `→` | Seek ∓5 s |
|
|
261
269
|
| `j` / `l` | Seek ∓1 s |
|
|
262
270
|
| `,` / `.` | Seek ∓30 s |
|
|
263
|
-
| `+` / `-` | Volume up / down |
|
|
271
|
+
| `+` / `-` | Volume up / down, here and on any list (the level shows for a moment over the middle of the screen); in a text field they're typed |
|
|
264
272
|
| `m` | Show or hide the track details line (year · genre · disc/track …); remembered. Also Settings → Track details in player |
|
|
265
273
|
| `w` | Choose the panels: a box over the player to tick Lyrics, People, Queue and Chapters (one this track has nothing for says so). Its **Arrange panels…** moves them: `Tab` from panel to panel (the one moving is outlined in the accent), `↑` / `↓` up or down its column, `←` / `→` to the other side of the player, `↵` done. A wide window and a tall one each keep their own arrangement: wide, the panels are columns beside the player (either side, or both); tall, they're under it, side by side when there's room. Remembered |
|
|
266
|
-
| `↑` / `↓` | With the queue panel showing: a cursor through the queue. `↵` plays the track at it, `J` / `K` move it up / down, `d` removes it, `x` shuffles what's coming, `c` clears what's coming, `u` undoes the last queue change |
|
|
267
|
-
| `?` | Show or hide the key hints, on every screen: they start hidden. Each screen's top line ends in `[?] help`; in a text field, where `?` is typed, it says `[^/] help` and the key is Ctrl+/ (
|
|
274
|
+
| `↑` / `↓` | With the queue panel showing: a cursor through the queue (the mouse wheel or a trackpad over the panel moves it too). `↵` plays the track at it, `J` / `K` move it up / down, `d` removes it, `x` shuffles what's coming, `c` clears what's coming, `u` undoes the last queue change |
|
|
275
|
+
| `?` | Show or hide the key hints, on every screen: they start hidden. Each screen's top line ends in `[?] help`; in a text field, where `?` is typed, it says `[^/] help` and the key is Ctrl+/ (macOS's Terminal sends nothing for that, so there it says `[^g] help` and the key is Ctrl+G; either works anywhere it arrives). Clicking the key in the corner works too. In the player it stays in the hint bar. Settings → [?] help toggle hides `[?] help` everywhere; `?` still works |
|
|
268
276
|
| `[` / `]` | Previous / next track |
|
|
269
277
|
| `e` | Edit the playing track's tags |
|
|
270
278
|
| `a` / `A` | Go to the playing track's album / artist in Browse (`Esc` from there goes up through Browse) |
|
|
@@ -235,7 +235,7 @@ def validate_schedule_rows(rows: list, n_tracks: int) -> tuple:
|
|
|
235
235
|
errors.append(f"row {i}: FROM/TO must both be track positions")
|
|
236
236
|
continue
|
|
237
237
|
if lo > hi:
|
|
238
|
-
errors.append(f"row {i}: FROM
|
|
238
|
+
errors.append(f"row {i}: FROM {lo} is after TO {hi}")
|
|
239
239
|
continue
|
|
240
240
|
if hi < 1 or lo > n_tracks:
|
|
241
241
|
errors.append(f"row {i}: positions {lo}-{hi} are outside 1-{n_tracks}")
|
|
@@ -124,14 +124,14 @@ def set_album_art_op(paths: list, library: list, header) -> None:
|
|
|
124
124
|
# 1) Matching strategy. Each answer is kept in `state`, so a screen reopened
|
|
125
125
|
# by walking back holds what it was left holding, including covers chosen by
|
|
126
126
|
# hand in the preview, which survive the plan being rebuilt.
|
|
127
|
-
_MODES = ["Auto-detect
|
|
127
|
+
_MODES = ["Auto-detect",
|
|
128
128
|
"One cover per disc / series / work",
|
|
129
129
|
"Matching file name",
|
|
130
130
|
"Track number / order",
|
|
131
|
-
"Name pattern
|
|
132
|
-
_GROUPS = {"Auto
|
|
131
|
+
"Name pattern"]
|
|
132
|
+
_GROUPS = {"Auto: disc, else series, else work": 'auto',
|
|
133
133
|
"Disc number": 'disc',
|
|
134
|
-
"Series
|
|
134
|
+
"Series or season, from SxxExx": 'season',
|
|
135
135
|
"Work / grouping": 'work'}
|
|
136
136
|
_POLICIES = ["Fill blanks only", "Overwrite existing"]
|
|
137
137
|
state: dict = {'mode': _MODES[0], 'group': list(_GROUPS)[0], 'pattern': None,
|
|
@@ -160,7 +160,7 @@ def set_album_art_op(paths: list, library: list, header) -> None:
|
|
|
160
160
|
|
|
161
161
|
def _ask_pattern():
|
|
162
162
|
"""The image-name pattern; only asked for pattern matching."""
|
|
163
|
-
if state['mode'] != "Name pattern
|
|
163
|
+
if state['mode'] != "Name pattern":
|
|
164
164
|
return _SKIP
|
|
165
165
|
picked = _cover_pattern_prompt(header)
|
|
166
166
|
if picked is None:
|
|
@@ -232,7 +232,7 @@ def set_album_art_op(paths: list, library: list, header) -> None:
|
|
|
232
232
|
"""Cell text for a track's planned cover: label, plus a "has art" badge if replacing."""
|
|
233
233
|
img = plan.get(p)
|
|
234
234
|
if not img:
|
|
235
|
-
return "none
|
|
235
|
+
return "none, d chooses"
|
|
236
236
|
label = cover_label(img, os.path.dirname(os.path.abspath(p)))
|
|
237
237
|
if existing_art[p]:
|
|
238
238
|
return [(label, 'normal'), (' · has art', 'static-dim')]
|
|
@@ -256,7 +256,7 @@ def set_album_art_op(paths: list, library: list, header) -> None:
|
|
|
256
256
|
state['manual'][p] = img # remembered if this screen is revisited
|
|
257
257
|
if img is None:
|
|
258
258
|
plan[p] = None
|
|
259
|
-
ch.cells = [os.path.basename(p), "none
|
|
259
|
+
ch.cells = [os.path.basename(p), "none, d chooses", '']
|
|
260
260
|
ch.checked = False
|
|
261
261
|
else:
|
|
262
262
|
plan[p] = img
|
|
@@ -338,11 +338,11 @@ def set_album_art_op(paths: list, library: list, header) -> None:
|
|
|
338
338
|
if nk:
|
|
339
339
|
bits.append(f"{nk} ticked")
|
|
340
340
|
elif n_match:
|
|
341
|
-
bits.append("0 ticked
|
|
341
|
+
bits.append("0 ticked: they have art already. Overwrite, or Space or a, ticks them")
|
|
342
342
|
return header(" · ".join(bits))()
|
|
343
343
|
|
|
344
344
|
sel = prompt.select(
|
|
345
|
-
"Preview
|
|
345
|
+
"Preview, ↵ applies:",
|
|
346
346
|
choices=preview_choices, columns=_COVER_PREVIEW_COLUMNS,
|
|
347
347
|
header=_preview_header, multi=True,
|
|
348
348
|
extra_hints={'tags.details': 'choose cover'}, on_inspect=_reassign,
|
|
@@ -264,7 +264,7 @@ def bulk_fraction_editor(paths: list, tag_id: str, library: list, header) -> Non
|
|
|
264
264
|
bits.append(f"{skipped_fmt} non-ID3 skipped")
|
|
265
265
|
return header(' · '.join(bits))()
|
|
266
266
|
|
|
267
|
-
sel = prompt.select("Preview
|
|
267
|
+
sel = prompt.select("Preview, ↵ applies:", choices=choices,
|
|
268
268
|
columns=_RENUMBER_COLUMNS, header=_frac_header, multi=True)
|
|
269
269
|
if sel is None:
|
|
270
270
|
return
|
|
@@ -360,9 +360,9 @@ def assign_by_pattern(paths: list, library: list, header) -> None:
|
|
|
360
360
|
def _modes() -> list:
|
|
361
361
|
"""The assignment modes this tag supports (schedules need a date frame)."""
|
|
362
362
|
info = get_tag_info(state['tag'])
|
|
363
|
-
modes = ["Ranges
|
|
363
|
+
modes = ["Ranges: from-to → value", "Every N tracks → value"]
|
|
364
364
|
if info and info.format_spec == 'ISO8601':
|
|
365
|
-
modes += ["Date schedule", "Schedule per range
|
|
365
|
+
modes += ["Date schedule", "Schedule per range: its own start and interval"]
|
|
366
366
|
return modes
|
|
367
367
|
|
|
368
368
|
def _ask_mode() -> bool:
|
|
@@ -576,7 +576,7 @@ def assign_by_pattern(paths: list, library: list, header) -> None:
|
|
|
576
576
|
return False
|
|
577
577
|
sub = f"{tag_id} · {ui.plural(len(choices), 'file')}" + (
|
|
578
578
|
f" · {n_mp4} non-ID3 skipped" if n_mp4 else "")
|
|
579
|
-
sel = prompt.select("Preview
|
|
579
|
+
sel = prompt.select("Preview, ↵ applies:", choices=choices,
|
|
580
580
|
columns=_PATTERN_COLUMNS, header=header(sub), multi=True)
|
|
581
581
|
if sel is None:
|
|
582
582
|
return False
|
|
@@ -68,8 +68,8 @@ def renumber_tracks_op(paths: list, library: list, header) -> None:
|
|
|
68
68
|
ui.show_status("No taggable tracks to renumber.")
|
|
69
69
|
return
|
|
70
70
|
|
|
71
|
-
_MODES = ["Continuous
|
|
72
|
-
"Per
|
|
71
|
+
_MODES = ["Continuous: 1…N across all discs",
|
|
72
|
+
"Per disc: restart at 1 each disc"]
|
|
73
73
|
state: dict = {'mode_sel': _MODES[0], 'apply_set': set()}
|
|
74
74
|
|
|
75
75
|
def _ask_mode() -> bool:
|
|
@@ -99,7 +99,7 @@ def renumber_tracks_op(paths: list, library: list, header) -> None:
|
|
|
99
99
|
for (pos, name, why), c in zip(bo.position_rows(plan), plan.changes)]
|
|
100
100
|
sub = ui.plural(len(choices), "file") + (
|
|
101
101
|
f" · {skipped_fmt} unsupported skipped" if skipped_fmt else "")
|
|
102
|
-
sel = prompt.select("Preview
|
|
102
|
+
sel = prompt.select("Preview, ↵ applies:", choices=choices,
|
|
103
103
|
columns=_RENUMBER_COLUMNS, header=header(sub), multi=True)
|
|
104
104
|
if sel is None:
|
|
105
105
|
return False
|
|
@@ -320,7 +320,7 @@ def bulk_id3_manager(library: list, album_name: str | None = None, paths: list |
|
|
|
320
320
|
"Set album art from files",
|
|
321
321
|
"Assign by range / schedule",
|
|
322
322
|
"Apply sort orders",
|
|
323
|
-
"Renumber tracks
|
|
323
|
+
"Renumber tracks",
|
|
324
324
|
"Reflow disc numbering",
|
|
325
325
|
"Remove single-disc numbering",
|
|
326
326
|
"Strip stale length tags",
|
|
@@ -346,7 +346,7 @@ def bulk_id3_manager(library: list, album_name: str | None = None, paths: list |
|
|
|
346
346
|
"Set album art from files": "Set Album Art",
|
|
347
347
|
"Assign by range / schedule": "Assign By Pattern",
|
|
348
348
|
"Apply sort orders": "Apply Sort Orders",
|
|
349
|
-
"Renumber tracks
|
|
349
|
+
"Renumber tracks": "Renumber Tracks",
|
|
350
350
|
"Reflow disc numbering": "Reflow Discs",
|
|
351
351
|
"Remove single-disc numbering": "Strip Single Disc",
|
|
352
352
|
"Strip stale length tags": "Strip Length Tags",
|
|
@@ -534,15 +534,15 @@ def bulk_id3_manager(library: list, album_name: str | None = None, paths: list |
|
|
|
534
534
|
|
|
535
535
|
source = prompt.select(
|
|
536
536
|
"Value source:",
|
|
537
|
-
choices=["Enter a value", "Find
|
|
538
|
-
"
|
|
537
|
+
choices=["Enter a value", "Find and replace with a regex",
|
|
538
|
+
"Regex on the file or folder name"],
|
|
539
539
|
header=_bulk_header())
|
|
540
540
|
if not source:
|
|
541
541
|
return
|
|
542
542
|
|
|
543
543
|
if source == "Enter a value":
|
|
544
544
|
target_val = prompt_for_value(first_tag, current_value=fallback_val)
|
|
545
|
-
elif source == "Find
|
|
545
|
+
elif source == "Find and replace with a regex":
|
|
546
546
|
pat = prompt.text("Regex to find in each value:")
|
|
547
547
|
if not pat:
|
|
548
548
|
return
|
|
@@ -556,7 +556,7 @@ def bulk_id3_manager(library: list, album_name: str | None = None, paths: list |
|
|
|
556
556
|
return
|
|
557
557
|
set_spec = {'mode': 'replace', 'rx': rx, 'repl': repl}
|
|
558
558
|
target_val = "_regex_" # sentinel so the None-guard below doesn't bail
|
|
559
|
-
else: #
|
|
559
|
+
else: # regex on the file or folder name
|
|
560
560
|
against = prompt.select("Match regex against:",
|
|
561
561
|
choices=["File name", "Folder path"],
|
|
562
562
|
header=_bulk_header())
|
|
@@ -686,7 +686,7 @@ def bulk_id3_manager(library: list, album_name: str | None = None, paths: list |
|
|
|
686
686
|
return _bulk_header(" · ".join(bits))()
|
|
687
687
|
|
|
688
688
|
picked_rows = prompt.select(
|
|
689
|
-
"Preview
|
|
689
|
+
"Preview, ↵ applies:",
|
|
690
690
|
choices=art_choices, columns=_COVER_PREVIEW_COLUMNS,
|
|
691
691
|
header=_art_header, multi=True)
|
|
692
692
|
if picked_rows is None:
|
|
@@ -64,7 +64,7 @@ def _detail_view(path, derived, plan: dict, present: dict, apply_fields: set,
|
|
|
64
64
|
elif f == 'disc':
|
|
65
65
|
val = _num_pair(d['disc'], d.get('total_discs'))
|
|
66
66
|
if f not in supported:
|
|
67
|
-
action = '
|
|
67
|
+
action = 'not in MP4'
|
|
68
68
|
elif f in plan:
|
|
69
69
|
action = 'write'
|
|
70
70
|
else:
|
|
@@ -156,9 +156,9 @@ def derive_from_filename(paths: list, library: list, header) -> None:
|
|
|
156
156
|
|
|
157
157
|
# 1) Which fields to write (Title on by default: the automatic baseline).
|
|
158
158
|
_FIELDS = [("title", "Title: from file name"),
|
|
159
|
-
("track", "Track number
|
|
160
|
-
("disc", "Disc number
|
|
161
|
-
("disc_subtitle", "Disc subtitle
|
|
159
|
+
("track", "Track number and total"),
|
|
160
|
+
("disc", "Disc number and total"),
|
|
161
|
+
("disc_subtitle", "Disc subtitle from the folder, not MP4"),
|
|
162
162
|
("album", "Album: from folder"),
|
|
163
163
|
("album_artist", "Album artist: from parent folder"),
|
|
164
164
|
("artist", "Track artist: from file name / folder"),
|
|
@@ -329,7 +329,7 @@ def derive_from_filename(paths: list, library: list, header) -> None:
|
|
|
329
329
|
apply_fields, overwrite, header)
|
|
330
330
|
|
|
331
331
|
selected = prompt.select(
|
|
332
|
-
"Preview
|
|
332
|
+
"Preview, ↵ applies:",
|
|
333
333
|
choices=preview_choices, columns=prev_cols,
|
|
334
334
|
header=header(sub), multi=True,
|
|
335
335
|
extra_hints={'tags.details': 'details'}, on_inspect=_show_detail,
|
|
@@ -447,7 +447,7 @@ def rename_files_op(paths: list, library: list, header) -> None:
|
|
|
447
447
|
for i, (p, o, n) in enumerate(changed)]
|
|
448
448
|
sub = f"{len(changed)} to rename" + (
|
|
449
449
|
f" · {skipped_fmt} unsupported skipped" if skipped_fmt else "")
|
|
450
|
-
sel = prompt.select("Preview
|
|
450
|
+
sel = prompt.select("Preview, ↵ renames:", choices=choices,
|
|
451
451
|
columns=_RENAME_COLUMNS, header=header(sub), multi=True)
|
|
452
452
|
if sel is None:
|
|
453
453
|
return False
|
|
@@ -238,7 +238,7 @@ def _review_sort_people(plan: _SortPlan, header, note: str = "") -> set | None:
|
|
|
238
238
|
choices = _choices()
|
|
239
239
|
files = len({p for ps in plan.entries.values() for p in ps})
|
|
240
240
|
sub = f"{ui.plural(len(choices), 'name')} · {ui.plural(files, 'file')}{note}"
|
|
241
|
-
sel = prompt.select("Preview
|
|
241
|
+
sel = prompt.select("Preview, ↵ applies:", choices=choices,
|
|
242
242
|
columns=_SORT_VALUE_COLUMNS, header=header(sub), multi=True,
|
|
243
243
|
row_edit=_options, row_edit_commit=_commit, row_edit_col=1)
|
|
244
244
|
return None if sel is None else set(sel)
|
|
@@ -37,7 +37,7 @@ _EXT_TO_MIME: dict[str, str] = {
|
|
|
37
37
|
keys.define("tags", "Tag editor", [
|
|
38
38
|
("add", ("a",), "add a tag, or a person"),
|
|
39
39
|
("trim", ("t",), "trim the audio"),
|
|
40
|
-
("details", ("d",), "the row's details
|
|
40
|
+
("details", ("d",), "the row's details: description, cover, preview"),
|
|
41
41
|
], within=("list", "global"))
|
|
42
42
|
|
|
43
43
|
|
|
@@ -148,7 +148,7 @@ def pick_nearby_cover(file_path: str, *, tokens: dict | None = None,
|
|
|
148
148
|
choices.append(prompt.Choice(title="No art for this file", value='__none__',
|
|
149
149
|
cells=["No art for this file", '', '']))
|
|
150
150
|
|
|
151
|
-
msg = title or "Album art
|
|
151
|
+
msg = title or "Album art, best guess first:"
|
|
152
152
|
hdr = header(ui.plural(len(ranked), "candidate")) if header else None
|
|
153
153
|
sel = prompt.select(msg, choices=choices, columns=_COVER_PICK_COLUMNS,
|
|
154
154
|
header=hdr, index=cur_idx)
|
|
@@ -510,7 +510,7 @@ def _prompt_for_equalisation(current_value: Any) -> dict | None:
|
|
|
510
510
|
def _prompt_for_rva2(current_value: Any) -> dict | None:
|
|
511
511
|
"""Interactive gain meter for a single-channel RVA2 frame."""
|
|
512
512
|
cur = float(current_value.gain) if (current_value is not None and hasattr(current_value, 'gain')) else 0.0
|
|
513
|
-
gain = prompt.rva2_edit("
|
|
513
|
+
gain = prompt.rva2_edit("Master volume adjustment:", gain=cur)
|
|
514
514
|
if gain is None:
|
|
515
515
|
return None
|
|
516
516
|
return {'__rva2__': True, 'gain': gain}
|
|
@@ -683,7 +683,7 @@ def _prompt_for_compilation(current_value: Any) -> str | None:
|
|
|
683
683
|
"""Yes/No toggle for the TCMP compilation flag (stored as '1'/'0')."""
|
|
684
684
|
cur = _current_text(current_value).strip()
|
|
685
685
|
is_comp = cur not in ('', '0')
|
|
686
|
-
sel = prompt.select("Part of a
|
|
686
|
+
sel = prompt.select("Part of a various-artists compilation?",
|
|
687
687
|
choices=[prompt.Choice(title='Yes', value='1'),
|
|
688
688
|
prompt.Choice(title='No', value='0')],
|
|
689
689
|
index=0 if is_comp else 1)
|
|
@@ -693,7 +693,7 @@ def _prompt_for_compilation(current_value: Any) -> str | None:
|
|
|
693
693
|
def _prompt_for_rbuf(current_value: Any) -> dict | None:
|
|
694
694
|
"""Numeric editor for the RBUF recommended-buffer-size frame (bytes)."""
|
|
695
695
|
cur = int(getattr(current_value, 'size', 0) or 0) if current_value is not None else 0
|
|
696
|
-
n = prompt.number_edit("Recommended buffer size
|
|
696
|
+
n = prompt.number_edit("Recommended buffer size in bytes:", value=cur, minimum=0)
|
|
697
697
|
if not isinstance(n, int):
|
|
698
698
|
return None
|
|
699
699
|
return {'__rbuf__': True, 'size': int(n)}
|
|
@@ -376,7 +376,7 @@ class _KeyHandlers:
|
|
|
376
376
|
_restore_term_attrs(self.fd, self.old)
|
|
377
377
|
sys.stdout.write("\033[?1000l\033[?1006l")
|
|
378
378
|
_ans = _prompt_text(
|
|
379
|
-
f"Write spoken words
|
|
379
|
+
f"Write spoken words in Whisper format to "
|
|
380
380
|
f"{os.path.basename(self.aux['jpath'])}? (y/N)")
|
|
381
381
|
_set_raw(self.fd)
|
|
382
382
|
sys.stdout.write("\033[?1000h\033[?1006h")
|
|
@@ -468,7 +468,7 @@ class _KeyHandlers:
|
|
|
468
468
|
_restore_term_attrs(self.fd, self.old)
|
|
469
469
|
sys.stdout.write("\033[?1000l\033[?1006l")
|
|
470
470
|
_prompt = ("Stage direction text:" if self.segs[self.cursor].get("kind") == "stage_dir"
|
|
471
|
-
else "Stage direction
|
|
471
|
+
else "Stage direction, or blank for silence:")
|
|
472
472
|
_new_lbl = _prompt_text(_prompt, default=_cur_lbl)
|
|
473
473
|
_set_raw(self.fd)
|
|
474
474
|
sys.stdout.write("\033[?1000h\033[?1006h")
|
|
@@ -36,8 +36,8 @@ keys.define("lyrics", "Lyrics editor", [
|
|
|
36
36
|
("play", ("p",), "play / stop"),
|
|
37
37
|
("save", ("s",), "save the working copy"),
|
|
38
38
|
("undo", ("u",), "undo"),
|
|
39
|
-
("verify", ("V",), "verify against the markdown
|
|
40
|
-
("write", ("W",), "write the transcript file
|
|
39
|
+
("verify", ("V",), "verify against the markdown, for transcripts"),
|
|
40
|
+
("write", ("W",), "write the transcript file, for transcripts"),
|
|
41
41
|
("back", ("ESC",), "back"),
|
|
42
42
|
("quit", ("q",), "quit the app"),
|
|
43
43
|
])
|
|
@@ -48,7 +48,7 @@ keys.define("lyrics_lines", "Lyrics editor: lines", [
|
|
|
48
48
|
("move_up", ("J",), "move the line up"),
|
|
49
49
|
("move_down", ("K",), "move the line down"),
|
|
50
50
|
("split", ("/",), "split the line"),
|
|
51
|
-
("words", ("w",), "open the line's words
|
|
51
|
+
("words", ("w",), "open the line's words, for transcripts"),
|
|
52
52
|
("tap", ("t",), "tap sync"),
|
|
53
53
|
("audition", ("b",), "audition"),
|
|
54
54
|
("dead_air", ("a",), "add dead air"),
|
|
@@ -60,7 +60,7 @@ keys.define("lyrics_lines", "Lyrics editor: lines", [
|
|
|
60
60
|
("overlay", ("m",), "import or remove the markdown"),
|
|
61
61
|
("commit_dirs", ("M",), "commit stage directions"),
|
|
62
62
|
("credits", ("c",), "add credits"),
|
|
63
|
-
("speaker_split", ("S",), "split by speaker
|
|
63
|
+
("speaker_split", ("S",), "split by speaker, for transcripts"),
|
|
64
64
|
("review", ("R",), "review issues"),
|
|
65
65
|
("review_dirs", ("D",), "review directions"),
|
|
66
66
|
("review_long", ("L",), "review long lines"),
|
|
@@ -343,7 +343,7 @@ def _draw(segs, cursor, seg_cursor, mode, prev_mode, selected, viewport,
|
|
|
343
343
|
(K('lyrics_lines.label'), 'label'), (K('lyrics_lines.air_dir'), 'air↔dir'),
|
|
344
344
|
(K('lyrics_lines.fill_gaps'), 'fill gaps'), (K('lyrics_lines.overlay'), _md_label)]
|
|
345
345
|
if is_sdir:
|
|
346
|
-
pairs.append((K('lyrics_lines.kind'), 'kind
|
|
346
|
+
pairs.append((K('lyrics_lines.kind'), 'kind'))
|
|
347
347
|
if _has_sdir: pairs.append((K('lyrics_lines.commit_dirs'), 'commit stage dirs'))
|
|
348
348
|
pairs += [(K('lyrics_lines.credits'), 'credits'), (K('lyrics_lines.mark'), 'mark')]
|
|
349
349
|
if _HAS_VLC: pairs.append((K('lyrics.play'), 'preview'))
|
|
@@ -491,7 +491,8 @@ def _draw_tap(B, avail, cursor, hit_map, indent, mode, n, out, play_pos, playing
|
|
|
491
491
|
if total_s > 0:
|
|
492
492
|
pct = min(play_pos / total_s, 1.0)
|
|
493
493
|
_clock = f"{timefmt.clock(play_pos)} / {timefmt.clock(total_s)}"
|
|
494
|
-
|
|
494
|
+
caps = not _promptmod.box_fits()
|
|
495
|
+
bar = ui.get_progress_bar(pct, max(4, B - len(_clock) - 2 - ui.progress_caps_width(caps)), caps=caps) # 2: gap before the clock
|
|
495
496
|
out.append(f"{indent}{C.DIM}{bar} {_clock}{C.RESET}")
|
|
496
497
|
else:
|
|
497
498
|
out.append("")
|
|
@@ -563,11 +564,12 @@ def _draw_audition(B, aud_editing, aud_now, avail, cursor, edit, hit_map, indent
|
|
|
563
564
|
if total_s > 0:
|
|
564
565
|
pct = min(play_pos / total_s, 1.0)
|
|
565
566
|
_clock = f"{timefmt.clock(play_pos)} / {timefmt.clock(total_s)}"
|
|
566
|
-
|
|
567
|
-
|
|
567
|
+
caps = not _promptmod.box_fits()
|
|
568
|
+
_bar_w = max(4, B - len(_clock) - 2 - ui.progress_caps_width(caps)) # 2: gap before the clock
|
|
569
|
+
bar = ui.get_progress_bar(pct, _bar_w, caps=caps)
|
|
568
570
|
out.append(f"{indent}{C.DIM}{bar} {_clock}{C.RESET}")
|
|
569
|
-
# Clickable: the bar's first cell sits
|
|
570
|
-
prog_geo = (len(out) - 1, ui.MARGIN_H +
|
|
571
|
+
# Clickable: the bar's first cell sits just past its left cap.
|
|
572
|
+
prog_geo = (len(out) - 1, ui.MARGIN_H + 1 + len(ui.progress_caps(caps)[0]), _bar_w)
|
|
571
573
|
else:
|
|
572
574
|
out.append("")
|
|
573
575
|
|
|
@@ -177,6 +177,10 @@ def _wire_playback() -> None:
|
|
|
177
177
|
a.next()
|
|
178
178
|
elif action == 'prev':
|
|
179
179
|
a.prev()
|
|
180
|
+
elif action in ('vol_up', 'vol_down'):
|
|
181
|
+
a.set_volume(max(0, min(100, a.get_volume() + (5 if action == 'vol_up' else -5))))
|
|
182
|
+
from backtrack.playback.player_ui import volume_changed
|
|
183
|
+
volume_changed() # the level, for a moment, over whatever's showing
|
|
180
184
|
elif action == 'time':
|
|
181
185
|
from backtrack.playback.player_ui import toggle_chapter_time
|
|
182
186
|
toggle_chapter_time()
|
|
@@ -12,9 +12,10 @@ def main_menu(library_ref: list, on_start=None) -> None:
|
|
|
12
12
|
"""Run the app as tabs, starting on Browse, until q (or Settings → Exit).
|
|
13
13
|
`on_start()` runs first, on Browse (the offer to resume), so what it opens
|
|
14
14
|
has the tabs too."""
|
|
15
|
-
from backtrack.playback.player import NOW_PLAYING_TAB
|
|
15
|
+
from backtrack.playback.player import NOW_PLAYING_TAB, cramped_view
|
|
16
16
|
from backtrack.menus.command_line import command_line
|
|
17
|
-
prompt.set_command_line(lambda: command_line(library_ref))
|
|
17
|
+
prompt.set_command_line(lambda around=None: command_line(library_ref, around))
|
|
18
|
+
prompt.core.set_cramped_view(cramped_view)
|
|
18
19
|
pending = [on_start]
|
|
19
20
|
|
|
20
21
|
def browse() -> None:
|
|
@@ -63,7 +63,7 @@ def activity_centre() -> None:
|
|
|
63
63
|
key = ''
|
|
64
64
|
if key == '\x1b' or keys.action(key, 'list') in ('list.back', 'list.quit'):
|
|
65
65
|
break
|
|
66
|
-
|
|
66
|
+
pc._wait_for_keypress(0.08) # the shared tick: the miniplayer when too small
|
|
67
67
|
finally:
|
|
68
68
|
sys.stdout.write("\033[?1000l\033[?1006l") # disable mouse
|
|
69
69
|
sys.stdout.flush()
|