srgplayer 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.
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: srgplayer
3
+ Version: 0.1.0
4
+ Summary: Terminal music player that streams from YouTube
5
+ Author-email: Your Name <you@example.com>
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Requires-Dist: python-mpv
9
+ Requires-Dist: readchar
10
+ Requires-Dist: yt-dlp
File without changes
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "srgplayer"
3
+ version = "0.1.0"
4
+ description = "Terminal music player that streams from YouTube"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = { text = "MIT" }
8
+ authors = [
9
+ { name = "Your Name", email = "you@example.com" }
10
+ ]
11
+ dependencies = [
12
+ "python-mpv",
13
+ "yt-dlp",
14
+ "readchar",
15
+ ]
16
+
17
+ [project.scripts]
18
+ srgplayer = "srgplayer:main"
19
+
20
+ [build-system]
21
+ requires = ["hatchling"]
22
+ build-backend = "hatchling.build"
@@ -0,0 +1,3 @@
1
+ from srgplayer.player import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Terminal Music Player
4
+ ----------------------
5
+ Search and stream songs from YouTube directly in your terminal,
6
+ with simple single-key shortcuts to control playback.
7
+
8
+ Requirements:
9
+ pip install python-mpv yt-dlp readchar --break-system-packages
10
+
11
+ Also requires the 'mpv' media player installed on your system
12
+ (python-mpv is just a binding to it):
13
+ Ubuntu/Debian: sudo apt install mpv
14
+ macOS: brew install mpv
15
+ Windows: https://mpv.io/installation/ (add to PATH)
16
+
17
+ Controls (press key, no Enter needed):
18
+ space pause / resume
19
+ n next song in queue
20
+ p previous song in queue
21
+ a search + add a song / playlist
22
+ l list current queue
23
+ +/- volume up / down
24
+ q quit
25
+
26
+ This version uses mpv's own native playlist engine to handle
27
+ auto-advance to the next track. That's what mpv is built for, so
28
+ it's far more reliable than trying to detect "song ended" from
29
+ Python and manually calling play() again.
30
+ """
31
+
32
+ import sys
33
+
34
+ try:
35
+ import mpv
36
+ import yt_dlp
37
+ import readchar
38
+ except ImportError as e:
39
+ print(f"Missing dependency: {e}")
40
+ print("Install with: pip install python-mpv yt-dlp readchar --break-system-packages")
41
+ sys.exit(1)
42
+
43
+
44
+ class MusicPlayer:
45
+ def __init__(self):
46
+ self.player = mpv.MPV(
47
+ ytdl=True,
48
+ video=False,
49
+ input_default_bindings=False,
50
+ terminal=False,
51
+ osc=False,
52
+ keep_open="no", # auto-advance to next playlist item at end of track
53
+ )
54
+ self.queue = [] # mirrors mpv's playlist: [{"title": str, "url": str}, ...]
55
+ self.current_index = -1
56
+
57
+ @self.player.property_observer('playlist-pos')
58
+ def _on_pos_change(_name, value):
59
+ if value is not None and 0 <= value < len(self.queue):
60
+ self.current_index = value
61
+ print(f"\n▶ Now playing: {self.queue[value]['title']}")
62
+
63
+ def _entry_to_url(self, e):
64
+ raw_url = e.get("url")
65
+ if raw_url and raw_url.startswith("http"):
66
+ return raw_url
67
+ vid_id = e.get("id")
68
+ if vid_id:
69
+ return f"https://www.youtube.com/watch?v={vid_id}"
70
+ return None
71
+
72
+ def search(self, query, limit=5):
73
+ ydl_opts = {
74
+ "quiet": True,
75
+ "no_warnings": True,
76
+ "extract_flat": "in_playlist",
77
+ "default_search": f"ytsearch{limit}",
78
+ "skip_download": True,
79
+ }
80
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
81
+ info = ydl.extract_info(query, download=False)
82
+ entries = info.get("entries", [])
83
+ results = []
84
+ for e in entries:
85
+ if not e:
86
+ continue
87
+ url = self._entry_to_url(e)
88
+ if not url:
89
+ continue
90
+ results.append({"title": e.get("title", "Unknown title"), "url": url})
91
+ return results
92
+
93
+ def is_playlist_url(self, text):
94
+ return text.strip().startswith(("http://", "https://")) and "list=" in text
95
+
96
+ def search_playlists(self, query, limit=5):
97
+ from urllib.parse import quote
98
+ # sp=EgIQAw%3D%3D filters YouTube search results to playlists only
99
+ search_url = f"https://www.youtube.com/results?search_query={quote(query)}&sp=EgIQAw%3D%3D"
100
+ ydl_opts = {
101
+ "quiet": True,
102
+ "no_warnings": True,
103
+ "extract_flat": "in_playlist",
104
+ "skip_download": True,
105
+ "playlistend": limit,
106
+ }
107
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
108
+ info = ydl.extract_info(search_url, download=False)
109
+ entries = info.get("entries", [])[:limit]
110
+ results = []
111
+ for e in entries:
112
+ if not e:
113
+ continue
114
+ pl_id = e.get("id") or e.get("url")
115
+ url = e.get("url") or f"https://www.youtube.com/playlist?list={pl_id}"
116
+ if not url.startswith("http"):
117
+ url = f"https://www.youtube.com/playlist?list={pl_id}"
118
+ results.append({"title": e.get("title", "Unknown playlist"), "url": url})
119
+ return results
120
+
121
+ def add_to_playlist(self, title, url, play_now=False):
122
+ """Add one song both to our display queue and mpv's real playlist."""
123
+ if play_now:
124
+ self.clear_queue()
125
+ self.queue.append({"title": title, "url": url})
126
+ mode = "append-play" if len(self.queue) == 1 else "append"
127
+ self.player.command("loadfile", url, mode)
128
+
129
+ def clear_queue(self):
130
+ self.player.command("playlist-clear")
131
+ self.player.command("stop")
132
+ self.queue = []
133
+ self.current_index = -1
134
+
135
+ def ask_play_now(self):
136
+ if not self.queue:
137
+ return True # nothing playing yet, no need to ask
138
+ choice = input("[n]ow (clears current queue) or add to [e]nd? (e): ").strip().lower()
139
+ return choice == "n"
140
+
141
+ def import_playlist(self, url):
142
+ play_now = self.ask_play_now()
143
+ ydl_opts = {
144
+ "quiet": True,
145
+ "no_warnings": True,
146
+ "extract_flat": "in_playlist",
147
+ "skip_download": True,
148
+ }
149
+ print("Importing playlist (this can take a moment for long playlists)...")
150
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
151
+ info = ydl.extract_info(url, download=False)
152
+ entries = info.get("entries", [])
153
+ added = 0
154
+ for e in entries:
155
+ if not e:
156
+ continue
157
+ vid_url = self._entry_to_url(e)
158
+ if not vid_url:
159
+ continue
160
+ # only clear the queue once, on the very first imported track
161
+ self.add_to_playlist(e.get("title", "Unknown title"), vid_url, play_now=(play_now and added == 0))
162
+ added += 1
163
+ print(f"Imported {added} songs from playlist.")
164
+
165
+ def add_song_interactive(self):
166
+ query = input(
167
+ "\nSearch a song, type 'playlist: <query>' to search playlists, "
168
+ "or paste a playlist URL: "
169
+ ).strip()
170
+ if not query:
171
+ return
172
+
173
+ if self.is_playlist_url(query):
174
+ self.import_playlist(query)
175
+ return
176
+
177
+ if query.lower().startswith("playlist:"):
178
+ pl_query = query.split(":", 1)[1].strip()
179
+ if not pl_query:
180
+ return
181
+ print("Searching playlists...")
182
+ results = self.search_playlists(pl_query)
183
+ if not results:
184
+ print("No playlists found.")
185
+ return
186
+ for i, r in enumerate(results):
187
+ print(f" [{i}] {r['title']}")
188
+ choice = input("Pick a playlist to import (Enter to cancel): ").strip()
189
+ if not choice.isdigit():
190
+ return
191
+ idx = int(choice)
192
+ if 0 <= idx < len(results):
193
+ self.import_playlist(results[idx]["url"])
194
+ return
195
+
196
+ print("Searching...")
197
+ results = self.search(query)
198
+ if not results:
199
+ print("No results found.")
200
+ return
201
+ for i, r in enumerate(results):
202
+ print(f" [{i}] {r['title']}")
203
+ choice = input("Pick number to add (Enter to cancel): ").strip()
204
+ if not choice.isdigit():
205
+ return
206
+ idx = int(choice)
207
+ if 0 <= idx < len(results):
208
+ play_now = self.ask_play_now()
209
+ self.add_to_playlist(results[idx]["title"], results[idx]["url"], play_now=play_now)
210
+ print(f"Added: {results[idx]['title']}")
211
+
212
+ def next_song(self):
213
+ try:
214
+ self.player.command("playlist-next", "weak")
215
+ except Exception:
216
+ print("\nAlready at the end of the queue.")
217
+
218
+ def prev_song(self):
219
+ try:
220
+ self.player.command("playlist-prev", "weak")
221
+ except Exception:
222
+ print("\nAlready at the start of the queue.")
223
+
224
+ def toggle_pause(self):
225
+ self.player.pause = not self.player.pause
226
+ state = "⏸ Paused" if self.player.pause else "▶ Playing"
227
+ print(f"\r{state} ", end="", flush=True)
228
+
229
+ def list_queue(self):
230
+ print("\n--- Queue ---")
231
+ if not self.queue:
232
+ print("(empty)")
233
+ for i, s in enumerate(self.queue):
234
+ marker = "→" if i == self.current_index else " "
235
+ print(f" {marker} [{i}] {s['title']}")
236
+ print("-------------")
237
+
238
+ def volume(self, delta):
239
+ try:
240
+ self.player.volume = max(0, min(100, self.player.volume + delta))
241
+ print(f"\rVolume: {int(self.player.volume)}% ", end="", flush=True)
242
+ except Exception:
243
+ pass
244
+
245
+ def run(self):
246
+ print("=== SRG Terminal Music Player ===")
247
+ print("Controls: [space]=pause/play [n]=next [p]=prev [a]=add song/playlist")
248
+ print(" [l]=list queue [c]=clear queue & stop [+/-]=volume [q]=quit")
249
+ print("Tip: at the prompt, type a song name to search songs,")
250
+ print(" 'playlist: <query>' to search playlists,")
251
+ print(" or paste a playlist URL directly.")
252
+ print(" When adding, you'll be asked to play it now (clears the")
253
+ print(" current queue) or add it to the end.\n")
254
+
255
+ self.add_song_interactive()
256
+
257
+ while True:
258
+ key = readchar.readkey()
259
+ if key == " ":
260
+ self.toggle_pause()
261
+ elif key.lower() == "n":
262
+ self.next_song()
263
+ elif key.lower() == "p":
264
+ self.prev_song()
265
+ elif key.lower() == "a":
266
+ self.add_song_interactive()
267
+ elif key.lower() == "l":
268
+ self.list_queue()
269
+ elif key.lower() == "c":
270
+ self.clear_queue()
271
+ print("\nQueue cleared and playback stopped.")
272
+ elif key == "+":
273
+ self.volume(5)
274
+ elif key == "-":
275
+ self.volume(-5)
276
+ elif key.lower() == "q":
277
+ print("\nBye!")
278
+ self.player.terminate()
279
+ sys.exit(0)
280
+
281
+
282
+ def main():
283
+ MusicPlayer().run()
284
+
285
+
286
+ if __name__ == "__main__":
287
+ main()
@@ -0,0 +1,67 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.9"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.10'",
6
+ "python_full_version < '3.10'",
7
+ ]
8
+
9
+ [[package]]
10
+ name = "python-mpv"
11
+ version = "1.0.8"
12
+ source = { registry = "https://pypi.org/simple" }
13
+ sdist = { url = "https://files.pythonhosted.org/packages/77/bc/6aa34c8805ff62e2fefc7d171563c60598aac215d5241186031a2c839935/python_mpv-1.0.8.tar.gz", hash = "sha256:017fa359da059c831a94c419083491903e6d2f7c81b9841c33c196cabf4b3fe3", size = 52680, upload-time = "2025-04-25T09:51:40.048Z" }
14
+ wheels = [
15
+ { url = "https://files.pythonhosted.org/packages/22/f3/4c632eaacebfc62ab9414586137aecf6c0ea2a1e99708cf5a4c0dec13ae0/python_mpv-1.0.8-py3-none-any.whl", hash = "sha256:b5296403e990fb7348df4ca2211937a030f524bf638657bda7e45a00bc2df0cd", size = 46169, upload-time = "2025-04-25T09:51:37.775Z" },
16
+ ]
17
+
18
+ [[package]]
19
+ name = "readchar"
20
+ version = "4.2.2"
21
+ source = { registry = "https://pypi.org/simple" }
22
+ sdist = { url = "https://files.pythonhosted.org/packages/ed/49/a10341024c45bed95d13197ec9ef0f4e2fd10b5ca6e7f8d7684d18082398/readchar-4.2.2.tar.gz", hash = "sha256:e3b270fe16fc90c50ac79107700330a133dd4c63d22939f5b03b4f24564d5dd8", size = 9762, upload-time = "2026-04-06T19:45:54.226Z" }
23
+ wheels = [
24
+ { url = "https://files.pythonhosted.org/packages/3d/ca/36133653e00939922dd1416f4c56177361289172a30563fcb9552c9ccde4/readchar-4.2.2-py3-none-any.whl", hash = "sha256:92daf7e42c52b0787e6c75d01ecfb9a94f4ceff3764958b570c1dddedd47b200", size = 9401, upload-time = "2026-04-06T19:45:52.993Z" },
25
+ ]
26
+
27
+ [[package]]
28
+ name = "srgplayer"
29
+ version = "0.1.0"
30
+ source = { editable = "." }
31
+ dependencies = [
32
+ { name = "python-mpv" },
33
+ { name = "readchar" },
34
+ { name = "yt-dlp", version = "2025.10.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
35
+ { name = "yt-dlp", version = "2026.7.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
36
+ ]
37
+
38
+ [package.metadata]
39
+ requires-dist = [
40
+ { name = "python-mpv" },
41
+ { name = "readchar" },
42
+ { name = "yt-dlp" },
43
+ ]
44
+
45
+ [[package]]
46
+ name = "yt-dlp"
47
+ version = "2025.10.14"
48
+ source = { registry = "https://pypi.org/simple" }
49
+ resolution-markers = [
50
+ "python_full_version < '3.10'",
51
+ ]
52
+ sdist = { url = "https://files.pythonhosted.org/packages/03/b7/dab729345e22891e79294273bc59c5213a1ec87331f49cb82ccea2b1bc9f/yt_dlp-2025.10.14.tar.gz", hash = "sha256:b18436aa9bb6f04354fd78d31ad9eeaae8c81b6a859f07072b25c18cd6c25844", size = 3045272, upload-time = "2025-10-14T23:39:52.688Z" }
53
+ wheels = [
54
+ { url = "https://files.pythonhosted.org/packages/b0/19/399c85d29bd7b366b31ede82698f7963374e5a3842ae9de0cde6514506b0/yt_dlp-2025.10.14-py3-none-any.whl", hash = "sha256:0b9da17eda1bbf48e2315130043d7993fd4ca1c5a35571f8231da1a910c9c115", size = 3248664, upload-time = "2025-10-14T23:39:49.95Z" },
55
+ ]
56
+
57
+ [[package]]
58
+ name = "yt-dlp"
59
+ version = "2026.7.4"
60
+ source = { registry = "https://pypi.org/simple" }
61
+ resolution-markers = [
62
+ "python_full_version >= '3.10'",
63
+ ]
64
+ sdist = { url = "https://files.pythonhosted.org/packages/47/c5/9972af4b472b0d55badf841ebafd2f98944cb0ae0f46e11d01f363ea5b91/yt_dlp-2026.7.4.tar.gz", hash = "sha256:b094813404f87a9dd2186f00815231df32e5fd8a5403be0f807b3bb2d21a4432", size = 3049326, upload-time = "2026-07-04T22:42:14.837Z" }
65
+ wheels = [
66
+ { url = "https://files.pythonhosted.org/packages/f9/8a/cd4c9b02c10c563adfe78118310129641900e1cd6de888cfae2452072696/yt_dlp-2026.7.4-py3-none-any.whl", hash = "sha256:f11f2b11d5a8ac4059f9bdf29fa4407dc7c6bb00c5097e95ca22a7a9db518266", size = 3184705, upload-time = "2026-07-04T22:42:12.989Z" },
67
+ ]