CLPYmusic 0.1.1__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,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: CLPYmusic
3
+ Version: 0.1.1
4
+ Summary: A CLI music player
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: textual
7
+ Requires-Dist: pynput
8
+ Requires-Dist: mutagen
9
+ Requires-Dist: python-vlc
10
+ Requires-Dist: librosa
11
+ Requires-Dist: numpy
12
+ Requires-Dist: Pillow
13
+ Requires-Dist: Pillow
14
+ Requires-Dist: pyperclip
@@ -0,0 +1,25 @@
1
+ README.md
2
+ pyproject.toml
3
+ CLPYmusic.egg-info/PKG-INFO
4
+ CLPYmusic.egg-info/SOURCES.txt
5
+ CLPYmusic.egg-info/dependency_links.txt
6
+ CLPYmusic.egg-info/entry_points.txt
7
+ CLPYmusic.egg-info/requires.txt
8
+ CLPYmusic.egg-info/top_level.txt
9
+ climusic/Interface.py
10
+ climusic/audioEngine.py
11
+ climusic/library.py
12
+ climusic/musicController.py
13
+ climusic/themes.py
14
+ climusic/assets/defaultAlbumCover.jpeg
15
+ climusic/components/audioVisualizer.py
16
+ climusic/components/miniTerminal.py
17
+ climusic/components/musicPlayerActions.py
18
+ climusic/components/nowPlaying.py
19
+ climusic/components/songProgress.py
20
+ climusic/components/songTable.py
21
+ climusic/functions/coverToAscii.py
22
+ climusic/functions/extractAudioWave.py
23
+ climusic/functions/filterFormats.py
24
+ climusic/functions/timeConvert.py
25
+ climusic/functions/turnicateText.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ climusic = climusic.Interface:main
@@ -0,0 +1,9 @@
1
+ textual
2
+ pynput
3
+ mutagen
4
+ python-vlc
5
+ librosa
6
+ numpy
7
+ Pillow
8
+ Pillow
9
+ pyperclip
@@ -0,0 +1 @@
1
+ climusic
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: CLPYmusic
3
+ Version: 0.1.1
4
+ Summary: A CLI music player
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: textual
7
+ Requires-Dist: pynput
8
+ Requires-Dist: mutagen
9
+ Requires-Dist: python-vlc
10
+ Requires-Dist: librosa
11
+ Requires-Dist: numpy
12
+ Requires-Dist: Pillow
13
+ Requires-Dist: Pillow
14
+ Requires-Dist: pyperclip
File without changes
@@ -0,0 +1,159 @@
1
+ from climusic import musicController
2
+ from textual.app import App, ComposeResult, Binding
3
+ from textual.widgets import Header, DataTable
4
+ from textual.containers import Horizontal, Vertical
5
+ import json
6
+ from climusic.components.audioVisualizer import AudioVisualizer
7
+ from climusic.components.songTable import SongTable
8
+ from climusic.components.nowPlaying import NowPlaying
9
+ from climusic.components.songProgress import SongProgress
10
+ from climusic.components.miniTerminal import MiniTerminal
11
+ from climusic.components.musicPlayerActions import MusicPlayerActions
12
+ from climusic.themes import build_css
13
+ from pynput import keyboard as pynput_keyboard
14
+
15
+ try:
16
+ import vlc
17
+ except Exception:
18
+ print("❌ VLC not found!")
19
+ print("Install VLC from https://www.videolan.org then try again.")
20
+ print("Mac: brew install vlc")
21
+ print("Linux: sudo apt install vlc")
22
+ exit(1)
23
+
24
+ class Main(MusicPlayerActions, App):
25
+ allSongs = []
26
+ songsList = []
27
+
28
+ CSS = build_css()
29
+
30
+ BINDINGS = [
31
+ Binding("space", "toggle_pause", "Pause/Resume", priority=True),
32
+ Binding("d", "next_song", "Next", priority=True),
33
+ Binding("a", "prev_song", "Previous", priority=True),
34
+ Binding("w", "vol_up", "Vol Up", priority=True),
35
+ Binding("s", "vol_down", "Vol Down", priority=True),
36
+ Binding("q", "back_song", "Back", priority=True),
37
+ Binding("e", "forward_song", "Forward", priority=True),
38
+ ]
39
+
40
+ def compose(self) -> ComposeResult:
41
+ yield Header()
42
+ with open(musicController.CONFIG_PATH, "r") as f:
43
+ config = json.load(f)
44
+ if config["visualizer"]:
45
+ right_panel = Vertical(
46
+ NowPlaying(),
47
+ AudioVisualizer(),
48
+
49
+ id="right_panel"
50
+ )
51
+ else:
52
+ right_panel = Horizontal(
53
+ NowPlaying(),
54
+ id="right_panel"
55
+ )
56
+
57
+ yield Horizontal(
58
+ Vertical(
59
+ SongTable(),
60
+ MiniTerminal(id="terminal"),
61
+ id="left_panel"
62
+ ),
63
+ right_panel
64
+ )
65
+
66
+ def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
67
+ self.index = event.cursor_row
68
+ if self.index is None or self.index < 0:
69
+ return
70
+ self.load_and_play(self.index)
71
+
72
+ def _setup_global_hotkeys(self):
73
+ def on_press(key):
74
+ if key == pynput_keyboard.Key.media_next:
75
+ self.call_from_thread(self.play_next_song)
76
+ elif key == pynput_keyboard.Key.media_previous:
77
+ self.call_from_thread(self.play_previous_song)
78
+ elif key == pynput_keyboard.Key.media_play_pause:
79
+ self.call_from_thread(self.action_toggle_pause)
80
+
81
+ listener = pynput_keyboard.Listener(on_press=on_press)
82
+ listener.start()
83
+
84
+ def on_mount(self) -> None:
85
+ self.index = 0 # Initialize FIRST
86
+ self.is_paused = False
87
+
88
+ self.allSongs = musicController.return_library()
89
+ with open(musicController.CONFIG_PATH, "r") as f:
90
+ config = json.load(f)
91
+ if config["shuffle"] == True:
92
+ self.songsList = musicController.shuffle_library(self.allSongs)
93
+ else:
94
+ self.songsList = musicController.filter_songs_alphabetically(self.allSongs, sort_by="title")
95
+
96
+ self.query_one(SongTable).load_songs(self.songsList)
97
+ self.progress_bar = self.query_one(SongProgress)
98
+
99
+ if config["visualizer"]:
100
+ self.visualizer = self.query_one(AudioVisualizer)
101
+
102
+ current_theme = config.get("theme", "purple")
103
+ self.add_class(current_theme)
104
+
105
+ self.set_interval(1 / 20, self.update_progress)
106
+ self._setup_global_hotkeys()
107
+
108
+ # ═══════════════════════════════════════════════════════════════
109
+ # Key Binding Actions
110
+ # ═══════════════════════════════════════════════════════════════
111
+
112
+ def action_toggle_pause(self) -> None:
113
+ if self.is_paused:
114
+ musicController.unpause_song()
115
+ self.print_to_terminal("[dim]resumed.[/dim]")
116
+ else:
117
+ musicController.pause_song()
118
+ self.print_to_terminal("[dim]paused.[/dim]")
119
+ self.is_paused = not self.is_paused
120
+
121
+ def action_next_song(self) -> None:
122
+
123
+ self.is_paused = False
124
+ self.play_next_song()
125
+
126
+ def action_prev_song(self) -> None:
127
+
128
+ self.is_paused = False
129
+ self.play_previous_song()
130
+
131
+ def action_forward_song(self) -> None:
132
+
133
+ self.is_paused = False
134
+ self.forward_song()
135
+
136
+ def action_back_song(self) -> None:
137
+
138
+ self.is_paused = False
139
+ self.back_song()
140
+
141
+ def action_vol_up(self) -> None:
142
+ new_vol = min(100, musicController.get_volume() + 10)
143
+ musicController.set_volume(new_vol)
144
+ self.print_to_terminal(f"[dim]vol: {new_vol}%[/dim]")
145
+
146
+ def action_vol_down(self) -> None:
147
+ new_vol = max(0, musicController.get_volume() - 10)
148
+ musicController.set_volume(new_vol)
149
+ self.print_to_terminal(f"[dim]vol: {new_vol}%[/dim]")
150
+
151
+
152
+ def main():
153
+ musicController.init_config()
154
+ app = Main()
155
+ app.run()
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
@@ -0,0 +1,41 @@
1
+ import vlc
2
+
3
+ instance = vlc.Instance("--quiet")
4
+ player = instance.media_player_new() # use instance, not vlc.MediaPlayer()
5
+
6
+ def play_song(path):
7
+ media = instance.media_new(path) # use instance here too
8
+ player.set_media(media)
9
+ player.play()
10
+
11
+ def pause_song():
12
+ player.pause()
13
+
14
+ def unpause_song():
15
+ player.set_pause(0)
16
+
17
+ def stop_song():
18
+ player.stop()
19
+
20
+ def get_position():
21
+ ms = player.get_time()
22
+ return ms / 1000 if ms >= 0 else 0.0
23
+
24
+ def get_length():
25
+ ms = player.get_length()
26
+ return ms / 1000 if ms > 0 else 0.0
27
+
28
+ def song_finished():
29
+ return player.get_state() == vlc.State.Ended
30
+
31
+ def set_volume(level):
32
+ level = max(0, min(100, level)) # clamp to 0-100
33
+ player.audio_set_volume(level)
34
+
35
+ def set_position(position):
36
+ length = get_length()
37
+ if length > 0:
38
+ position = max(0, min(position, length)) # clamp to valid range
39
+ player.set_time(int(position * 1000)) # convert to ms
40
+ def get_volume():
41
+ return player.audio_get_volume()
@@ -0,0 +1,34 @@
1
+ from textual.widgets import Static
2
+
3
+
4
+ class AudioVisualizer(Static):
5
+ def __init__(self):
6
+ super().__init__(id="audio-visualizer")
7
+ def get_char(self,h, row):
8
+ ratio = row / h if h > 0 else 0
9
+ if ratio > 0.8:
10
+ return "░"
11
+ elif ratio > 0.6:
12
+ return "▒"
13
+ elif ratio > 0.4:
14
+ return "▓"
15
+ else:
16
+ return "█"
17
+ def update_wave(self, frame):
18
+ if not frame:
19
+ return
20
+
21
+ visible = frame
22
+
23
+ height = 8
24
+ rows = []
25
+
26
+ bar_heights = [int(v * height) for v in visible]
27
+
28
+ for row in range(height, 0, -1):
29
+ line = "".join(
30
+ self.get_char(h, row) if h >= row else " "
31
+ for h in bar_heights
32
+ )
33
+ rows.append(line)
34
+ self.update("\n".join(rows))
@@ -0,0 +1,75 @@
1
+ from textual.widgets import Input, Static, RichLog
2
+ from textual.containers import Vertical
3
+ from textual.app import ComposeResult
4
+ import pyperclip
5
+
6
+
7
+ class MiniTerminal(Vertical):
8
+
9
+ def compose(self) -> ComposeResult:
10
+ yield Static("terminal", id="terminal-header")
11
+ yield RichLog(id="terminal-log", markup=True, highlight=True)
12
+ yield Input(placeholder="enter command...", id="terminal-input")
13
+
14
+ def on_mount(self) -> None:
15
+ self.history = []
16
+ self.history_index = -1
17
+ self.write("[dim]ready. type a command below.[/dim]")
18
+ self.write("[dim][/dim]")
19
+
20
+ input_widget = self.query_one("#terminal-input", Input)
21
+ input_widget.focus()
22
+
23
+ def write(self, msg: str) -> None:
24
+ log = self.query_one("#terminal-log", RichLog)
25
+ log.write(msg)
26
+ log.scroll_end(animate=True)
27
+
28
+ def on_input_submitted(self, event: Input.Submitted) -> None:
29
+ cmd = event.value.strip()
30
+ if not cmd:
31
+ return
32
+
33
+ if not self.history or self.history[0] != cmd:
34
+ self.history.insert(0, cmd)
35
+ self.history_index = -1
36
+
37
+ event.input.value = ""
38
+ self.write(f"[dim]>[/dim] {cmd}")
39
+ self.app.handle_command(cmd)
40
+
41
+ # Refocus after command
42
+ self.query_one("#terminal-input", Input).focus()
43
+
44
+ def _on_key(self, event) -> None:
45
+ """Handle Ctrl+V for paste with length limit"""
46
+ input_widget = self.query_one("#terminal-input", Input)
47
+
48
+ if event.key == "ctrl+v":
49
+ try:
50
+ clipboard = pyperclip.paste()
51
+ max_length = 100
52
+
53
+ if len(clipboard) > max_length:
54
+ self.write(f"[red]text too long ({len(clipboard)} > {max_length})[/red]")
55
+ else:
56
+ input_widget.value = clipboard
57
+ input_widget.cursor_position = len(clipboard)
58
+ except Exception as e:
59
+ self.write(f"[red]paste error: {e}[/red]")
60
+ event.prevent_default()
61
+ elif event.key == "up":
62
+ if self.history and self.history_index < len(self.history) - 1:
63
+ self.history_index += 1
64
+ input_widget.value = self.history[self.history_index]
65
+ input_widget.cursor_position = len(input_widget.value)
66
+ event.prevent_default()
67
+ elif event.key == "down":
68
+ if self.history and self.history_index > 0:
69
+ self.history_index -= 1
70
+ input_widget.value = self.history[self.history_index]
71
+ input_widget.cursor_position = len(input_widget.value)
72
+ elif self.history_index == 0:
73
+ self.history_index = -1
74
+ input_widget.value = ""
75
+ event.prevent_default()