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 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
@@ -0,0 +1,3 @@
1
+ # Navidrome On Console Player
2
+
3
+ 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).
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,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ nocp/__init__.py
4
+ nocp/__main__.py
5
+ nocp.egg-info/PKG-INFO
6
+ nocp.egg-info/SOURCES.txt
7
+ nocp.egg-info/dependency_links.txt
8
+ nocp.egg-info/entry_points.txt
9
+ nocp.egg-info/requires.txt
10
+ nocp.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nocp = nocp.__main__:main
@@ -0,0 +1,4 @@
1
+ python-vlc
2
+ urwid
3
+ click
4
+ requests
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+