hoopoe-player 0.1.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.
- hoopoe_player-0.1.0/LICENSE +21 -0
- hoopoe_player-0.1.0/PKG-INFO +108 -0
- hoopoe_player-0.1.0/README.md +78 -0
- hoopoe_player-0.1.0/hoopoe/__init__.py +3 -0
- hoopoe_player-0.1.0/hoopoe/main.py +286 -0
- hoopoe_player-0.1.0/hoopoe_player.egg-info/PKG-INFO +108 -0
- hoopoe_player-0.1.0/hoopoe_player.egg-info/SOURCES.txt +11 -0
- hoopoe_player-0.1.0/hoopoe_player.egg-info/dependency_links.txt +1 -0
- hoopoe_player-0.1.0/hoopoe_player.egg-info/entry_points.txt +2 -0
- hoopoe_player-0.1.0/hoopoe_player.egg-info/requires.txt +2 -0
- hoopoe_player-0.1.0/hoopoe_player.egg-info/top_level.txt +1 -0
- hoopoe_player-0.1.0/setup.cfg +4 -0
- hoopoe_player-0.1.0/setup.py +34 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adriel Molina
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hoopoe-player
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Play any video as colorful ASCII art in your terminal
|
|
5
|
+
Home-page: https://github.com/axol8002/hoopoe-player
|
|
6
|
+
Author: Adriel Molina
|
|
7
|
+
Author-email: adrielmolinacaceres@gmail.com
|
|
8
|
+
Keywords: ascii art terminal video youtube player cli hoopoe
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Topic :: Multimedia :: Video
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: yt-dlp
|
|
18
|
+
Requires-Dist: opencv-python
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: keywords
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
30
|
+
|
|
31
|
+
# hoopoe-player
|
|
32
|
+
|
|
33
|
+
> Play any video as colorful ASCII art directly in your terminal.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Most systems
|
|
39
|
+
pip install hoopoe-player
|
|
40
|
+
|
|
41
|
+
# Ubuntu/Debian
|
|
42
|
+
pipx install hoopoe-player
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You also need `ffmpeg` for audio:
|
|
46
|
+
```bash
|
|
47
|
+
sudo apt install ffmpeg # Ubuntu/Debian
|
|
48
|
+
sudo pacman -S ffmpeg # Arch
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Play a YouTube video
|
|
55
|
+
hoopoe https://www.youtube.com/watch?v=xxxxx
|
|
56
|
+
|
|
57
|
+
# Play a local file
|
|
58
|
+
hoopoe -l video.mp4
|
|
59
|
+
|
|
60
|
+
# Enable audio
|
|
61
|
+
hoopoe -s https://www.youtube.com/watch?v=xxxxx
|
|
62
|
+
|
|
63
|
+
# Change character mode
|
|
64
|
+
hoopoe -m blocks https://www.youtube.com/watch?v=xxxxx
|
|
65
|
+
|
|
66
|
+
# Show status bar (time, volume, controls)
|
|
67
|
+
hoopoe --hud https://www.youtube.com/watch?v=xxxxx
|
|
68
|
+
|
|
69
|
+
# Combine options
|
|
70
|
+
hoopoe -l -s -m invert --hud video.mp4
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Controls
|
|
74
|
+
|
|
75
|
+
| Key | Action |
|
|
76
|
+
|-----|--------|
|
|
77
|
+
| `Space` | Pause / Play |
|
|
78
|
+
| `←` / `→` | Seek −10s / +10s |
|
|
79
|
+
| `↑` / `↓` | Volume +10 / −10 (only with `-s`) |
|
|
80
|
+
| `Q` or `Ctrl+C` | Quit |
|
|
81
|
+
|
|
82
|
+
## Character modes
|
|
83
|
+
|
|
84
|
+
| Mode | Style |
|
|
85
|
+
|------|-------|
|
|
86
|
+
| `classic` | `. : - = + * # % @` — default, coloured |
|
|
87
|
+
| `blocks` | `░ ▒ ▓ █` — bold blocks, coloured |
|
|
88
|
+
| `braille` | `⠁ ⠃ ⠇ ⠿ ⣿` — dense dots, coloured |
|
|
89
|
+
| `minimal` | `· • ● ■` — clean and minimal, coloured |
|
|
90
|
+
| `invert` | colour as background — selection effect |
|
|
91
|
+
| `nocolor` | classic chars, no colour — for legacy terminals |
|
|
92
|
+
|
|
93
|
+
## Requirements
|
|
94
|
+
|
|
95
|
+
- Python 3.8+
|
|
96
|
+
- ffmpeg (optional, needed for `-s` audio)
|
|
97
|
+
- A terminal with true color support (for all modes except `nocolor`)
|
|
98
|
+
|
|
99
|
+
## Roadmap
|
|
100
|
+
|
|
101
|
+
- [ ] Fix audio/video sync — audio can drift ahead when rendering is slow
|
|
102
|
+
- [ ] Fix scaling when paused — terminal resize not applied until next frame
|
|
103
|
+
- [ ] Optimize rendering performance — reduce CPU usage per frame
|
|
104
|
+
- [ ] Screenshot to file — press a key to save the current frame as a colored text file (ANSI)
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# hoopoe-player
|
|
2
|
+
|
|
3
|
+
> Play any video as colorful ASCII art directly in your terminal.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Most systems
|
|
9
|
+
pip install hoopoe-player
|
|
10
|
+
|
|
11
|
+
# Ubuntu/Debian
|
|
12
|
+
pipx install hoopoe-player
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
You also need `ffmpeg` for audio:
|
|
16
|
+
```bash
|
|
17
|
+
sudo apt install ffmpeg # Ubuntu/Debian
|
|
18
|
+
sudo pacman -S ffmpeg # Arch
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Play a YouTube video
|
|
25
|
+
hoopoe https://www.youtube.com/watch?v=xxxxx
|
|
26
|
+
|
|
27
|
+
# Play a local file
|
|
28
|
+
hoopoe -l video.mp4
|
|
29
|
+
|
|
30
|
+
# Enable audio
|
|
31
|
+
hoopoe -s https://www.youtube.com/watch?v=xxxxx
|
|
32
|
+
|
|
33
|
+
# Change character mode
|
|
34
|
+
hoopoe -m blocks https://www.youtube.com/watch?v=xxxxx
|
|
35
|
+
|
|
36
|
+
# Show status bar (time, volume, controls)
|
|
37
|
+
hoopoe --hud https://www.youtube.com/watch?v=xxxxx
|
|
38
|
+
|
|
39
|
+
# Combine options
|
|
40
|
+
hoopoe -l -s -m invert --hud video.mp4
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Controls
|
|
44
|
+
|
|
45
|
+
| Key | Action |
|
|
46
|
+
|-----|--------|
|
|
47
|
+
| `Space` | Pause / Play |
|
|
48
|
+
| `←` / `→` | Seek −10s / +10s |
|
|
49
|
+
| `↑` / `↓` | Volume +10 / −10 (only with `-s`) |
|
|
50
|
+
| `Q` or `Ctrl+C` | Quit |
|
|
51
|
+
|
|
52
|
+
## Character modes
|
|
53
|
+
|
|
54
|
+
| Mode | Style |
|
|
55
|
+
|------|-------|
|
|
56
|
+
| `classic` | `. : - = + * # % @` — default, coloured |
|
|
57
|
+
| `blocks` | `░ ▒ ▓ █` — bold blocks, coloured |
|
|
58
|
+
| `braille` | `⠁ ⠃ ⠇ ⠿ ⣿` — dense dots, coloured |
|
|
59
|
+
| `minimal` | `· • ● ■` — clean and minimal, coloured |
|
|
60
|
+
| `invert` | colour as background — selection effect |
|
|
61
|
+
| `nocolor` | classic chars, no colour — for legacy terminals |
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- Python 3.8+
|
|
66
|
+
- ffmpeg (optional, needed for `-s` audio)
|
|
67
|
+
- A terminal with true color support (for all modes except `nocolor`)
|
|
68
|
+
|
|
69
|
+
## Roadmap
|
|
70
|
+
|
|
71
|
+
- [ ] Fix audio/video sync — audio can drift ahead when rendering is slow
|
|
72
|
+
- [ ] Fix scaling when paused — terminal resize not applied until next frame
|
|
73
|
+
- [ ] Optimize rendering performance — reduce CPU usage per frame
|
|
74
|
+
- [ ] Screenshot to file — press a key to save the current frame as a colored text file (ANSI)
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
hoopoe - Play videos in your terminal as colorful ASCII art
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import argparse
|
|
8
|
+
import time
|
|
9
|
+
import os
|
|
10
|
+
import tty
|
|
11
|
+
import termios
|
|
12
|
+
import threading
|
|
13
|
+
import subprocess
|
|
14
|
+
|
|
15
|
+
import cv2
|
|
16
|
+
|
|
17
|
+
CHAR_MODES = {
|
|
18
|
+
"classic": " .:-=+*#%@",
|
|
19
|
+
"blocks": " ░▒▓█",
|
|
20
|
+
"braille": " ⠁⠃⠇⠿⣿",
|
|
21
|
+
"minimal": " ·•●■",
|
|
22
|
+
"invert": "@%#*+=-:. ",
|
|
23
|
+
"nocolor": " .:-=+*#%@",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_terminal_size():
|
|
28
|
+
size = os.get_terminal_size()
|
|
29
|
+
return size.columns, size.lines
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def frame_to_lines(frame, width, height, mode):
|
|
33
|
+
"""Convert a frame to a list of strings, one per terminal row."""
|
|
34
|
+
chars = CHAR_MODES.get(mode, CHAR_MODES["classic"])
|
|
35
|
+
# Each character is ~2x taller than wide, so use width directly
|
|
36
|
+
frame_resized = cv2.resize(frame, (width, height))
|
|
37
|
+
gray = cv2.cvtColor(frame_resized, cv2.COLOR_BGR2GRAY)
|
|
38
|
+
|
|
39
|
+
lines = []
|
|
40
|
+
for y in range(height):
|
|
41
|
+
row = ""
|
|
42
|
+
for x in range(width):
|
|
43
|
+
brightness = gray[y, x]
|
|
44
|
+
char = chars[int(brightness / 255 * (len(chars) - 1))]
|
|
45
|
+
if mode == "nocolor":
|
|
46
|
+
row += char
|
|
47
|
+
elif mode == "invert":
|
|
48
|
+
b, g, r = frame_resized[y, x]
|
|
49
|
+
row += f"\033[48;2;{r};{g};{b}m\033[38;2;0;0;0m{char}\033[0m"
|
|
50
|
+
else:
|
|
51
|
+
b, g, r = frame_resized[y, x]
|
|
52
|
+
row += f"\033[38;2;{r};{g};{b}m{char}\033[0m"
|
|
53
|
+
lines.append(row)
|
|
54
|
+
return lines
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def render_frame(lines, hud_line=None):
|
|
58
|
+
"""
|
|
59
|
+
Paint the entire terminal in one write:
|
|
60
|
+
- Move to 1,1
|
|
61
|
+
- Print each line followed by \033[K (erase to end of line) and \r\n
|
|
62
|
+
- If hud_line, paint it on the last row
|
|
63
|
+
"""
|
|
64
|
+
buf = "\033[H" # cursor to top-left
|
|
65
|
+
for line in lines:
|
|
66
|
+
buf += line + "\033[K\r\n"
|
|
67
|
+
if hud_line is not None:
|
|
68
|
+
buf += "\033[7m" + hud_line + "\033[0m\033[K"
|
|
69
|
+
sys.stdout.write(buf)
|
|
70
|
+
sys.stdout.flush()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_video_capture(source, local=False):
|
|
74
|
+
if local:
|
|
75
|
+
if not os.path.exists(source):
|
|
76
|
+
print(f"File not found: {source}")
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
return cv2.VideoCapture(source), os.path.basename(source)
|
|
79
|
+
try:
|
|
80
|
+
import yt_dlp
|
|
81
|
+
except ImportError:
|
|
82
|
+
print("yt-dlp not installed. Run: pip install yt-dlp")
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
print("hoopoe-player - fetching video info...")
|
|
85
|
+
ydl_opts = {"format": "best[height<=480]", "quiet": True}
|
|
86
|
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
87
|
+
info = ydl.extract_info(source, download=False)
|
|
88
|
+
return cv2.VideoCapture(info["url"]), info.get("title", "Unknown")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_audio_url(source, local=False):
|
|
92
|
+
if local:
|
|
93
|
+
return source
|
|
94
|
+
try:
|
|
95
|
+
import yt_dlp
|
|
96
|
+
with yt_dlp.YoutubeDL({"format": "bestaudio", "quiet": True}) as ydl:
|
|
97
|
+
return ydl.extract_info(source, download=False)["url"]
|
|
98
|
+
except Exception:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AudioPlayer:
|
|
103
|
+
def __init__(self, url):
|
|
104
|
+
self.url = url
|
|
105
|
+
self.volume = 50
|
|
106
|
+
self._proc = None
|
|
107
|
+
|
|
108
|
+
def start(self, offset=0):
|
|
109
|
+
self._kill()
|
|
110
|
+
self._proc = subprocess.Popen(
|
|
111
|
+
["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet",
|
|
112
|
+
"-volume", str(self.volume), "-ss", str(int(offset)), self.url],
|
|
113
|
+
stdin=subprocess.DEVNULL
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _kill(self):
|
|
117
|
+
if self._proc:
|
|
118
|
+
self._proc.terminate()
|
|
119
|
+
self._proc = None
|
|
120
|
+
|
|
121
|
+
def stop(self):
|
|
122
|
+
self._kill()
|
|
123
|
+
|
|
124
|
+
def change_volume(self, delta, offset=0):
|
|
125
|
+
self.volume = max(0, min(100, self.volume + delta))
|
|
126
|
+
self.start(offset=offset)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class KeyListener:
|
|
130
|
+
def __init__(self):
|
|
131
|
+
self.key = None
|
|
132
|
+
self._stop = False
|
|
133
|
+
self._fd = sys.stdin.fileno()
|
|
134
|
+
self._old = termios.tcgetattr(self._fd)
|
|
135
|
+
threading.Thread(target=self._run, daemon=True).start()
|
|
136
|
+
|
|
137
|
+
def _run(self):
|
|
138
|
+
tty.setraw(self._fd)
|
|
139
|
+
while not self._stop:
|
|
140
|
+
try:
|
|
141
|
+
self.key = os.read(self._fd, 3)
|
|
142
|
+
except Exception:
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
def stop(self):
|
|
146
|
+
self._stop = True
|
|
147
|
+
try:
|
|
148
|
+
termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old)
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
def pop(self):
|
|
153
|
+
k, self.key = self.key, None
|
|
154
|
+
return k
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def format_time(secs):
|
|
158
|
+
secs = max(0, int(secs))
|
|
159
|
+
m, s = divmod(secs, 60)
|
|
160
|
+
h, m = divmod(m, 60)
|
|
161
|
+
return f"{h}:{m:02d}:{s:02d}" if h else f"{m:02d}:{s:02d}"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def make_hud(paused, cur_frame, total_frames, fps, volume, mode, has_sound, cols):
|
|
165
|
+
elapsed = format_time(cur_frame / fps)
|
|
166
|
+
total = format_time(total_frames / fps) if total_frames else "--:--"
|
|
167
|
+
state = "⏸ PAUSE" if paused else "▶ PLAY"
|
|
168
|
+
vol_str = f" 🔊{volume}%" if has_sound else ""
|
|
169
|
+
bar = f" {state} {elapsed}/{total} [{mode}]{vol_str} ←→ 10s ↑↓ vol Spc pause Q quit "
|
|
170
|
+
return bar[:cols].ljust(cols)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def play_video(source, local=False, sound=False, mode="classic", hud=False):
|
|
174
|
+
try:
|
|
175
|
+
cap, title = get_video_capture(source, local=local)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
print(f"Error: {e}")
|
|
178
|
+
sys.exit(1)
|
|
179
|
+
|
|
180
|
+
fps = cap.get(cv2.CAP_PROP_FPS) or 24
|
|
181
|
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
182
|
+
frame_delay = 1.0 / fps
|
|
183
|
+
|
|
184
|
+
print(f"Playing: {title}")
|
|
185
|
+
print(f"Mode: {mode}" + (" | Sound on" if sound else " | Sound off"))
|
|
186
|
+
print("Controls: Space pause ←→ seek ↑↓ volume Q quit")
|
|
187
|
+
time.sleep(1)
|
|
188
|
+
|
|
189
|
+
audio = None
|
|
190
|
+
if sound:
|
|
191
|
+
url = get_audio_url(source, local=local)
|
|
192
|
+
if url:
|
|
193
|
+
audio = AudioPlayer(url)
|
|
194
|
+
audio.start()
|
|
195
|
+
else:
|
|
196
|
+
print("Could not get audio stream.")
|
|
197
|
+
|
|
198
|
+
keys = KeyListener()
|
|
199
|
+
paused = False
|
|
200
|
+
cur_frame = 0
|
|
201
|
+
|
|
202
|
+
# Hide cursor + clear screen once
|
|
203
|
+
sys.stdout.write("\033[?25l\033[2J")
|
|
204
|
+
sys.stdout.flush()
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
while cap.isOpened():
|
|
208
|
+
# ── Keys ──────────────────────────────────────────────────────────
|
|
209
|
+
key = keys.pop()
|
|
210
|
+
if key is not None:
|
|
211
|
+
if key in (b'q', b'Q', b'\x03'):
|
|
212
|
+
break
|
|
213
|
+
elif key == b' ':
|
|
214
|
+
paused = not paused
|
|
215
|
+
if audio:
|
|
216
|
+
audio.stop() if paused else audio.start(offset=cur_frame / fps)
|
|
217
|
+
elif key == b'\x1b[C': # →
|
|
218
|
+
cur_frame = min(total_frames, cur_frame + int(fps * 10))
|
|
219
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, cur_frame)
|
|
220
|
+
if audio: audio.start(offset=cur_frame / fps)
|
|
221
|
+
elif key == b'\x1b[D': # ←
|
|
222
|
+
cur_frame = max(0, cur_frame - int(fps * 10))
|
|
223
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, cur_frame)
|
|
224
|
+
if audio: audio.start(offset=cur_frame / fps)
|
|
225
|
+
elif key == b'\x1b[A' and audio: # ↑
|
|
226
|
+
audio.change_volume(+10, offset=cur_frame / fps)
|
|
227
|
+
elif key == b'\x1b[B' and audio: # ↓
|
|
228
|
+
audio.change_volume(-10, offset=cur_frame / fps)
|
|
229
|
+
|
|
230
|
+
if paused:
|
|
231
|
+
time.sleep(0.05)
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
# ── Read frame ────────────────────────────────────────────────────
|
|
235
|
+
t0 = time.time()
|
|
236
|
+
ret, frame = cap.read()
|
|
237
|
+
if not ret:
|
|
238
|
+
break
|
|
239
|
+
cur_frame += 1
|
|
240
|
+
|
|
241
|
+
cols, rows = get_terminal_size()
|
|
242
|
+
video_rows = rows - 1 if hud else rows
|
|
243
|
+
|
|
244
|
+
lines = frame_to_lines(frame, cols, video_rows, mode)
|
|
245
|
+
hud_line = None
|
|
246
|
+
if hud:
|
|
247
|
+
vol = audio.volume if audio else 0
|
|
248
|
+
hud_line = make_hud(paused, cur_frame, total_frames, fps, vol, mode, bool(audio), cols)
|
|
249
|
+
|
|
250
|
+
render_frame(lines, hud_line)
|
|
251
|
+
|
|
252
|
+
elapsed = time.time() - t0
|
|
253
|
+
wait = frame_delay - elapsed
|
|
254
|
+
if wait > 0:
|
|
255
|
+
time.sleep(wait)
|
|
256
|
+
|
|
257
|
+
except KeyboardInterrupt:
|
|
258
|
+
pass
|
|
259
|
+
finally:
|
|
260
|
+
keys.stop()
|
|
261
|
+
cap.release()
|
|
262
|
+
if audio:
|
|
263
|
+
audio.stop()
|
|
264
|
+
sys.stdout.write("\033[?25h\033[0m\033[2J\033[H")
|
|
265
|
+
sys.stdout.flush()
|
|
266
|
+
print("hoopoe stopped. See you next time!")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def main():
|
|
270
|
+
parser = argparse.ArgumentParser(
|
|
271
|
+
description="hoopoe-player - Videos as colorful ASCII art in your terminal",
|
|
272
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
273
|
+
)
|
|
274
|
+
parser.add_argument("source", help="YouTube URL or path to local video file")
|
|
275
|
+
parser.add_argument("-l", "--local", action="store_true", help="Play a local video file")
|
|
276
|
+
parser.add_argument("-s", "--sound", action="store_true", help="Enable audio (requires ffmpeg)")
|
|
277
|
+
parser.add_argument("-m", "--mode", choices=list(CHAR_MODES.keys()), default="classic",
|
|
278
|
+
help="Rendering mode: classic blocks braille minimal invert nocolor")
|
|
279
|
+
parser.add_argument("--hud", action="store_true",
|
|
280
|
+
help="Show status bar at the bottom")
|
|
281
|
+
args = parser.parse_args()
|
|
282
|
+
play_video(args.source, local=args.local, sound=args.sound, mode=args.mode, hud=args.hud)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
main()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hoopoe-player
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Play any video as colorful ASCII art in your terminal
|
|
5
|
+
Home-page: https://github.com/axol8002/hoopoe-player
|
|
6
|
+
Author: Adriel Molina
|
|
7
|
+
Author-email: adrielmolinacaceres@gmail.com
|
|
8
|
+
Keywords: ascii art terminal video youtube player cli hoopoe
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Topic :: Multimedia :: Video
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: yt-dlp
|
|
18
|
+
Requires-Dist: opencv-python
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: keywords
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
30
|
+
|
|
31
|
+
# hoopoe-player
|
|
32
|
+
|
|
33
|
+
> Play any video as colorful ASCII art directly in your terminal.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Most systems
|
|
39
|
+
pip install hoopoe-player
|
|
40
|
+
|
|
41
|
+
# Ubuntu/Debian
|
|
42
|
+
pipx install hoopoe-player
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You also need `ffmpeg` for audio:
|
|
46
|
+
```bash
|
|
47
|
+
sudo apt install ffmpeg # Ubuntu/Debian
|
|
48
|
+
sudo pacman -S ffmpeg # Arch
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Play a YouTube video
|
|
55
|
+
hoopoe https://www.youtube.com/watch?v=xxxxx
|
|
56
|
+
|
|
57
|
+
# Play a local file
|
|
58
|
+
hoopoe -l video.mp4
|
|
59
|
+
|
|
60
|
+
# Enable audio
|
|
61
|
+
hoopoe -s https://www.youtube.com/watch?v=xxxxx
|
|
62
|
+
|
|
63
|
+
# Change character mode
|
|
64
|
+
hoopoe -m blocks https://www.youtube.com/watch?v=xxxxx
|
|
65
|
+
|
|
66
|
+
# Show status bar (time, volume, controls)
|
|
67
|
+
hoopoe --hud https://www.youtube.com/watch?v=xxxxx
|
|
68
|
+
|
|
69
|
+
# Combine options
|
|
70
|
+
hoopoe -l -s -m invert --hud video.mp4
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Controls
|
|
74
|
+
|
|
75
|
+
| Key | Action |
|
|
76
|
+
|-----|--------|
|
|
77
|
+
| `Space` | Pause / Play |
|
|
78
|
+
| `←` / `→` | Seek −10s / +10s |
|
|
79
|
+
| `↑` / `↓` | Volume +10 / −10 (only with `-s`) |
|
|
80
|
+
| `Q` or `Ctrl+C` | Quit |
|
|
81
|
+
|
|
82
|
+
## Character modes
|
|
83
|
+
|
|
84
|
+
| Mode | Style |
|
|
85
|
+
|------|-------|
|
|
86
|
+
| `classic` | `. : - = + * # % @` — default, coloured |
|
|
87
|
+
| `blocks` | `░ ▒ ▓ █` — bold blocks, coloured |
|
|
88
|
+
| `braille` | `⠁ ⠃ ⠇ ⠿ ⣿` — dense dots, coloured |
|
|
89
|
+
| `minimal` | `· • ● ■` — clean and minimal, coloured |
|
|
90
|
+
| `invert` | colour as background — selection effect |
|
|
91
|
+
| `nocolor` | classic chars, no colour — for legacy terminals |
|
|
92
|
+
|
|
93
|
+
## Requirements
|
|
94
|
+
|
|
95
|
+
- Python 3.8+
|
|
96
|
+
- ffmpeg (optional, needed for `-s` audio)
|
|
97
|
+
- A terminal with true color support (for all modes except `nocolor`)
|
|
98
|
+
|
|
99
|
+
## Roadmap
|
|
100
|
+
|
|
101
|
+
- [ ] Fix audio/video sync — audio can drift ahead when rendering is slow
|
|
102
|
+
- [ ] Fix scaling when paused — terminal resize not applied until next frame
|
|
103
|
+
- [ ] Optimize rendering performance — reduce CPU usage per frame
|
|
104
|
+
- [ ] Screenshot to file — press a key to save the current frame as a colored text file (ANSI)
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
hoopoe/__init__.py
|
|
5
|
+
hoopoe/main.py
|
|
6
|
+
hoopoe_player.egg-info/PKG-INFO
|
|
7
|
+
hoopoe_player.egg-info/SOURCES.txt
|
|
8
|
+
hoopoe_player.egg-info/dependency_links.txt
|
|
9
|
+
hoopoe_player.egg-info/entry_points.txt
|
|
10
|
+
hoopoe_player.egg-info/requires.txt
|
|
11
|
+
hoopoe_player.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hoopoe
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as f:
|
|
4
|
+
long_description = f.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name="hoopoe-player",
|
|
8
|
+
version="0.1.0",
|
|
9
|
+
description="Play any video as colorful ASCII art in your terminal",
|
|
10
|
+
long_description=long_description,
|
|
11
|
+
long_description_content_type="text/markdown",
|
|
12
|
+
author="Adriel Molina",
|
|
13
|
+
author_email="adrielmolinacaceres@gmail.com",
|
|
14
|
+
url="https://github.com/axol8002/hoopoe-player",
|
|
15
|
+
packages=find_packages(),
|
|
16
|
+
install_requires=[
|
|
17
|
+
"yt-dlp",
|
|
18
|
+
"opencv-python",
|
|
19
|
+
],
|
|
20
|
+
entry_points={
|
|
21
|
+
"console_scripts": [
|
|
22
|
+
"hoopoe=hoopoe.main:main",
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
python_requires=">=3.8",
|
|
26
|
+
classifiers=[
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Operating System :: POSIX :: Linux",
|
|
30
|
+
"Environment :: Console",
|
|
31
|
+
"Topic :: Multimedia :: Video",
|
|
32
|
+
],
|
|
33
|
+
keywords="ascii art terminal video youtube player cli hoopoe",
|
|
34
|
+
)
|