nocp 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.
- nocp-0.1.0/PKG-INFO +19 -0
- nocp-0.1.0/README.md +3 -0
- nocp-0.1.0/nocp/__init__.py +0 -0
- nocp-0.1.0/nocp/__main__.py +606 -0
- nocp-0.1.0/nocp.egg-info/PKG-INFO +19 -0
- nocp-0.1.0/nocp.egg-info/SOURCES.txt +10 -0
- nocp-0.1.0/nocp.egg-info/dependency_links.txt +1 -0
- nocp-0.1.0/nocp.egg-info/entry_points.txt +2 -0
- nocp-0.1.0/nocp.egg-info/requires.txt +4 -0
- nocp-0.1.0/nocp.egg-info/top_level.txt +1 -0
- nocp-0.1.0/pyproject.toml +26 -0
- nocp-0.1.0/setup.cfg +4 -0
nocp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nocp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Navidrome On Console Player
|
|
5
|
+
Author-email: fraoustin <fraoustin@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fraoustin/nocp
|
|
8
|
+
Project-URL: Repository, https://github.com/fraoustin/nocp
|
|
9
|
+
Project-URL: Issues, https://github.com/fraoustin/nocp/issues
|
|
10
|
+
Requires-Python: >=3.7
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: python-vlc
|
|
13
|
+
Requires-Dist: urwid
|
|
14
|
+
Requires-Dist: click
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
|
|
17
|
+
# Navidrome On Console Player
|
|
18
|
+
|
|
19
|
+
NOCP (Navidrome On Console Player) is a console audio player designed to be powerful and easy to use inpired bien MOCP (Music On Console Player).
|
nocp-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
import urwid
|
|
2
|
+
import random
|
|
3
|
+
import string
|
|
4
|
+
import hashlib
|
|
5
|
+
import requests
|
|
6
|
+
import vlc
|
|
7
|
+
import argparse
|
|
8
|
+
import configparser
|
|
9
|
+
import click
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
DEFAULT_CONFIG_PATH = os.path.join(os.getenv("APPDATA", os.path.expanduser("~")), ".nocp", "config.ini")
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_config(config_path):
|
|
19
|
+
config = configparser.ConfigParser()
|
|
20
|
+
if os.path.exists(config_path):
|
|
21
|
+
config.read(config_path)
|
|
22
|
+
return config["DEFAULT"]
|
|
23
|
+
return {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def save_config(username, password, server_url, config_path):
|
|
27
|
+
config = configparser.ConfigParser()
|
|
28
|
+
config['DEFAULT'] = {
|
|
29
|
+
'username': username,
|
|
30
|
+
'password': password,
|
|
31
|
+
'server_url': server_url
|
|
32
|
+
}
|
|
33
|
+
config_dir = os.path.dirname(config_path)
|
|
34
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
35
|
+
with open(config_path, 'w') as configfile:
|
|
36
|
+
config.write(configfile)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Generic:
|
|
40
|
+
|
|
41
|
+
def __init__(self, **kw):
|
|
42
|
+
for elt in kw:
|
|
43
|
+
setattr(self, elt, kw[elt])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Song(Generic):
|
|
47
|
+
|
|
48
|
+
def __init__(self, **kw):
|
|
49
|
+
Generic.__init__(self, **kw)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def timer(self):
|
|
53
|
+
return f"{self.duration // 60}:{self.duration % 60:02d}"
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def streamUrl(self):
|
|
57
|
+
params = self.nav.build_params()
|
|
58
|
+
params['id'] = self.id
|
|
59
|
+
req = requests.Request('GET', f"{self.nav.url}/stream.view", params=params)
|
|
60
|
+
return req.prepare().url
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Album(Generic):
|
|
64
|
+
|
|
65
|
+
def __init__(self, **kw):
|
|
66
|
+
Generic.__init__(self, **kw)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def songs(self):
|
|
70
|
+
data = self.nav.request("getMusicDirectory.view", id=self.id)
|
|
71
|
+
songs = [Song(nav=self.nav, prev=None, next=None, **song) for song in data['subsonic-response']['directory']['child']]
|
|
72
|
+
for i, song in enumerate(songs):
|
|
73
|
+
song.next = songs[i + 1] if i + 1 < len(songs) else None
|
|
74
|
+
song.prev = songs[i - 1] if i - 1 >= 0 else None
|
|
75
|
+
return songs
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Artist(Generic):
|
|
79
|
+
|
|
80
|
+
def __init__(self, **kw):
|
|
81
|
+
Generic.__init__(self, **kw)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def albums(self):
|
|
85
|
+
data = self.nav.request("getArtist.view", id=self.id)
|
|
86
|
+
albums = data['subsonic-response']['artist']['album']
|
|
87
|
+
return [Album(nav=self.nav, **album) for album in albums]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Radio(Generic):
|
|
91
|
+
|
|
92
|
+
def __init__(self, **kw):
|
|
93
|
+
Generic.__init__(self, **kw)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def title(self):
|
|
97
|
+
return self.name
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Playlist(Generic):
|
|
101
|
+
|
|
102
|
+
def __init__(self, **kw):
|
|
103
|
+
Generic.__init__(self, **kw)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def songs(self):
|
|
107
|
+
data = self.nav.request("getPlaylist.view", id=self.id)
|
|
108
|
+
songs = [Song(nav=self.nav, next=None, **song) for song in data['subsonic-response']['playlist'].get('entry', [])]
|
|
109
|
+
for i, song in enumerate(songs):
|
|
110
|
+
song.next = songs[i + 1] if i + 1 < len(songs) else None
|
|
111
|
+
song.prev = songs[i - 1] if i - 1 >= 0 else None
|
|
112
|
+
return songs
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Navidrome:
|
|
116
|
+
|
|
117
|
+
def __init__(self, url, username, client, password, version):
|
|
118
|
+
self.url = url
|
|
119
|
+
self.username = username
|
|
120
|
+
self.client = client
|
|
121
|
+
self.password = password
|
|
122
|
+
self.version = version
|
|
123
|
+
self.salt = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
|
124
|
+
self.token = hashlib.md5((self.password + self.salt).encode('utf-8')).hexdigest()
|
|
125
|
+
|
|
126
|
+
def build_params(self):
|
|
127
|
+
return {
|
|
128
|
+
'u': self.username,
|
|
129
|
+
't': self.token,
|
|
130
|
+
's': self.salt,
|
|
131
|
+
'v': self.version,
|
|
132
|
+
'c': self.client,
|
|
133
|
+
'f': 'json'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def request(self, view, **kw):
|
|
137
|
+
params = self.build_params()
|
|
138
|
+
for elt in kw:
|
|
139
|
+
params[elt] = kw[elt]
|
|
140
|
+
response = requests.get(f"{self.url}/{view}", params=params)
|
|
141
|
+
return response.json()
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def artists(self):
|
|
145
|
+
data = self.request("getArtists.view")
|
|
146
|
+
artists = data['subsonic-response']['artists']['index']
|
|
147
|
+
result = []
|
|
148
|
+
for index in artists:
|
|
149
|
+
for artist in index['artist']:
|
|
150
|
+
result.append(Artist(nav=self, **artist))
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def radios(self):
|
|
155
|
+
data = self.request("getInternetRadioStations.view")
|
|
156
|
+
stations = data['subsonic-response'].get('internetRadioStations', {}).get('internetRadioStation', [])
|
|
157
|
+
return [Radio(nav=self, **station) for station in stations]
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def playlists(self):
|
|
161
|
+
data = self.request("getPlaylists.view")
|
|
162
|
+
playlists = data['subsonic-response']['playlists']['playlist']
|
|
163
|
+
return [Playlist(nav=self, **playlist) for playlist in playlists]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class HelpOverlay(urwid.WidgetWrap):
|
|
167
|
+
def __init__(self, on_exit):
|
|
168
|
+
shortcuts = [
|
|
169
|
+
"🎵 Musique : m",
|
|
170
|
+
"📻 Radio : r",
|
|
171
|
+
"📂 Playlists : l",
|
|
172
|
+
"🔊 Volume : v",
|
|
173
|
+
"❓ Aide : h",
|
|
174
|
+
"⏹ Quitter : q",
|
|
175
|
+
"pause/play : <space>",
|
|
176
|
+
"Suivant : n",
|
|
177
|
+
"Précedent : p",
|
|
178
|
+
"",
|
|
179
|
+
"Navigation : ↑ ↓",
|
|
180
|
+
"Changer de panneau : tab",
|
|
181
|
+
"Sélectionner : enter",
|
|
182
|
+
"Fermer cette aide : esc ou enter",
|
|
183
|
+
"",
|
|
184
|
+
f"version {__version__}",
|
|
185
|
+
]
|
|
186
|
+
help_text = urwid.Text("\n".join(shortcuts), align='left')
|
|
187
|
+
padded = urwid.Padding(help_text, left=2, right=2)
|
|
188
|
+
box = urwid.LineBox(urwid.Filler(padded, valign='top'), title="❓ Raccourcis clavier")
|
|
189
|
+
super().__init__(box)
|
|
190
|
+
self.on_exit = on_exit
|
|
191
|
+
|
|
192
|
+
def selectable(self):
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
def keypress(self, size, key):
|
|
196
|
+
if key in ('esc', 'enter'):
|
|
197
|
+
self.on_exit()
|
|
198
|
+
return None
|
|
199
|
+
return key
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class VolumeGauge(urwid.WidgetWrap):
|
|
203
|
+
def __init__(self, on_exit, on_change=None, initial=50):
|
|
204
|
+
self.value = initial
|
|
205
|
+
self.on_change = on_change
|
|
206
|
+
self.on_exit = on_exit
|
|
207
|
+
self.gauge = urwid.Text(self.render_gauge(), align='center')
|
|
208
|
+
self.layout = urwid.Filler(self.gauge, valign='middle')
|
|
209
|
+
super().__init__(urwid.LineBox(self.layout, title="🔊 Volume"))
|
|
210
|
+
|
|
211
|
+
def render_gauge(self):
|
|
212
|
+
filled = int(self.value // 5)
|
|
213
|
+
empty = 20 - filled
|
|
214
|
+
bar = "\n".join([" "] * empty + ["██"] * filled)
|
|
215
|
+
return f"Volume: {self.value}%\n\n{bar}"
|
|
216
|
+
|
|
217
|
+
def selectable(self):
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
def keypress(self, size, key):
|
|
221
|
+
if key == 'up':
|
|
222
|
+
self.value = min(100, self.value + 5)
|
|
223
|
+
elif key == 'down':
|
|
224
|
+
self.value = max(0, self.value - 5)
|
|
225
|
+
elif key in ('esc', 'enter'):
|
|
226
|
+
self.on_exit()
|
|
227
|
+
return None
|
|
228
|
+
self.gauge.set_text(self.render_gauge())
|
|
229
|
+
if self.on_change:
|
|
230
|
+
self.on_change(self.value)
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class PlainButton(urwid.WidgetWrap):
|
|
235
|
+
def __init__(self, label_widget, on_press=None, user_data=None):
|
|
236
|
+
self.label_widget = label_widget
|
|
237
|
+
self.attr_map = urwid.AttrMap(label_widget, None, focus_map='reversed')
|
|
238
|
+
super().__init__(self.attr_map)
|
|
239
|
+
self._on_press = on_press
|
|
240
|
+
self._user_data = user_data
|
|
241
|
+
|
|
242
|
+
def selectable(self):
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
def keypress(self, size, key):
|
|
246
|
+
if key == 'enter' and self._on_press:
|
|
247
|
+
self._on_press(self, self._user_data)
|
|
248
|
+
return None
|
|
249
|
+
return key
|
|
250
|
+
|
|
251
|
+
def get_label(self):
|
|
252
|
+
if isinstance(self.label_widget, urwid.Columns):
|
|
253
|
+
return self.label_widget.contents[0][0].get_text()[0]
|
|
254
|
+
return self.label_widget.get_text()[0]
|
|
255
|
+
|
|
256
|
+
def set_selected(self, selected):
|
|
257
|
+
self.attr_map.set_attr_map({None: 'selected' if selected else None})
|
|
258
|
+
|
|
259
|
+
def mouse_event(self, size, event, button, x, y, focus):
|
|
260
|
+
if event == 'mouse press' and button == 1: # bouton gauche
|
|
261
|
+
if self._on_press:
|
|
262
|
+
self._on_press(self, self._user_data)
|
|
263
|
+
return True
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class MusicBrowser:
|
|
268
|
+
def __init__(self, nav):
|
|
269
|
+
self.nav = nav
|
|
270
|
+
self.mode = "music"
|
|
271
|
+
self.selected_artist = None
|
|
272
|
+
self.selected_album = None
|
|
273
|
+
self.selected_playlist = None
|
|
274
|
+
self.focus_index = 0
|
|
275
|
+
self.playlist_focus_index = 0
|
|
276
|
+
self.playlist_focus_list = [None, None] # placeholders
|
|
277
|
+
|
|
278
|
+
self.artist_listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
|
|
279
|
+
self.album_listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
|
|
280
|
+
self.song_listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
|
|
281
|
+
self.radio_listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
|
|
282
|
+
self.playlist_listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
|
|
283
|
+
self.playlist_song_listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
|
|
284
|
+
|
|
285
|
+
self.playlist_focus_list = [self.playlist_listbox, self.playlist_song_listbox]
|
|
286
|
+
|
|
287
|
+
self.footer_left = urwid.Text("🎵 Aucun morceau en cours", align='left')
|
|
288
|
+
self.footer_right = urwid.Text("00:00", align='right')
|
|
289
|
+
self.footer_columns = urwid.Columns([self.footer_left, self.footer_right])
|
|
290
|
+
|
|
291
|
+
self.focus_list = [self.artist_listbox, self.album_listbox, self.song_listbox]
|
|
292
|
+
|
|
293
|
+
self.left_panel = urwid.LineBox(self.artist_listbox, title="🎤 Artistes")
|
|
294
|
+
self.top_right = urwid.LineBox(self.album_listbox, title="💿 Albums")
|
|
295
|
+
self.bottom_right = urwid.LineBox(self.song_listbox, title="🎵 Chansons")
|
|
296
|
+
|
|
297
|
+
self.radio_panel = urwid.LineBox(self.radio_listbox, title="📻 Radios")
|
|
298
|
+
|
|
299
|
+
self.playlist_panel = urwid.Pile([
|
|
300
|
+
('weight', 1, urwid.LineBox(self.playlist_listbox, title="📂 Playlists")),
|
|
301
|
+
('weight', 1, urwid.LineBox(self.playlist_song_listbox, title="🎵 Morceaux"))
|
|
302
|
+
])
|
|
303
|
+
|
|
304
|
+
self.right_panel = urwid.Pile([
|
|
305
|
+
('weight', 1, self.top_right),
|
|
306
|
+
('weight', 1, self.bottom_right)
|
|
307
|
+
])
|
|
308
|
+
|
|
309
|
+
self.main_columns = urwid.Columns([
|
|
310
|
+
('weight', 1, self.left_panel),
|
|
311
|
+
('weight', 2, self.right_panel)
|
|
312
|
+
])
|
|
313
|
+
|
|
314
|
+
self.main_layout = urwid.Frame(body=self.main_columns, footer=urwid.LineBox(self.footer_columns))
|
|
315
|
+
|
|
316
|
+
self.loop = urwid.MainLoop(
|
|
317
|
+
self.main_layout,
|
|
318
|
+
palette=[
|
|
319
|
+
('reversed', 'standout', ''),
|
|
320
|
+
('selected', 'dark green', ''),
|
|
321
|
+
],
|
|
322
|
+
unhandled_input=self.handle_input,
|
|
323
|
+
handle_mouse=True
|
|
324
|
+
)
|
|
325
|
+
self.update_artist_list()
|
|
326
|
+
self.update_radio_list()
|
|
327
|
+
self.update_playlist_list()
|
|
328
|
+
|
|
329
|
+
def update_artist_list(self):
|
|
330
|
+
artists = self.nav.artists
|
|
331
|
+
self.artist_listbox.body.clear()
|
|
332
|
+
for artist in artists:
|
|
333
|
+
txt = urwid.Text(artist.name)
|
|
334
|
+
btn = PlainButton(txt, on_press=self.on_artist_selected, user_data=artist)
|
|
335
|
+
self.artist_listbox.body.append(btn)
|
|
336
|
+
self.on_artist_selected(None, artists[0])
|
|
337
|
+
|
|
338
|
+
def update_album_list(self, albums):
|
|
339
|
+
self.album_listbox.body.clear()
|
|
340
|
+
for album in albums:
|
|
341
|
+
txt = urwid.Text(album.name)
|
|
342
|
+
btn = PlainButton(txt, on_press=self.on_album_selected, user_data=album)
|
|
343
|
+
self.album_listbox.body.append(btn)
|
|
344
|
+
|
|
345
|
+
def update_song_list(self, songs):
|
|
346
|
+
self.song_listbox.body.clear()
|
|
347
|
+
for song in songs:
|
|
348
|
+
row = urwid.Columns([
|
|
349
|
+
urwid.Text(song.title),
|
|
350
|
+
urwid.Text(str(song.timer), align='right')
|
|
351
|
+
])
|
|
352
|
+
btn = PlainButton(row, on_press=self.on_song_selected, user_data=song)
|
|
353
|
+
self.song_listbox.body.append(btn)
|
|
354
|
+
|
|
355
|
+
def update_radio_list(self):
|
|
356
|
+
self.radio_listbox.body.clear()
|
|
357
|
+
for radio in self.nav.radios:
|
|
358
|
+
txt = urwid.Text(radio.name)
|
|
359
|
+
btn = PlainButton(txt, on_press=self.on_radio_selected, user_data=radio)
|
|
360
|
+
self.radio_listbox.body.append(btn)
|
|
361
|
+
|
|
362
|
+
def update_playlist_list(self):
|
|
363
|
+
playlists = self.nav.playlists
|
|
364
|
+
self.playlist_listbox.body.clear()
|
|
365
|
+
for playlist in playlists:
|
|
366
|
+
txt = urwid.Text(playlist.name)
|
|
367
|
+
btn = PlainButton(txt, on_press=self.on_playlist_selected, user_data=playlist)
|
|
368
|
+
self.playlist_listbox.body.append(btn)
|
|
369
|
+
self.on_playlist_selected(None, playlists[0])
|
|
370
|
+
|
|
371
|
+
def update_playlist_song_list(self, songs):
|
|
372
|
+
self.playlist_song_listbox.body.clear()
|
|
373
|
+
for song in songs:
|
|
374
|
+
txt = urwid.Text(song.title)
|
|
375
|
+
btn = PlainButton(txt, on_press=self.on_song_selected, user_data=song)
|
|
376
|
+
self.playlist_song_listbox.body.append(btn)
|
|
377
|
+
|
|
378
|
+
def clear_selection(self, listbox):
|
|
379
|
+
for btn in listbox.body:
|
|
380
|
+
btn.set_selected(False)
|
|
381
|
+
|
|
382
|
+
def on_artist_selected(self, button, artist):
|
|
383
|
+
self.selected_artist = artist
|
|
384
|
+
self.clear_selection(self.artist_listbox)
|
|
385
|
+
for btn in self.artist_listbox.body:
|
|
386
|
+
if btn.get_label() == artist.name:
|
|
387
|
+
btn.set_selected(True)
|
|
388
|
+
break
|
|
389
|
+
albums = artist.albums
|
|
390
|
+
self.update_album_list(albums)
|
|
391
|
+
if len(albums) > 0:
|
|
392
|
+
self.on_album_selected(None, albums[0])
|
|
393
|
+
|
|
394
|
+
def on_album_selected(self, button, album):
|
|
395
|
+
self.selected_album = album
|
|
396
|
+
self.clear_selection(self.album_listbox)
|
|
397
|
+
for btn in self.album_listbox.body:
|
|
398
|
+
if btn.get_label() == album.name:
|
|
399
|
+
btn.set_selected(True)
|
|
400
|
+
break
|
|
401
|
+
songs = album.songs
|
|
402
|
+
self.update_song_list(songs)
|
|
403
|
+
|
|
404
|
+
def on_song_selected(self, button, song):
|
|
405
|
+
self.current_song = song
|
|
406
|
+
self.clear_selection(self.song_listbox)
|
|
407
|
+
self.clear_selection(self.playlist_song_listbox)
|
|
408
|
+
for listbox in [self.song_listbox, self.playlist_song_listbox]:
|
|
409
|
+
for btn in listbox.body:
|
|
410
|
+
if btn.get_label() == song.title:
|
|
411
|
+
btn.set_selected(True)
|
|
412
|
+
break
|
|
413
|
+
self.play_song()
|
|
414
|
+
|
|
415
|
+
def play_song(self, bystop=True):
|
|
416
|
+
try:
|
|
417
|
+
full_url = requests.Request('GET', self.current_song.streamUrl).prepare().url
|
|
418
|
+
if hasattr(self, 'player') and self.player:
|
|
419
|
+
if self.player.get_state() != vlc.State.Ended:
|
|
420
|
+
self.player.stop()
|
|
421
|
+
self.player = vlc.MediaPlayer(full_url)
|
|
422
|
+
self.player.play()
|
|
423
|
+
self.player.event_manager().event_attach(
|
|
424
|
+
vlc.EventType.MediaPlayerEndReached,
|
|
425
|
+
self.on_song_end
|
|
426
|
+
)
|
|
427
|
+
self.update_footer_now_playing()
|
|
428
|
+
self.loop.set_alarm_in(1, self.update_playback_time)
|
|
429
|
+
except Exception as e:
|
|
430
|
+
self.footer_left.set_text(f"❌ Erreur lecture VLC : {str(e)}")
|
|
431
|
+
|
|
432
|
+
def on_song_end(self, event=None):
|
|
433
|
+
if self.current_song.next is not None:
|
|
434
|
+
self.current_song = self.current_song.next
|
|
435
|
+
self.loop.set_alarm_in(0.1, lambda loop, user_data: self.play_song())
|
|
436
|
+
|
|
437
|
+
def on_song_prev(self, event=None):
|
|
438
|
+
if self.current_song.prev is not None:
|
|
439
|
+
self.current_song = self.current_song.prev
|
|
440
|
+
self.play_song()
|
|
441
|
+
|
|
442
|
+
def update_footer_now_playing(self):
|
|
443
|
+
if self.current_song:
|
|
444
|
+
self.footer_left.set_text(f"🎵 {self.current_song.title}")
|
|
445
|
+
|
|
446
|
+
def update_playback_time(self, loop=None, user_data=None):
|
|
447
|
+
if self.player and self.player.is_playing():
|
|
448
|
+
ms = self.player.get_time()
|
|
449
|
+
if ms != -1:
|
|
450
|
+
seconds = ms // 1000
|
|
451
|
+
minutes = seconds // 60
|
|
452
|
+
seconds = seconds % 60
|
|
453
|
+
self.footer_right.set_text(f"{minutes:02}:{seconds:02}")
|
|
454
|
+
self.loop.set_alarm_in(1, self.update_playback_time)
|
|
455
|
+
|
|
456
|
+
def on_radio_selected(self, button, radio):
|
|
457
|
+
self.current_song = radio
|
|
458
|
+
self.clear_selection(self.radio_listbox)
|
|
459
|
+
for btn in self.radio_listbox.body:
|
|
460
|
+
if btn.get_label() == radio.name:
|
|
461
|
+
btn.set_selected(True)
|
|
462
|
+
break
|
|
463
|
+
try:
|
|
464
|
+
full_url = requests.Request('GET', radio.streamUrl).prepare().url
|
|
465
|
+
if hasattr(self, 'player') and self.player:
|
|
466
|
+
self.player.stop()
|
|
467
|
+
self.player = vlc.MediaPlayer(full_url)
|
|
468
|
+
self.player.play()
|
|
469
|
+
self.update_footer_now_playing()
|
|
470
|
+
self.loop.set_alarm_in(1, self.update_playback_time)
|
|
471
|
+
except Exception as e:
|
|
472
|
+
self.footer_left.set_text(f"❌ Erreur lecture VLC : {str(e)}")
|
|
473
|
+
|
|
474
|
+
def on_playlist_selected(self, button, playlist):
|
|
475
|
+
self.selected_playlist = playlist.name
|
|
476
|
+
self.clear_selection(self.playlist_listbox)
|
|
477
|
+
for btn in self.playlist_listbox.body:
|
|
478
|
+
if btn.get_label() == playlist.name:
|
|
479
|
+
btn.set_selected(True)
|
|
480
|
+
break
|
|
481
|
+
songs = playlist.songs
|
|
482
|
+
self.update_playlist_song_list(songs)
|
|
483
|
+
|
|
484
|
+
def handle_input(self, key):
|
|
485
|
+
if isinstance(key, str):
|
|
486
|
+
if key in ('q', 'Q'):
|
|
487
|
+
raise urwid.ExitMainLoop()
|
|
488
|
+
elif key == 'tab':
|
|
489
|
+
if self.mode == "music":
|
|
490
|
+
self.move_focus(1)
|
|
491
|
+
elif self.mode == "playlist":
|
|
492
|
+
self.move_playlist_focus(1)
|
|
493
|
+
elif key == 'enter':
|
|
494
|
+
self.trigger_selection()
|
|
495
|
+
elif key.lower() == 'v':
|
|
496
|
+
self.show_volume_gauge()
|
|
497
|
+
elif key.lower() == 'r':
|
|
498
|
+
self.switch_to_radio_view()
|
|
499
|
+
elif key.lower() == 'm':
|
|
500
|
+
self.switch_to_music_view()
|
|
501
|
+
elif key.lower() == 'h':
|
|
502
|
+
self.show_help_overlay()
|
|
503
|
+
elif key.lower() == 'l':
|
|
504
|
+
self.switch_to_playlist_view()
|
|
505
|
+
elif key.lower() == 'n':
|
|
506
|
+
self.on_song_end()
|
|
507
|
+
elif key.lower() == 'p':
|
|
508
|
+
self.on_song_prev()
|
|
509
|
+
elif key == ' ':
|
|
510
|
+
if self.player:
|
|
511
|
+
state = self.player.get_state()
|
|
512
|
+
if state in [vlc.State.Playing]:
|
|
513
|
+
self.player.pause()
|
|
514
|
+
elif state in [vlc.State.Paused]:
|
|
515
|
+
self.player.play()
|
|
516
|
+
|
|
517
|
+
def move_focus(self, step):
|
|
518
|
+
self.focus_index = (self.focus_index + step) % 3
|
|
519
|
+
if self.focus_index == 0:
|
|
520
|
+
self.main_columns.focus_position = 0
|
|
521
|
+
else:
|
|
522
|
+
self.main_columns.focus_position = 1
|
|
523
|
+
self.right_panel.focus_position = self.focus_index - 1
|
|
524
|
+
|
|
525
|
+
def move_playlist_focus(self, step):
|
|
526
|
+
self.playlist_focus_index = (self.playlist_focus_index + step) % 2
|
|
527
|
+
self.playlist_panel.focus_position = self.playlist_focus_index
|
|
528
|
+
|
|
529
|
+
def trigger_selection(self):
|
|
530
|
+
if self.mode == "music":
|
|
531
|
+
listbox = self.focus_list[self.focus_index]
|
|
532
|
+
elif self.mode == "radio":
|
|
533
|
+
listbox = self.radio_listbox
|
|
534
|
+
elif self.mode == "playlist":
|
|
535
|
+
listbox = self.playlist_focus_list[self.playlist_focus_index]
|
|
536
|
+
else:
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
focus_widget, _ = listbox.get_focus()
|
|
540
|
+
if isinstance(focus_widget.base_widget, PlainButton):
|
|
541
|
+
focus_widget.base_widget.keypress((0,), 'enter')
|
|
542
|
+
|
|
543
|
+
def switch_to_radio_view(self):
|
|
544
|
+
self.mode = "radio"
|
|
545
|
+
self.main_layout.body = self.radio_panel
|
|
546
|
+
|
|
547
|
+
def switch_to_music_view(self):
|
|
548
|
+
self.mode = "music"
|
|
549
|
+
self.main_layout.body = self.main_columns
|
|
550
|
+
|
|
551
|
+
def switch_to_playlist_view(self):
|
|
552
|
+
self.mode = "playlist"
|
|
553
|
+
self.main_layout.body = self.playlist_panel
|
|
554
|
+
|
|
555
|
+
def show_help_overlay(self):
|
|
556
|
+
def exit_help():
|
|
557
|
+
self.loop.widget = self.main_layout
|
|
558
|
+
help_overlay = HelpOverlay(on_exit=exit_help)
|
|
559
|
+
overlay = urwid.Overlay(help_overlay, self.main_layout, 'center', 40, 'middle', 20)
|
|
560
|
+
self.loop.widget = overlay
|
|
561
|
+
|
|
562
|
+
def show_volume_gauge(self):
|
|
563
|
+
def exit_gauge():
|
|
564
|
+
self.loop.widget = self.main_layout
|
|
565
|
+
|
|
566
|
+
def on_change_volume(value):
|
|
567
|
+
if hasattr(self, 'player') and self.player:
|
|
568
|
+
self.player.audio_set_volume(value)
|
|
569
|
+
|
|
570
|
+
if hasattr(self, 'player') and self.player:
|
|
571
|
+
volume = self.player.audio_get_volume()
|
|
572
|
+
else:
|
|
573
|
+
volume = 50
|
|
574
|
+
gauge = VolumeGauge(on_exit=exit_gauge, on_change=on_change_volume, initial=volume)
|
|
575
|
+
overlay = urwid.Overlay(gauge, self.main_layout, 'center', 20, 'middle', 25)
|
|
576
|
+
self.loop.widget = overlay
|
|
577
|
+
|
|
578
|
+
def run(self):
|
|
579
|
+
self.loop.run()
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
@click.command()
|
|
583
|
+
@click.option('--server-url', help="URL du serveur")
|
|
584
|
+
@click.option('--username', help="Nom d'utilisateur")
|
|
585
|
+
@click.option('--password', hide_input=True, help="Mot de passe (ou vide pour demander)")
|
|
586
|
+
@click.option('--config-path', default=DEFAULT_CONFIG_PATH, help="Chemin du fichier de configuration")
|
|
587
|
+
@click.pass_context
|
|
588
|
+
def main(ctx, server_url, username, password, config_path):
|
|
589
|
+
config = load_config(config_path)
|
|
590
|
+
server_url = server_url or config.get("server_url")
|
|
591
|
+
username = username or config.get("username")
|
|
592
|
+
password = password or config.get("password")
|
|
593
|
+
|
|
594
|
+
if not password or not username or not server_url:
|
|
595
|
+
click.echo(ctx.get_help())
|
|
596
|
+
ctx.exit(1)
|
|
597
|
+
else:
|
|
598
|
+
save_config(username, password, server_url, config_path)
|
|
599
|
+
|
|
600
|
+
nav = Navidrome(server_url, username, "nocp", password, "1.16.1")
|
|
601
|
+
app = MusicBrowser(nav)
|
|
602
|
+
app.run()
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
if __name__ == "__main__":
|
|
606
|
+
main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nocp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Navidrome On Console Player
|
|
5
|
+
Author-email: fraoustin <fraoustin@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fraoustin/nocp
|
|
8
|
+
Project-URL: Repository, https://github.com/fraoustin/nocp
|
|
9
|
+
Project-URL: Issues, https://github.com/fraoustin/nocp/issues
|
|
10
|
+
Requires-Python: >=3.7
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: python-vlc
|
|
13
|
+
Requires-Dist: urwid
|
|
14
|
+
Requires-Dist: click
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
|
|
17
|
+
# Navidrome On Console Player
|
|
18
|
+
|
|
19
|
+
NOCP (Navidrome On Console Player) is a console audio player designed to be powerful and easy to use inpired bien MOCP (Music On Console Player).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nocp
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nocp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Navidrome On Console Player"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name="fraoustin", email="fraoustin@gmail.com" }]
|
|
7
|
+
license = {text = "MIT"}
|
|
8
|
+
requires-python = ">=3.7"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"python-vlc",
|
|
11
|
+
"urwid",
|
|
12
|
+
"click",
|
|
13
|
+
"requests"
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/fraoustin/nocp"
|
|
18
|
+
Repository = "https://github.com/fraoustin/nocp"
|
|
19
|
+
Issues = "https://github.com/fraoustin/nocp/issues"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["setuptools>=61.0"]
|
|
23
|
+
build-backend = "setuptools.build_meta"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
nocp = "nocp.__main__:main"
|
nocp-0.1.0/setup.cfg
ADDED