audioPlayerTermPy.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  1. #!/usr/bin/env python3
  2. import urwid
  3. import os
  4. import pygame.mixer
  5. import mutagen
  6. import sys
  7. from datetime import datetime, timedelta
  8. import subprocess
  9. # Палитра (без изменений)
  10. palette = [
  11. ('header', 'light blue', 'default'),
  12. ('path_label', 'light blue', 'default'),
  13. ('path_value', 'dark gray', 'default'),
  14. ('directory', 'dark blue,bold', 'default'),
  15. ('audio_file', 'light cyan', 'default'),
  16. ('normal', 'white', 'default'),
  17. ('selected', 'black', 'light green'),
  18. ('perm_denied', 'light red', 'default'),
  19. ('error', 'light red', 'default'),
  20. ('playing', 'light green', 'default'),
  21. ('pink_frame', 'light magenta', 'default'),
  22. ('percent', 'white,bold', 'default'),
  23. ]
  24. class PlaybackMode(urwid.ListBox):
  25. def __init__(self, main_loop, root_dir, input_path=None):
  26. pygame.mixer.init()
  27. self.main_loop = main_loop
  28. self.root_dir = root_dir
  29. self.current_dir = os.getcwd()
  30. self.dir_history = []
  31. self.file_list = urwid.SimpleFocusListWalker([])
  32. self.playlist = [] # Список для плейлиста
  33. self.playlist_index = 0 # Текущий индекс в плейлисте
  34. # Левая рамка: Box00 (без изменений)
  35. self.progress_bar = urwid.Text([('path_value', " 0"), ('percent', '%'), (None, " | " + " " * 83)], align='left')
  36. self.file_frame = urwid.LineBox(self.progress_bar, title="Playback progress", title_align='center',
  37. tlcorner='┌', tline='─', trcorner='┐',
  38. lline='│', rline='│',
  39. blcorner='└', bline='─', brcorner='┘')
  40. self.file_frame = urwid.AttrMap(self.file_frame, 'pink_frame')
  41. # Правая рамка: Volume Level (без изменений)
  42. # self.volume_bar = urwid.Text(f" 50% {'░' * 17 + ' ' * 17}")
  43. self.volume_bar = urwid.Text(f" 50% | {'░' * 22 + ' ' * 22}")
  44. self.metadata_frame = urwid.LineBox(self.volume_bar, title='Pygame.mixer volume Level', title_align='center',
  45. tlcorner='┌', tline='─', trcorner='┐',
  46. lline='│', rline='│',
  47. blcorner='└', bline='─', brcorner='┘')
  48. self.metadata_frame = urwid.AttrMap(self.metadata_frame, 'pink_frame')
  49. # Рамка пути (без изменений)
  50. self.path_text_inner = urwid.Text([('path_value', self.current_dir)], align='left')
  51. self.path_text = urwid.Padding(self.path_text_inner, left=1)
  52. self.path_filler = urwid.Filler(self.path_text, valign='top')
  53. self.path_frame = urwid.LineBox(self.path_filler, title="Path", title_align='center',
  54. tlcorner='┌', tline='─', trcorner='┐',
  55. lline='│', rline='│',
  56. blcorner='└', bline='─', brcorner='┘')
  57. self.path_widget = urwid.AttrMap(self.path_frame, 'pink_frame')
  58. # Status (без изменений)
  59. self.status_output = urwid.Text("", align='left')
  60. self.status_filler = urwid.Filler(self.status_output, valign='top')
  61. self.playing = False
  62. self.paused = False
  63. self.volume = 0.5
  64. self.current_audio_duration = 0
  65. # Инициализация системной громкости (без изменений)
  66. try:
  67. result = subprocess.check_output("amixer get Master | grep -o '[0-9]*%' | uniq", shell=True, text=True).strip()
  68. percent = int(result.rstrip('%'))
  69. filled = min(34, percent // 3)
  70. initial_volume_text = f" {percent}% {'░' * filled + ' ' * (34 - filled)}"
  71. except subprocess.CalledProcessError:
  72. initial_volume_text = " --% " + " " * 34
  73. self.system_volume_bar = urwid.Text(initial_volume_text)
  74. # Инициализация прогресс-бара для левого наушника (без изменений)
  75. try:
  76. result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Left' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  77. percent = int(result.rstrip('%'))
  78. filled = min(29, percent // 3)
  79. initial_headphone_left_text = f" {percent}% | {'░' * filled + ' ' * (29 - filled)}"
  80. except (subprocess.CalledProcessError, ValueError):
  81. initial_headphone_left_text = " --% | " + " " * 29
  82. self.headphone_left_bar = urwid.Text(initial_headphone_left_text, align='left')
  83. # Инициализация прогресс-бара для правого наушника (без изменений)
  84. try:
  85. result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Right' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  86. percent = int(result.rstrip('%'))
  87. filled = min(29, percent // 3)
  88. initial_headphone_right_text = f" {percent}% | {'░' * filled + ' ' * (29 - filled)}"
  89. except (subprocess.CalledProcessError, ValueError):
  90. initial_headphone_right_text = " --% | " + " " * 29
  91. self.headphone_right_bar = urwid.Text(initial_headphone_right_text, align='left')
  92. # Alisa (часы) (без изменений)
  93. self.alisa_text = urwid.Text("", align='center')
  94. super().__init__(self.file_list)
  95. self.input_path = input_path
  96. if not input_path:
  97. self.refresh_list()
  98. self.widget = None
  99. self.initialize_widget()
  100. def start(self):
  101. if self.input_path:
  102. if os.path.isdir(self.input_path):
  103. self.load_and_play_directory(self.input_path)
  104. elif os.path.isfile(self.input_path):
  105. self.load_and_play_audio(self.input_path)
  106. def update_clock(self, loop=None, data=None):
  107. SEASONS = {
  108. 1: "Winter", 2: "Winter", 3: "Spring",
  109. 4: "Spring", 5: "Spring", 6: "Summer",
  110. 7: "Summer", 8: "Summer", 9: "Autumn",
  111. 10: "Autumn", 11: "Autumn", 12: "Winter"
  112. }
  113. MONTHS = {
  114. 1: "January", 2: "February", 3: "March",
  115. 4: "April", 5: "May", 6: "June",
  116. 7: "July", 8: "August", 9: "September",
  117. 10: "October", 11: "November", 12: "December"
  118. }
  119. WEEKDAYS = {
  120. 0: "Monday", 1: "Tuesday", 2: "Wednesday",
  121. 3: "Thursday", 4: "Friday", 5: "Saturday", 6: "Sunday"
  122. }
  123. now = datetime.now()
  124. year = "2025"
  125. season = SEASONS[now.month]
  126. month = MONTHS[now.month]
  127. weekday = WEEKDAYS[now.weekday()]
  128. time_str = now.strftime('%H:%M:%S')
  129. clock_text = f"{year}\n{season} {month} {weekday}\n{time_str}"
  130. self.alisa_text.set_text(clock_text)
  131. if self.main_loop:
  132. self.main_loop.set_alarm_in(1 / 30, self.update_clock)
  133. def format_time(self, seconds):
  134. return str(timedelta(seconds=int(seconds))).zfill(8)
  135. def update_progress_bar(self, loop=None, data=None):
  136. if self.playing and not self.paused and pygame.mixer.music.get_busy():
  137. elapsed = pygame.mixer.music.get_pos() / 1000
  138. duration = self.current_audio_duration
  139. if duration > 0:
  140. progress_percent = min(100, int((elapsed / duration) * 100))
  141. filled = min(83, int(progress_percent / 1.2048))
  142. unfilled = 83 - filled
  143. progress_str = [('path_value', f"{progress_percent:3d}"), ('percent', '%'), (None, f" | {'░' * filled}{' ' * unfilled}")]
  144. self.progress_bar.set_text(progress_str)
  145. elapsed_str = self.format_time(elapsed)
  146. duration_str = self.format_time(duration)
  147. self.grannik_text.set_text(f" {elapsed_str} / {duration_str}")
  148. else:
  149. self.progress_bar.set_text([('path_value', " 0"), ('percent', '%'), (None, " | " + " " * 83)])
  150. self.grannik_text.set_text(" 00:00:00 / 00:00:00")
  151. if self.main_loop:
  152. self.main_loop.set_alarm_in(1, self.update_progress_bar)
  153. def initialize_widget(self):
  154. self.widget = self.wrap_in_three_frames()
  155. def wrap_in_three_frames(self):
  156. term_size = os.get_terminal_size()
  157. term_width = term_size.columns
  158. term_height = term_size.lines
  159. total_border_chars = 6
  160. available_width = term_width - total_border_chars
  161. num_upper_boxes = 3
  162. base_status_width = (available_width // 3 + available_width // 4) // 2
  163. status_width = max(15, base_status_width + 3)
  164. remaining_width = available_width - status_width - 7
  165. files_width = remaining_width // 2
  166. metadata_width = available_width - files_width - status_width + 2
  167. total_width_so_far = files_width + status_width + metadata_width + 8
  168. if total_width_so_far < available_width:
  169. metadata_width += available_width - total_width_so_far
  170. total_width = term_width - 1
  171. left_width = int(total_width * 0.62)
  172. combined_widget = urwid.Columns([
  173. (left_width, self.file_frame),
  174. ('weight', 1, self.metadata_frame),
  175. ], dividechars=1, box_columns=[0, 1])
  176. height_limited_widget = urwid.Filler(combined_widget, height=3, valign='top')
  177. box02_clone = urwid.LineBox(self.system_volume_bar, title='Amixer master volume Level', title_align='center',
  178. tlcorner='┌', tline='─', trcorner='┐',
  179. lline='│', rline='│',
  180. blcorner='└', bline='─', brcorner='┘')
  181. box02_clone = urwid.AttrMap(box02_clone, 'pink_frame')
  182. box02_clone2 = urwid.LineBox(self.headphone_left_bar, title='Amixer headphone left', title_align='center',
  183. tlcorner='┌', tline='─', trcorner='┐',
  184. lline='│', rline='│',
  185. blcorner='└', bline='─', brcorner='┘')
  186. box02_clone2 = urwid.AttrMap(box02_clone2, 'pink_frame')
  187. box02_clone3 = urwid.LineBox(self.headphone_right_bar, title='Amixer headphone right', title_align='center',
  188. tlcorner='┌', tline='─', trcorner='┐',
  189. lline='│', rline='│',
  190. blcorner='└', bline='─', brcorner='┘')
  191. box02_clone3 = urwid.AttrMap(box02_clone3, 'pink_frame')
  192. header_height = 3
  193. frame_border_height = 2
  194. available_height = term_height - header_height - frame_border_height
  195. upper_boxes_height = 3
  196. min_footer_height = 8
  197. columns_height = max(21, available_height - upper_boxes_height - min_footer_height - 1)
  198. new_left_frame = urwid.LineBox(self, title="Files and directories of the Linux OS", title_align='center',
  199. tlcorner='┌', tline='─', trcorner='┐',
  200. lline='│', rline='│',
  201. blcorner='└', bline='─', brcorner='┘')
  202. new_left_frame = urwid.AttrMap(new_left_frame, 'pink_frame')
  203. new_left_frame_filler = urwid.Filler(new_left_frame, height=columns_height, valign='top')
  204. self.metadata_output = urwid.Text("", align='left')
  205. self.metadata_filler = urwid.Filler(self.metadata_output, valign='top')
  206. new_right_frame = urwid.LineBox(self.metadata_filler, title="Info", title_align='center',
  207. tlcorner='┌', tline='─', trcorner='┐',
  208. lline='│', rline='│',
  209. blcorner='└', bline='─', brcorner='┘')
  210. new_right_frame = urwid.AttrMap(new_right_frame, 'pink_frame')
  211. new_right_frame_filler = urwid.Filler(new_right_frame, height=columns_height, valign='top')
  212. new_frames_widget = urwid.Columns([
  213. (left_width, new_left_frame_filler),
  214. ('weight', 1, new_right_frame_filler)
  215. ], dividechars=1)
  216. footer_width = left_width
  217. footer_height = available_height - columns_height - upper_boxes_height
  218. box_height = max(min_footer_height + 1, footer_height)
  219. divider_width = 2
  220. available_footer_width = footer_width - divider_width
  221. box1_width = available_footer_width // 3
  222. box2_width = available_footer_width // 3
  223. box4_width = available_footer_width // 3
  224. self.grannik_text = urwid.Text(" 00:00:00 / 00:00:00", align='left')
  225. box1 = urwid.LineBox(self.grannik_text, title="Playback time", title_align='center',
  226. tlcorner='┌', tline='─', trcorner='┐',
  227. lline='│', rline='│',
  228. blcorner='└', bline='─', brcorner='┘')
  229. box1 = urwid.AttrMap(box1, 'pink_frame')
  230. box1_filler = urwid.Filler(box1, height=box_height, valign='middle')
  231. box2 = urwid.LineBox(self.alisa_text, title="Current time", title_align='center',
  232. tlcorner='┌', tline='─', trcorner='┐',
  233. lline='│', rline='│',
  234. blcorner='└', bline='─', brcorner='┘')
  235. box2 = urwid.AttrMap(box2, 'pink_frame')
  236. box2_filler = urwid.Filler(box2, height=box_height, valign='middle')
  237. box4 = urwid.LineBox(self.status_filler, title="Status", title_align='center',
  238. tlcorner='┌', tline='─', trcorner='┐',
  239. lline='│', rline='│',
  240. blcorner='└', bline='─', brcorner='┘')
  241. box4 = urwid.AttrMap(box4, 'pink_frame')
  242. box4_filler = urwid.Filler(box4, height=box_height, valign='middle')
  243. footer_columns = urwid.Columns([
  244. (box1_width, box1_filler),
  245. (box2_width, box2_filler),
  246. (box4_width, box4_filler),
  247. ], dividechars=1, box_columns=[0, 1, 2])
  248. clone_width = metadata_width
  249. clones_pile = urwid.Pile([
  250. (3, box02_clone),
  251. (3, box02_clone2),
  252. (3, box02_clone3),
  253. ])
  254. clones_filler = urwid.Filler(clones_pile, height=9, valign='middle')
  255. footer_with_clones = urwid.Columns([
  256. (footer_width, footer_columns),
  257. (clone_width, clones_filler),
  258. ], dividechars=1, box_columns=[0, 1])
  259. footer_widget = urwid.Filler(footer_with_clones, height=box_height, valign='middle')
  260. body_widget = urwid.Pile([
  261. (columns_height, new_frames_widget),
  262. (upper_boxes_height, height_limited_widget),
  263. (box_height, footer_widget),
  264. ])
  265. frame_with_path = urwid.Frame(
  266. body=body_widget,
  267. header=self.path_widget,
  268. )
  269. return frame_with_path
  270. def load_and_play_audio(self, audio_file):
  271. full_path = os.path.abspath(audio_file)
  272. self.current_dir = os.path.dirname(full_path)
  273. file_name = os.path.basename(full_path)
  274. self.file_list.clear()
  275. padded_text = urwid.Padding(urwid.Text(file_name), left=1, right=1)
  276. self.file_list.append(urwid.AttrMap(padded_text, 'normal', 'selected'))
  277. self.set_focus(0)
  278. self.path_text_inner.set_text([('path_value', self.current_dir)])
  279. self.play_media(full_path)
  280. def load_and_play_directory(self, directory):
  281. full_path = os.path.abspath(directory)
  282. self.current_dir = full_path
  283. self.path_text_inner.set_text([('path_value', self.current_dir)])
  284. AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma', 'opus'}
  285. try:
  286. all_files = sorted(os.listdir(self.current_dir))
  287. audio_files = [f for f in all_files
  288. if not f.startswith('.') and
  289. f.lower().split('.')[-1] in AUDIO_EXTENSIONS]
  290. if not audio_files:
  291. self.file_list.clear()
  292. self.file_list.append(urwid.AttrMap(urwid.Padding(urwid.Text("(empty)"), left=1, right=1), 'normal', 'selected'))
  293. return
  294. self.file_list.clear()
  295. self.playlist = [os.path.join(self.current_dir, f) for f in audio_files]
  296. self.playlist_index = 0
  297. for file in audio_files:
  298. padded_text = urwid.Padding(urwid.Text(file), left=1, right=1)
  299. self.file_list.append(urwid.AttrMap(padded_text, 'audio_file', 'selected'))
  300. self.set_focus(0)
  301. self.play_media(self.playlist[self.playlist_index])
  302. except PermissionError:
  303. self.file_list.clear()
  304. self.file_list.append(urwid.AttrMap(urwid.Padding(urwid.Text("(access denied)"), left=1, right=1), 'perm_denied', 'selected'))
  305. def check_playback_end(self):
  306. if self.main_loop is not None and self.playing and not pygame.mixer.music.get_busy() and not self.paused:
  307. self.next_track()
  308. if self.main_loop is not None:
  309. self.main_loop.set_alarm_in(0.1, lambda loop, data: self.check_playback_end())
  310. def next_track(self):
  311. if self.playlist and self.playlist_index < len(self.playlist) - 1:
  312. self.playlist_index += 1
  313. self.set_focus(self.playlist_index)
  314. self.play_media(self.playlist[self.playlist_index])
  315. else:
  316. pygame.mixer.music.stop()
  317. self.playing = False
  318. self.status_output.set_text(" Playlist ended")
  319. self.metadata_output.set_text("")
  320. def update_file_list(self):
  321. AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma', 'opus'}
  322. try:
  323. all_files = sorted(os.listdir(self.current_dir))
  324. files = [f for f in all_files
  325. if not f.startswith('.') and
  326. (os.path.isdir(os.path.join(self.current_dir, f)) or
  327. f.lower().split('.')[-1] in AUDIO_EXTENSIONS)]
  328. if not files:
  329. files = ["(empty)"]
  330. except PermissionError:
  331. files = ["(access denied)"]
  332. file_items = []
  333. for file in files:
  334. full_path = os.path.join(self.current_dir, file)
  335. if os.path.isdir(full_path):
  336. attr = 'directory'
  337. display_name = file + "/"
  338. elif os.path.isfile(full_path):
  339. attr = 'audio_file'
  340. display_name = file
  341. else:
  342. attr = 'normal'
  343. display_name = file
  344. padded_text = urwid.Padding(urwid.Text(display_name), left=1, right=1)
  345. file_items.append(urwid.AttrMap(padded_text, attr, 'selected'))
  346. return file_items
  347. def refresh_list(self):
  348. old_focus = self.focus_position if self.file_list else 0
  349. self.file_list[:] = self.update_file_list()
  350. self.path_text_inner.set_text([('path_value', self.current_dir)])
  351. if self.file_list:
  352. self.set_focus(min(old_focus, len(self.file_list) - 1))
  353. def get_widget(self):
  354. return self.widget
  355. def cleanup(self):
  356. if self.playing:
  357. pygame.mixer.music.stop()
  358. self.status_output.set_text("")
  359. self.metadata_output.set_text("")
  360. self.playing = False
  361. self.paused = False
  362. def show_message(self, message, duration=1):
  363. if "Permission denied" in message:
  364. self.status_output.set_text([('perm_denied', f" {message}")])
  365. duration = 0
  366. elif "not found" in message or "Error" in message:
  367. self.status_output.set_text([('error', f" {message}")])
  368. duration = 2
  369. else:
  370. self.status_output.set_text([('normal', f" {message}")])
  371. self.main_loop.draw_screen()
  372. if duration > 0:
  373. def clear_message(loop, data):
  374. if self.status_output.text in ([('perm_denied', f" {message}"), ('error', f" {message}"), ('normal', f" {message}")]):
  375. self.status_output.set_text("")
  376. self.main_loop.draw_screen()
  377. self.main_loop.set_alarm_in(duration, clear_message)
  378. def clear_message(self):
  379. self.status_output.set_text("")
  380. self.main_loop.draw_screen()
  381. def get_metadata(self, filepath):
  382. try:
  383. audio = mutagen.File(filepath)
  384. if audio is None:
  385. return " No metadata available"
  386. metadata = []
  387. if hasattr(audio, 'info'):
  388. metadata.append(f" Duration: {audio.info.length:.2f} sec")
  389. metadata.append(f" Bitrate: {audio.info.bitrate // 1000} kbps")
  390. metadata.append(f" Channels: {audio.info.channels}")
  391. metadata.append(f" Sample Rate: {audio.info.sample_rate} Hz")
  392. if audio.tags:
  393. for key, value in audio.tags.items():
  394. value_str = str(value)[:50] + "..." if len(str(value)) > 50 else str(value)
  395. metadata.append(f" {key}: {value_str}")
  396. max_lines = 10
  397. if len(metadata) > max_lines:
  398. metadata = metadata[:max_lines - 1] + [" ... (truncated)"]
  399. return "\n".join(metadata) if metadata else " No metadata available"
  400. except Exception as e:
  401. return f" Error reading metadata: {e}"
  402. def play_media(self, filepath):
  403. if not os.path.exists(filepath):
  404. self.show_message(f"File not found: {filepath}")
  405. return
  406. if not os.access(filepath, os.R_OK):
  407. self.show_message("Permission denied!")
  408. return
  409. if self.playing:
  410. pygame.mixer.music.stop()
  411. try:
  412. pygame.mixer.music.load(filepath)
  413. pygame.mixer.music.set_volume(self.volume)
  414. pygame.mixer.music.play()
  415. self.playing = True
  416. self.paused = False
  417. self.status_output.set_text([("playing", f" Playing: {os.path.basename(filepath)}")])
  418. self.metadata_output.set_text(self.get_metadata(filepath))
  419. # filled = int(self.volume * 40)
  420. # self.volume_bar.set_text(f" {int(self.volume * 100)}% {'░' * filled + ' ' * (40 - filled)}")
  421. filled = int(self.volume * 44)
  422. self.volume_bar.set_text(f" {int(self.volume * 100)}% | {'░' * filled + ' ' * (44 - filled)}")
  423. audio = mutagen.File(filepath)
  424. if audio and hasattr(audio, 'info'):
  425. self.current_audio_duration = audio.info.length
  426. else:
  427. sound = pygame.mixer.Sound(filepath)
  428. self.current_audio_duration = sound.get_length()
  429. except Exception as e:
  430. self.show_message(f"Error playing media: {str(e)}")
  431. def keypress(self, size, key):
  432. current_message = self.status_output.text
  433. is_perm_denied = isinstance(current_message, list) and len(current_message) > 0 and "Permission denied" in current_message[0][1]
  434. help_text = [
  435. ('normal,bold', ' left'), ('path_value', ' - Go to parent directory.\n'),
  436. ('normal,bold', ' right'), ('path_value', ' - Go back in directory history.\n'),
  437. ('normal,bold', ' up'), ('path_value', ' - Move focus up in file list.\n'),
  438. ('normal,bold', ' down'), ('path_value', ' - Move focus down in file list.\n'),
  439. ('normal,bold', ' enter'), ('path_value', ' - Open folder or play file.\n'),
  440. ('normal,bold', ' space'), ('path_value', ' - Play directory as playlist.\n'),
  441. ('normal,bold', ' + -'), ('path_value', ' - Increase/Decrease volume (pygame).\n'),
  442. ('normal,bold', ' a'), ('path_value', ' - Увеличить громкость правого наушника\n'),
  443. ('normal,bold', ' b'), ('path_value', ' - Уменьшить громкость правого наушника\n'),
  444. ('normal,bold', ' c'), ('path_value', ' - Увеличить громкость левого наушника\n'),
  445. ('normal,bold', ' d'), ('path_value', ' - Decrease system volume.\n'),
  446. ('normal,bold', ' e'), ('path_value', ' - Увеличить громкость обоих наушников\n'),
  447. ('normal,bold', ' f'), ('path_value', ' - Уменьшить громкость обоих наушников\n'),
  448. ('normal,bold', ' g'), ('path_value', ' - Уменьшить громкость левого наушника\n'),
  449. ('normal,bold', ' p'), ('path_value', ' - Pause or resume playback.\n'),
  450. ('normal,bold', ' s'), ('path_value', ' - Stop playback.\n'),
  451. ('normal,bold', ' r'), ('path_value', ' - Restart current track.\n'),
  452. ('normal,bold', ' i'), ('path_value', ' - Increase system volume.\n'),
  453. ('normal,bold', ' n'), ('path_value', ' - Next track.\n'),
  454. ('normal,bold', ' q or Q'), ('path_value', ' - Quit program.\n'),
  455. ('normal,bold', ' h'), ('path_value', ' - Show help.')
  456. ]
  457. if key == 'h' and not self.playing and not self.paused:
  458. self.metadata_output.set_text(help_text)
  459. self.main_loop.draw_screen()
  460. elif key != 'h':
  461. if self.metadata_output.text == help_text:
  462. self.metadata_output.set_text("")
  463. self.main_loop.draw_screen()
  464. if key == 'left':
  465. if self.current_dir != "/":
  466. try:
  467. self.dir_history.append(self.current_dir)
  468. os.chdir("..")
  469. self.current_dir = os.getcwd()
  470. self.refresh_list()
  471. self.clear_message()
  472. self.main_loop.draw_screen()
  473. except PermissionError:
  474. self.show_message("Permission denied!")
  475. elif key == 'right':
  476. if self.dir_history:
  477. try:
  478. os.chdir(self.dir_history.pop())
  479. self.current_dir = os.getcwd()
  480. self.refresh_list()
  481. self.clear_message()
  482. self.main_loop.draw_screen()
  483. except PermissionError:
  484. self.show_message("Permission denied!")
  485. elif key == 'up' and self.focus_position > 0:
  486. self.set_focus(self.focus_position - 1)
  487. if not is_perm_denied:
  488. self.clear_message()
  489. elif key == 'down' and self.focus_position < len(self.file_list) - 1:
  490. self.set_focus(self.focus_position + 1)
  491. if not is_perm_denied:
  492. self.clear_message()
  493. elif key == 'enter':
  494. if not self.file_list or self.focus.original_widget.original_widget.text.strip() in ["(empty)", "(access denied)"]:
  495. return
  496. selected = self.focus.original_widget.original_widget.text.rstrip('/')
  497. full_path = os.path.join(self.current_dir, selected)
  498. try:
  499. if os.path.isdir(full_path):
  500. self.dir_history.append(self.current_dir)
  501. os.chdir(full_path)
  502. self.current_dir = os.getcwd()
  503. self.refresh_list()
  504. self.clear_message()
  505. self.main_loop.draw_screen()
  506. elif os.path.isfile(full_path):
  507. self.play_media(full_path)
  508. self.playlist = [full_path]
  509. self.playlist_index = 0
  510. except Exception as e:
  511. self.show_message(f"Error: {str(e)}")
  512. elif key == ' ':
  513. if self.playing or self.paused:
  514. pygame.mixer.music.stop()
  515. self.playing = False
  516. self.paused = False
  517. self.file_list.clear()
  518. self.load_and_play_directory(self.current_dir)
  519. self.check_playback_end()
  520. self.main_loop.draw_screen()
  521. elif key == 'p':
  522. if self.playing:
  523. if self.paused:
  524. pygame.mixer.music.unpause()
  525. self.paused = False
  526. filepath = os.path.join(self.current_dir, self.focus.original_widget.original_widget.text.rstrip('/'))
  527. self.status_output.set_text([("playing", f" Resumed: {os.path.basename(filepath)}")])
  528. else:
  529. pygame.mixer.music.pause()
  530. self.paused = True
  531. self.status_output.set_text(" Paused")
  532. elif key == 's':
  533. if self.playing:
  534. pygame.mixer.music.stop()
  535. self.playing = False
  536. self.paused = False
  537. self.status_output.set_text(" Stopped")
  538. self.metadata_output.set_text("")
  539. elif key == 'r':
  540. if self.playing or self.paused:
  541. filepath = os.path.join(self.current_dir, self.focus.original_widget.original_widget.text.rstrip('/'))
  542. pygame.mixer.music.stop()
  543. pygame.mixer.music.load(filepath)
  544. pygame.mixer.music.set_volume(self.volume)
  545. pygame.mixer.music.play()
  546. self.playing = True
  547. self.paused = False
  548. self.status_output.set_text([("playing", f" Replaying: {os.path.basename(filepath)}")])
  549. self.metadata_output.set_text(self.get_metadata(filepath))
  550. elif key == '+':
  551. self.volume = min(1.0, self.volume + 0.03)
  552. if self.playing:
  553. pygame.mixer.music.set_volume(self.volume)
  554. # filled = int(self.volume * 34)
  555. filled = int(self.volume * 44)
  556. # self.volume_bar.set_text(f" {int(self.volume * 100)}% {'░' * filled + ' ' * (34 - filled)}")
  557. self.volume_bar.set_text(f" {int(self.volume * 100)}% | {'░' * filled + ' ' * (44 - filled)}")
  558. self.main_loop.draw_screen()
  559. elif key == '-':
  560. self.volume = max(0.0, self.volume - 0.03)
  561. if self.playing:
  562. pygame.mixer.music.set_volume(self.volume)
  563. # filled = int(self.volume * 34)
  564. filled = int(self.volume * 44)
  565. # self.volume_bar.set_text(f" {int(self.volume * 100)}% {'░' * filled + ' ' * (34 - filled)}")
  566. self.volume_bar.set_text(f" {int(self.volume * 100)}% | {'░' * filled + ' ' * (44 - filled)}")
  567. self.main_loop.draw_screen()
  568. elif key == 'i':
  569. try:
  570. result = subprocess.check_output("amixer set Master 3%+ | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  571. percent = int(result.rstrip('%'))
  572. filled = min(34, percent // 3)
  573. self.system_volume_bar.set_text(f" {percent}% {'░' * filled + ' ' * (34 - filled)}")
  574. self.main_loop.draw_screen()
  575. except subprocess.CalledProcessError as e:
  576. self.show_message(f"Error adjusting system volume: {e}")
  577. elif key == 'd':
  578. try:
  579. result = subprocess.check_output("amixer set Master 3%- | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  580. percent = int(result.rstrip('%'))
  581. filled = min(34, percent // 3)
  582. self.system_volume_bar.set_text(f" {percent}% {'░' * filled + ' ' * (34 - filled)}")
  583. self.main_loop.draw_screen()
  584. except subprocess.CalledProcessError as e:
  585. self.show_message(f"Error adjusting system volume: {e}")
  586. elif key == 'a':
  587. try:
  588. result = subprocess.check_output("amixer sset 'Headphone' frontright 5%+ -q && amixer sget 'Headphone' | grep 'Front Right' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  589. percent = int(result.rstrip('%'))
  590. filled = min(29, percent // 3)
  591. self.headphone_right_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (29 - filled)}")
  592. # self.status_output.set_text(f"Headphone Front Right: {result}")
  593. self.main_loop.draw_screen()
  594. except subprocess.CalledProcessError as e:
  595. pass # Пустой оператор, чтобы блок не был пустым
  596. # self.show_message(f"Error adjusting headphone volume: {e}")
  597. elif key == 'b':
  598. try:
  599. result = subprocess.check_output("amixer sset 'Headphone' frontright 5%- -q && amixer sget 'Headphone' | grep 'Front Right' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  600. percent = int(result.rstrip('%'))
  601. filled = min(29, percent // 3)
  602. self.headphone_right_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (29 - filled)}")
  603. # self.status_output.set_text(f"Headphone Front Right: {result}")
  604. self.main_loop.draw_screen()
  605. except subprocess.CalledProcessError as e:
  606. self.show_message(f"Error adjusting headphone volume: {e}")
  607. elif key == 'c':
  608. try:
  609. result = subprocess.check_output("amixer sset 'Headphone' frontleft 5%+ -q && amixer sget 'Headphone' | grep 'Front Left' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  610. percent = int(result.rstrip('%'))
  611. filled = min(29, percent // 3)
  612. self.headphone_left_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (29 - filled)}")
  613. self.status_output.set_text(f"Headphone Front Left: {result}")
  614. self.main_loop.draw_screen()
  615. except subprocess.CalledProcessError as e:
  616. self.show_message(f"Error adjusting headphone volume: {e}")
  617. elif key == 'g':
  618. try:
  619. result = subprocess.check_output("amixer sset 'Headphone' frontleft 5%- -q && amixer sget 'Headphone' | grep 'Front Left' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  620. percent = int(result.rstrip('%'))
  621. filled = min(29, percent // 3)
  622. self.headphone_left_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (29 - filled)}")
  623. self.status_output.set_text(f"Headphone Front Left: {result}")
  624. self.main_loop.draw_screen()
  625. except subprocess.CalledProcessError as e:
  626. self.show_message(f"Error adjusting headphone volume: {e}")
  627. elif key == 'e':
  628. try:
  629. # Увеличиваем громкость обоих наушников
  630. subprocess.check_output("amixer sset 'Headphone' 5%+ -q", shell=True, text=True)
  631. # Получаем значение для левого наушника
  632. left_result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Left' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  633. left_percent = int(left_result.rstrip('%'))
  634. left_filled = min(29, left_percent // 3)
  635. self.headphone_left_bar.set_text(f" {left_percent}% | {'░' * left_filled + ' ' * (29 - left_filled)}")
  636. # Получаем значение для правого наушника
  637. right_result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Right' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  638. right_percent = int(right_result.rstrip('%'))
  639. right_filled = min(29, right_percent // 3)
  640. self.headphone_right_bar.set_text(f" {right_percent}% | {'░' * right_filled + ' ' * (29 - right_filled)}")
  641. self.status_output.set_text(f"Headphone Left Right: {left_result} {right_result}")
  642. self.main_loop.draw_screen()
  643. except subprocess.CalledProcessError as e:
  644. self.show_message(f"Error adjusting headphone volume: {e}")
  645. elif key == 'f':
  646. try:
  647. # Уменьшаем громкость обоих наушников
  648. subprocess.check_output("amixer sset 'Headphone' 5%- -q", shell=True, text=True)
  649. # Получаем значение для левого наушника
  650. left_result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Left' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  651. left_percent = int(left_result.rstrip('%'))
  652. left_filled = min(29, left_percent // 3)
  653. self.headphone_left_bar.set_text(f" {left_percent}% | {'░' * left_filled + ' ' * (29 - left_filled)}")
  654. # Получаем значение для правого наушника
  655. right_result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Right' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  656. right_percent = int(right_result.rstrip('%'))
  657. right_filled = min(29, right_percent // 3)
  658. self.headphone_right_bar.set_text(f" {right_percent}% | {'░' * right_filled + ' ' * (29 - right_filled)}")
  659. self.status_output.set_text(f"Headphone Left Right: {left_result} {right_result}")
  660. self.main_loop.draw_screen()
  661. except subprocess.CalledProcessError as e:
  662. self.show_message(f"Error adjusting headphone volume: {e}")
  663. elif key == 'n':
  664. self.next_track()
  665. elif key in ('q', 'Q'):
  666. self.cleanup()
  667. return 'q'
  668. else:
  669. super().keypress(size, key)
  670. return key
  671. def handle_input(self, key):
  672. if isinstance(key, str):
  673. return key
  674. return None
  675. class FileManager:
  676. def __init__(self, input_path=None):
  677. self.main_loop = None
  678. self.root_dir = os.path.dirname(os.path.abspath(__file__))
  679. self.mode = PlaybackMode(None, self.root_dir, input_path)
  680. initial_widget = self.wrap_mode_widget(self.mode.get_widget())
  681. self.frame = urwid.Frame(body=initial_widget)
  682. def wrap_mode_widget(self, widget):
  683. framed_widget = urwid.LineBox(
  684. widget, title="grnManagerTerm", title_align='center',
  685. tlcorner='╔', tline='═', trcorner='╗',
  686. lline='║', rline='║',
  687. blcorner='╚', bline='═', brcorner='╝'
  688. )
  689. return urwid.AttrMap(framed_widget, 'header')
  690. def unhandled_input(self, key):
  691. mode_key = self.mode.handle_input(key)
  692. if mode_key == 'q':
  693. self.mode.cleanup()
  694. raise urwid.ExitMainLoop()
  695. def run(self):
  696. os.system('clear')
  697. self.main_loop = urwid.MainLoop(self.frame, palette=palette, unhandled_input=self.unhandled_input)
  698. self.mode.main_loop = self.main_loop
  699. self.mode.start()
  700. self.mode.check_playback_end()
  701. self.main_loop.set_alarm_in(0.1, self.mode.update_clock)
  702. self.main_loop.set_alarm_in(0.1, self.mode.update_progress_bar)
  703. try:
  704. self.main_loop.run()
  705. finally:
  706. os.system('stty sane')
  707. os.system('clear')
  708. if __name__ == "__main__":
  709. input_path = None
  710. if len(sys.argv) > 1:
  711. input_path = sys.argv[1]
  712. fm = FileManager(input_path)
  713. fm.run()