rhythmsync 1.0.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,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: rhythmsync
3
+ Version: 1.0.0
4
+ Summary: CLI music player with synchronized lyrics (LRC) support
5
+ Author: KON/NOS R
6
+ License: MIT
7
+ Keywords: music,cli,audio,lrc,player,terminal
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: pygame
11
+ Requires-Dist: rich
12
+ Requires-Dist: mutagen
13
+
14
+ # RhythmSync
15
+
16
+ CLI audio player that plays music while displaying synchronized lyrics (LRC).
17
+
18
+ ---
19
+
20
+ ## Features
21
+
22
+ - Play single audio files:
23
+ - Once
24
+ - In loop (`-r`)
25
+ - Play all supported audio files in a directory:
26
+ - Alphabetical order (`-d`)
27
+ - Alphabetical loop (`-dr`)
28
+ - Shuffle (`-ds`)
29
+ - Display synchronized lyrics during playback
30
+ - Show song metadata tags
31
+ - Styled terminal music player UI
32
+
33
+ ---
34
+
35
+ ## Supported Audio Formats
36
+
37
+ - `.mp3`
38
+ - `.flac`
39
+ - `.wav`
40
+ - `.ogg`
41
+
42
+ ### Notes
43
+ - Other audio formats might work, but full functionality is not guaranteed.
44
+
45
+ ---
46
+
47
+ ## Lyrics Sync
48
+
49
+ RhythmSync reads embedded lyric metadata from audio files.
50
+
51
+ Supported tags include:
52
+ - `SYLT`, `SYLT::eng`
53
+ - `LYRICS`, `LYRICS:eng`
54
+ - `LYRICS-ENG`, `LYRICS_EN`
55
+ - `LYRICS_SYNCED`, `SYNCEDLYRICS`
56
+
57
+ ### LRC Format
58
+
59
+ ```
60
+ [00:12.34] First line
61
+ [00:15.67] Second line
62
+ [00:18.90] Third line
63
+ ```
64
+
65
+ - Format: `[mm:ss.xx] Line`
66
+
67
+ ### Notes
68
+ - If no lyrics are found, a placeholder message is shown.
69
+
70
+ ---
71
+
72
+ ## Commands
73
+
74
+ ### General
75
+
76
+ ```
77
+ help - Lists all available commands.
78
+ clear - Clears the terminal.
79
+ ```
80
+
81
+
82
+ ### Playback
83
+
84
+ ```
85
+ play {path} - Plays a single audio file once.
86
+ play -r {path} - Plays a single audio file in repeat mode until stopped (Ctrl+C).
87
+ play -d {directory} - Plays all supported audio files in alphabetical order.
88
+ play -dr {directory} - Plays all supported audio files in alphabetical order and loops around until stopped (Ctrl+C).
89
+ play -ds {directory} - Plays all supported audio files in shuffled order.
90
+ ```
91
+
92
+
93
+ ### Metadata
94
+
95
+ ```
96
+ info {path} - Displays all available metadata.
97
+ info {path} [tags] - Displays only given metadata tags.
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Notes
103
+
104
+ - Paths containing spaces must be wrapped in quotes:
105
+
106
+ ```
107
+ play "My Music/song.mp3"
108
+ ```
109
+
110
+ - Use `Ctrl+C` to stop playback or exit the app
@@ -0,0 +1,97 @@
1
+ # RhythmSync
2
+
3
+ CLI audio player that plays music while displaying synchronized lyrics (LRC).
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ - Play single audio files:
10
+ - Once
11
+ - In loop (`-r`)
12
+ - Play all supported audio files in a directory:
13
+ - Alphabetical order (`-d`)
14
+ - Alphabetical loop (`-dr`)
15
+ - Shuffle (`-ds`)
16
+ - Display synchronized lyrics during playback
17
+ - Show song metadata tags
18
+ - Styled terminal music player UI
19
+
20
+ ---
21
+
22
+ ## Supported Audio Formats
23
+
24
+ - `.mp3`
25
+ - `.flac`
26
+ - `.wav`
27
+ - `.ogg`
28
+
29
+ ### Notes
30
+ - Other audio formats might work, but full functionality is not guaranteed.
31
+
32
+ ---
33
+
34
+ ## Lyrics Sync
35
+
36
+ RhythmSync reads embedded lyric metadata from audio files.
37
+
38
+ Supported tags include:
39
+ - `SYLT`, `SYLT::eng`
40
+ - `LYRICS`, `LYRICS:eng`
41
+ - `LYRICS-ENG`, `LYRICS_EN`
42
+ - `LYRICS_SYNCED`, `SYNCEDLYRICS`
43
+
44
+ ### LRC Format
45
+
46
+ ```
47
+ [00:12.34] First line
48
+ [00:15.67] Second line
49
+ [00:18.90] Third line
50
+ ```
51
+
52
+ - Format: `[mm:ss.xx] Line`
53
+
54
+ ### Notes
55
+ - If no lyrics are found, a placeholder message is shown.
56
+
57
+ ---
58
+
59
+ ## Commands
60
+
61
+ ### General
62
+
63
+ ```
64
+ help - Lists all available commands.
65
+ clear - Clears the terminal.
66
+ ```
67
+
68
+
69
+ ### Playback
70
+
71
+ ```
72
+ play {path} - Plays a single audio file once.
73
+ play -r {path} - Plays a single audio file in repeat mode until stopped (Ctrl+C).
74
+ play -d {directory} - Plays all supported audio files in alphabetical order.
75
+ play -dr {directory} - Plays all supported audio files in alphabetical order and loops around until stopped (Ctrl+C).
76
+ play -ds {directory} - Plays all supported audio files in shuffled order.
77
+ ```
78
+
79
+
80
+ ### Metadata
81
+
82
+ ```
83
+ info {path} - Displays all available metadata.
84
+ info {path} [tags] - Displays only given metadata tags.
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Notes
90
+
91
+ - Paths containing spaces must be wrapped in quotes:
92
+
93
+ ```
94
+ play "My Music/song.mp3"
95
+ ```
96
+
97
+ - Use `Ctrl+C` to stop playback or exit the app
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rhythmsync"
7
+ version = "1.0.0"
8
+ description = "CLI music player with synchronized lyrics (LRC) support"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "KON/NOS R"}
14
+ ]
15
+
16
+ keywords = ["music", "cli", "audio", "lrc", "player", "terminal"]
17
+
18
+ dependencies = [
19
+ "pygame",
20
+ "rich",
21
+ "mutagen"
22
+ ]
23
+
24
+ [project.scripts]
25
+ rhythmsync = "rhythmsync.main:main"
File without changes
@@ -0,0 +1,411 @@
1
+ import os
2
+ os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1"
3
+ import pygame
4
+ from rich.console import Console
5
+ console = Console()
6
+ from rich.console import Group
7
+ from rich.panel import Panel
8
+ from rich.align import Align
9
+ from rich.progress import Progress, BarColumn, TextColumn
10
+ from rich.live import Live
11
+ from rich.layout import Layout
12
+ from mutagen import File
13
+ from re import match
14
+ from pathlib import Path
15
+ from random import shuffle
16
+ import shlex
17
+
18
+
19
+ #create rich layout
20
+ def make_layout():
21
+ layout = Layout()
22
+ layout.split_column(
23
+ Layout(name="header", size=4),
24
+ Layout(name="lyrics", ratio=1),
25
+ Layout(name="player", size=3)
26
+ )
27
+ return layout
28
+
29
+ #header section
30
+ def make_header(title, artist, mode = None):
31
+ if mode is not None:
32
+ mode = mode.split()
33
+ if mode[0] == "repeat":
34
+ return Panel(
35
+ Group(
36
+ Align.center(f"[bold]{title}[/bold]"),
37
+ Align.center(f"[#5900ab]{artist}[/#5900ab]")
38
+ ),
39
+ title="⭮",
40
+ title_align="right",
41
+ style="white",
42
+ )
43
+ elif mode[0][:9] == "directory":
44
+ now, total = mode[1:3]
45
+ if mode[0] == "directory-shuffle":
46
+ return Panel(
47
+ Group(
48
+ Align.center(f"[bold]{title}[/bold]"),
49
+ Align.center(f"[#5900ab]{artist}[/#5900ab]")
50
+ ),
51
+ title=f"🔀︎Playing {now} of {total}",
52
+ title_align="right",
53
+ style="white",
54
+ )
55
+ elif mode[0] == "directory-repeat":
56
+ return Panel(
57
+ Group(
58
+ Align.center(f"[bold]{title}[/bold]"),
59
+ Align.center(f"[#5900ab]{artist}[/#5900ab]")
60
+ ),
61
+ title=f"⭮ Playing {now} of {total}",
62
+ title_align="right",
63
+ style="white",
64
+ )
65
+
66
+ return Panel(
67
+ Group(
68
+ Align.center(f"[bold]{title}[/bold]"),
69
+ Align.center(f"[#5900ab]{artist}[/#5900ab]")
70
+ ),
71
+ title=f"Playing {now} of {total}",
72
+ title_align="right",
73
+ style="white",
74
+ )
75
+
76
+ return Panel(
77
+ Group(Align.center(f"[bold]{title}[/bold]"),
78
+ Align.center(f"[#5900ab]{artist}[/#5900ab]")),
79
+ style="white",
80
+ )
81
+
82
+ #lyrics section
83
+ def make_lyrics(lyrics):
84
+ line1, line2, line3, line4, line5 = lyrics
85
+ return Align.center(Group(
86
+ Align.center(f"[#1f1f1f]{line1}[#1f1f1f]"),
87
+ Align.center(f"[#2f2f2f]{line2}[#2f2f2f]"),
88
+ Align.center(f"[bold #00d0ff]{line3}[/bold #00d0ff]"),
89
+ Align.center(f"[white]{line4}[white]"),
90
+ Align.center(f"[#afafaf]{line5}[#afafaf]")
91
+ ),
92
+ vertical="middle"
93
+ )
94
+
95
+ #player section
96
+ def make_player():
97
+ return Progress(
98
+ TextColumn("{task.description}[/]", justify="right"),
99
+ BarColumn(bar_width=None),
100
+ TextColumn("{task.fields[suffix]}", justify="right"),
101
+ )
102
+
103
+ #clear the terminal
104
+ def clear_screen():
105
+ os.system('cls' if os.name == 'nt' else 'clear')
106
+
107
+ #format time in mm:ss.xx format
108
+ def format_time(milliseconds):
109
+ min = int((milliseconds // 1000) // 60)
110
+ sec = (milliseconds // 1000) % 60
111
+ mil = milliseconds % 1000
112
+ hund = int(mil // 10)
113
+ return f"{min:02}:{sec:02}.{hund:02}"
114
+
115
+ #format time to milliseconds
116
+ def unformat_time(time_str):
117
+ min, sec = time_str.split(":")
118
+ sec, hund = sec.split(".")
119
+ milliseconds = (
120
+ int(min) * 60 * 1000 + int(sec) * 1000 + int(hund) * 10)
121
+ return milliseconds
122
+
123
+ #get meatadata
124
+ def get_metadata(file_path, tags = None):
125
+ try:
126
+ audio = File(file_path)
127
+
128
+ if audio is None:
129
+ print(f"Error: Could not open file {file_path}")
130
+ return None
131
+
132
+ lines = []
133
+
134
+ if tags is None:
135
+ for key, value in audio.items():
136
+ if isinstance(value, list):
137
+ value = "; ".join(str(v) for v in value)
138
+
139
+ lines.append(f"[green]{key}[/green]: {value}")
140
+ else:
141
+ for key, value in audio.items():
142
+ if isinstance(value, list):
143
+
144
+ if key in tags:
145
+ value = "; ".join(str(v) for v in value)
146
+
147
+ lines.append(f"[green]{key}[/green]: {value}")
148
+
149
+ return "\n".join(lines)
150
+
151
+ except Exception as e:
152
+ print(f"Error extracting metadata: {e}")
153
+ return None
154
+
155
+ #get title and artist info
156
+ def get_ti_ar(file_path):
157
+ try:
158
+ audio = File(file_path)
159
+
160
+ if audio is None:
161
+ return "Unknown Title", "Unknown Artist"
162
+
163
+ title = audio.get("title", ["Unknown Title"])[0]
164
+ artist = audio.get("artist", ["Unknown Artist"])[0]
165
+
166
+ return title, artist
167
+
168
+ except Exception as e:
169
+ print(f"Error extracting metadata: {e}")
170
+ return "Unknown Title", "Unknown Artist"
171
+
172
+ #get lrc data from the audio file
173
+ def get_lrc(file_path):
174
+ try:
175
+ audio = File(file_path)
176
+
177
+ if audio is None:
178
+ print(f"Error: Could not open file {file_path}")
179
+ return None
180
+
181
+ lrc_tag_names = ['SYLT', 'SYLT::eng', 'LYRICS', 'LYRICS:eng', 'LYRICS-ENG', 'LYRICS_EN', 'LYRICS_SYNCED', 'SYNCEDLYRICS']
182
+
183
+ for tag_name in lrc_tag_names:
184
+ if tag_name in audio:
185
+ return audio[tag_name][0]
186
+ return None
187
+
188
+ except Exception as e:
189
+ print(f"Error extracting LRC data: {e}")
190
+ return None
191
+
192
+ #get lyric lines without the tags from the lrc data
193
+ def format_lrc(lrc_data):
194
+ timestamp = r"^\[\d{2}:\d{2}\.\d{2}\]"
195
+ lrc_lines = lrc_data.split("\n")
196
+ lyrics = [[line[1:9],line[10:].strip()] for line in lrc_lines if match(timestamp, line)]
197
+ lyrics.insert(0,['00:00.00', ""])
198
+ for x in lyrics:
199
+ if x[1] == "":
200
+ x[1] = "♫"
201
+ return lyrics
202
+
203
+ #music player
204
+ def run_player(file_path, mode = None):
205
+ global repeat
206
+ try:
207
+ layout = make_layout()
208
+
209
+ pygame.mixer.init()
210
+ pygame.mixer.music.load(file_path)
211
+ pygame.mixer.music.play()
212
+
213
+ total_length = int(pygame.mixer.Sound(file_path).get_length()*1000)
214
+
215
+ title, artist = get_ti_ar(file_path)
216
+
217
+ lyrics_exist = False
218
+ raw_lrc = get_lrc(file_path)
219
+ if raw_lrc is not None:
220
+ lyrics = format_lrc(raw_lrc)
221
+ lyrics_exist = True
222
+
223
+ clear_screen()
224
+
225
+ layout["header"].update(make_header(title, artist, mode))
226
+
227
+ layout["lyrics"].update(Align.center("No lyrics to display", vertical="middle"))
228
+
229
+ progress = make_player()
230
+ layout["player"].update(progress)
231
+
232
+ with Live(layout, refresh_per_second=100):
233
+ playback = progress.add_task(
234
+ f"[red]< [#00d0ff]",
235
+ total=total_length,
236
+ suffix="[#00d0ff] [red]>")
237
+
238
+ lyric_index = 0
239
+
240
+ while pygame.mixer.music.get_busy(): #music starts
241
+ current_time = pygame.mixer.music.get_pos()
242
+
243
+ if lyrics_exist and lyric_index < len(lyrics):
244
+ if unformat_time(lyrics[lyric_index][0]) <= current_time:
245
+ layout["lyrics"].update(make_lyrics((
246
+ lyrics[lyric_index-2][1] if lyric_index > 1 else "",
247
+ lyrics[lyric_index-1][1] if lyric_index > 0 else "",
248
+ lyrics[lyric_index][1],
249
+ lyrics[lyric_index+1][1] if lyric_index < len(lyrics)-1 else "",
250
+ lyrics[lyric_index+2][1] if lyric_index < len(lyrics)-2 else ""
251
+ )))
252
+ lyric_index += 1
253
+
254
+ progress.update(playback,
255
+ completed=current_time,
256
+ description=f"[red]< [#00d0ff]{format_time(current_time)}",
257
+ suffix=f"[#00d0ff]{format_time(total_length - current_time)} [red]>"
258
+ )
259
+ except KeyboardInterrupt:
260
+ repeat = False
261
+ clear_screen()
262
+ print(f"Exitting...")
263
+ pygame.mixer.music.stop()
264
+
265
+ #main program
266
+ def main():
267
+ clear_screen()
268
+ rhythmsync_ascii ='''[#5900ab]
269
+ [#5900ab] _____ _ _ _ [#00d0ff]_____
270
+ [#5900ab]| __ \\| | | | | | [#00d0ff]/ ____|
271
+ [#5900ab]| |__) | |__ _ _| |_| |__ _ __ ___ [#00d0ff]| (___ _ _ _ __ ___
272
+ [#5900ab]| _ /| '_ \\| | | | __| '_ \\| '_ ` _ \\ [#00d0ff]\\___ \\| | | | '_ \\ / __|
273
+ [#5900ab]| | \\ \\| | | | |_| | |_| | | | | | | | |[#00d0ff]____) | |_| | | | | (__
274
+ [#5900ab]|_| \\_\\_| |_|\\__, |\\__|_| |_|_| |_| |_|[#00d0ff]_____/ \__, |_| |_|\\___|
275
+ [#5900ab] __/ | [#00d0ff] __/ |
276
+ [#5900ab] |___/ [#00d0ff] |___/
277
+ '''
278
+ console.print(Align.center(rhythmsync_ascii))
279
+ while True:
280
+ try:
281
+ global repeat
282
+
283
+ raw_command = input(">")
284
+
285
+ if raw_command.strip():
286
+ command = shlex.split(raw_command)
287
+ command_parts = len(command)
288
+
289
+ #help command
290
+ if command[0] == "help" and command_parts == 1:
291
+ console.print('''command list:
292
+ [green]help[/green] - Lists all available commands.
293
+ [green]clear[/green] - Clears the terminal.
294
+ [green]play[/green] [cyan]{par} {path}[/cyan] - Plays the audio file located at the specified [cyan]{path}[/cyan].
295
+ If [cyan]{par}[/cyan] is "-r", the audio file plays in repeat until stopped.
296
+ If [cyan]{par}[/cyan] is "-d" and [cyan]{path}[/cyan] is a directory, the audio files of given directory play in alphabetical order.
297
+ If [cyan]{par}[/cyan] is "-dr" and [cyan]{path}[/cyan] is a directory, the audio files of given directory play in alphabetical order and loop around until stopped.
298
+ If [cyan]{par}[/cyan] is "-ds" and [cyan]{path}[/cyan] is a directory, the audio files of given directory play in shuffled order.
299
+ If [cyan]{par}[/cyan] is omitted, the audio file plays once.
300
+ [green]info[/green] [cyan]{path} {tags}[/cyan] - Displays metadata for the file at [cyan]{path}[/cyan].
301
+ If [cyan]{tags}[/cyan] is provided (separate them with space for multiple tags), shows only those specific tags and their respective values.
302
+ If [cyan]{tags}[/cyan] is omitted, shows all available tags and their respective values.
303
+ '''
304
+ )
305
+
306
+ #clear command
307
+ elif command[0] == "clear" and command_parts == 1:
308
+ clear_screen()
309
+ console.print(Align.center(rhythmsync_ascii))
310
+
311
+ #play command
312
+ elif command[0] == "play":
313
+ if command_parts == 2:
314
+ file_path = str(Path(command[1]).expanduser().resolve())
315
+ if os.path.exists(file_path):
316
+ run_player(file_path)
317
+ clear_screen()
318
+ console.print(Align.center(rhythmsync_ascii))
319
+ else:
320
+ print("Please enter a valid file path.")
321
+ elif command_parts == 3:
322
+ par = command[1]
323
+ file_path = str(Path(command[2]).expanduser().resolve())
324
+ if os.path.exists(file_path):
325
+ if par == "-r":
326
+ repeat = True
327
+ while repeat:
328
+ run_player(file_path, "repeat")
329
+ clear_screen()
330
+ console.print(Align.center(rhythmsync_ascii))
331
+ elif par == "-d":
332
+ extensions = (".mp3", ".flac", ".wav", ".ogg")
333
+ audio_files = [
334
+ f for f in Path(file_path).iterdir()
335
+ if f.suffix.lower() in extensions
336
+ ]
337
+ audio_files.sort()
338
+ repeat = True
339
+ i = 0
340
+ for file in audio_files:
341
+ i += 1
342
+ run_player(file, f"directory {i} {len(audio_files)}")
343
+ if repeat == False:
344
+ break
345
+ clear_screen()
346
+ console.print(Align.center(rhythmsync_ascii))
347
+ elif par == "-ds":
348
+ extensions = (".mp3", ".flac", ".wav", ".ogg")
349
+ audio_files = [
350
+ f for f in Path(file_path).iterdir()
351
+ if f.suffix.lower() in extensions
352
+ ]
353
+ shuffle(audio_files)
354
+ repeat = True
355
+ i = 0
356
+ for file in audio_files:
357
+ i += 1
358
+ run_player(file, f"directory-shuffle {i} {len(audio_files)}")
359
+ if repeat == False:
360
+ break
361
+ clear_screen()
362
+ console.print(Align.center(rhythmsync_ascii))
363
+ elif par == "-dr":
364
+ extensions = (".mp3", ".flac", ".wav", ".ogg")
365
+ audio_files = [
366
+ f for f in Path(file_path).iterdir()
367
+ if f.suffix.lower() in extensions
368
+ ]
369
+ audio_files.sort()
370
+ repeat = True
371
+ while repeat:
372
+ i = 0
373
+ for file in audio_files:
374
+ i += 1
375
+ run_player(file, f"directory-repeat {i} {len(audio_files)}")
376
+ if repeat == False:
377
+ break
378
+ clear_screen()
379
+ console.print(Align.center(rhythmsync_ascii))
380
+ else:
381
+ print("Please enter a valid file path.")
382
+ else:
383
+ print("Please enter a valid file path and parameters.")
384
+
385
+ #info command
386
+ elif command[0] == "info":
387
+ if command_parts >= 2:
388
+ file_path = Path(command[1]).expanduser().resolve()
389
+ par = tuple(command[2:]) or None
390
+ if os.path.exists(file_path):
391
+ console.print(get_metadata(file_path, par), highlight=False)
392
+ else:
393
+ print("Please enter a valid file path.")
394
+ else:
395
+ print("Please enter a valid file path and parameters.")
396
+
397
+ #invalid command
398
+ else:
399
+ print("Invalid command! Enter 'help' to display command list.")
400
+
401
+ except KeyboardInterrupt:
402
+ print(f"Exitting...")
403
+ break
404
+ except Exception as e:
405
+ clear_screen()
406
+ console.print(Align.center(rhythmsync_ascii))
407
+ print(f"Error: {e}")
408
+
409
+
410
+ if __name__ == "__main__":
411
+ main()
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: rhythmsync
3
+ Version: 1.0.0
4
+ Summary: CLI music player with synchronized lyrics (LRC) support
5
+ Author: KON/NOS R
6
+ License: MIT
7
+ Keywords: music,cli,audio,lrc,player,terminal
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: pygame
11
+ Requires-Dist: rich
12
+ Requires-Dist: mutagen
13
+
14
+ # RhythmSync
15
+
16
+ CLI audio player that plays music while displaying synchronized lyrics (LRC).
17
+
18
+ ---
19
+
20
+ ## Features
21
+
22
+ - Play single audio files:
23
+ - Once
24
+ - In loop (`-r`)
25
+ - Play all supported audio files in a directory:
26
+ - Alphabetical order (`-d`)
27
+ - Alphabetical loop (`-dr`)
28
+ - Shuffle (`-ds`)
29
+ - Display synchronized lyrics during playback
30
+ - Show song metadata tags
31
+ - Styled terminal music player UI
32
+
33
+ ---
34
+
35
+ ## Supported Audio Formats
36
+
37
+ - `.mp3`
38
+ - `.flac`
39
+ - `.wav`
40
+ - `.ogg`
41
+
42
+ ### Notes
43
+ - Other audio formats might work, but full functionality is not guaranteed.
44
+
45
+ ---
46
+
47
+ ## Lyrics Sync
48
+
49
+ RhythmSync reads embedded lyric metadata from audio files.
50
+
51
+ Supported tags include:
52
+ - `SYLT`, `SYLT::eng`
53
+ - `LYRICS`, `LYRICS:eng`
54
+ - `LYRICS-ENG`, `LYRICS_EN`
55
+ - `LYRICS_SYNCED`, `SYNCEDLYRICS`
56
+
57
+ ### LRC Format
58
+
59
+ ```
60
+ [00:12.34] First line
61
+ [00:15.67] Second line
62
+ [00:18.90] Third line
63
+ ```
64
+
65
+ - Format: `[mm:ss.xx] Line`
66
+
67
+ ### Notes
68
+ - If no lyrics are found, a placeholder message is shown.
69
+
70
+ ---
71
+
72
+ ## Commands
73
+
74
+ ### General
75
+
76
+ ```
77
+ help - Lists all available commands.
78
+ clear - Clears the terminal.
79
+ ```
80
+
81
+
82
+ ### Playback
83
+
84
+ ```
85
+ play {path} - Plays a single audio file once.
86
+ play -r {path} - Plays a single audio file in repeat mode until stopped (Ctrl+C).
87
+ play -d {directory} - Plays all supported audio files in alphabetical order.
88
+ play -dr {directory} - Plays all supported audio files in alphabetical order and loops around until stopped (Ctrl+C).
89
+ play -ds {directory} - Plays all supported audio files in shuffled order.
90
+ ```
91
+
92
+
93
+ ### Metadata
94
+
95
+ ```
96
+ info {path} - Displays all available metadata.
97
+ info {path} [tags] - Displays only given metadata tags.
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Notes
103
+
104
+ - Paths containing spaces must be wrapped in quotes:
105
+
106
+ ```
107
+ play "My Music/song.mp3"
108
+ ```
109
+
110
+ - Use `Ctrl+C` to stop playback or exit the app
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ rhythmsync/__init__.py
4
+ rhythmsync/main.py
5
+ rhythmsync.egg-info/PKG-INFO
6
+ rhythmsync.egg-info/SOURCES.txt
7
+ rhythmsync.egg-info/dependency_links.txt
8
+ rhythmsync.egg-info/entry_points.txt
9
+ rhythmsync.egg-info/requires.txt
10
+ rhythmsync.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rhythmsync = rhythmsync.main:main
@@ -0,0 +1,3 @@
1
+ pygame
2
+ rich
3
+ mutagen
@@ -0,0 +1 @@
1
+ rhythmsync
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+