yit-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.
- yit_player-0.1.0/.github/workflows/publish.yml +62 -0
- yit_player-0.1.0/.gitignore +20 -0
- yit_player-0.1.0/AI_INSTRUCTIONS.md +74 -0
- yit_player-0.1.0/PKG-INFO +119 -0
- yit_player-0.1.0/README.md +109 -0
- yit_player-0.1.0/pyproject.toml +21 -0
- yit_player-0.1.0/requirements.txt +1 -0
- yit_player-0.1.0/src/yit/__init__.py +600 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# This workflow will upload a Python Package to PyPI when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
name: Upload Python Package
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [published]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
release-build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.x"
|
|
23
|
+
|
|
24
|
+
- name: Install build dependencies
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install --upgrade pip
|
|
27
|
+
python -m pip install build
|
|
28
|
+
|
|
29
|
+
- name: Build release distributions
|
|
30
|
+
run: |
|
|
31
|
+
python -m build
|
|
32
|
+
|
|
33
|
+
- name: Upload distributions
|
|
34
|
+
uses: actions/upload-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: release-dists
|
|
37
|
+
path: dist/
|
|
38
|
+
|
|
39
|
+
pypi-publish:
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
needs:
|
|
42
|
+
- release-build
|
|
43
|
+
permissions:
|
|
44
|
+
# IMPORTANT: this permission is mandatory for trusted publishing
|
|
45
|
+
id-token: write
|
|
46
|
+
|
|
47
|
+
# Dedicated environments with protections for publishing are strongly recommended.
|
|
48
|
+
environment:
|
|
49
|
+
name: pypi
|
|
50
|
+
url: https://pypi.org/p/yit-player
|
|
51
|
+
|
|
52
|
+
steps:
|
|
53
|
+
- name: Retrieve release distributions
|
|
54
|
+
uses: actions/download-artifact@v4
|
|
55
|
+
with:
|
|
56
|
+
name: release-dists
|
|
57
|
+
path: dist/
|
|
58
|
+
|
|
59
|
+
- name: Publish release distributions to PyPI
|
|
60
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
61
|
+
with:
|
|
62
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Yit - Agent Context & Instructions
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
**Yit** (Your Instant Terminal) is a headless, CLI-based music player for Windows. It uses `mpv` as the audio engine and `yt-dlp` for media resolution. The architecture separates the client (CLI) from the daemon (background mpv process).
|
|
5
|
+
|
|
6
|
+
## Agent Interaction Protocol
|
|
7
|
+
|
|
8
|
+
### 1. Self-Discovery
|
|
9
|
+
An agent should first discover what Yit can do:
|
|
10
|
+
```bash
|
|
11
|
+
yit commands
|
|
12
|
+
```
|
|
13
|
+
**Output (JSON):**
|
|
14
|
+
```json
|
|
15
|
+
[
|
|
16
|
+
{"cmd": "search", "usage": "yit search <query> [-p]", "desc": "Search YouTube..."},
|
|
17
|
+
{"cmd": "play", "usage": "yit play <index>", "desc": "Play track..."},
|
|
18
|
+
...
|
|
19
|
+
]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 2. State Retrieval
|
|
23
|
+
To get the full system state in a machine-readable format, run:
|
|
24
|
+
```bash
|
|
25
|
+
yit agent
|
|
26
|
+
```
|
|
27
|
+
**Output Format (JSON):**
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"status": "playing",
|
|
31
|
+
"track": {
|
|
32
|
+
"title": "Song Title",
|
|
33
|
+
"url": "https://youtube.com/..."
|
|
34
|
+
},
|
|
35
|
+
"position": 125.5,
|
|
36
|
+
"duration": 300.0,
|
|
37
|
+
"volume": 100,
|
|
38
|
+
"loop": false,
|
|
39
|
+
"queue_length": 5
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Control Commands
|
|
44
|
+
Use these commands to manipulate playback. They return exit code `0` on success.
|
|
45
|
+
|
|
46
|
+
| Command | Action | Notes |
|
|
47
|
+
| :--- | :--- | :--- |
|
|
48
|
+
| `yit search "<query>" -p` | Search & Auto-Play | Best for "Play X" requests. |
|
|
49
|
+
| `yit pause` | Pause playback | Idempotent. |
|
|
50
|
+
| `yit resume` | Resume playback | Idempotent. |
|
|
51
|
+
| `yit toggle` | Toggle play/pause | |
|
|
52
|
+
| `yit stop` | Kill player | Hard stop. |
|
|
53
|
+
| `yit next` | Skip track | |
|
|
54
|
+
| `yit back` | Previous track | |
|
|
55
|
+
| `yit loop` | Loop current | Infinite loop. |
|
|
56
|
+
| `yit unloop` | Disable loop | |
|
|
57
|
+
| `yit volume <0-100>` | Set volume | *Planned Feature* |
|
|
58
|
+
|
|
59
|
+
### 3. File Structure
|
|
60
|
+
* `yit.py`: Main entry point. Handles CLI args and IPC communication.
|
|
61
|
+
* `yit.bat`: Windows wrapper. Ensures `venv` usage.
|
|
62
|
+
* `install_yit.ps1`: Self-healing installer. Creates `venv` if missing.
|
|
63
|
+
* `.yit/history.json`: Persistent history of played tracks.
|
|
64
|
+
* `.yit/results.json`: Last search results.
|
|
65
|
+
|
|
66
|
+
### 4. IPC Mechanism
|
|
67
|
+
Yit communicates with `mpv` via a named pipe: `\\.\pipe\yit_socket`.
|
|
68
|
+
* The `send_ipc_command` function in `yit.py` handles raw JSON IPC messages.
|
|
69
|
+
* If extending functionality, use `input-ipc-server` commands from MPV documentation.
|
|
70
|
+
|
|
71
|
+
## Critical Rules for Agents
|
|
72
|
+
1. **Always use `yit.bat` or `yit`**: Never call `python yit.py` directly; it bypasses the venv check.
|
|
73
|
+
2. **Check `yit agent` first**: Before deciding to play/pause, check the current state.
|
|
74
|
+
3. **Search is blocking**: `yit search` waits for `yt-dlp`. Playback commands are non-blocking.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yit-player
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: YouTube in Terminal - Fire-and-Forget Music CLI
|
|
5
|
+
Author: Yit Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.7
|
|
8
|
+
Requires-Dist: yt-dlp>=2023.0.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Yit (YouTube in Terminal) Player 🎵
|
|
12
|
+
|
|
13
|
+
[](https://badge.fury.io/py/yit-player)
|
|
14
|
+
[](https://opensource.org/licenses/MIT)
|
|
15
|
+
[](https://www.python.org/downloads/)
|
|
16
|
+
|
|
17
|
+
**The Fire-and-Forget Music Player for Developers.**
|
|
18
|
+
|
|
19
|
+
Yit (YouTube in Terminal) is a lightweight, headless, terminal-based audio player designed for flow states. It allows you to search, queue, and control music directly from your CLI without ever touching a browser or a heavy GUI.
|
|
20
|
+
|
|
21
|
+
It runs in the background (daemonized), meaning you can close your terminal, switch tabs, or keep coding while the music plays.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🚀 Features
|
|
26
|
+
|
|
27
|
+
* **Daemon Architecture**: The player runs as a detached background process. Your terminal is never blocked.
|
|
28
|
+
* **Instant Search**: Uses `yt-dlp` to fetch metadata in milliseconds.
|
|
29
|
+
* **Smart Queue**: Manage your playlist (`add`, `next`, `back`, `Loop`) with simple commands.
|
|
30
|
+
* **Cross-Platform**: Works natively on **Windows**, **macOS**, and **Linux**.
|
|
31
|
+
* **Agent-Native**: Built from the ground up to be controlled by AI Agents (Vibe Coding).
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 📦 Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install yit-player
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Requirements
|
|
42
|
+
Yit uses **[mpv](https://mpv.io/)** as its audio engine.
|
|
43
|
+
* **Windows**: Yit will attempt to auto-install it via `winget` if missing.
|
|
44
|
+
* **macOS**: `brew install mpv`
|
|
45
|
+
* **Linux**: `sudo apt install mpv` (or your distro's equivalent).
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## ⚡ Quick Start
|
|
50
|
+
|
|
51
|
+
### 1. Search & Play
|
|
52
|
+
```bash
|
|
53
|
+
# Search for a song
|
|
54
|
+
yit search "lofi hip hop"
|
|
55
|
+
|
|
56
|
+
# Auto-play the first result immediately
|
|
57
|
+
yit search "daft punk" -p
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Control Playback
|
|
61
|
+
```bash
|
|
62
|
+
yit pause # (or 'p')
|
|
63
|
+
yit resume # (or 'r')
|
|
64
|
+
yit toggle # Toggle play/pause
|
|
65
|
+
yit stop # Kill the player
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 3. Queue Management
|
|
69
|
+
```bash
|
|
70
|
+
yit add 1 # Add result #1 from your last search to the queue
|
|
71
|
+
yit queue # Show the current playlist
|
|
72
|
+
yit next # Skip track (or 'n')
|
|
73
|
+
yit back # Previous track (or 'b')
|
|
74
|
+
yit clear # Wipe the queue
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 4. Looping
|
|
78
|
+
```bash
|
|
79
|
+
yit loop # Loop the current track indefinitely
|
|
80
|
+
yit unloop # Return to normal playback
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 5. Status
|
|
84
|
+
```bash
|
|
85
|
+
yit status # Check if currently Playing/Paused/Looped
|
|
86
|
+
```
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 🤖 For AI Agents & Vibe Coding
|
|
90
|
+
|
|
91
|
+
Yit is designed to be **self-documenting** for AI context.
|
|
92
|
+
If you are building an AI agent or using an LLM in your IDE:
|
|
93
|
+
|
|
94
|
+
1. **Read context**: Point your agent to [AI_INSTRUCTIONS.md](AI_INSTRUCTIONS.md) (included in the repo).
|
|
95
|
+
2. **Discovery**: Run `yit commands` to get a JSON list of all capabilities.
|
|
96
|
+
3. **State**: Run `yit agent` to get the full player state (Track, Time, Queue) in pure JSON.
|
|
97
|
+
|
|
98
|
+
**Example Agent Output (`yit agent`):**
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"status": "playing",
|
|
102
|
+
"track": {
|
|
103
|
+
"title": "Never Gonna Give You Up",
|
|
104
|
+
"url": "https://..."
|
|
105
|
+
},
|
|
106
|
+
"position": 45.2,
|
|
107
|
+
"duration": 212.0,
|
|
108
|
+
"queue_length": 5
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 🛠️ Architecture
|
|
115
|
+
|
|
116
|
+
* **Client**: Python CLI (`yit`) handles argument parsing and user signals.
|
|
117
|
+
* **Daemon**: A detached `mpv` process handles audio decoding and network streaming.
|
|
118
|
+
* **Communication**: IPC (Inter-Process Communication) via Named Pipes (Windows) or Unix Sockets (Linux/Mac).
|
|
119
|
+
* **Persistence**: `~/.yit/history.json` stores your playback history and queue metadata.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Yit (YouTube in Terminal) Player 🎵
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/yit-player)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
|
|
7
|
+
**The Fire-and-Forget Music Player for Developers.**
|
|
8
|
+
|
|
9
|
+
Yit (YouTube in Terminal) is a lightweight, headless, terminal-based audio player designed for flow states. It allows you to search, queue, and control music directly from your CLI without ever touching a browser or a heavy GUI.
|
|
10
|
+
|
|
11
|
+
It runs in the background (daemonized), meaning you can close your terminal, switch tabs, or keep coding while the music plays.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 🚀 Features
|
|
16
|
+
|
|
17
|
+
* **Daemon Architecture**: The player runs as a detached background process. Your terminal is never blocked.
|
|
18
|
+
* **Instant Search**: Uses `yt-dlp` to fetch metadata in milliseconds.
|
|
19
|
+
* **Smart Queue**: Manage your playlist (`add`, `next`, `back`, `Loop`) with simple commands.
|
|
20
|
+
* **Cross-Platform**: Works natively on **Windows**, **macOS**, and **Linux**.
|
|
21
|
+
* **Agent-Native**: Built from the ground up to be controlled by AI Agents (Vibe Coding).
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 📦 Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install yit-player
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Requirements
|
|
32
|
+
Yit uses **[mpv](https://mpv.io/)** as its audio engine.
|
|
33
|
+
* **Windows**: Yit will attempt to auto-install it via `winget` if missing.
|
|
34
|
+
* **macOS**: `brew install mpv`
|
|
35
|
+
* **Linux**: `sudo apt install mpv` (or your distro's equivalent).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## ⚡ Quick Start
|
|
40
|
+
|
|
41
|
+
### 1. Search & Play
|
|
42
|
+
```bash
|
|
43
|
+
# Search for a song
|
|
44
|
+
yit search "lofi hip hop"
|
|
45
|
+
|
|
46
|
+
# Auto-play the first result immediately
|
|
47
|
+
yit search "daft punk" -p
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Control Playback
|
|
51
|
+
```bash
|
|
52
|
+
yit pause # (or 'p')
|
|
53
|
+
yit resume # (or 'r')
|
|
54
|
+
yit toggle # Toggle play/pause
|
|
55
|
+
yit stop # Kill the player
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Queue Management
|
|
59
|
+
```bash
|
|
60
|
+
yit add 1 # Add result #1 from your last search to the queue
|
|
61
|
+
yit queue # Show the current playlist
|
|
62
|
+
yit next # Skip track (or 'n')
|
|
63
|
+
yit back # Previous track (or 'b')
|
|
64
|
+
yit clear # Wipe the queue
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 4. Looping
|
|
68
|
+
```bash
|
|
69
|
+
yit loop # Loop the current track indefinitely
|
|
70
|
+
yit unloop # Return to normal playback
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 5. Status
|
|
74
|
+
```bash
|
|
75
|
+
yit status # Check if currently Playing/Paused/Looped
|
|
76
|
+
```
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 🤖 For AI Agents & Vibe Coding
|
|
80
|
+
|
|
81
|
+
Yit is designed to be **self-documenting** for AI context.
|
|
82
|
+
If you are building an AI agent or using an LLM in your IDE:
|
|
83
|
+
|
|
84
|
+
1. **Read context**: Point your agent to [AI_INSTRUCTIONS.md](AI_INSTRUCTIONS.md) (included in the repo).
|
|
85
|
+
2. **Discovery**: Run `yit commands` to get a JSON list of all capabilities.
|
|
86
|
+
3. **State**: Run `yit agent` to get the full player state (Track, Time, Queue) in pure JSON.
|
|
87
|
+
|
|
88
|
+
**Example Agent Output (`yit agent`):**
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"status": "playing",
|
|
92
|
+
"track": {
|
|
93
|
+
"title": "Never Gonna Give You Up",
|
|
94
|
+
"url": "https://..."
|
|
95
|
+
},
|
|
96
|
+
"position": 45.2,
|
|
97
|
+
"duration": 212.0,
|
|
98
|
+
"queue_length": 5
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 🛠️ Architecture
|
|
105
|
+
|
|
106
|
+
* **Client**: Python CLI (`yit`) handles argument parsing and user signals.
|
|
107
|
+
* **Daemon**: A detached `mpv` process handles audio decoding and network streaming.
|
|
108
|
+
* **Communication**: IPC (Inter-Process Communication) via Named Pipes (Windows) or Unix Sockets (Linux/Mac).
|
|
109
|
+
* **Persistence**: `~/.yit/history.json` stores your playback history and queue metadata.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "yit-player"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "YouTube in Terminal - Fire-and-Forget Music CLI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{name = "Yit Team"}]
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
requires-python = ">=3.7"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"yt-dlp>=2023.0.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
yit = "yit:main"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/yit"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
yt-dlp
|
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
|
|
11
|
+
# Constants
|
|
12
|
+
# Constants
|
|
13
|
+
YIT_DIR = Path.home() / ".yit"
|
|
14
|
+
RESULTS_FILE = YIT_DIR / "results.json"
|
|
15
|
+
HISTORY_FILE = YIT_DIR / "history.json"
|
|
16
|
+
|
|
17
|
+
if os.name == 'nt':
|
|
18
|
+
IPC_PIPE = r"\\.\pipe\yit_socket"
|
|
19
|
+
else:
|
|
20
|
+
IPC_PIPE = str(Path.home() / ".yit" / "socket")
|
|
21
|
+
|
|
22
|
+
def install_mpv():
|
|
23
|
+
"""Attempts to install MPV based on OS."""
|
|
24
|
+
system = platform.system()
|
|
25
|
+
print(f"MPV not found. Attempting to install for {system}...")
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
if system == "Windows":
|
|
29
|
+
print("Running: winget install mpv.mpv")
|
|
30
|
+
subprocess.run(["winget", "install", "mpv.mpv"], check=True)
|
|
31
|
+
print("Installation complete. Please restart your terminal if Yit doesn't find it immediately.")
|
|
32
|
+
elif system == "Darwin": # macOS
|
|
33
|
+
if subprocess.run(["which", "brew"], capture_output=True).returncode == 0:
|
|
34
|
+
print("Running: brew install mpv")
|
|
35
|
+
subprocess.run(["brew", "install", "mpv"], check=True)
|
|
36
|
+
else:
|
|
37
|
+
print("Homebrew not found. Please install mpv manually: brew install mpv")
|
|
38
|
+
elif system == "Linux":
|
|
39
|
+
print("Please install mpv manually (e.g., sudo apt install mpv).")
|
|
40
|
+
# Linux distros vary too much to auto-install safely without sudo
|
|
41
|
+
except Exception as e:
|
|
42
|
+
print(f"Installation failed: {e}")
|
|
43
|
+
print("Please install mpv manually.")
|
|
44
|
+
|
|
45
|
+
def check_dependencies():
|
|
46
|
+
"""Checks if MPV is installed."""
|
|
47
|
+
try:
|
|
48
|
+
subprocess.run(["mpv", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
49
|
+
return True
|
|
50
|
+
except FileNotFoundError:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def ensure_yit_dir():
|
|
54
|
+
if not YIT_DIR.exists():
|
|
55
|
+
YIT_DIR.mkdir()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def save_to_history(track):
|
|
60
|
+
"""Saves a track to the persistent history file."""
|
|
61
|
+
ensure_yit_dir()
|
|
62
|
+
history = []
|
|
63
|
+
if HISTORY_FILE.exists():
|
|
64
|
+
try:
|
|
65
|
+
with open(HISTORY_FILE, "r") as f:
|
|
66
|
+
history = json.load(f)
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
# Check for duplicates or update
|
|
71
|
+
existing = None
|
|
72
|
+
for item in history:
|
|
73
|
+
if item.get("url") == track["url"]:
|
|
74
|
+
existing = item
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
if not existing:
|
|
78
|
+
history.append(track)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
with open(HISTORY_FILE, "w") as f:
|
|
82
|
+
json.dump(history, f, indent=4)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"Warning: Could not save history: {e}")
|
|
85
|
+
|
|
86
|
+
def send_ipc_command(command):
|
|
87
|
+
"""Sends a JSON-formatted command to the MPV IPC pipe."""
|
|
88
|
+
try:
|
|
89
|
+
with open(IPC_PIPE, "r+b", buffering=0) as f:
|
|
90
|
+
payload = json.dumps(command).encode("utf-8") + b"\n"
|
|
91
|
+
f.write(payload)
|
|
92
|
+
response_line = f.readline().decode("utf-8")
|
|
93
|
+
if response_line:
|
|
94
|
+
return json.loads(response_line)
|
|
95
|
+
return {"error": "no_response"}
|
|
96
|
+
except FileNotFoundError:
|
|
97
|
+
print("Yit is not running.")
|
|
98
|
+
return None
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print(f"Error communicating with player: {e}")
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def get_ipc_property(prop):
|
|
104
|
+
"""Gets a property from MPV."""
|
|
105
|
+
try:
|
|
106
|
+
with open(IPC_PIPE, "r+b", buffering=0) as f:
|
|
107
|
+
cmd = {"command": ["get_property", prop]}
|
|
108
|
+
payload = json.dumps(cmd).encode("utf-8") + b"\n"
|
|
109
|
+
f.write(payload)
|
|
110
|
+
|
|
111
|
+
# Simple read line
|
|
112
|
+
response = f.readline().decode("utf-8")
|
|
113
|
+
return json.loads(response)
|
|
114
|
+
except FileNotFoundError:
|
|
115
|
+
return None
|
|
116
|
+
except Exception:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
def cmd_search(args):
|
|
120
|
+
"""Searches YouTube and stores results."""
|
|
121
|
+
ensure_yit_dir()
|
|
122
|
+
query = " ".join(args.query)
|
|
123
|
+
print(f"Searching for '{query}'...")
|
|
124
|
+
|
|
125
|
+
# using yt-dlp via subprocess for simplicity and speed
|
|
126
|
+
# ytsearch5: gets 5 results
|
|
127
|
+
cmd = [
|
|
128
|
+
"yt-dlp",
|
|
129
|
+
"--print", "%(title)s|%(id)s",
|
|
130
|
+
"--flat-playlist",
|
|
131
|
+
f"ytsearch5:{query}"
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# Use full path to yt-dlp if needed, assuming it's in venv or path
|
|
136
|
+
# In this environment, we should rely on the venv activation or use sys.executable to find it
|
|
137
|
+
# But for now, let's assume 'yt-dlp' is in PATH or we use the one we just installed.
|
|
138
|
+
# Ideally, we should use the library, but subprocess is often more stable for simple "get strings"
|
|
139
|
+
|
|
140
|
+
# Let's try to use the python library method to be cleaner if possible,
|
|
141
|
+
# but subprocess is standard for this tool type.
|
|
142
|
+
|
|
143
|
+
# Use system yt-dlp since we are a package now
|
|
144
|
+
yt_dlp_path = "yt-dlp"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
command = [str(yt_dlp_path), "--print", "%(title)s||||%(webpage_url)s", "--flat-playlist", f"ytsearch5:{query}"]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
result = subprocess.run(
|
|
151
|
+
command,
|
|
152
|
+
capture_output=True,
|
|
153
|
+
text=True,
|
|
154
|
+
encoding='utf-8',
|
|
155
|
+
errors='replace',
|
|
156
|
+
check=False
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if result.returncode != 0:
|
|
160
|
+
print(f"Error: yt-dlp returned {result.returncode}")
|
|
161
|
+
print(f"Stderr: {result.stderr}")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
if result.stdout is None:
|
|
165
|
+
print("Error: stdout is None")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
lines = result.stdout.strip().split('\n')
|
|
169
|
+
results = []
|
|
170
|
+
print("\nResults:")
|
|
171
|
+
for i, line in enumerate(lines):
|
|
172
|
+
if "||||" in line:
|
|
173
|
+
title, url = line.split("||||", 1)
|
|
174
|
+
print(f"{i+1}. {title}")
|
|
175
|
+
results.append({"title": title, "url": url})
|
|
176
|
+
|
|
177
|
+
if not results:
|
|
178
|
+
print("No results found.")
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
with open(RESULTS_FILE, "w") as f:
|
|
182
|
+
json.dump(results, f, indent=4)
|
|
183
|
+
|
|
184
|
+
except subprocess.CalledProcessError as e:
|
|
185
|
+
print(f"Error searching: {e}")
|
|
186
|
+
except Exception as e:
|
|
187
|
+
print(f"Unexpected error: {e}\nTry running the setup_installer.bat")
|
|
188
|
+
|
|
189
|
+
if args.play and results:
|
|
190
|
+
print("\nAuto-playing result #1...")
|
|
191
|
+
cmd_play(SimpleNamespace(number=1))
|
|
192
|
+
|
|
193
|
+
def cmd_play(args):
|
|
194
|
+
"""Plays the selected track number."""
|
|
195
|
+
if not RESULTS_FILE.exists():
|
|
196
|
+
print("No search results found. Run 'yit search <query>' first.")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
with open(RESULTS_FILE, "r") as f:
|
|
201
|
+
results = json.load(f)
|
|
202
|
+
|
|
203
|
+
idx = args.number - 1
|
|
204
|
+
if idx < 0 or idx >= len(results):
|
|
205
|
+
print("Invalid selection number.")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
track = results[idx]
|
|
209
|
+
print(f"Playing: {track['title']}")
|
|
210
|
+
save_to_history(track)
|
|
211
|
+
|
|
212
|
+
# Check if running
|
|
213
|
+
is_running = send_ipc_command({"command": ["loadfile", track["url"]]})
|
|
214
|
+
|
|
215
|
+
if is_running:
|
|
216
|
+
print("Added to existing player.")
|
|
217
|
+
# Ensure it plays immediately even if previously paused
|
|
218
|
+
send_ipc_command({"command": ["set_property", "pause", False]})
|
|
219
|
+
else:
|
|
220
|
+
# Spawn new
|
|
221
|
+
cmd = [
|
|
222
|
+
"mpv",
|
|
223
|
+
"--no-video",
|
|
224
|
+
"--idle",
|
|
225
|
+
"--cache=yes",
|
|
226
|
+
"--prefetch-playlist=yes",
|
|
227
|
+
"--demuxer-max-bytes=128M",
|
|
228
|
+
"--demuxer-max-back-bytes=128M",
|
|
229
|
+
f"--input-ipc-server={IPC_PIPE}",
|
|
230
|
+
track["url"]
|
|
231
|
+
]
|
|
232
|
+
# Prepare env with yt-dlp in path
|
|
233
|
+
env = os.environ.copy()
|
|
234
|
+
yt_dlp_path = Path(sys.executable).parent
|
|
235
|
+
env["PATH"] = str(yt_dlp_path) + os.pathsep + env["PATH"]
|
|
236
|
+
|
|
237
|
+
# Prepare subprocess args based on OS
|
|
238
|
+
kwargs = {
|
|
239
|
+
"stdout": subprocess.DEVNULL,
|
|
240
|
+
"stderr": subprocess.DEVNULL,
|
|
241
|
+
"env": env,
|
|
242
|
+
"close_fds": True
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if os.name == 'nt':
|
|
246
|
+
kwargs["creationflags"] = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
|
247
|
+
else:
|
|
248
|
+
kwargs["start_new_session"] = True
|
|
249
|
+
|
|
250
|
+
# Detach process
|
|
251
|
+
subprocess.Popen(cmd, **kwargs)
|
|
252
|
+
print("Player started in background.")
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
print(f"Error playing: {e}")
|
|
256
|
+
|
|
257
|
+
def cmd_pause(args):
|
|
258
|
+
send_ipc_command({"command": ["set_property", "pause", True]})
|
|
259
|
+
print("Paused.")
|
|
260
|
+
|
|
261
|
+
def cmd_resume(args):
|
|
262
|
+
send_ipc_command({"command": ["set_property", "pause", False]})
|
|
263
|
+
print("Resumed.")
|
|
264
|
+
|
|
265
|
+
def cmd_toggle(args):
|
|
266
|
+
send_ipc_command({"command": ["cycle", "pause"]})
|
|
267
|
+
print("Toggled playback.")
|
|
268
|
+
|
|
269
|
+
def cmd_stop(args):
|
|
270
|
+
send_ipc_command({"command": ["quit"]})
|
|
271
|
+
print("Stopped.")
|
|
272
|
+
|
|
273
|
+
def cmd_loop(args):
|
|
274
|
+
send_ipc_command({"command": ["set_property", "loop-file", "inf"]})
|
|
275
|
+
print("Looping current track.")
|
|
276
|
+
|
|
277
|
+
def cmd_unloop(args):
|
|
278
|
+
send_ipc_command({"command": ["set_property", "loop-file", "no"]})
|
|
279
|
+
print("Unlooped. Playback will continue normally.")
|
|
280
|
+
|
|
281
|
+
def cmd_add(args):
|
|
282
|
+
"""Appends the selected track number to the queue."""
|
|
283
|
+
if not RESULTS_FILE.exists():
|
|
284
|
+
print("No search results found. Run 'yit search <query>' first.")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
with open(RESULTS_FILE, "r") as f:
|
|
289
|
+
results = json.load(f)
|
|
290
|
+
|
|
291
|
+
idx = args.number - 1
|
|
292
|
+
if idx < 0 or idx >= len(results):
|
|
293
|
+
print("Invalid selection number.")
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
track = results[idx]
|
|
297
|
+
print(f"Adding to queue: {track['title']}")
|
|
298
|
+
save_to_history(track)
|
|
299
|
+
|
|
300
|
+
# Determine if we need to spawn mpv or just append
|
|
301
|
+
# Try to append first
|
|
302
|
+
res = send_ipc_command({"command": ["loadfile", track["url"], "append-play"]})
|
|
303
|
+
|
|
304
|
+
if not res or res.get("error") != "success":
|
|
305
|
+
# If not running, play normally (which spawns)
|
|
306
|
+
print("Player not running (or append failed), starting new queue...")
|
|
307
|
+
cmd_play(args) # This will spawn it
|
|
308
|
+
else:
|
|
309
|
+
print("Added to queue.")
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
print(f"Error adding to queue: {e}")
|
|
313
|
+
|
|
314
|
+
def cmd_next(args):
|
|
315
|
+
send_ipc_command({"command": ["playlist-next"]})
|
|
316
|
+
print("Skipping to next track...")
|
|
317
|
+
|
|
318
|
+
def cmd_prev(args):
|
|
319
|
+
send_ipc_command({"command": ["playlist-prev"]})
|
|
320
|
+
print("Going to previous track...")
|
|
321
|
+
|
|
322
|
+
def cmd_restart(args):
|
|
323
|
+
send_ipc_command({"command": ["seek", 0, "absolute"]})
|
|
324
|
+
send_ipc_command({"command": ["set_property", "pause", False]})
|
|
325
|
+
print("Restarting current track...")
|
|
326
|
+
|
|
327
|
+
def cmd_clear(args):
|
|
328
|
+
send_ipc_command({"command": ["playlist-clear"]})
|
|
329
|
+
print("Queue cleared.")
|
|
330
|
+
|
|
331
|
+
def extract_video_id(url):
|
|
332
|
+
"""Extracts YouTube Video ID from URL."""
|
|
333
|
+
if not url: return None
|
|
334
|
+
# Standard v= parameter
|
|
335
|
+
if "v=" in url:
|
|
336
|
+
try:
|
|
337
|
+
return url.split("v=")[1].split("&")[0][:11] # ID is 11 chars
|
|
338
|
+
except:
|
|
339
|
+
pass
|
|
340
|
+
# Shortened youtu.be/ID
|
|
341
|
+
if "youtu.be/" in url:
|
|
342
|
+
try:
|
|
343
|
+
return url.split("youtu.be/")[1][:11]
|
|
344
|
+
except:
|
|
345
|
+
pass
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
def cmd_queue(args):
|
|
349
|
+
# Get playlist info
|
|
350
|
+
resp = get_ipc_property("playlist")
|
|
351
|
+
if not resp or resp.get("error") != "success":
|
|
352
|
+
print("Queue is empty (or player not running).")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
playlist = resp.get("data", [])
|
|
356
|
+
if not playlist:
|
|
357
|
+
print("Queue is empty.")
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
# Load local results to resolve titles if missing in MPV
|
|
361
|
+
# Map both full URL and Video ID to title
|
|
362
|
+
url_map = {}
|
|
363
|
+
id_map = {}
|
|
364
|
+
|
|
365
|
+
# Helper to load a list of items into the maps
|
|
366
|
+
def load_into_maps(items):
|
|
367
|
+
for item in items:
|
|
368
|
+
url = item["url"].strip("| ")
|
|
369
|
+
title = item["title"]
|
|
370
|
+
url_map[url] = title
|
|
371
|
+
vid = extract_video_id(url)
|
|
372
|
+
if vid:
|
|
373
|
+
id_map[vid] = title
|
|
374
|
+
|
|
375
|
+
# 1. Load Results (Current Search)
|
|
376
|
+
if RESULTS_FILE.exists():
|
|
377
|
+
try:
|
|
378
|
+
with open(RESULTS_FILE, "r") as f:
|
|
379
|
+
load_into_maps(json.load(f))
|
|
380
|
+
except Exception: pass
|
|
381
|
+
|
|
382
|
+
# 2. Load History (Persistent Cache)
|
|
383
|
+
if HISTORY_FILE.exists():
|
|
384
|
+
try:
|
|
385
|
+
with open(HISTORY_FILE, "r") as f:
|
|
386
|
+
load_into_maps(json.load(f))
|
|
387
|
+
except Exception: pass
|
|
388
|
+
|
|
389
|
+
print("\nCurrent Queue:")
|
|
390
|
+
for i, item in enumerate(playlist):
|
|
391
|
+
prefix = "-> " if item.get("current") else " "
|
|
392
|
+
|
|
393
|
+
# Priority: MPV Title -> Local Cache (ID) -> Local Cache (URL) -> Filename -> Unknown
|
|
394
|
+
title = item.get("title")
|
|
395
|
+
|
|
396
|
+
if not title:
|
|
397
|
+
# MPV 'filename' field holds the URL
|
|
398
|
+
url = item.get("filename", "")
|
|
399
|
+
|
|
400
|
+
# Try exact match
|
|
401
|
+
if url in url_map:
|
|
402
|
+
title = url_map[url]
|
|
403
|
+
else:
|
|
404
|
+
# Try ID match
|
|
405
|
+
vid = extract_video_id(url)
|
|
406
|
+
if vid and vid in id_map:
|
|
407
|
+
title = id_map[vid]
|
|
408
|
+
else:
|
|
409
|
+
title = url or "Unknown"
|
|
410
|
+
|
|
411
|
+
print(f"{prefix}{i+1}. {title}")
|
|
412
|
+
|
|
413
|
+
def cmd_status(args):
|
|
414
|
+
resp = get_ipc_property("media-title")
|
|
415
|
+
if resp and resp.get("error") == "success" and resp.get("data"):
|
|
416
|
+
title = resp.get("data")
|
|
417
|
+
|
|
418
|
+
# Check pause status
|
|
419
|
+
paused = get_ipc_property("pause")
|
|
420
|
+
looping = get_ipc_property("loop-file")
|
|
421
|
+
|
|
422
|
+
status_str = "[Paused]" if paused and paused.get("data") else "[Playing]"
|
|
423
|
+
|
|
424
|
+
if looping and looping.get("data") in ["inf", "yes"]:
|
|
425
|
+
status_str += " [Looped]"
|
|
426
|
+
|
|
427
|
+
print(f"{status_str} {title}")
|
|
428
|
+
else:
|
|
429
|
+
# Check if running at least
|
|
430
|
+
if get_ipc_property("idle-active"):
|
|
431
|
+
print("Queue is empty.")
|
|
432
|
+
else:
|
|
433
|
+
print("Yit is not running.")
|
|
434
|
+
|
|
435
|
+
def cmd_agent(args):
|
|
436
|
+
"""Outputs full player state as JSON for AI agents."""
|
|
437
|
+
state = {
|
|
438
|
+
"status": "stopped",
|
|
439
|
+
"track": {},
|
|
440
|
+
"position": 0,
|
|
441
|
+
"duration": 0,
|
|
442
|
+
"volume": 0,
|
|
443
|
+
"loop": False,
|
|
444
|
+
"queue_length": 0
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
# Check if running
|
|
448
|
+
idle_resp = get_ipc_property("idle-active")
|
|
449
|
+
if not idle_resp:
|
|
450
|
+
print(json.dumps(state, indent=2))
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
# Gather data sequentially
|
|
454
|
+
pause_resp = get_ipc_property("pause")
|
|
455
|
+
title_resp = get_ipc_property("media-title")
|
|
456
|
+
path_resp = get_ipc_property("path") # often URL
|
|
457
|
+
time_resp = get_ipc_property("time-pos")
|
|
458
|
+
dur_resp = get_ipc_property("duration")
|
|
459
|
+
vol_resp = get_ipc_property("volume")
|
|
460
|
+
loop_resp = get_ipc_property("loop-file")
|
|
461
|
+
playlist_resp = get_ipc_property("playlist-count")
|
|
462
|
+
|
|
463
|
+
# Process Status
|
|
464
|
+
if pause_resp and pause_resp.get("data") is True:
|
|
465
|
+
state["status"] = "paused"
|
|
466
|
+
elif pause_resp and pause_resp.get("data") is False:
|
|
467
|
+
state["status"] = "playing"
|
|
468
|
+
|
|
469
|
+
# Process Track Info
|
|
470
|
+
if title_resp and title_resp.get("data"):
|
|
471
|
+
state["track"]["title"] = title_resp["data"]
|
|
472
|
+
if path_resp and path_resp.get("data"):
|
|
473
|
+
state["track"]["url"] = path_resp["data"]
|
|
474
|
+
|
|
475
|
+
# Playback Info
|
|
476
|
+
if time_resp and time_resp.get("data"):
|
|
477
|
+
state["position"] = time_resp["data"]
|
|
478
|
+
if dur_resp and dur_resp.get("data"):
|
|
479
|
+
state["duration"] = dur_resp["data"]
|
|
480
|
+
if vol_resp and vol_resp.get("data"):
|
|
481
|
+
state["volume"] = vol_resp["data"]
|
|
482
|
+
|
|
483
|
+
# Loop Status
|
|
484
|
+
if loop_resp and loop_resp.get("data") in ["inf", "yes"]:
|
|
485
|
+
state["loop"] = True
|
|
486
|
+
|
|
487
|
+
# Queue Info
|
|
488
|
+
if playlist_resp and playlist_resp.get("data"):
|
|
489
|
+
state["queue_length"] = playlist_resp["data"]
|
|
490
|
+
|
|
491
|
+
print(json.dumps(state, indent=2))
|
|
492
|
+
|
|
493
|
+
def cmd_commands(args):
|
|
494
|
+
"""Outputs available commands as JSON for AI agents."""
|
|
495
|
+
cmds = [
|
|
496
|
+
{"cmd": "search", "usage": "yit search <query> [-p]", "desc": "Search YouTube. -p to auto-play."},
|
|
497
|
+
{"cmd": "play", "usage": "yit play <index>", "desc": "Play a track from results."},
|
|
498
|
+
{"cmd": "add", "usage": "yit add <index>", "desc": "Add a track to queue."},
|
|
499
|
+
{"cmd": "pause", "usage": "yit pause", "desc": "Pause playback."},
|
|
500
|
+
{"cmd": "resume", "usage": "yit resume", "desc": "Resume playback."},
|
|
501
|
+
{"cmd": "stop", "usage": "yit stop", "desc": "Stop playback completely."},
|
|
502
|
+
{"cmd": "next", "usage": "yit next", "desc": "Skip to next track."},
|
|
503
|
+
{"cmd": "back", "usage": "yit back", "desc": "Go to previous track."},
|
|
504
|
+
{"cmd": "loop", "usage": "yit loop", "desc": "Loop current track indefinitely."},
|
|
505
|
+
{"cmd": "unloop", "usage": "yit unloop", "desc": "Stop looping."},
|
|
506
|
+
{"cmd": "queue", "usage": "yit queue", "desc": "Show current queue."},
|
|
507
|
+
{"cmd": "clear", "usage": "yit clear", "desc": "Clear the queue."},
|
|
508
|
+
{"cmd": "status", "usage": "yit status", "desc": "Show playback status (text)."},
|
|
509
|
+
{"cmd": "agent", "usage": "yit agent", "desc": "Get full system state (JSON)."},
|
|
510
|
+
{"cmd": "commands", "usage": "yit commands", "desc": "Get this list (JSON)."},
|
|
511
|
+
{"cmd": "0", "usage": "yit 0", "desc": "Replay current track."}
|
|
512
|
+
]
|
|
513
|
+
print(json.dumps(cmds, indent=2))
|
|
514
|
+
|
|
515
|
+
def main():
|
|
516
|
+
if not check_dependencies():
|
|
517
|
+
print("MPV is required but not found in PATH.")
|
|
518
|
+
print("Yit can attempt to install it for you.")
|
|
519
|
+
response = input("Install MPV now? (Y/n): ").strip().lower()
|
|
520
|
+
if response in ["", "y", "yes"]:
|
|
521
|
+
install_mpv()
|
|
522
|
+
if not check_dependencies():
|
|
523
|
+
print("Installation completed but 'mpv' is not yet in PATH.")
|
|
524
|
+
print("Please restart your terminal/shell and try again.")
|
|
525
|
+
sys.exit(1)
|
|
526
|
+
else:
|
|
527
|
+
print("MPV is required for Yit. Exiting.")
|
|
528
|
+
sys.exit(1)
|
|
529
|
+
|
|
530
|
+
parser = argparse.ArgumentParser(description="Yit (YouTube in Terminal) - Fire-and-Forget Music Player")
|
|
531
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
532
|
+
|
|
533
|
+
# Search
|
|
534
|
+
parser_search = subparsers.add_parser("search", help="Search YouTube")
|
|
535
|
+
parser_search.add_argument("query", nargs="+", help="Search query")
|
|
536
|
+
parser_search.add_argument("-p", "--play", action="store_true", help="Auto-play the first result")
|
|
537
|
+
parser_search.set_defaults(func=cmd_search)
|
|
538
|
+
|
|
539
|
+
# Play
|
|
540
|
+
parser_play = subparsers.add_parser("play", help="Play a song by number")
|
|
541
|
+
parser_play.add_argument("number", type=int, help="Track number from search results")
|
|
542
|
+
parser_play.set_defaults(func=cmd_play)
|
|
543
|
+
|
|
544
|
+
# Controls
|
|
545
|
+
parser_pause = subparsers.add_parser("pause", aliases=["p"], help="Pause playback")
|
|
546
|
+
parser_pause.set_defaults(func=cmd_pause)
|
|
547
|
+
|
|
548
|
+
parser_resume = subparsers.add_parser("resume", aliases=["r"], help="Resume playback")
|
|
549
|
+
parser_resume.set_defaults(func=cmd_resume)
|
|
550
|
+
|
|
551
|
+
parser_toggle = subparsers.add_parser("toggle", help="Toggle pause/resume")
|
|
552
|
+
parser_toggle.set_defaults(func=cmd_toggle)
|
|
553
|
+
|
|
554
|
+
parser_stop = subparsers.add_parser("stop", help="Stop playback")
|
|
555
|
+
parser_stop.set_defaults(func=cmd_stop)
|
|
556
|
+
|
|
557
|
+
# Add to queue
|
|
558
|
+
parser_add = subparsers.add_parser("add", help="Add song to queue")
|
|
559
|
+
parser_add.add_argument("number", type=int, help="Track number")
|
|
560
|
+
parser_add.set_defaults(func=cmd_add)
|
|
561
|
+
|
|
562
|
+
# Queue management
|
|
563
|
+
parser_queue = subparsers.add_parser("queue", help="Show queue")
|
|
564
|
+
parser_queue.set_defaults(func=cmd_queue)
|
|
565
|
+
|
|
566
|
+
parser_clear = subparsers.add_parser("clear", help="Clear queue")
|
|
567
|
+
parser_clear.set_defaults(func=cmd_clear)
|
|
568
|
+
|
|
569
|
+
# Fast Navigation (Safe Aliases)
|
|
570
|
+
# Next
|
|
571
|
+
parser_next = subparsers.add_parser("next", aliases=["n"], help="Next track")
|
|
572
|
+
parser_next.set_defaults(func=cmd_next)
|
|
573
|
+
|
|
574
|
+
# Previous (Back)
|
|
575
|
+
parser_prev = subparsers.add_parser("back", aliases=["b"], help="Previous track")
|
|
576
|
+
parser_prev.set_defaults(func=cmd_prev)
|
|
577
|
+
|
|
578
|
+
# Replay/Restart
|
|
579
|
+
parser_restart = subparsers.add_parser("replay", aliases=["0"], help="Replay current track")
|
|
580
|
+
parser_restart.set_defaults(func=cmd_restart)
|
|
581
|
+
|
|
582
|
+
parser_status = subparsers.add_parser("status", help="Show status")
|
|
583
|
+
parser_status.set_defaults(func=cmd_status)
|
|
584
|
+
|
|
585
|
+
# Agent Interface
|
|
586
|
+
parser_agent = subparsers.add_parser("agent", help="JSON output for AI agents")
|
|
587
|
+
parser_agent.set_defaults(func=cmd_agent)
|
|
588
|
+
|
|
589
|
+
parser_cmds = subparsers.add_parser("commands", help="JSON command list for AI agents")
|
|
590
|
+
parser_cmds.set_defaults(func=cmd_commands)
|
|
591
|
+
|
|
592
|
+
# Loop
|
|
593
|
+
subparsers.add_parser("loop", help="Loop current track").set_defaults(func=cmd_loop)
|
|
594
|
+
subparsers.add_parser("unloop", help="Stop looping").set_defaults(func=cmd_unloop)
|
|
595
|
+
|
|
596
|
+
args = parser.parse_args()
|
|
597
|
+
args.func(args)
|
|
598
|
+
|
|
599
|
+
if __name__ == "__main__":
|
|
600
|
+
main()
|