audioPlayerTermPy.py 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077
  1. #!/usr/bin/env python3
  2. import urwid, time
  3. import os
  4. import pygame.mixer
  5. import mutagen
  6. import sys
  7. from datetime import datetime, timedelta
  8. import subprocess
  9. import signal
  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', 'light green,bold', 'default'),
  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. ('time_separator', 'dark gray', 'default'),
  24. ('time_separator,bold', 'dark gray,bold', 'default'),
  25. ]
  26. font = {
  27. '0': ["┌─┐", "│ │", "└─┘"],
  28. '1': [" ┌┐", " │", " ┘"],
  29. '2': ["┌─┐", "┌─┘", "└─┘"],
  30. '3': ["┌─┐", " ─┤", "└─┘"],
  31. '4': ["┌ ┐", "└─┤", " ┘"],
  32. '5': ["┌─┐", "└─┐", "└─┘"],
  33. '6': ["┌─┐", "├─┐", "└─┘"],
  34. '7': ["┌─┐", " ┤", " ┘"],
  35. '8': ["┌─┐", "├─┤", "└─┘"],
  36. '9': ["┌─┐", "└─┤", "└─┘"],
  37. ':': [" ┌┐ ", " ├┤ ", " └┘ "]
  38. }
  39. empty_char = [" ", " ", " "]
  40. def get_pseudographic_char(c):
  41. return font.get(c, empty_char)
  42. def print_pseudographic_time(hours, mins, secs):
  43. if not (0 <= hours <= 23 and 0 <= mins <= 59 and 0 <= secs <= 59):
  44. return [('error', f"Invalid time: {hours:02d}:{mins:02d}:{secs:02d}")]
  45. time_str = f"{hours:02d}:{mins:02d}:{secs:02d}"
  46. chars = [get_pseudographic_char(c) for c in time_str]
  47. result = []
  48. for row in range(3):
  49. line = []
  50. for i, char in enumerate(chars):
  51. style = 'time_separator' if i in [2, 5] else 'normal'
  52. line.append((style, char[row].rstrip().ljust(4)))
  53. result.append(line)
  54. return result
  55. def get_month_name(month):
  56. months = ["January", "February", "March", "April", "May", "June",
  57. "July", "August", "September", "October", "November", "December"]
  58. return months[month - 1] if 1 <= month <= 12 else "Unknown"
  59. def get_weekday_name(weekday):
  60. days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
  61. return days[weekday] if 0 <= weekday <= 6 else "Unknown"
  62. def get_date_string():
  63. t = time.localtime()
  64. month_name = get_month_name(t.tm_mon)
  65. weekday_name = get_weekday_name(t.tm_wday)
  66. return " {}/{}|{}/{} ".format(
  67. t.tm_year,
  68. month_name,
  69. t.tm_mday,
  70. weekday_name
  71. )
  72. class PlaybackMode(urwid.ListBox):
  73. def __init__(self, main_loop, root_dir, input_path=None):
  74. pygame.mixer.init()
  75. self.main_loop = main_loop
  76. self.root_dir = root_dir
  77. self.current_dir = os.getcwd()
  78. self.dir_history = []
  79. self.file_list = urwid.SimpleFocusListWalker([])
  80. self.playlist = []
  81. self.playlist_index = 0
  82. self.progress_bar = urwid.Text([('normal', " 0"), ('time_separator', '%'), (None, " | " + " " * 83)], align='left') #self.progress_bar = urwid.Text([('path_value', " 0"), ('percent', '%'), (None, " | " + " " * 83)], align='left')
  83. term_size = os.get_terminal_size()
  84. term_width = term_size.columns
  85. total_width = term_width - 1
  86. left_width = int(total_width * 0.62)
  87. title = "PLAYBACK PROGRESS"
  88. title_with_symbols = f"┤ {title} ├"
  89. title_len = len(title_with_symbols)
  90. adjusted_width = left_width - 2
  91. side_len = max(0, (adjusted_width - title_len) // 2)
  92. top_line = f'┌{"─" * side_len}{title_with_symbols}{"─" * (adjusted_width - title_len - side_len)}┐'
  93. if len(top_line) > left_width:
  94. top_line = top_line[:left_width - 1] + '┐'
  95. elif len(top_line) < left_width:
  96. top_line = top_line[:-1] + '─' * (left_width - len(top_line)) + '┐'
  97. top_text = urwid.Filler(urwid.Text(('pink_frame', top_line), align='left'), valign='top')
  98. side_borders = urwid.LineBox(self.progress_bar, lline='│', rline='│',
  99. tline='', bline='',
  100. tlcorner='', trcorner='',
  101. blcorner='', brcorner='')
  102. footer_line = f'└{"─" * (left_width - 2)}┘'
  103. footer_text = urwid.Filler(urwid.Text(('pink_frame', footer_line), align='left'), valign='top')
  104. framed_widget = urwid.Pile([(1, top_text), ('weight', 1, side_borders), (1, footer_text)])
  105. self.file_frame = urwid.AttrMap(framed_widget, 'pink_frame')
  106. self.volume_bar = urwid.Text([('normal', " 50"), ('time_separator', '%'), (None, " | " + '░' * 25 + ' ' * 25)]) #self.volume_bar = urwid.Text(f" 50% | {'░' * 25 + ' ' * 25}")
  107. term_size = os.get_terminal_size()
  108. term_width = term_size.columns
  109. total_border_chars = 6
  110. available_width = term_width - total_border_chars
  111. num_upper_boxes = 3
  112. base_status_width = (available_width // 3 + available_width // 4) // 2
  113. status_width = max(15, base_status_width + 3)
  114. remaining_width = available_width - status_width - 7
  115. files_width = remaining_width // 2
  116. metadata_width = available_width - files_width - status_width + 2
  117. total_width_so_far = files_width + status_width + metadata_width + 8
  118. if total_width_so_far < available_width:
  119. metadata_width += available_width - total_width_so_far
  120. title = "PYGAME.MIXER VOLUME LEVEL"
  121. title_with_symbols = f"┤ {title} ├"
  122. title_len = len(title_with_symbols)
  123. adjusted_width = metadata_width - 2
  124. side_len = max(0, (adjusted_width - title_len) // 2)
  125. top_line = f'┌{"─" * side_len}{title_with_symbols}{"─" * (adjusted_width - title_len - side_len)}┐'
  126. if len(top_line) > metadata_width:
  127. top_line = top_line[:metadata_width - 1] + '┐'
  128. elif len(top_line) < metadata_width:
  129. top_line = top_line[:-1] + '─' * (metadata_width - len(top_line)) + '┐'
  130. top_text = urwid.Filler(urwid.Text(('pink_frame', top_line), align='left'), valign='top')
  131. side_borders = urwid.LineBox(self.volume_bar, lline='│', rline='│', tline='', bline='─', tlcorner='', trcorner='', blcorner='└', brcorner='┘')
  132. new_right_frame = urwid.Pile([(1, top_text), ('weight', 1, side_borders)])
  133. self.metadata_frame = urwid.AttrMap(new_right_frame, 'pink_frame')
  134. self.path_text_inner = urwid.Text([('path_value', self.current_dir)], align='left')
  135. self.path_text = urwid.Padding(self.path_text_inner, left=1)
  136. self.path_filler = urwid.Filler(self.path_text, valign='top')
  137. term_width = os.get_terminal_size().columns
  138. title = "Path"
  139. title_len = len("┤ Path ├")
  140. adjusted_width = term_width - 4
  141. side_len = max(0, (adjusted_width - title_len) // 2)
  142. top_line = f'┌{"─" * side_len}┤ PATH ├{"─" * side_len}'
  143. if len(top_line) < term_width - 2:
  144. top_line += "─" * (term_width - len(top_line) - 3) + "┐"
  145. elif len(top_line) >= term_width - 1:
  146. top_line = top_line[:term_width - 3] + "┐"
  147. else:
  148. top_line += "┐"
  149. top_text = urwid.Filler(urwid.Text(('pink_frame', top_line), align='left'), valign='top')
  150. side_borders = urwid.LineBox(self.path_filler, lline='│', rline='│', tline='', bline='', tlcorner='', trcorner='', blcorner='', brcorner='')
  151. footer_line = f'└{"─" * (len(top_line) - 2)}┘'
  152. if len(footer_line) > term_width - 1:
  153. footer_line = footer_line[:term_width - 2]
  154. footer_text = urwid.Filler(urwid.Text(('pink_frame', footer_line), align='left'), valign='top')
  155. framed_widget = urwid.Pile([(1, top_text), ('weight', 1, side_borders), (1, footer_text)])
  156. self.path_widget = urwid.AttrMap(framed_widget, 'pink_frame')
  157. self.status_output = urwid.Text("", align='left')
  158. self.status_filler = urwid.Filler(self.status_output, valign='top')
  159. self.playing = False
  160. self.paused = False
  161. self.volume = 0.5
  162. self.current_audio_duration = 0
  163. try:
  164. result = subprocess.check_output("amixer get Master | grep -o '[0-9]*%' | uniq", shell=True, text=True).strip()
  165. percent = int(result.rstrip('%'))
  166. filled = min(50, int(percent / 2))
  167. initial_volume_text = [('normal', f" {percent}"), ('time_separator', '%'), (None, f" | {'░' * filled + ' ' * (50 - filled)}")] #initial_volume_text = f" {percent}% | {'░' * filled + ' ' * (50 - filled)}"
  168. except subprocess.CalledProcessError:
  169. initial_volume_text = " --% " + " " * 50
  170. self.system_volume_bar = urwid.Text(initial_volume_text)
  171. try:
  172. result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Left' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  173. percent = int(result.rstrip('%'))
  174. filled = min(50, int(percent / 2))
  175. initial_headphone_left_text = [('normal', f" {percent}"), ('time_separator', '%'), (None, f" | {'░' * filled + ' ' * (50 - filled)}")] #initial_headphone_left_text = f" {percent}% | {'░' * filled + ' ' * (50 - filled)}"
  176. except (subprocess.CalledProcessError, ValueError):
  177. initial_headphone_left_text = " --% | " + " " * 50
  178. self.headphone_left_bar = urwid.Text(initial_headphone_left_text, align='left')
  179. try:
  180. result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Right' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  181. percent = int(result.rstrip('%'))
  182. filled = min(50, int(percent / 2))
  183. initial_headphone_right_text = [('normal', f" {percent}"), ('time_separator', '%'), (None, f" | {'░' * filled + ' ' * (50 - filled)}")] #initial_headphone_right_text = f" {percent}% | {'░' * filled + ' ' * (50 - filled)}"
  184. except (subprocess.CalledProcessError, ValueError):
  185. initial_headphone_right_text = " --% | " + " " * 50
  186. self.headphone_right_bar = urwid.Text(initial_headphone_right_text, align='left')
  187. super().__init__(self.file_list)
  188. self.input_path = input_path
  189. if not input_path:
  190. self.refresh_list()
  191. self.widget = None
  192. self.initialize_widget()
  193. def start(self):
  194. if self.input_path:
  195. if os.path.isdir(self.input_path):
  196. self.load_and_play_directory(self.input_path)
  197. elif os.path.isfile(self.input_path):
  198. self.load_and_play_audio(self.input_path)
  199. def format_time(self, seconds):
  200. return str(timedelta(seconds=int(seconds))).zfill(8)
  201. def format_time(self, seconds):
  202. return str(timedelta(seconds=int(seconds))).zfill(8)
  203. def format_active_time(self, elapsed_str, duration_str):
  204. result = [('normal', " ")]
  205. for i, char in enumerate(elapsed_str):
  206. if char.isdigit():
  207. result.append(('normal', char))
  208. else:
  209. result.append(('time_separator,bold', char))
  210. result.append(('time_separator,bold', " / "))
  211. for i, char in enumerate(duration_str):
  212. if char.isdigit():
  213. result.append(('normal', char))
  214. else:
  215. result.append(('time_separator,bold', char))
  216. return result
  217. def update_progress_bar(self, loop=None, data=None):
  218. if self.playing and not self.paused and pygame.mixer.music.get_busy():
  219. elapsed = pygame.mixer.music.get_pos() / 1000
  220. duration = self.current_audio_duration
  221. if duration > 0:
  222. progress_percent = min(100, int((elapsed / duration) * 100))
  223. filled = min(83, int(progress_percent / 1.2048))
  224. unfilled = 83 - filled
  225. progress_str = [('normal', f"{progress_percent:3d}"), ('time_separator', '%'), (None, f" | {'░' * filled}{' ' * unfilled}")] #progress_str = [('path_value', f"{progress_percent:3d}"), ('percent', '%'), (None, f" | {'░' * filled}{' ' * unfilled}")]
  226. self.progress_bar.set_text(progress_str)
  227. elapsed_str = self.format_time(elapsed)
  228. duration_str = self.format_time(duration)
  229. self.grannik_text.set_text(self.format_active_time(elapsed_str, duration_str))
  230. else:
  231. self.progress_bar.set_text([('normal', " 0"), ('time_separator', '%'), (None, " | " + " " * 83)])
  232. self.grannik_text.set_text([('pink_frame', " 00:00:00 / 00:00:00")])
  233. if self.main_loop:
  234. self.main_loop.set_alarm_in(1, self.update_progress_bar)
  235. def update_clock(self, loop=None, data=None):
  236. current_time = time.localtime()
  237. self.clock_text.set_text(print_pseudographic_time(current_time.tm_hour, current_time.tm_min, current_time.tm_sec))
  238. if self.main_loop:
  239. self.main_loop.set_alarm_in(1, self.update_clock)
  240. def initialize_widget(self):
  241. self.widget = self.wrap_in_three_frames()
  242. def wrap_in_three_frames(self):
  243. term_size = os.get_terminal_size()
  244. term_width = term_size.columns
  245. term_height = term_size.lines
  246. total_border_chars = 6
  247. available_width = term_width - total_border_chars
  248. num_upper_boxes = 3
  249. base_status_width = (available_width // 3 + available_width // 4) // 2
  250. status_width = max(15, base_status_width + 3)
  251. remaining_width = available_width - status_width - 7
  252. files_width = remaining_width // 2
  253. metadata_width = available_width - files_width - status_width + 2
  254. total_width_so_far = files_width + status_width + metadata_width + 8
  255. if total_width_so_far < available_width:
  256. metadata_width += available_width - total_width_so_far
  257. total_width = term_width - 1
  258. left_width = int(total_width * 0.62)
  259. #
  260. if term_height < 10:
  261. return urwid.Filler(self, height=term_height, valign='top')
  262. elif term_height < 15:
  263. combined_widget = urwid.Columns([
  264. (left_width, self.file_frame),
  265. ], dividechars=1)
  266. return urwid.Filler(combined_widget, height=term_height, valign='top')
  267. else:
  268. combined_widget = urwid.Columns([
  269. (left_width, self.file_frame),
  270. ('weight', 1, self.metadata_frame),
  271. ], dividechars=1, box_columns=[0, 1])
  272. height_limited_widget = urwid.Filler(combined_widget, height=max(1, min(3, term_height - 10)), valign='top')
  273. #
  274. term_size = os.get_terminal_size()
  275. term_width = term_size.columns
  276. total_border_chars = 6
  277. available_width = term_width - total_border_chars
  278. num_upper_boxes = 3
  279. base_status_width = (available_width // 3 + available_width // 4) // 2
  280. status_width = max(15, base_status_width + 3)
  281. remaining_width = available_width - status_width - 7
  282. files_width = remaining_width // 2
  283. metadata_width = available_width - files_width - status_width + 2
  284. total_width_so_far = files_width + status_width + metadata_width + 8
  285. if total_width_so_far < available_width:
  286. metadata_width += available_width - total_width_so_far
  287. title = "AMIXER MASTER VOLUME LEVEL"
  288. title_with_symbols = f"┤ {title} ├"
  289. title_len = len(title_with_symbols)
  290. adjusted_width = metadata_width - 2
  291. side_len = max(0, (adjusted_width - title_len) // 2)
  292. top_line = f'┌{"─" * side_len}{title_with_symbols}{"─" * (adjusted_width - title_len - side_len)}┐'
  293. if len(top_line) > metadata_width:
  294. top_line = top_line[:metadata_width - 1] + '┐'
  295. elif len(top_line) < metadata_width:
  296. top_line = top_line[:-1] + '─' * (metadata_width - len(top_line)) + '┐'
  297. top_text = urwid.Filler(urwid.Text(('pink_frame', top_line), align='left'), valign='top')
  298. side_borders = urwid.LineBox(self.system_volume_bar, lline='│', rline='│', tline='', bline='─', tlcorner='', trcorner='', blcorner='└', brcorner='┘')
  299. new_right_frame = urwid.Pile([(1, top_text), ('weight', 1, side_borders)])
  300. box02_clone = urwid.AttrMap(new_right_frame, 'pink_frame')
  301. term_size = os.get_terminal_size()
  302. term_width = term_size.columns
  303. total_border_chars = 6
  304. available_width = term_width - total_border_chars
  305. num_upper_boxes = 3
  306. base_status_width = (available_width // 3 + available_width // 4) // 2
  307. status_width = max(15, base_status_width + 3)
  308. remaining_width = available_width - status_width - 7
  309. files_width = remaining_width // 2
  310. metadata_width = available_width - files_width - status_width + 2
  311. total_width_so_far = files_width + status_width + metadata_width + 8
  312. if total_width_so_far < available_width:
  313. metadata_width += available_width - total_width_so_far
  314. title = "AMIXER HEADPHONE LEFT VOLUME LEVEL"
  315. title_with_symbols = f"┤ {title} ├"
  316. title_len = len(title_with_symbols)
  317. adjusted_width = metadata_width - 2
  318. side_len = max(0, (adjusted_width - title_len) // 2)
  319. top_line = f'┌{"─" * side_len}{title_with_symbols}{"─" * (adjusted_width - title_len - side_len)}┐'
  320. if len(top_line) > metadata_width:
  321. top_line = top_line[:metadata_width - 1] + '┐'
  322. elif len(top_line) < metadata_width:
  323. top_line = top_line[:-1] + '─' * (metadata_width - len(top_line)) + '┐'
  324. top_text = urwid.Filler(urwid.Text(('pink_frame', top_line), align='left'), valign='top')
  325. side_borders = urwid.LineBox(self.headphone_left_bar, lline='│', rline='│', tline='', bline='─', tlcorner='', trcorner='', blcorner='└', brcorner='┘')
  326. new_right_frame = urwid.Pile([(1, top_text), ('weight', 1, side_borders)])
  327. box02_clone2 = urwid.AttrMap(new_right_frame, 'pink_frame')
  328. term_size = os.get_terminal_size()
  329. term_width = term_size.columns
  330. total_border_chars = 6
  331. available_width = term_width - total_border_chars
  332. num_upper_boxes = 3
  333. base_status_width = (available_width // 3 + available_width // 4) // 2
  334. status_width = max(15, base_status_width + 3)
  335. remaining_width = available_width - status_width - 7
  336. files_width = remaining_width // 2
  337. metadata_width = available_width - files_width - status_width + 2
  338. total_width_so_far = files_width + status_width + metadata_width + 8
  339. if total_width_so_far < available_width:
  340. metadata_width += available_width - total_width_so_far
  341. title = "AMIXER HEADPHONE RIGHT VOLUME LEVEL"
  342. title_with_symbols = f"┤ {title} ├"
  343. title_len = len(title_with_symbols)
  344. adjusted_width = metadata_width - 2
  345. side_len = max(0, (adjusted_width - title_len) // 2)
  346. top_line = f'┌{"─" * side_len}{title_with_symbols}{"─" * (adjusted_width - title_len - side_len)}┐'
  347. if len(top_line) > metadata_width:
  348. top_line = top_line[:metadata_width - 1] + '┐'
  349. elif len(top_line) < metadata_width:
  350. top_line = top_line[:-1] + '─' * (metadata_width - len(top_line)) + '┐'
  351. top_text = urwid.Filler(urwid.Text(('pink_frame', top_line), align='left'), valign='top')
  352. side_borders = urwid.LineBox(self.headphone_right_bar, lline='│', rline='│', tline='', bline='─', tlcorner='', trcorner='', blcorner='└', brcorner='┘')
  353. new_right_frame = urwid.Pile([(1, top_text), ('weight', 1, side_borders)])
  354. box02_clone3 = urwid.AttrMap(new_right_frame, 'pink_frame')
  355. #
  356. header_height = max(1, min(3, term_height - 10))
  357. frame_border_height = max(1, min(2, term_height - 10))
  358. available_height = term_height - header_height - frame_border_height
  359. if available_height < 10:
  360. raise ValueError("Not enough height for main content: need at least 10 lines, got %d" % available_height)
  361. #
  362. upper_boxes_height = 3
  363. min_footer_height = 8
  364. columns_height = max(21, available_height - upper_boxes_height - min_footer_height - 1) # 21 -1
  365. title = "FILES AND DIRECTORIES OF THE LINUX OS"
  366. title_with_symbols = f"┤ {title} ├"
  367. title_len = len(title_with_symbols)
  368. adjusted_width = left_width - 2
  369. side_len = max(0, (adjusted_width - title_len) // 2)
  370. top_line = f'┌{"─" * side_len}{title_with_symbols}{"─" * (adjusted_width - title_len - side_len)}┐'
  371. if len(top_line) > left_width:
  372. top_line = top_line[:left_width - 1] + '┐'
  373. elif len(top_line) < left_width:
  374. top_line = top_line[:-1] + '─' * (left_width - len(top_line)) + '┐'
  375. top_text = urwid.Filler(urwid.Text(('pink_frame', top_line), align='left'), valign='top')
  376. side_borders = urwid.LineBox(self, lline='│', rline='│', tline='', bline='─', tlcorner='', trcorner='', blcorner='└', brcorner='┘')
  377. new_left_frame = urwid.Pile([(1, top_text), ('weight', 1, side_borders)])
  378. new_left_frame = urwid.AttrMap(new_left_frame, 'pink_frame')
  379. new_left_frame_filler = urwid.Filler(new_left_frame, height=columns_height, valign='top')
  380. self.metadata_output = urwid.Text("", align='left')
  381. self.metadata_filler = urwid.Filler(self.metadata_output, valign='top')
  382. self.metadata_output = urwid.Text("", align='left')
  383. self.metadata_filler = urwid.Filler(self.metadata_output, valign='top')
  384. title = "INFO"
  385. title_with_symbols = f"┤ {title} ├"
  386. title_len = len(title_with_symbols)
  387. adjusted_width = metadata_width - 2
  388. side_len = max(0, (adjusted_width - title_len) // 2)
  389. top_line = f'┌{"─" * side_len}┤ {title} ├{"─" * (adjusted_width - title_len - side_len)}┐'
  390. if len(top_line) > metadata_width:
  391. top_line = top_line[:metadata_width - 1] + '┐'
  392. elif len(top_line) < metadata_width:
  393. top_line = top_line[:-1] + '─' * (metadata_width - len(top_line)) + '┐'
  394. top_text = urwid.Filler(urwid.Text(('pink_frame', top_line), align='left'), valign='top')
  395. side_borders = urwid.LineBox(self.metadata_filler, lline='│', rline='│', tline='', bline='─', tlcorner='', trcorner='', blcorner='└', brcorner='┘')
  396. new_right_frame = urwid.Pile([(1, top_text), ('weight', 1, side_borders)])
  397. new_right_frame = urwid.AttrMap(new_right_frame, 'pink_frame')
  398. new_right_frame_filler = urwid.Filler(new_right_frame, height=columns_height, valign='top')
  399. new_frames_widget = urwid.Columns([
  400. (left_width, new_left_frame_filler),
  401. ('weight', 1, new_right_frame_filler)
  402. ], dividechars=1)
  403. new_frames_widget = urwid.Columns([
  404. (left_width, new_left_frame_filler),
  405. ('weight', 1, new_right_frame_filler)
  406. ], dividechars=1)
  407. footer_width = left_width
  408. footer_height = available_height - columns_height - upper_boxes_height
  409. box_height = max(4, footer_height)
  410. divider_width = 2
  411. available_footer_width = footer_width - divider_width
  412. box1_width = 23
  413. box2_width = 33
  414. box4_width = available_footer_width - box1_width - box2_width - divider_width + 2
  415. self.grannik_text = urwid.Text(" 00:00:00 / 00:00:00", align='left')
  416. title = "PLAYBACK TIME"
  417. title_with_symbols = f"┤ {title} ├"
  418. title_len = len(title_with_symbols)
  419. adjusted_width = box1_width - 2
  420. side_len = max(0, (adjusted_width - title_len) // 2)
  421. top_line = f'┌{"─" * side_len}{title_with_symbols}{"─" * (adjusted_width - title_len - side_len)}┐'
  422. top_text = urwid.Filler(urwid.Text(('pink_frame', top_line), align='left'), valign='top')
  423. side_borders = urwid.LineBox(self.grannik_text, lline='│', rline='│',
  424. tline='', bline='',
  425. tlcorner='', trcorner='',
  426. blcorner='', brcorner='')
  427. footer_line = f'└{"─" * (len(top_line) - 2)}┘'
  428. footer_text = urwid.Filler(urwid.Text(('pink_frame', footer_line), align='left'), valign='top')
  429. framed_widget = urwid.Pile([(1, top_text), ('weight', 1, side_borders), (1, footer_text)])
  430. box1 = urwid.AttrMap(framed_widget, 'pink_frame')
  431. box1_filler = urwid.Filler(box1, height=box_height, valign='middle')
  432. title = "CURRENT DATE"
  433. title_with_symbols = f"┤ {title} ├"
  434. title_len = len(title_with_symbols)
  435. adjusted_width = box2_width - 2
  436. side_len = max(0, (adjusted_width - title_len) // 2)
  437. top_line = f'┌{"─" * side_len}{title_with_symbols}{"─" * (adjusted_width - title_len - side_len)}┐'
  438. top_text = urwid.Filler(urwid.Text(('pink_frame', top_line), align='left'), valign='top')
  439. side_borders = urwid.LineBox(urwid.Text([
  440. ('normal', get_date_string().split('|')[0].split('/')[0]),
  441. ('path_value', '/'),
  442. ('normal', get_date_string().split('|')[0].split('/')[1]),
  443. ('path_value', '|'),
  444. ('normal', get_date_string().split('|')[1].split('/')[0]),
  445. ('path_value', '/'),
  446. ('normal', get_date_string().split('|')[1].split('/')[1]),
  447. ], align='center'), lline='│', rline='│', tline='', bline='', tlcorner='', trcorner='', blcorner='', brcorner='')
  448. footer_line = f'└{"─" * (len(top_line) - 2)}┘'
  449. footer_text = urwid.Filler(urwid.Text(('pink_frame', footer_line), align='left'), valign='top')
  450. framed_widget = urwid.Pile([(1, top_text), ('weight', 1, side_borders), (1, footer_text)])
  451. box2 = urwid.AttrMap(framed_widget, 'pink_frame')
  452. box2_filler = urwid.Filler(box2, height=3, valign='middle')
  453. current_time = time.localtime()
  454. test_text = urwid.Text(print_pseudographic_time(current_time.tm_hour, current_time.tm_min, current_time.tm_sec), align='center')
  455. self.clock_text = test_text
  456. title = "CURRENT TIME"
  457. title_with_symbols = f"┤ {title} ├"
  458. title_len = len(title_with_symbols)
  459. adjusted_width = box2_width - 2
  460. side_len = max(0, (adjusted_width - title_len) // 2)
  461. top_line = f'┌{"─" * side_len}{title_with_symbols}{"─" * (adjusted_width - title_len - side_len)}┐'
  462. top_text = urwid.Filler(urwid.Text(('pink_frame', top_line), align='left'), valign='top')
  463. side_borders = urwid.LineBox(test_text, lline='│', rline='│',
  464. tline='', bline='',
  465. tlcorner='', trcorner='',
  466. blcorner='', brcorner='')
  467. footer_line = f'└{"─" * (len(top_line) - 2)}┘'
  468. footer_text = urwid.Filler(urwid.Text(('pink_frame', footer_line), align='left'), valign='top')
  469. framed_widget = urwid.Pile([(1, top_text), ('weight', 1, side_borders), (1, footer_text)])
  470. test_box_attr = urwid.AttrMap(framed_widget, 'pink_frame')
  471. test_box_filler = urwid.Filler(test_box_attr, height=6, valign='top')
  472. #
  473. box2_with_test = urwid.Filler(
  474. urwid.Pile([
  475. (3, box2_filler),
  476. (6, test_box_filler)
  477. ]),
  478. height=box_height, valign='middle'
  479. )
  480. #
  481. title = "STATUS"
  482. title_with_symbols = f"┤ {title} ├"
  483. title_len = len(title_with_symbols)
  484. adjusted_width = box4_width - 2
  485. side_len = max(0, (adjusted_width - title_len) // 2)
  486. top_line = f'┌{"─" * side_len}{title_with_symbols}{"─" * (adjusted_width - title_len - side_len)}┐'
  487. top_text = urwid.Filler(urwid.Text(('pink_frame', top_line), align='left'), valign='top')
  488. side_borders = urwid.LineBox(self.status_filler, lline='│', rline='│',
  489. tline='', bline='',
  490. tlcorner='', trcorner='',
  491. blcorner='', brcorner='')
  492. footer_line = f'└{"─" * (len(top_line) - 2)}┘'
  493. footer_text = urwid.Filler(urwid.Text(('pink_frame', footer_line), align='left'), valign='top')
  494. framed_widget = urwid.Pile([(1, top_text), ('weight', 1, side_borders), (1, footer_text)])
  495. box4 = urwid.AttrMap(framed_widget, 'pink_frame')
  496. box4_filler = urwid.Filler(box4, height=box_height, valign='middle')
  497. #
  498. footer_columns = urwid.Filler(
  499. urwid.Columns([
  500. (box1_width, box1_filler),
  501. (box2_width, box2_with_test),
  502. (box4_width, box4_filler),
  503. ], dividechars=1, box_columns=[0, 1, 2]),
  504. height=box_height, valign='middle'
  505. )
  506. #
  507. clone_width = metadata_width
  508. clones_pile = urwid.Pile([
  509. (3, box02_clone),
  510. (3, box02_clone2),
  511. (3, box02_clone3),
  512. ])
  513. clones_filler = urwid.Filler(clones_pile, height=9, valign='middle')
  514. #
  515. footer_with_clones = urwid.Columns([
  516. (footer_width, footer_columns),
  517. (clone_width, clones_filler),
  518. ], dividechars=1, box_columns=[0, 1])
  519. footer_widget = urwid.Filler(footer_with_clones, height=box_height, valign='middle')
  520. #
  521. body_widget = urwid.Pile([
  522. (columns_height, new_frames_widget),
  523. (upper_boxes_height, height_limited_widget),
  524. (box_height, footer_widget) if term_height >= 15 else (0, urwid.Filler(urwid.Text(""))),
  525. ])
  526. #
  527. frame_with_path = urwid.Frame(
  528. body=body_widget,
  529. header=self.path_widget,
  530. )
  531. return frame_with_path
  532. def load_and_play_audio(self, audio_file):
  533. full_path = os.path.abspath(audio_file)
  534. self.current_dir = os.path.dirname(full_path)
  535. file_name = os.path.basename(full_path)
  536. self.file_list.clear()
  537. padded_text = urwid.Padding(urwid.Text(file_name), left=1, right=1)
  538. self.file_list.append(urwid.AttrMap(padded_text, 'normal', 'selected'))
  539. self.set_focus(0)
  540. self.path_text_inner.set_text([('path_value', self.current_dir)])
  541. self.play_media(full_path)
  542. def load_and_play_directory(self, directory):
  543. full_path = os.path.abspath(directory)
  544. self.current_dir = full_path
  545. self.path_text_inner.set_text([('path_value', self.current_dir)])
  546. AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma', 'opus'}
  547. try:
  548. all_files = sorted(os.listdir(self.current_dir))
  549. audio_files = [f for f in all_files
  550. if not f.startswith('.') and
  551. f.lower().split('.')[-1] in AUDIO_EXTENSIONS]
  552. if not audio_files:
  553. self.file_list.clear()
  554. self.file_list.append(urwid.AttrMap(urwid.Padding(urwid.Text("(empty)"), left=1, right=1), 'normal', 'selected'))
  555. return
  556. self.file_list.clear()
  557. self.playlist = [os.path.join(self.current_dir, f) for f in audio_files]
  558. self.playlist_index = 0
  559. for file in audio_files:
  560. padded_text = urwid.Padding(urwid.Text(file), left=1, right=1)
  561. self.file_list.append(urwid.AttrMap(padded_text, 'audio_file', 'selected'))
  562. self.set_focus(0)
  563. self.play_media(self.playlist[self.playlist_index])
  564. except PermissionError:
  565. self.file_list.clear()
  566. self.file_list.append(urwid.AttrMap(urwid.Padding(urwid.Text("(access denied)"), left=1, right=1), 'perm_denied', 'selected'))
  567. def check_playback_end(self):
  568. if self.main_loop is not None and self.playing and not pygame.mixer.music.get_busy() and not self.paused:
  569. self.next_track()
  570. if self.main_loop is not None:
  571. self.main_loop.set_alarm_in(0.1, lambda loop, data: self.check_playback_end())
  572. def next_track(self):
  573. if self.playlist and self.playlist_index < len(self.playlist) - 1:
  574. self.playlist_index += 1
  575. self.set_focus(self.playlist_index)
  576. self.play_media(self.playlist[self.playlist_index])
  577. else:
  578. pygame.mixer.music.stop()
  579. self.playing = False
  580. self.status_output.set_text([('time_separator,bold', " Playlist ended")])
  581. self.metadata_output.set_text([('path_value', ' No metadata available')])
  582. def update_file_list(self):
  583. AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma', 'opus'}
  584. try:
  585. all_files = sorted(os.listdir(self.current_dir))
  586. files = [f for f in all_files
  587. if not f.startswith('.') and
  588. (os.path.isdir(os.path.join(self.current_dir, f)) or
  589. f.lower().split('.')[-1] in AUDIO_EXTENSIONS)]
  590. if not files:
  591. files = ["(empty)"]
  592. except PermissionError:
  593. files = ["(access denied)"]
  594. file_items = []
  595. for file in files:
  596. full_path = os.path.join(self.current_dir, file)
  597. if os.path.isdir(full_path):
  598. attr = 'directory'
  599. display_name = file + "/"
  600. elif os.path.isfile(full_path):
  601. attr = 'audio_file'
  602. display_name = file
  603. else:
  604. attr = 'normal'
  605. display_name = file
  606. padded_text = urwid.Padding(urwid.Text(display_name), left=1, right=1)
  607. file_items.append(urwid.AttrMap(padded_text, attr, 'selected'))
  608. return file_items
  609. def refresh_list(self):
  610. old_focus = self.focus_position if self.file_list else 0
  611. self.file_list[:] = self.update_file_list()
  612. self.path_text_inner.set_text([('path_value', self.current_dir)])
  613. if self.file_list:
  614. self.set_focus(min(old_focus, len(self.file_list) - 1))
  615. def get_widget(self):
  616. return self.widget
  617. def cleanup(self):
  618. if self.playing:
  619. pygame.mixer.music.stop()
  620. self.status_output.set_text([('path_value', ' No status available')])
  621. self.metadata_output.set_text([('path_value', ' No metadata available')])
  622. self.playing = False
  623. self.paused = False
  624. def show_message(self, message, duration=1):
  625. if "Permission denied" in message:
  626. self.status_output.set_text([('perm_denied', f" {message}")])
  627. duration = 0
  628. elif "not found" in message or "Error" in message:
  629. self.status_output.set_text([('error', f" {message}")])
  630. duration = 2
  631. else:
  632. self.status_output.set_text([('normal', f" {message}")])
  633. self.main_loop.draw_screen()
  634. if duration > 0:
  635. def clear_message(loop, data):
  636. if self.status_output.text in ([('perm_denied', f" {message}"), ('error', f" {message}"), ('normal', f" {message}")]):
  637. self.status_output.set_text("")
  638. self.main_loop.draw_screen()
  639. self.main_loop.set_alarm_in(duration, clear_message)
  640. def clear_message(self):
  641. self.status_output.set_text("")
  642. self.main_loop.draw_screen()
  643. def get_metadata(self, filepath):
  644. try:
  645. audio = mutagen.File(filepath)
  646. if audio is None:
  647. return " No metadata available"
  648. metadata = []
  649. if hasattr(audio, 'info'):
  650. metadata.append([('path_value', ' Duration: '), ('normal', f'{audio.info.length:.2f} sec')])
  651. metadata.append([('path_value', ' Bitrate: '), ('normal', f'{audio.info.bitrate // 1000} kbps')])
  652. metadata.append([('path_value', ' Channels: '), ('normal', f'{audio.info.channels}')])
  653. metadata.append([('path_value', ' Sample Rate: '), ('normal', f'{audio.info.sample_rate} Hz')])
  654. if audio.tags:
  655. for key, value in audio.tags.items():
  656. value_str = str(value)[:50] + "..." if len(str(value)) > 50 else str(value)
  657. metadata.append([('path_value', f' {key}: '), ('normal', value_str)])
  658. max_lines = 10
  659. if len(metadata) > max_lines:
  660. metadata = metadata[:max_lines - 1] + [[('path_value', ' ... (truncated)')]]
  661. result = []
  662. for i, line in enumerate(metadata):
  663. result.extend(line)
  664. if i < len(metadata) - 1:
  665. result.append(('normal', '\n'))
  666. return result if result else [('path_value', ' No metadata available')]
  667. except Exception as e:
  668. return [('path_value', ' Error reading metadata: '), ('normal', str(e))]
  669. def play_media(self, filepath):
  670. if not os.path.exists(filepath):
  671. self.show_message(f"File not found: {filepath}")
  672. return
  673. if not os.access(filepath, os.R_OK):
  674. self.show_message("Permission denied!")
  675. return
  676. if self.playing:
  677. pygame.mixer.music.stop()
  678. try:
  679. pygame.mixer.music.load(filepath)
  680. pygame.mixer.music.set_volume(self.volume)
  681. pygame.mixer.music.play()
  682. self.playing = True
  683. self.paused = False
  684. self.status_output.set_text([('time_separator,bold', " Playing:\n "), ('normal', f"{os.path.basename(filepath)}")])
  685. self.metadata_output.set_text(self.get_metadata(filepath))
  686. filled = min(50, int(self.volume * 50))
  687. self.volume_bar.set_text([('normal', f" {int(self.volume * 100)}"), ('time_separator', '%'), (None, f" | {'░' * filled + ' ' * (50 - filled)}")]) #self.volume_bar.set_text(f" {int(self.volume * 100)}% | {'░' * filled + ' ' * (50 - filled)}")
  688. audio = mutagen.File(filepath)
  689. if audio and hasattr(audio, 'info'):
  690. self.current_audio_duration = audio.info.length
  691. else:
  692. sound = pygame.mixer.Sound(filepath)
  693. self.current_audio_duration = sound.get_length()
  694. except Exception as e:
  695. self.show_message(f"Error playing media: {str(e)}")
  696. def keypress(self, size, key):
  697. current_message = self.status_output.text
  698. is_perm_denied = isinstance(current_message, list) and len(current_message) > 0 and "Permission denied" in current_message[0][1]
  699. def keypress(self, size, key):
  700. current_message = self.status_output.text
  701. is_perm_denied = isinstance(current_message, list) and len(current_message) > 0 and "Permission denied" in current_message[0][1]
  702. help_text = [
  703. ('normal,bold', ' left'), ('path_value', ' - Go to parent directory.\n'),
  704. ('normal,bold', ' right'), ('path_value', ' - Go back in directory history.\n'),
  705. ('normal,bold', ' up'), ('path_value', ' - Move focus up in file list.\n'),
  706. ('normal,bold', ' down'), ('path_value', ' - Move focus down in file list.\n'),
  707. ('normal,bold', ' enter'), ('path_value', ' - Open folder or play file.\n'),
  708. ('normal,bold', ' space'), ('path_value', ' - Play directory as playlist.\n'),
  709. ('normal,bold', ' + -'), ('path_value', ' - Increase/Decrease volume (pygame).\n'),
  710. ('normal,bold', ' a'), ('path_value', ' - Increase right headphone volume\n'),
  711. ('normal,bold', ' b'), ('path_value', ' - Decrease right headphone volume\n'),
  712. ('normal,bold', ' c'), ('path_value', ' - Increase left headphone volume\n'),
  713. ('normal,bold', ' d'), ('path_value', ' - Decrease system volume.\n'),
  714. ('normal,bold', ' e'), ('path_value', ' - Increase both headphones volume\n'),
  715. ('normal,bold', ' f'), ('path_value', ' - Decrease both headphones volume\n'),
  716. ('normal,bold', ' g'), ('path_value', ' - Decrease left headphone volume\n'),
  717. ('normal,bold', ' p'), ('path_value', ' - Pause or resume playback.\n'),
  718. ('normal,bold', ' s'), ('path_value', ' - Stop playback.\n'),
  719. ('normal,bold', ' r'), ('path_value', ' - Restart current track.\n'),
  720. ('normal,bold', ' i'), ('path_value', ' - Increase system volume.\n'),
  721. ('normal,bold', ' n'), ('path_value', ' - Next track.\n'),
  722. ('normal,bold', ' q or Q'), ('path_value', ' - Quit program.\n'),
  723. ('normal,bold', ' h'), ('path_value', ' - Show help.')
  724. ]
  725. if key == 'h' and not self.playing and not self.paused:
  726. self.metadata_output.set_text(help_text)
  727. self.main_loop.draw_screen()
  728. elif key != 'h':
  729. if self.metadata_output.text == help_text:
  730. self.metadata_output.set_text([('path_value', ' No metadata available')])
  731. self.main_loop.draw_screen()
  732. if key == 'h' and not self.playing and not self.paused:
  733. self.metadata_output.set_text(help_text)
  734. self.main_loop.draw_screen()
  735. elif key != 'h':
  736. if self.metadata_output.text == help_text:
  737. self.metadata_output.set_text([('path_value', ' No metadata available')])
  738. self.main_loop.draw_screen()
  739. if key == 'left':
  740. if self.current_dir != "/":
  741. try:
  742. self.dir_history.append(self.current_dir)
  743. os.chdir("..")
  744. self.current_dir = os.getcwd()
  745. self.refresh_list()
  746. self.clear_message()
  747. self.main_loop.draw_screen()
  748. except PermissionError:
  749. self.show_message("Permission denied!")
  750. elif key == 'right':
  751. if self.dir_history:
  752. try:
  753. os.chdir(self.dir_history.pop())
  754. self.current_dir = os.getcwd()
  755. self.refresh_list()
  756. self.clear_message()
  757. self.main_loop.draw_screen()
  758. except PermissionError:
  759. self.show_message("Permission denied!")
  760. elif key == 'up' and self.focus_position > 0:
  761. self.set_focus(self.focus_position - 1)
  762. if not is_perm_denied:
  763. self.clear_message()
  764. elif key == 'down' and self.focus_position < len(self.file_list) - 1:
  765. self.set_focus(self.focus_position + 1)
  766. if not is_perm_denied:
  767. self.clear_message()
  768. elif key == 'enter':
  769. if not self.file_list or self.focus.original_widget.original_widget.text.strip() in ["(empty)", "(access denied)"]:
  770. return
  771. selected = self.focus.original_widget.original_widget.text.rstrip('/')
  772. full_path = os.path.join(self.current_dir, selected)
  773. try:
  774. if os.path.isdir(full_path):
  775. self.dir_history.append(self.current_dir)
  776. os.chdir(full_path)
  777. self.current_dir = os.getcwd()
  778. self.refresh_list()
  779. self.clear_message()
  780. self.main_loop.draw_screen()
  781. elif os.path.isfile(full_path):
  782. self.play_media(full_path)
  783. self.playlist = [full_path]
  784. self.playlist_index = 0
  785. except Exception as e:
  786. self.show_message(f"Error: {str(e)}")
  787. elif key == ' ':
  788. if self.playing or self.paused:
  789. pygame.mixer.music.stop()
  790. self.playing = False
  791. self.paused = False
  792. self.file_list.clear()
  793. self.load_and_play_directory(self.current_dir)
  794. self.check_playback_end()
  795. self.main_loop.draw_screen()
  796. elif key == 'p':
  797. if self.playing:
  798. if self.paused:
  799. pygame.mixer.music.unpause()
  800. self.paused = False
  801. filepath = os.path.join(self.current_dir, self.focus.original_widget.original_widget.text.rstrip('/'))
  802. self.status_output.set_text([('time_separator,bold', " Resumed: "), ('normal', f"{os.path.basename(filepath)}")])
  803. else:
  804. pygame.mixer.music.pause()
  805. self.paused = True
  806. self.status_output.set_text([('time_separator,bold', " Paused")])
  807. elif key == 's':
  808. if self.playing:
  809. pygame.mixer.music.stop()
  810. self.playing = False
  811. self.paused = False
  812. self.status_output.set_text([('time_separator,bold', " Stopped")])
  813. self.metadata_output.set_text([('path_value', ' No metadata available')])
  814. elif key == 'r':
  815. if self.playing or self.paused:
  816. filepath = os.path.join(self.current_dir, self.focus.original_widget.original_widget.text.rstrip('/'))
  817. pygame.mixer.music.stop()
  818. pygame.mixer.music.load(filepath)
  819. pygame.mixer.music.set_volume(self.volume)
  820. pygame.mixer.music.play()
  821. self.playing = True
  822. self.paused = False
  823. self.status_output.set_text([('time_separator,bold', " Replaying: "), ('normal', f"{os.path.basename(filepath)}")])
  824. self.metadata_output.set_text(self.get_metadata(filepath))
  825. elif key == '+':
  826. self.volume = min(1.0, self.volume + 0.02)
  827. if self.playing:
  828. pygame.mixer.music.set_volume(self.volume)
  829. filled = int(self.volume * 50)
  830. self.volume_bar.set_text([('normal', f" {int(self.volume * 100)}"), ('time_separator', '%'), (None, f" | {'░' * filled + ' ' * (50 - filled)}")]) #self.volume_bar.set_text(f" {int(self.volume * 100)}% | {'░' * filled + ' ' * (50 - filled)}")
  831. self.main_loop.draw_screen()
  832. elif key == '-':
  833. self.volume = max(0.0, self.volume - 0.02)
  834. if self.playing:
  835. pygame.mixer.music.set_volume(self.volume)
  836. filled = int(self.volume * 44)
  837. self.volume_bar.set_text([('normal', f" {int(self.volume * 100)}"), ('time_separator', '%'), (None, f" | {'░' * filled + ' ' * (44 - filled)}")]) #self.volume_bar.set_text(f" {int(self.volume * 100)}% | {'░' * filled + ' ' * (44 - filled)}")
  838. self.main_loop.draw_screen()
  839. elif key == 'i':
  840. try:
  841. result = subprocess.check_output("amixer set Master 2%+ | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  842. percent = int(result.rstrip('%'))
  843. filled = min(50, int(percent / 2))
  844. self.system_volume_bar.set_text([('normal', f" {percent}"), ('time_separator', '%'), (None, f" | {'░' * filled + ' ' * (50 - filled)}")]) #self.system_volume_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (50 - filled)}")
  845. self.main_loop.draw_screen()
  846. except subprocess.CalledProcessError as e:
  847. self.show_message(f"Error adjusting system volume: {e}")
  848. elif key == 'd':
  849. try:
  850. result = subprocess.check_output("amixer set Master 2%- | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  851. percent = int(result.rstrip('%'))
  852. filled = min(50, int(percent / 2))
  853. self.system_volume_bar.set_text([('normal', f" {percent}"), ('time_separator', '%'), (None, f" | {'░' * filled + ' ' * (50 - filled)}")]) #self.system_volume_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (50 - filled)}")
  854. self.main_loop.draw_screen()
  855. except subprocess.CalledProcessError as e:
  856. self.show_message(f"Error adjusting system volume: {e}")
  857. elif key == 'a':
  858. try:
  859. result = subprocess.check_output("amixer sset 'Headphone' frontright 2%+ -q && amixer sget 'Headphone' | grep 'Front Right' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  860. percent = int(result.rstrip('%'))
  861. filled = min(50, int(percent / 2))
  862. self.headphone_right_bar.set_text([('normal', f" {percent}"), ('time_separator', '%'), (None, f" | {'░' * filled + ' ' * (50 - filled)}")]) #self.headphone_right_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (50 - filled)}")
  863. self.main_loop.draw_screen()
  864. except subprocess.CalledProcessError as e:
  865. pass
  866. elif key == 'b':
  867. try:
  868. result = subprocess.check_output("amixer sset 'Headphone' frontright 2%- -q && amixer sget 'Headphone' | grep 'Front Right' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  869. percent = int(result.rstrip('%'))
  870. filled = min(50, int(percent / 2))
  871. self.headphone_right_bar.set_text([('normal', f" {percent}"), ('time_separator', '%'), (None, f" | {'░' * filled + ' ' * (50 - filled)}")]) #self.headphone_right_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (50 - filled)}")
  872. self.main_loop.draw_screen()
  873. except subprocess.CalledProcessError as e:
  874. self.show_message(f"Error adjusting headphone volume: {e}")
  875. elif key == 'c':
  876. try:
  877. result = subprocess.check_output("amixer sset 'Headphone' frontleft 2%+ -q && amixer sget 'Headphone' | grep 'Front Left' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  878. percent = int(result.rstrip('%'))
  879. filled = min(50, int(percent / 2))
  880. self.headphone_left_bar.set_text([('normal', f" {percent}"), ('time_separator', '%'), (None, f" | {'░' * filled + ' ' * (50 - filled)}")]) #self.headphone_left_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (50 - filled)}")
  881. self.main_loop.draw_screen()
  882. except subprocess.CalledProcessError as e:
  883. self.show_message(f"Error adjusting headphone volume: {e}")
  884. elif key == 'g':
  885. try:
  886. result = subprocess.check_output("amixer sset 'Headphone' frontleft 2%- -q && amixer sget 'Headphone' | grep 'Front Left' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  887. percent = int(result.rstrip('%'))
  888. filled = min(50, int(percent / 2))
  889. self.headphone_left_bar.set_text([('normal', f" {percent}"), ('time_separator', '%'), (None, f" | {'░' * filled + ' ' * (50 - filled)}")]) #self.headphone_left_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (50 - filled)}")
  890. self.main_loop.draw_screen()
  891. except subprocess.CalledProcessError as e:
  892. self.show_message(f"Error adjusting headphone volume: {e}")
  893. elif key == 'e':
  894. try:
  895. subprocess.check_output("amixer sset 'Headphone' 2%+ -q", shell=True, text=True)
  896. left_result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Left' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  897. left_percent = int(left_result.rstrip('%'))
  898. left_filled = min(50, left_percent // 2)
  899. self.headphone_left_bar.set_text([('normal', f" {left_percent}"), ('time_separator', '%'), (None, f" | {'░' * left_filled + ' ' * (50 - left_filled)}")]) #self.headphone_left_bar.set_text(f" {left_percent}% | {'░' * left_filled + ' ' * (50 - left_filled)}")
  900. right_result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Right' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  901. right_percent = int(right_result.rstrip('%'))
  902. right_filled = min(50, right_percent // 2)
  903. self.headphone_right_bar.set_text([('normal', f" {right_percent}"), ('time_separator', '%'), (None, f" | {'░' * right_filled + ' ' * (50 - right_filled)}")]) #self.headphone_right_bar.set_text(f" {right_percent}% | {'░' * right_filled + ' ' * (50 - right_filled)}")
  904. self.main_loop.draw_screen()
  905. except subprocess.CalledProcessError as e:
  906. self.show_message(f"Error adjusting headphone volume: {e}")
  907. elif key == 'f':
  908. try:
  909. subprocess.check_output("amixer sset 'Headphone' 2%- -q", shell=True, text=True)
  910. left_result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Left' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  911. left_percent = int(left_result.rstrip('%'))
  912. left_filled = min(50, left_percent // 2)
  913. self.headphone_left_bar.set_text([('normal', f" {left_percent}"), ('time_separator', '%'), (None, f" | {'░' * left_filled + ' ' * (50 - left_filled)}")]) #self.headphone_left_bar.set_text(f" {left_percent}% | {'░' * left_filled + ' ' * (50 - left_filled)}")
  914. right_result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Right' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
  915. right_percent = int(right_result.rstrip('%'))
  916. right_filled = min(50, right_percent // 2)
  917. self.headphone_right_bar.set_text([('normal', f" {right_percent}"), ('time_separator', '%'), (None, f" | {'░' * right_filled + ' ' * (50 - right_filled)}")]) #self.headphone_right_bar.set_text(f" {right_percent}% | {'░' * right_filled + ' ' * (50 - right_filled)}")
  918. self.main_loop.draw_screen()
  919. except subprocess.CalledProcessError as e:
  920. self.show_message(f"Error adjusting headphone volume: {e}")
  921. elif key == 'n':
  922. self.next_track()
  923. elif key in ('q', 'Q'):
  924. self.cleanup()
  925. return 'q'
  926. else:
  927. super().keypress(size, key)
  928. return key
  929. def handle_input(self, key):
  930. if isinstance(key, str):
  931. return key
  932. return None
  933. class FileManager:
  934. def __init__(self, input_path=None):
  935. self.main_loop = None
  936. self.root_dir = os.path.dirname(os.path.abspath(__file__))
  937. self.mode = PlaybackMode(None, self.root_dir, input_path)
  938. initial_widget = self.wrap_mode_widget(self.mode.get_widget())
  939. self.frame = urwid.Frame(body=initial_widget)
  940. def wrap_mode_widget(self, widget):
  941. title = "╡ AUDIO PLAYER TERM PY ╞"
  942. term_width = os.get_terminal_size().columns
  943. title_len = len(title)
  944. side_len = max(1, (term_width - title_len - 2) // 2)
  945. top_line = f'╔{"═" * side_len}{title}{"═" * (side_len + term_width % 2)}╗'
  946. top_text = urwid.Text(('header', top_line), align='center')
  947. side_borders = urwid.LineBox(widget, lline='║', rline='║', tline='', bline='', tlcorner='', trcorner='', blcorner='', brcorner='')
  948. side_borders = urwid.AttrMap(side_borders, 'header')
  949. framed_widget = urwid.Frame(
  950. body=side_borders,
  951. header=top_text,
  952. focus_part='body',
  953. footer=urwid.Text(('header', '╚' + '═' * (len(top_line) - 2) + '╝'))
  954. )
  955. return urwid.AttrMap(framed_widget, 'header')
  956. def unhandled_input(self, key):
  957. mode_key = self.mode.handle_input(key)
  958. if mode_key == 'q':
  959. self.mode.cleanup()
  960. raise urwid.ExitMainLoop()
  961. #
  962. def run(self):
  963. os.system('clear')
  964. try:
  965. self.main_loop = urwid.MainLoop(self.frame, palette=palette, unhandled_input=self.unhandled_input)
  966. self.mode.main_loop = self.main_loop
  967. self.mode.start()
  968. self.mode.check_playback_end()
  969. self.mode.update_clock()
  970. self.main_loop.set_alarm_in(0.1, self.mode.update_progress_bar)
  971. self.main_loop.run()
  972. except Exception as e:
  973. with open("error_log.txt", "w") as f:
  974. f.write(f"Error in run: {str(e)}\n")
  975. import traceback
  976. traceback.print_exc(file=f)
  977. print(f"Error occurred! Check error_log.txt for details.")
  978. import time
  979. time.sleep(3) # Пауза 3 секунды, чтобы увидеть сообщение
  980. finally:
  981. os.system('stty sane')
  982. os.system('clear')
  983. #
  984. if __name__ == "__main__":
  985. input_path = None
  986. if len(sys.argv) > 1:
  987. input_path = sys.argv[1]
  988. fm = FileManager(input_path)
  989. fm.run()