ytplay-cli 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.
- ytplay_cli-0.4.0/LICENSE +0 -0
- ytplay_cli-0.4.0/PKG-INFO +61 -0
- ytplay_cli-0.4.0/README.md +44 -0
- ytplay_cli-0.4.0/pyproject.toml +38 -0
- ytplay_cli-0.4.0/setup.cfg +4 -0
- ytplay_cli-0.4.0/ytcli/__init__.py +1 -0
- ytplay_cli-0.4.0/ytcli/cli.py +179 -0
- ytplay_cli-0.4.0/ytcli/config.py +29 -0
- ytplay_cli-0.4.0/ytcli/deps.py +52 -0
- ytplay_cli-0.4.0/ytcli/player.py +41 -0
- ytplay_cli-0.4.0/ytcli/search.py +20 -0
- ytplay_cli-0.4.0/ytcli/ui.py +57 -0
- ytplay_cli-0.4.0/ytcli/utils.py +35 -0
- ytplay_cli-0.4.0/ytplay_cli.egg-info/PKG-INFO +61 -0
- ytplay_cli-0.4.0/ytplay_cli.egg-info/SOURCES.txt +17 -0
- ytplay_cli-0.4.0/ytplay_cli.egg-info/dependency_links.txt +1 -0
- ytplay_cli-0.4.0/ytplay_cli.egg-info/entry_points.txt +2 -0
- ytplay_cli-0.4.0/ytplay_cli.egg-info/requires.txt +1 -0
- ytplay_cli-0.4.0/ytplay_cli.egg-info/top_level.txt +1 -0
ytplay_cli-0.4.0/LICENSE
ADDED
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ytplay-cli
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Search and play YouTube videos from the terminal using mpv and fzf
|
|
5
|
+
Author: Naveen
|
|
6
|
+
Project-URL: Homepage, https://github.com/naveen07-c/yt-cli
|
|
7
|
+
Project-URL: Repository, https://github.com/naveen07-c/yt-cli
|
|
8
|
+
Project-URL: Issues, https://github.com/naveen07-c/yt-cli/issues
|
|
9
|
+
Keywords: youtube,cli,terminal,mpv,fzf
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: yt-dlp
|
|
17
|
+
|
|
18
|
+
# yt-cli
|
|
19
|
+
|
|
20
|
+
Search, preview, and play YouTube videos directly from the terminal.
|
|
21
|
+
|
|
22
|
+
`yt-cli` is a lightweight command-line tool that allows users to search YouTube, browse results interactively, preview video information, and play or download videos — all without leaving the terminal.
|
|
23
|
+
|
|
24
|
+
The tool combines several powerful utilities:
|
|
25
|
+
|
|
26
|
+
- **yt-dlp** → fetches YouTube metadata and streams
|
|
27
|
+
- **fzf** → interactive fuzzy search interface
|
|
28
|
+
- **mpv** → video/audio playback
|
|
29
|
+
- **jq** → JSON parsing for preview data
|
|
30
|
+
|
|
31
|
+
This makes it possible to build a fast, terminal-based YouTube experience.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# Features
|
|
36
|
+
|
|
37
|
+
- Search YouTube from the terminal
|
|
38
|
+
- Interactive video selection using **fzf**
|
|
39
|
+
- Preview video metadata while browsing results
|
|
40
|
+
- Play videos directly with **mpv**
|
|
41
|
+
- Play **audio-only** mode
|
|
42
|
+
- Download videos using **yt-dlp**
|
|
43
|
+
- Open selected video in the browser
|
|
44
|
+
- Choose playback **quality**
|
|
45
|
+
- Adjust playback **speed**
|
|
46
|
+
- Maintain **search history**
|
|
47
|
+
- Clean CLI interface designed for Linux environments
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
# Installation
|
|
52
|
+
|
|
53
|
+
The recommended installation method is **pipx**.
|
|
54
|
+
|
|
55
|
+
`pipx` installs Python CLI applications globally but keeps dependencies isolated.
|
|
56
|
+
|
|
57
|
+
Install pipx if not installed:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
sudo apt install pipx
|
|
61
|
+
pipx ensurepath
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# yt-cli
|
|
2
|
+
|
|
3
|
+
Search, preview, and play YouTube videos directly from the terminal.
|
|
4
|
+
|
|
5
|
+
`yt-cli` is a lightweight command-line tool that allows users to search YouTube, browse results interactively, preview video information, and play or download videos — all without leaving the terminal.
|
|
6
|
+
|
|
7
|
+
The tool combines several powerful utilities:
|
|
8
|
+
|
|
9
|
+
- **yt-dlp** → fetches YouTube metadata and streams
|
|
10
|
+
- **fzf** → interactive fuzzy search interface
|
|
11
|
+
- **mpv** → video/audio playback
|
|
12
|
+
- **jq** → JSON parsing for preview data
|
|
13
|
+
|
|
14
|
+
This makes it possible to build a fast, terminal-based YouTube experience.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Features
|
|
19
|
+
|
|
20
|
+
- Search YouTube from the terminal
|
|
21
|
+
- Interactive video selection using **fzf**
|
|
22
|
+
- Preview video metadata while browsing results
|
|
23
|
+
- Play videos directly with **mpv**
|
|
24
|
+
- Play **audio-only** mode
|
|
25
|
+
- Download videos using **yt-dlp**
|
|
26
|
+
- Open selected video in the browser
|
|
27
|
+
- Choose playback **quality**
|
|
28
|
+
- Adjust playback **speed**
|
|
29
|
+
- Maintain **search history**
|
|
30
|
+
- Clean CLI interface designed for Linux environments
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# Installation
|
|
35
|
+
|
|
36
|
+
The recommended installation method is **pipx**.
|
|
37
|
+
|
|
38
|
+
`pipx` installs Python CLI applications globally but keeps dependencies isolated.
|
|
39
|
+
|
|
40
|
+
Install pipx if not installed:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
sudo apt install pipx
|
|
44
|
+
pipx ensurepath
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61,<70", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ytplay-cli"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "Search and play YouTube videos from the terminal using mpv and fzf"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Naveen" }
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
"yt-dlp"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
keywords = ["youtube", "cli", "terminal", "mpv", "fzf"]
|
|
21
|
+
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Environment :: Console",
|
|
25
|
+
"Operating System :: POSIX :: Linux"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/naveen07-c/yt-cli"
|
|
30
|
+
Repository = "https://github.com/naveen07-c/yt-cli"
|
|
31
|
+
Issues = "https://github.com/naveen07-c/yt-cli/issues"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
yt = "ytcli.cli:main"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["."]
|
|
38
|
+
include = ["ytcli*"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import webbrowser
|
|
5
|
+
|
|
6
|
+
from ytcli.search import search_youtube
|
|
7
|
+
from ytcli.player import play_video, play_audio, download_video
|
|
8
|
+
from ytcli.ui import select_video
|
|
9
|
+
from ytcli.utils import format_duration, format_views
|
|
10
|
+
from ytcli.config import load_config
|
|
11
|
+
from ytcli.deps import check_dependencies
|
|
12
|
+
from ytcli import __version__
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
HISTORY_FILE = os.path.expanduser("~/.local/share/yt-cli/history")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def save_history(query):
|
|
19
|
+
|
|
20
|
+
os.makedirs(os.path.dirname(HISTORY_FILE), exist_ok=True)
|
|
21
|
+
|
|
22
|
+
with open(HISTORY_FILE, "a") as f:
|
|
23
|
+
f.write(query + "\n")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def show_history():
|
|
27
|
+
|
|
28
|
+
if not os.path.exists(HISTORY_FILE):
|
|
29
|
+
print("No history yet.")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
with open(HISTORY_FILE) as f:
|
|
33
|
+
print(f.read())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def fzf_menu(options, prompt):
|
|
37
|
+
|
|
38
|
+
fzf = subprocess.Popen(
|
|
39
|
+
[
|
|
40
|
+
"fzf",
|
|
41
|
+
"--height=40%",
|
|
42
|
+
"--layout=reverse",
|
|
43
|
+
"--border",
|
|
44
|
+
"--prompt", prompt
|
|
45
|
+
],
|
|
46
|
+
stdin=subprocess.PIPE,
|
|
47
|
+
stdout=subprocess.PIPE,
|
|
48
|
+
text=True
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
stdout, _ = fzf.communicate("\n".join(options))
|
|
52
|
+
|
|
53
|
+
return stdout.strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def choose_action():
|
|
57
|
+
|
|
58
|
+
actions = [
|
|
59
|
+
"Play Video",
|
|
60
|
+
"Play Audio Only",
|
|
61
|
+
"Download Video",
|
|
62
|
+
"Open in Browser"
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
return fzf_menu(actions, "Action > ")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def choose_quality():
|
|
69
|
+
|
|
70
|
+
qualities = [
|
|
71
|
+
"1080",
|
|
72
|
+
"720",
|
|
73
|
+
"480",
|
|
74
|
+
"best"
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
return fzf_menu(qualities, "Quality > ")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def choose_speed():
|
|
81
|
+
|
|
82
|
+
speeds = [
|
|
83
|
+
"1",
|
|
84
|
+
"1.25",
|
|
85
|
+
"1.5",
|
|
86
|
+
"2"
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
return fzf_menu(speeds, "Speed > ")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main():
|
|
93
|
+
|
|
94
|
+
check_dependencies()
|
|
95
|
+
|
|
96
|
+
parser = argparse.ArgumentParser(
|
|
97
|
+
description="YouTube CLI player"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
parser.add_argument("query", nargs="*")
|
|
101
|
+
parser.add_argument("--audio", action="store_true")
|
|
102
|
+
parser.add_argument("--download", action="store_true")
|
|
103
|
+
parser.add_argument("--play", type=int)
|
|
104
|
+
parser.add_argument("--history", action="store_true")
|
|
105
|
+
parser.add_argument("--version", action="store_true")
|
|
106
|
+
|
|
107
|
+
args = parser.parse_args()
|
|
108
|
+
|
|
109
|
+
if args.version:
|
|
110
|
+
print("yt-cli version", __version__)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if args.history:
|
|
114
|
+
show_history()
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
if not args.query:
|
|
118
|
+
parser.print_help()
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
query = " ".join(args.query)
|
|
122
|
+
|
|
123
|
+
save_history(query)
|
|
124
|
+
|
|
125
|
+
config = load_config()
|
|
126
|
+
|
|
127
|
+
videos = search_youtube(query)
|
|
128
|
+
|
|
129
|
+
lines = []
|
|
130
|
+
|
|
131
|
+
for v in videos:
|
|
132
|
+
|
|
133
|
+
title = v.get("title", "Unknown")
|
|
134
|
+
channel = v.get("channel", "Unknown")
|
|
135
|
+
duration = format_duration(v.get("duration"))
|
|
136
|
+
views = format_views(v.get("view_count"))
|
|
137
|
+
vid = v.get("id")
|
|
138
|
+
|
|
139
|
+
lines.append(
|
|
140
|
+
f"{title} | {channel} | {duration} | {views} | {vid}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
selection = select_video(lines)
|
|
144
|
+
|
|
145
|
+
if not selection:
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
index = lines.index(selection)
|
|
149
|
+
video = videos[index]
|
|
150
|
+
|
|
151
|
+
video_id = video["id"]
|
|
152
|
+
|
|
153
|
+
# ACTION MENU
|
|
154
|
+
action = choose_action()
|
|
155
|
+
|
|
156
|
+
if action == "Play Audio Only":
|
|
157
|
+
|
|
158
|
+
play_audio(video_id)
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
if action == "Download Video":
|
|
162
|
+
|
|
163
|
+
download_video(video_id)
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
if action == "Open in Browser":
|
|
167
|
+
|
|
168
|
+
webbrowser.open(f"https://youtube.com/watch?v={video_id}")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
# QUALITY MENU
|
|
172
|
+
quality = choose_quality()
|
|
173
|
+
|
|
174
|
+
# SPEED MENU
|
|
175
|
+
speed = choose_speed()
|
|
176
|
+
|
|
177
|
+
play_video(video_id, quality, speed)
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
main()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
CONFIG_DIR = os.path.expanduser("~/.config/yt-cli")
|
|
5
|
+
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
|
|
6
|
+
|
|
7
|
+
DEFAULT_CONFIG = {
|
|
8
|
+
"default_quality": "720",
|
|
9
|
+
"default_speed": "1"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_config():
|
|
14
|
+
|
|
15
|
+
if not os.path.exists(CONFIG_DIR):
|
|
16
|
+
os.makedirs(CONFIG_DIR)
|
|
17
|
+
|
|
18
|
+
if not os.path.exists(CONFIG_FILE):
|
|
19
|
+
|
|
20
|
+
with open(CONFIG_FILE, "w") as f:
|
|
21
|
+
json.dump(DEFAULT_CONFIG, f, indent=2)
|
|
22
|
+
|
|
23
|
+
return DEFAULT_CONFIG
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
with open(CONFIG_FILE) as f:
|
|
27
|
+
return json.load(f)
|
|
28
|
+
except Exception:
|
|
29
|
+
return DEFAULT_CONFIG
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def detect_package_manager():
|
|
7
|
+
|
|
8
|
+
managers = ["apt", "dnf", "pacman"]
|
|
9
|
+
|
|
10
|
+
for m in managers:
|
|
11
|
+
if shutil.which(m):
|
|
12
|
+
return m
|
|
13
|
+
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def install_package(pkg):
|
|
18
|
+
|
|
19
|
+
manager = detect_package_manager()
|
|
20
|
+
|
|
21
|
+
if manager == "apt":
|
|
22
|
+
subprocess.run(["sudo", "apt", "install", "-y", pkg])
|
|
23
|
+
|
|
24
|
+
elif manager == "dnf":
|
|
25
|
+
subprocess.run(["sudo", "dnf", "install", "-y", pkg])
|
|
26
|
+
|
|
27
|
+
elif manager == "pacman":
|
|
28
|
+
subprocess.run(["sudo", "pacman", "-S", "--noconfirm", pkg])
|
|
29
|
+
|
|
30
|
+
else:
|
|
31
|
+
print("No supported package manager found.")
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ensure_tool(tool, package):
|
|
36
|
+
|
|
37
|
+
if shutil.which(tool) is None:
|
|
38
|
+
|
|
39
|
+
print(f"{tool} not found. Installing {package}...")
|
|
40
|
+
install_package(package)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def check_dependencies():
|
|
44
|
+
|
|
45
|
+
ensure_tool("fzf", "fzf")
|
|
46
|
+
ensure_tool("mpv", "mpv")
|
|
47
|
+
ensure_tool("jq", "jq")
|
|
48
|
+
|
|
49
|
+
if shutil.which("yt-dlp") is None:
|
|
50
|
+
|
|
51
|
+
print("Installing yt-dlp via pip...")
|
|
52
|
+
subprocess.run([sys.executable, "-m", "pip", "install", "yt-dlp"])
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def play_video(video_id, quality="720", speed="1"):
|
|
5
|
+
|
|
6
|
+
url = f"https://youtube.com/watch?v={video_id}"
|
|
7
|
+
|
|
8
|
+
if quality == "best":
|
|
9
|
+
fmt = "best"
|
|
10
|
+
else:
|
|
11
|
+
fmt = f"bestvideo[height<={quality}]+bestaudio/best"
|
|
12
|
+
|
|
13
|
+
subprocess.run([
|
|
14
|
+
"mpv",
|
|
15
|
+
"--ytdl-format=" + fmt,
|
|
16
|
+
"--speed=" + speed,
|
|
17
|
+
url
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def play_audio(video_id):
|
|
23
|
+
|
|
24
|
+
url = f"https://youtube.com/watch?v={video_id}"
|
|
25
|
+
|
|
26
|
+
subprocess.run([
|
|
27
|
+
"mpv",
|
|
28
|
+
"--force-window=yes",
|
|
29
|
+
"--no-video",
|
|
30
|
+
url
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def download_video(video_id):
|
|
35
|
+
|
|
36
|
+
url = f"https://youtube.com/watch?v={video_id}"
|
|
37
|
+
|
|
38
|
+
subprocess.run([
|
|
39
|
+
"yt-dlp",
|
|
40
|
+
url
|
|
41
|
+
])
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import yt_dlp
|
|
2
|
+
|
|
3
|
+
RESULT_LIMIT = 30
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def search_youtube(query):
|
|
7
|
+
|
|
8
|
+
ydl_opts = {
|
|
9
|
+
"quiet": True,
|
|
10
|
+
"extract_flat": "in_playlist",
|
|
11
|
+
"skip_download": True
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
15
|
+
info = ydl.extract_info(
|
|
16
|
+
f"ytsearch{RESULT_LIMIT}:{query}",
|
|
17
|
+
download=False
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
return info.get("entries", [])
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def select_video(lines):
|
|
5
|
+
|
|
6
|
+
preview_script = r'''
|
|
7
|
+
id=$(echo {} | awk -F'|' '{print $NF}')
|
|
8
|
+
|
|
9
|
+
json=$(yt-dlp --skip-download --dump-json https://youtube.com/watch?v=$id 2>/dev/null)
|
|
10
|
+
|
|
11
|
+
title=$(echo "$json" | jq -r ".title")
|
|
12
|
+
channel=$(echo "$json" | jq -r ".channel")
|
|
13
|
+
views=$(echo "$json" | jq -r ".view_count")
|
|
14
|
+
duration=$(echo "$json" | jq -r ".duration")
|
|
15
|
+
date=$(echo "$json" | jq -r ".upload_date")
|
|
16
|
+
|
|
17
|
+
# format duration
|
|
18
|
+
h=$((duration / 3600))
|
|
19
|
+
m=$(((duration % 3600) / 60))
|
|
20
|
+
s=$((duration % 60))
|
|
21
|
+
|
|
22
|
+
if [ "$h" -gt 0 ]; then
|
|
23
|
+
duration_fmt=$(printf "%d:%02d:%02d" "$h" "$m" "$s")
|
|
24
|
+
else
|
|
25
|
+
duration_fmt=$(printf "%d:%02d" "$m" "$s")
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# format date YYYYMMDD -> YYYY-MM-DD
|
|
29
|
+
date_fmt=$(echo "$date" | sed 's/\(....\)\(..\)\(..\)/\1-\2-\3/')
|
|
30
|
+
|
|
31
|
+
echo "Title: $title"
|
|
32
|
+
echo
|
|
33
|
+
echo "Channel: $channel"
|
|
34
|
+
echo "Views: $views"
|
|
35
|
+
echo "Duration: $duration_fmt"
|
|
36
|
+
echo "Upload Date: $date_fmt"
|
|
37
|
+
'''
|
|
38
|
+
|
|
39
|
+
fzf = subprocess.Popen(
|
|
40
|
+
[
|
|
41
|
+
"fzf",
|
|
42
|
+
"--height=90%",
|
|
43
|
+
"--layout=reverse",
|
|
44
|
+
"--border",
|
|
45
|
+
"--prompt=YouTube > ",
|
|
46
|
+
"--preview",
|
|
47
|
+
preview_script,
|
|
48
|
+
"--preview-window=right:60%"
|
|
49
|
+
],
|
|
50
|
+
stdin=subprocess.PIPE,
|
|
51
|
+
stdout=subprocess.PIPE,
|
|
52
|
+
text=True
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
stdout, _ = fzf.communicate("\n".join(lines))
|
|
56
|
+
|
|
57
|
+
return stdout.strip()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
def format_duration(seconds):
|
|
2
|
+
"""
|
|
3
|
+
Convert seconds to YouTube-like format:
|
|
4
|
+
HH:MM:SS or MM:SS
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
if seconds is None:
|
|
8
|
+
return "??:??"
|
|
9
|
+
|
|
10
|
+
seconds = int(seconds)
|
|
11
|
+
|
|
12
|
+
minutes, sec = divmod(seconds, 60)
|
|
13
|
+
hours, minutes = divmod(minutes, 60)
|
|
14
|
+
|
|
15
|
+
if hours > 0:
|
|
16
|
+
return f"{hours}:{minutes:02d}:{sec:02d}"
|
|
17
|
+
|
|
18
|
+
return f"{minutes}:{sec:02d}"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def format_views(v):
|
|
22
|
+
"""
|
|
23
|
+
Convert views to readable format
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
if not v:
|
|
27
|
+
return "0"
|
|
28
|
+
|
|
29
|
+
if v >= 1_000_000:
|
|
30
|
+
return f"{v/1_000_000:.1f}M"
|
|
31
|
+
|
|
32
|
+
if v >= 1_000:
|
|
33
|
+
return f"{v/1_000:.1f}K"
|
|
34
|
+
|
|
35
|
+
return str(v)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ytplay-cli
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Search and play YouTube videos from the terminal using mpv and fzf
|
|
5
|
+
Author: Naveen
|
|
6
|
+
Project-URL: Homepage, https://github.com/naveen07-c/yt-cli
|
|
7
|
+
Project-URL: Repository, https://github.com/naveen07-c/yt-cli
|
|
8
|
+
Project-URL: Issues, https://github.com/naveen07-c/yt-cli/issues
|
|
9
|
+
Keywords: youtube,cli,terminal,mpv,fzf
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: yt-dlp
|
|
17
|
+
|
|
18
|
+
# yt-cli
|
|
19
|
+
|
|
20
|
+
Search, preview, and play YouTube videos directly from the terminal.
|
|
21
|
+
|
|
22
|
+
`yt-cli` is a lightweight command-line tool that allows users to search YouTube, browse results interactively, preview video information, and play or download videos — all without leaving the terminal.
|
|
23
|
+
|
|
24
|
+
The tool combines several powerful utilities:
|
|
25
|
+
|
|
26
|
+
- **yt-dlp** → fetches YouTube metadata and streams
|
|
27
|
+
- **fzf** → interactive fuzzy search interface
|
|
28
|
+
- **mpv** → video/audio playback
|
|
29
|
+
- **jq** → JSON parsing for preview data
|
|
30
|
+
|
|
31
|
+
This makes it possible to build a fast, terminal-based YouTube experience.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# Features
|
|
36
|
+
|
|
37
|
+
- Search YouTube from the terminal
|
|
38
|
+
- Interactive video selection using **fzf**
|
|
39
|
+
- Preview video metadata while browsing results
|
|
40
|
+
- Play videos directly with **mpv**
|
|
41
|
+
- Play **audio-only** mode
|
|
42
|
+
- Download videos using **yt-dlp**
|
|
43
|
+
- Open selected video in the browser
|
|
44
|
+
- Choose playback **quality**
|
|
45
|
+
- Adjust playback **speed**
|
|
46
|
+
- Maintain **search history**
|
|
47
|
+
- Clean CLI interface designed for Linux environments
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
# Installation
|
|
52
|
+
|
|
53
|
+
The recommended installation method is **pipx**.
|
|
54
|
+
|
|
55
|
+
`pipx` installs Python CLI applications globally but keeps dependencies isolated.
|
|
56
|
+
|
|
57
|
+
Install pipx if not installed:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
sudo apt install pipx
|
|
61
|
+
pipx ensurepath
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
ytcli/__init__.py
|
|
5
|
+
ytcli/cli.py
|
|
6
|
+
ytcli/config.py
|
|
7
|
+
ytcli/deps.py
|
|
8
|
+
ytcli/player.py
|
|
9
|
+
ytcli/search.py
|
|
10
|
+
ytcli/ui.py
|
|
11
|
+
ytcli/utils.py
|
|
12
|
+
ytplay_cli.egg-info/PKG-INFO
|
|
13
|
+
ytplay_cli.egg-info/SOURCES.txt
|
|
14
|
+
ytplay_cli.egg-info/dependency_links.txt
|
|
15
|
+
ytplay_cli.egg-info/entry_points.txt
|
|
16
|
+
ytplay_cli.egg-info/requires.txt
|
|
17
|
+
ytplay_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
yt-dlp
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ytcli
|