ttyplayer 0.3.0__tar.gz → 0.4.0__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.
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/PKG-INFO +58 -7
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/README.md +57 -6
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/pyproject.toml +1 -1
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/pyproject.toml.orig +1 -1
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/src/ttyplayer/cli.py +168 -4
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/src/ttyplayer/favorites.py +5 -22
- ttyplayer-0.4.0/src/ttyplayer/history.py +38 -0
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/src/ttyplayer/player.py +11 -0
- ttyplayer-0.4.0/src/ttyplayer/playlists.py +99 -0
- ttyplayer-0.4.0/src/ttyplayer/settings.py +136 -0
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/src/ttyplayer/tui.py +383 -25
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/src/ttyplayer/tui.tcss +49 -0
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/src/ttyplayer/utils.py +39 -0
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/src/ttyplayer/youtube.py +7 -2
- ttyplayer-0.3.0/src/ttyplayer/history.py +0 -64
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/LICENSE +0 -0
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/src/ttyplayer/__init__.py +0 -0
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/src/ttyplayer/control.py +0 -0
- {ttyplayer-0.3.0 → ttyplayer-0.4.0}/src/ttyplayer/models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ttyplayer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: ttyplayer: a modern YouTube player for the terminal — search, queue, and play audio or video via mpv
|
|
5
5
|
Keywords: youtube,music,player,mpv,cli,terminal,tui,yt-dlp
|
|
6
6
|
License-Expression: MIT
|
|
@@ -43,7 +43,9 @@ ttyplayer favorites favorites, newest first
|
|
|
43
43
|
ttyplayer favorites --play pick from favorites and play them
|
|
44
44
|
ttyplayer favorites --remove 2 drop the second favorite as listed
|
|
45
45
|
ttyplayer favorites --clear forget all favorites
|
|
46
|
+
ttyplayer playlist ... your own named playlists (see Playlists below)
|
|
46
47
|
ttyplayer tui [--video] full-screen: search box, results list, now-playing bar
|
|
48
|
+
ttyplayer config list the settings (see Settings below)
|
|
47
49
|
ttyplayer doctor check Python, yt-dlp, mpv and ttyplayer's folders
|
|
48
50
|
ttyplayer version
|
|
49
51
|
```
|
|
@@ -58,13 +60,32 @@ ttyplayer stop quit the player
|
|
|
58
60
|
ttyplayer status 1:23 / 4:56 Playing <title>
|
|
59
61
|
```
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
## Playlists
|
|
64
|
+
|
|
65
|
+
Playlists are named lists you keep, in the order you choose, separate from history and favorites. A name is 1 to 40 letters, digits, spaces, `_` or `-`.
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
ttyplayer playlist list every playlist and how many videos it holds
|
|
69
|
+
ttyplayer playlist show <name> its videos, numbered
|
|
70
|
+
ttyplayer playlist create <name> a new, empty playlist
|
|
71
|
+
ttyplayer playlist add <name> <url | words...> append a link, every entry of a playlist link, or search picks
|
|
72
|
+
ttyplayer playlist remove <name> 3 drop the third video
|
|
73
|
+
ttyplayer playlist move <name> 3 1 move the third video to the top
|
|
74
|
+
ttyplayer playlist delete <name> [--yes] delete it (asks first unless --yes)
|
|
75
|
+
ttyplayer playlist play <name> [--video] [--shuffle] queue the whole playlist, in order or shuffled
|
|
76
|
+
ttyplayer playlist import <playlist url> [--as <name>] save a YouTube playlist, named after its title
|
|
77
|
+
ttyplayer playlist save-queue <name> save what the playing ttyplayer has queued, replacing <name>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`save-queue` works from another terminal while ttyplayer plays, like `pause` and `status`; it creates the playlist if there is none by that name. `import` refuses a name that is already taken; give another with `--as`.
|
|
81
|
+
|
|
82
|
+
Keys in `ttyplayer play` (the terminal player; `ttyplayer tui` has its own table below):
|
|
62
83
|
|
|
63
84
|
| Key | Action |
|
|
64
85
|
|---|---|
|
|
65
86
|
| space | pause / resume |
|
|
66
87
|
| left / right, `,` / `.` | seek 5 seconds |
|
|
67
|
-
| up / down | volume |
|
|
88
|
+
| up / down, `-` / `+` | volume |
|
|
68
89
|
| `n` / `p` | next / previous in the queue |
|
|
69
90
|
| `q` or Ctrl-C | quit, restores the terminal and stops mpv |
|
|
70
91
|
|
|
@@ -78,16 +99,19 @@ Picks accept several numbers at once: `1 3 5` queues those three in that order.
|
|
|
78
99
|
|
|
79
100
|
## TUI
|
|
80
101
|
|
|
81
|
-
`ttyplayer tui` opens a full-screen player: a search box, Search / Queue / History / Favorites tabs, and a now-playing panel. Type a search or paste a link and press Enter. Ctrl-P opens the command palette (search, next theme, help, quit, pause, next, previous, mute, and Textual's own theme picker); `?` lists every key and command.
|
|
102
|
+
`ttyplayer tui` opens a full-screen player: a search box, Search / Queue / History / Favorites / Playlists tabs, and a now-playing panel. Type a search or paste a link and press Enter. Ctrl-P opens the command palette (search, playlists, save queue as playlist, next theme, settings, help, quit, pause, next, previous, mute, and Textual's own theme picker); `?` lists every key and command.
|
|
82
103
|
|
|
83
104
|
| Where | Key | Action |
|
|
84
105
|
|---|---|---|
|
|
106
|
+
| table | `↑` / `↓` | move through the list (volume is `-` / `+`) |
|
|
85
107
|
| anywhere | `/` | focus the search box (`esc` returns to the table) |
|
|
86
108
|
| anywhere | `?` | help: every key and command (`esc` closes) |
|
|
87
|
-
| anywhere | `1` `2` `3` `4` | Search / Queue / History / Favorites tab |
|
|
109
|
+
| anywhere | `1` `2` `3` `4` `5` | Search / Queue / History / Favorites / Playlists tab |
|
|
88
110
|
| anywhere | Ctrl-C | quit and stop mpv |
|
|
89
111
|
| anywhere | Ctrl-P | command palette |
|
|
90
|
-
| anywhere | `t` | next theme |
|
|
112
|
+
| anywhere | `t` | next theme (remembered for next time) |
|
|
113
|
+
| table | `P` | save the queue as a playlist (asks for a name; an existing playlist of that name is replaced) |
|
|
114
|
+
| table | `S` | settings: Enter flips a true / false one, `esc` closes |
|
|
91
115
|
| table | `q` | quit and stop mpv (in the search box it is just a letter) |
|
|
92
116
|
| table | `space` | pause / resume |
|
|
93
117
|
| table | `n` / `p` | next / previous |
|
|
@@ -98,15 +122,42 @@ Picks accept several numbers at once: `1 3 5` queues those three in that order.
|
|
|
98
122
|
| table | `f` | favorite / unfavorite this row (the track playing when there is no row) |
|
|
99
123
|
| Search · History · Favorites row | Enter | play this one, then the rows after it |
|
|
100
124
|
| Search · History · Favorites row | `a` | add to the queue |
|
|
125
|
+
| Search · History · Favorites · Queue row | `A` | add to a playlist (pick one, or New playlist…) |
|
|
101
126
|
| Search | `m` | more results |
|
|
102
127
|
| Queue row | Enter | jump to this item |
|
|
103
128
|
| Queue row | `d` | remove from the queue |
|
|
104
129
|
| Queue row | `K` / `J` (or `shift+↑` / `shift+↓`) | move up / down |
|
|
105
130
|
| Queue | `c` | clear the queue (keeps the current track playing) |
|
|
106
131
|
| Favorites row | `d` | remove from favorites |
|
|
132
|
+
| Playlists | Enter | open the playlist (`esc` or Backspace goes back to the list) |
|
|
133
|
+
| Playlists | `d` | delete the playlist (asks first: `y` or Enter deletes, `esc` keeps it) |
|
|
134
|
+
| playlist track | Enter | play the whole playlist from this track |
|
|
135
|
+
| playlist track | `a` | add to the queue |
|
|
136
|
+
| playlist track | `d` | remove from the playlist |
|
|
137
|
+
| playlist track | `K` / `J` (or `shift+↑` / `shift+↓`) | move up / down in the playlist |
|
|
138
|
+
| open playlist | `s` | shuffle-play the playlist |
|
|
107
139
|
|
|
108
140
|
In the search box, letters, digits, `/` and `?` are typed as text; `esc` leaves it for the table.
|
|
109
141
|
|
|
142
|
+
## Settings
|
|
143
|
+
|
|
144
|
+
ttyplayer keeps its preferences in `~/.config/ttyplayer/settings.toml` (`$XDG_CONFIG_HOME/ttyplayer/` when that is set, `%APPDATA%\ttyplayer\` on Windows). Every key is optional; a missing file means the defaults.
|
|
145
|
+
|
|
146
|
+
| Key | Default | What it does |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `show_clock` | `true` | the clock in the TUI's header |
|
|
149
|
+
| `theme` | `textual-dark` | the TUI's color theme; `t` in the TUI picks the next one and saves it |
|
|
150
|
+
| `search_limit` | `10` | how many results a TUI search fetches, and `m` adds (1–50) |
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
ttyplayer config every setting, (default) when unchanged
|
|
154
|
+
ttyplayer config get <key> one setting's value
|
|
155
|
+
ttyplayer config set <key> <value> change it: ttyplayer config set show_clock false
|
|
156
|
+
ttyplayer config path where the file is
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
In the TUI, `S` (or Settings… in Ctrl-P) lists the settings: Enter on a true / false one flips it and saves it (the clock shows or hides at once); the others are set with `ttyplayer config set`.
|
|
160
|
+
|
|
110
161
|
## Install
|
|
111
162
|
|
|
112
163
|
One command installs everything ttyplayer needs.
|
|
@@ -199,7 +250,7 @@ uv run pytest -q # default suite, live test
|
|
|
199
250
|
TTYPLAYER_LIVE=1 uv run pytest -q tests/test_live.py # live tests, needs network
|
|
200
251
|
```
|
|
201
252
|
|
|
202
|
-
History lives in `$XDG_DATA_HOME/ttyplayer/history.jsonl`, by default `~/.local/share/ttyplayer/history.jsonl` (`%LOCALAPPDATA%\ttyplayer\history.jsonl` on Windows). Favorites live next to it in `favorites.jsonl`. History and favorites kept under the player's earlier name are moved here on the first run.
|
|
253
|
+
History lives in `$XDG_DATA_HOME/ttyplayer/history.jsonl`, by default `~/.local/share/ttyplayer/history.jsonl` (`%LOCALAPPDATA%\ttyplayer\history.jsonl` on Windows). Favorites live next to it in `favorites.jsonl`, and each playlist in `playlists/<name>.jsonl`. History and favorites kept under the player's earlier name are moved here on the first run.
|
|
203
254
|
|
|
204
255
|
### Releasing
|
|
205
256
|
|
|
@@ -20,7 +20,9 @@ ttyplayer favorites favorites, newest first
|
|
|
20
20
|
ttyplayer favorites --play pick from favorites and play them
|
|
21
21
|
ttyplayer favorites --remove 2 drop the second favorite as listed
|
|
22
22
|
ttyplayer favorites --clear forget all favorites
|
|
23
|
+
ttyplayer playlist ... your own named playlists (see Playlists below)
|
|
23
24
|
ttyplayer tui [--video] full-screen: search box, results list, now-playing bar
|
|
25
|
+
ttyplayer config list the settings (see Settings below)
|
|
24
26
|
ttyplayer doctor check Python, yt-dlp, mpv and ttyplayer's folders
|
|
25
27
|
ttyplayer version
|
|
26
28
|
```
|
|
@@ -35,13 +37,32 @@ ttyplayer stop quit the player
|
|
|
35
37
|
ttyplayer status 1:23 / 4:56 Playing <title>
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
## Playlists
|
|
41
|
+
|
|
42
|
+
Playlists are named lists you keep, in the order you choose, separate from history and favorites. A name is 1 to 40 letters, digits, spaces, `_` or `-`.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
ttyplayer playlist list every playlist and how many videos it holds
|
|
46
|
+
ttyplayer playlist show <name> its videos, numbered
|
|
47
|
+
ttyplayer playlist create <name> a new, empty playlist
|
|
48
|
+
ttyplayer playlist add <name> <url | words...> append a link, every entry of a playlist link, or search picks
|
|
49
|
+
ttyplayer playlist remove <name> 3 drop the third video
|
|
50
|
+
ttyplayer playlist move <name> 3 1 move the third video to the top
|
|
51
|
+
ttyplayer playlist delete <name> [--yes] delete it (asks first unless --yes)
|
|
52
|
+
ttyplayer playlist play <name> [--video] [--shuffle] queue the whole playlist, in order or shuffled
|
|
53
|
+
ttyplayer playlist import <playlist url> [--as <name>] save a YouTube playlist, named after its title
|
|
54
|
+
ttyplayer playlist save-queue <name> save what the playing ttyplayer has queued, replacing <name>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`save-queue` works from another terminal while ttyplayer plays, like `pause` and `status`; it creates the playlist if there is none by that name. `import` refuses a name that is already taken; give another with `--as`.
|
|
58
|
+
|
|
59
|
+
Keys in `ttyplayer play` (the terminal player; `ttyplayer tui` has its own table below):
|
|
39
60
|
|
|
40
61
|
| Key | Action |
|
|
41
62
|
|---|---|
|
|
42
63
|
| space | pause / resume |
|
|
43
64
|
| left / right, `,` / `.` | seek 5 seconds |
|
|
44
|
-
| up / down | volume |
|
|
65
|
+
| up / down, `-` / `+` | volume |
|
|
45
66
|
| `n` / `p` | next / previous in the queue |
|
|
46
67
|
| `q` or Ctrl-C | quit, restores the terminal and stops mpv |
|
|
47
68
|
|
|
@@ -55,16 +76,19 @@ Picks accept several numbers at once: `1 3 5` queues those three in that order.
|
|
|
55
76
|
|
|
56
77
|
## TUI
|
|
57
78
|
|
|
58
|
-
`ttyplayer tui` opens a full-screen player: a search box, Search / Queue / History / Favorites tabs, and a now-playing panel. Type a search or paste a link and press Enter. Ctrl-P opens the command palette (search, next theme, help, quit, pause, next, previous, mute, and Textual's own theme picker); `?` lists every key and command.
|
|
79
|
+
`ttyplayer tui` opens a full-screen player: a search box, Search / Queue / History / Favorites / Playlists tabs, and a now-playing panel. Type a search or paste a link and press Enter. Ctrl-P opens the command palette (search, playlists, save queue as playlist, next theme, settings, help, quit, pause, next, previous, mute, and Textual's own theme picker); `?` lists every key and command.
|
|
59
80
|
|
|
60
81
|
| Where | Key | Action |
|
|
61
82
|
|---|---|---|
|
|
83
|
+
| table | `↑` / `↓` | move through the list (volume is `-` / `+`) |
|
|
62
84
|
| anywhere | `/` | focus the search box (`esc` returns to the table) |
|
|
63
85
|
| anywhere | `?` | help: every key and command (`esc` closes) |
|
|
64
|
-
| anywhere | `1` `2` `3` `4` | Search / Queue / History / Favorites tab |
|
|
86
|
+
| anywhere | `1` `2` `3` `4` `5` | Search / Queue / History / Favorites / Playlists tab |
|
|
65
87
|
| anywhere | Ctrl-C | quit and stop mpv |
|
|
66
88
|
| anywhere | Ctrl-P | command palette |
|
|
67
|
-
| anywhere | `t` | next theme |
|
|
89
|
+
| anywhere | `t` | next theme (remembered for next time) |
|
|
90
|
+
| table | `P` | save the queue as a playlist (asks for a name; an existing playlist of that name is replaced) |
|
|
91
|
+
| table | `S` | settings: Enter flips a true / false one, `esc` closes |
|
|
68
92
|
| table | `q` | quit and stop mpv (in the search box it is just a letter) |
|
|
69
93
|
| table | `space` | pause / resume |
|
|
70
94
|
| table | `n` / `p` | next / previous |
|
|
@@ -75,15 +99,42 @@ Picks accept several numbers at once: `1 3 5` queues those three in that order.
|
|
|
75
99
|
| table | `f` | favorite / unfavorite this row (the track playing when there is no row) |
|
|
76
100
|
| Search · History · Favorites row | Enter | play this one, then the rows after it |
|
|
77
101
|
| Search · History · Favorites row | `a` | add to the queue |
|
|
102
|
+
| Search · History · Favorites · Queue row | `A` | add to a playlist (pick one, or New playlist…) |
|
|
78
103
|
| Search | `m` | more results |
|
|
79
104
|
| Queue row | Enter | jump to this item |
|
|
80
105
|
| Queue row | `d` | remove from the queue |
|
|
81
106
|
| Queue row | `K` / `J` (or `shift+↑` / `shift+↓`) | move up / down |
|
|
82
107
|
| Queue | `c` | clear the queue (keeps the current track playing) |
|
|
83
108
|
| Favorites row | `d` | remove from favorites |
|
|
109
|
+
| Playlists | Enter | open the playlist (`esc` or Backspace goes back to the list) |
|
|
110
|
+
| Playlists | `d` | delete the playlist (asks first: `y` or Enter deletes, `esc` keeps it) |
|
|
111
|
+
| playlist track | Enter | play the whole playlist from this track |
|
|
112
|
+
| playlist track | `a` | add to the queue |
|
|
113
|
+
| playlist track | `d` | remove from the playlist |
|
|
114
|
+
| playlist track | `K` / `J` (or `shift+↑` / `shift+↓`) | move up / down in the playlist |
|
|
115
|
+
| open playlist | `s` | shuffle-play the playlist |
|
|
84
116
|
|
|
85
117
|
In the search box, letters, digits, `/` and `?` are typed as text; `esc` leaves it for the table.
|
|
86
118
|
|
|
119
|
+
## Settings
|
|
120
|
+
|
|
121
|
+
ttyplayer keeps its preferences in `~/.config/ttyplayer/settings.toml` (`$XDG_CONFIG_HOME/ttyplayer/` when that is set, `%APPDATA%\ttyplayer\` on Windows). Every key is optional; a missing file means the defaults.
|
|
122
|
+
|
|
123
|
+
| Key | Default | What it does |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `show_clock` | `true` | the clock in the TUI's header |
|
|
126
|
+
| `theme` | `textual-dark` | the TUI's color theme; `t` in the TUI picks the next one and saves it |
|
|
127
|
+
| `search_limit` | `10` | how many results a TUI search fetches, and `m` adds (1–50) |
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
ttyplayer config every setting, (default) when unchanged
|
|
131
|
+
ttyplayer config get <key> one setting's value
|
|
132
|
+
ttyplayer config set <key> <value> change it: ttyplayer config set show_clock false
|
|
133
|
+
ttyplayer config path where the file is
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
In the TUI, `S` (or Settings… in Ctrl-P) lists the settings: Enter on a true / false one flips it and saves it (the clock shows or hides at once); the others are set with `ttyplayer config set`.
|
|
137
|
+
|
|
87
138
|
## Install
|
|
88
139
|
|
|
89
140
|
One command installs everything ttyplayer needs.
|
|
@@ -176,7 +227,7 @@ uv run pytest -q # default suite, live test
|
|
|
176
227
|
TTYPLAYER_LIVE=1 uv run pytest -q tests/test_live.py # live tests, needs network
|
|
177
228
|
```
|
|
178
229
|
|
|
179
|
-
History lives in `$XDG_DATA_HOME/ttyplayer/history.jsonl`, by default `~/.local/share/ttyplayer/history.jsonl` (`%LOCALAPPDATA%\ttyplayer\history.jsonl` on Windows). Favorites live next to it in `favorites.jsonl`. History and favorites kept under the player's earlier name are moved here on the first run.
|
|
230
|
+
History lives in `$XDG_DATA_HOME/ttyplayer/history.jsonl`, by default `~/.local/share/ttyplayer/history.jsonl` (`%LOCALAPPDATA%\ttyplayer\history.jsonl` on Windows). Favorites live next to it in `favorites.jsonl`, and each playlist in `playlists/<name>.jsonl`. History and favorites kept under the player's earlier name are moved here on the first run.
|
|
180
231
|
|
|
181
232
|
### Releasing
|
|
182
233
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import random
|
|
2
3
|
import shutil
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
@@ -8,10 +9,16 @@ from importlib import metadata
|
|
|
8
9
|
|
|
9
10
|
import typer
|
|
10
11
|
|
|
11
|
-
from ttyplayer import control, favorites, history, player
|
|
12
|
-
from ttyplayer.utils import APP_NAME, data_path, format_time, parse_picks, unseen
|
|
12
|
+
from ttyplayer import control, favorites, history, player, playlists, settings
|
|
13
|
+
from ttyplayer.utils import APP_NAME, data_path, format_time, parse_picks, unseen, video_from_info
|
|
13
14
|
|
|
14
15
|
app = typer.Typer()
|
|
16
|
+
config_app = typer.Typer()
|
|
17
|
+
app.add_typer(config_app, name="config")
|
|
18
|
+
playlist_app = typer.Typer()
|
|
19
|
+
app.add_typer(playlist_app, name="playlist")
|
|
20
|
+
|
|
21
|
+
shuffler = random.Random() # playlist play --shuffle; tests swap in a seeded one
|
|
15
22
|
|
|
16
23
|
# How to install mpv, per sys.platform prefix; the README's Install section and the
|
|
17
24
|
# install.sh / install.ps1 scripts repeat these, and tests keep all three in step.
|
|
@@ -118,7 +125,7 @@ def check_control_dir():
|
|
|
118
125
|
def play(target: list[str], video: bool = False, limit: int = 5):
|
|
119
126
|
"""Play a YouTube link or playlist, or search and pick what to play.
|
|
120
127
|
|
|
121
|
-
Keys while playing: space pause, left/right or , . seek, up/down volume,
|
|
128
|
+
Keys while playing: space pause, left/right or , . seek, up/down or - + volume,
|
|
122
129
|
n next, p previous, q quit.
|
|
123
130
|
"""
|
|
124
131
|
start_playback(resolve(target, limit), video)
|
|
@@ -194,7 +201,164 @@ def tui(video: bool = False):
|
|
|
194
201
|
"""
|
|
195
202
|
from ttyplayer import tui as screen # Textual loads only for this command
|
|
196
203
|
|
|
197
|
-
screen.TtyplayerApp(
|
|
204
|
+
screen.TtyplayerApp(
|
|
205
|
+
client_factory=player.MpvClient, resolve=screen.resolve, video=video, settings=load_settings()
|
|
206
|
+
).run()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@config_app.callback(invoke_without_command=True)
|
|
210
|
+
def config(context: typer.Context):
|
|
211
|
+
"""List the settings, or get, set or locate them"""
|
|
212
|
+
if context.invoked_subcommand is not None:
|
|
213
|
+
return
|
|
214
|
+
current = load_settings()
|
|
215
|
+
for key in settings.KEYS:
|
|
216
|
+
value = getattr(current, key)
|
|
217
|
+
default = " (default)" if value == getattr(settings.DEFAULTS, key) else ""
|
|
218
|
+
typer.echo(f"{key} = {settings.display(value)}{default}")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@config_app.command(name="get")
|
|
222
|
+
def config_get(key: str):
|
|
223
|
+
"""Print one setting's value"""
|
|
224
|
+
try:
|
|
225
|
+
settings.check_key(key)
|
|
226
|
+
except settings.SettingsError as error:
|
|
227
|
+
fail(str(error))
|
|
228
|
+
typer.echo(settings.display(getattr(load_settings(), key)))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@config_app.command(name="set")
|
|
232
|
+
def config_set(key: str, value: str):
|
|
233
|
+
"""Change one setting and save it"""
|
|
234
|
+
try:
|
|
235
|
+
changed = settings.update(key, value)
|
|
236
|
+
except settings.SettingsError as error:
|
|
237
|
+
fail(str(error))
|
|
238
|
+
typer.echo(f"{key} = {settings.display(getattr(changed, key))}")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@config_app.command(name="path")
|
|
242
|
+
def config_path():
|
|
243
|
+
"""Print where the settings file lives"""
|
|
244
|
+
typer.echo(settings.settings_path())
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def load_settings():
|
|
248
|
+
"""The saved settings, or a one-line message and exit 1 when the file is broken."""
|
|
249
|
+
try:
|
|
250
|
+
return settings.load()
|
|
251
|
+
except settings.SettingsError as error:
|
|
252
|
+
fail(str(error))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@playlist_app.callback()
|
|
256
|
+
def playlist():
|
|
257
|
+
"""Make, edit and play your own playlists"""
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@playlist_app.command(name="list")
|
|
261
|
+
def playlist_list():
|
|
262
|
+
"""List the playlists and how many videos each holds"""
|
|
263
|
+
names = playlists.names()
|
|
264
|
+
if not names:
|
|
265
|
+
fail("No playlists yet")
|
|
266
|
+
for name in names:
|
|
267
|
+
typer.echo(f"{name} ({len(on_playlist(playlists.load, name))} videos)")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@playlist_app.command(name="show")
|
|
271
|
+
def playlist_show(name: str):
|
|
272
|
+
"""List a playlist's videos, numbered"""
|
|
273
|
+
videos = on_playlist(playlists.load, name)
|
|
274
|
+
if not videos:
|
|
275
|
+
typer.echo(f"{name} is empty")
|
|
276
|
+
print_videos(videos)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@playlist_app.command(name="create")
|
|
280
|
+
def playlist_create(name: str):
|
|
281
|
+
"""Make a new, empty playlist"""
|
|
282
|
+
on_playlist(playlists.create, name)
|
|
283
|
+
typer.echo(f"Created playlist {name}")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@playlist_app.command(name="add")
|
|
287
|
+
def playlist_add(name: str, target: list[str], limit: int = 5):
|
|
288
|
+
"""Add a YouTube link or playlist, or search picks, to the end of a playlist"""
|
|
289
|
+
on_playlist(playlists.require, name)
|
|
290
|
+
count = on_playlist(playlists.add, name, resolve(target, limit))
|
|
291
|
+
typer.echo(f"Added {count} videos to {name}")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@playlist_app.command(name="remove")
|
|
295
|
+
def playlist_remove(name: str, number: int):
|
|
296
|
+
"""Drop the video at this position"""
|
|
297
|
+
removed = on_playlist(playlists.remove, name, number)
|
|
298
|
+
typer.echo(f"Removed: {removed.title}")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@playlist_app.command(name="move")
|
|
302
|
+
def playlist_move(name: str, source: int, target: int):
|
|
303
|
+
"""Move the video at one position to another"""
|
|
304
|
+
moved = on_playlist(playlists.move, name, source, target)
|
|
305
|
+
typer.echo(f"Moved to {target}: {moved.title}")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@playlist_app.command(name="delete")
|
|
309
|
+
def playlist_delete(name: str, yes: bool = typer.Option(False, "--yes", help="Do not ask first")):
|
|
310
|
+
"""Delete a playlist, after asking"""
|
|
311
|
+
on_playlist(playlists.require, name)
|
|
312
|
+
if not yes and not typer.confirm(f"Delete playlist {name}?", default=False):
|
|
313
|
+
typer.echo(f"Kept {name}")
|
|
314
|
+
return
|
|
315
|
+
on_playlist(playlists.delete, name)
|
|
316
|
+
typer.echo(f"Deleted playlist {name}")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@playlist_app.command(name="play")
|
|
320
|
+
def playlist_play(name: str, video: bool = False, shuffle: bool = False):
|
|
321
|
+
"""Play a whole playlist, in order or shuffled"""
|
|
322
|
+
videos = on_playlist(playlists.load, name)
|
|
323
|
+
if shuffle:
|
|
324
|
+
shuffler.shuffle(videos)
|
|
325
|
+
start_playback(videos, video)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@playlist_app.command(name="import")
|
|
329
|
+
def playlist_import(url: str, name: str | None = typer.Option(None, "--as", help="Name it this, not its title")):
|
|
330
|
+
"""Save a YouTube playlist as a playlist of your own"""
|
|
331
|
+
from ttyplayer import youtube
|
|
332
|
+
|
|
333
|
+
title, videos = lookup(youtube.fetch_playlist, url)
|
|
334
|
+
if title is None:
|
|
335
|
+
fail(f"Not a playlist link: {url}")
|
|
336
|
+
name = name or playlists.sanitize(title)
|
|
337
|
+
if not name:
|
|
338
|
+
fail("The playlist title has nothing to name it by; name it with --as")
|
|
339
|
+
exit_if_empty(videos)
|
|
340
|
+
on_playlist(playlists.create, name)
|
|
341
|
+
on_playlist(playlists.add, name, videos)
|
|
342
|
+
typer.echo(f"Imported {len(videos)} videos as {name}")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@playlist_app.command(name="save-queue")
|
|
346
|
+
def playlist_save_queue(name: str):
|
|
347
|
+
"""Save the playing ttyplayer's queue as a playlist, replacing what it held"""
|
|
348
|
+
on_playlist(playlists.playlist_path, name)
|
|
349
|
+
videos = [video_from_info(entry) for entry in remote("queue")["videos"]]
|
|
350
|
+
if not videos:
|
|
351
|
+
fail("The queue is empty")
|
|
352
|
+
on_playlist(playlists.replace, name, videos)
|
|
353
|
+
typer.echo(f"Saved {len(videos)} videos to {name}")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def on_playlist(func, *args):
|
|
357
|
+
"""Run a playlists.* call, turning its errors into a one-line message and exit 1."""
|
|
358
|
+
try:
|
|
359
|
+
return func(*args)
|
|
360
|
+
except playlists.PlaylistError as error:
|
|
361
|
+
fail(str(error))
|
|
198
362
|
|
|
199
363
|
|
|
200
364
|
@app.command()
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
"""Videos the user chose to keep, one JSON object per line, newest at the bottom."""
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
|
-
from datetime import datetime, timezone
|
|
5
3
|
from pathlib import Path
|
|
6
4
|
|
|
7
5
|
from ttyplayer.models import Video
|
|
8
|
-
from ttyplayer.utils import data_path, video_from_info
|
|
6
|
+
from ttyplayer.utils import append_entries, data_path, read_entries, video_entry, video_from_info, write_entries
|
|
9
7
|
|
|
10
8
|
|
|
11
9
|
def favorites_path() -> Path:
|
|
@@ -17,16 +15,7 @@ def add(video: Video, path: Path | None = None) -> bool:
|
|
|
17
15
|
path = path or favorites_path()
|
|
18
16
|
if any(entry["id"] == video.id for entry in _entries(path)):
|
|
19
17
|
return False
|
|
20
|
-
path
|
|
21
|
-
entry = {
|
|
22
|
-
"id": video.id,
|
|
23
|
-
"title": video.title,
|
|
24
|
-
"uploader": video.uploader,
|
|
25
|
-
"duration": video.duration,
|
|
26
|
-
"favorited_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
27
|
-
}
|
|
28
|
-
with path.open("a", encoding="utf-8") as f:
|
|
29
|
-
f.write(json.dumps(entry) + "\n")
|
|
18
|
+
append_entries(path, [video_entry(video, "favorited_at")])
|
|
30
19
|
return True
|
|
31
20
|
|
|
32
21
|
|
|
@@ -48,7 +37,7 @@ def remove_id(video_id: str, path: Path | None = None) -> bool:
|
|
|
48
37
|
kept = [entry for entry in reversed(entries) if entry["id"] != video_id]
|
|
49
38
|
if len(kept) == len(entries):
|
|
50
39
|
return False
|
|
51
|
-
path
|
|
40
|
+
write_entries(path, kept)
|
|
52
41
|
return True
|
|
53
42
|
|
|
54
43
|
|
|
@@ -74,16 +63,10 @@ def load(path: Path | None = None, limit: int | None = None) -> list[Video]:
|
|
|
74
63
|
|
|
75
64
|
def _entries(path: Path) -> list[dict]:
|
|
76
65
|
"""Stored entries newest first, one per video id, skipping corrupt lines."""
|
|
77
|
-
if not path.exists():
|
|
78
|
-
return []
|
|
79
66
|
seen = set()
|
|
80
67
|
entries = []
|
|
81
|
-
for
|
|
82
|
-
|
|
83
|
-
entry = json.loads(line)
|
|
84
|
-
except json.JSONDecodeError:
|
|
85
|
-
continue
|
|
86
|
-
if not isinstance(entry, dict) or "id" not in entry or entry["id"] in seen:
|
|
68
|
+
for entry in reversed(read_entries(path)):
|
|
69
|
+
if entry["id"] in seen:
|
|
87
70
|
continue
|
|
88
71
|
seen.add(entry["id"])
|
|
89
72
|
entries.append(entry)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""What has been played, one JSON object per line, newest at the bottom."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ttyplayer.models import Video
|
|
6
|
+
from ttyplayer.utils import append_entries, data_path, read_entries, video_entry, video_from_info
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def history_path() -> Path:
|
|
10
|
+
return data_path("history.jsonl")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def record(video: Video, path: Path | None = None):
|
|
14
|
+
append_entries(path or history_path(), [video_entry(video, "played_at")])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def clear(path: Path | None = None) -> int:
|
|
18
|
+
"""Forget everything. Returns how many entries were there."""
|
|
19
|
+
path = path or history_path()
|
|
20
|
+
if not path.exists():
|
|
21
|
+
return 0
|
|
22
|
+
count = len(path.read_text(encoding="utf-8").splitlines())
|
|
23
|
+
path.unlink()
|
|
24
|
+
return count
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load(path: Path | None = None, limit: int = 20) -> list[Video]:
|
|
28
|
+
"""Most recently played first, each video once, at most `limit` of them."""
|
|
29
|
+
seen = set()
|
|
30
|
+
videos = []
|
|
31
|
+
for entry in reversed(read_entries(path or history_path())):
|
|
32
|
+
if entry["id"] in seen:
|
|
33
|
+
continue
|
|
34
|
+
seen.add(entry["id"])
|
|
35
|
+
videos.append(video_from_info(entry))
|
|
36
|
+
if len(videos) == limit:
|
|
37
|
+
break
|
|
38
|
+
return videos
|
|
@@ -11,6 +11,7 @@ import sys
|
|
|
11
11
|
import tempfile
|
|
12
12
|
import threading
|
|
13
13
|
import time
|
|
14
|
+
from dataclasses import asdict
|
|
14
15
|
|
|
15
16
|
from ttyplayer import control
|
|
16
17
|
from ttyplayer.models import Video
|
|
@@ -40,6 +41,9 @@ KEYS = {
|
|
|
40
41
|
"right": ("seek", SEEK_SECONDS),
|
|
41
42
|
"up": ("change_volume", VOLUME_STEP),
|
|
42
43
|
"down": ("change_volume", -VOLUME_STEP),
|
|
44
|
+
"+": ("change_volume", VOLUME_STEP),
|
|
45
|
+
"=": ("change_volume", VOLUME_STEP), # the + key without shift
|
|
46
|
+
"-": ("change_volume", -VOLUME_STEP),
|
|
43
47
|
"n": ("next",),
|
|
44
48
|
"p": ("prev",),
|
|
45
49
|
}
|
|
@@ -476,6 +480,8 @@ class MpvClient:
|
|
|
476
480
|
interrupt_main() # run()'s Ctrl-C path quits and restores the terminal
|
|
477
481
|
elif name == "status":
|
|
478
482
|
return control.ok(**self.status())
|
|
483
|
+
elif name == "queue":
|
|
484
|
+
return control.ok(**self.queue_listing())
|
|
479
485
|
else:
|
|
480
486
|
return control.failure(f"unknown command {name}")
|
|
481
487
|
return control.ok()
|
|
@@ -586,6 +592,11 @@ class MpvClient:
|
|
|
586
592
|
self.index = 0
|
|
587
593
|
return True
|
|
588
594
|
|
|
595
|
+
def queue_listing(self):
|
|
596
|
+
"""Every queued video and the 1-based index of the current one, as the control socket sends them."""
|
|
597
|
+
with self.queue_lock:
|
|
598
|
+
return {"videos": [asdict(video) for video in self.queue], "index": self.index + 1}
|
|
599
|
+
|
|
589
600
|
def current_title(self):
|
|
590
601
|
# The queue knows the title before mpv does, so no file-name flicker on load.
|
|
591
602
|
if self.queue:
|