791 lines
39 KiB
Python
791 lines
39 KiB
Python
#!/usr/bin/env python3
|
|
import urwid
|
|
import os
|
|
import pygame.mixer
|
|
import mutagen
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
import subprocess
|
|
|
|
# Палитра (без изменений)
|
|
palette = [
|
|
('header', 'light blue', 'default'),
|
|
('path_label', 'light blue', 'default'),
|
|
('path_value', 'dark gray', 'default'),
|
|
('directory', 'dark blue,bold', 'default'),
|
|
('audio_file', 'light cyan', 'default'),
|
|
('normal', 'white', 'default'),
|
|
('selected', 'black', 'light green'),
|
|
('perm_denied', 'light red', 'default'),
|
|
('error', 'light red', 'default'),
|
|
('playing', 'light green', 'default'),
|
|
('pink_frame', 'light magenta', 'default'),
|
|
('percent', 'white,bold', 'default'),
|
|
]
|
|
|
|
class PlaybackMode(urwid.ListBox):
|
|
def __init__(self, main_loop, root_dir, input_path=None):
|
|
pygame.mixer.init()
|
|
self.main_loop = main_loop
|
|
self.root_dir = root_dir
|
|
self.current_dir = os.getcwd()
|
|
self.dir_history = []
|
|
self.file_list = urwid.SimpleFocusListWalker([])
|
|
self.playlist = [] # Список для плейлиста
|
|
self.playlist_index = 0 # Текущий индекс в плейлисте
|
|
|
|
# Левая рамка: Box00 (без изменений)
|
|
self.progress_bar = urwid.Text([('path_value', " 0"), ('percent', '%'), (None, " | " + " " * 83)], align='left')
|
|
self.file_frame = urwid.LineBox(self.progress_bar, title="Playback progress", title_align='center',
|
|
tlcorner='┌', tline='─', trcorner='┐',
|
|
lline='│', rline='│',
|
|
blcorner='└', bline='─', brcorner='┘')
|
|
self.file_frame = urwid.AttrMap(self.file_frame, 'pink_frame')
|
|
|
|
# Правая рамка: Volume Level (без изменений)
|
|
# self.volume_bar = urwid.Text(f" 50% {'░' * 17 + ' ' * 17}")
|
|
self.volume_bar = urwid.Text(f" 50% | {'░' * 22 + ' ' * 22}")
|
|
self.metadata_frame = urwid.LineBox(self.volume_bar, title='Pygame.mixer volume Level', title_align='center',
|
|
tlcorner='┌', tline='─', trcorner='┐',
|
|
lline='│', rline='│',
|
|
blcorner='└', bline='─', brcorner='┘')
|
|
self.metadata_frame = urwid.AttrMap(self.metadata_frame, 'pink_frame')
|
|
|
|
# Рамка пути (без изменений)
|
|
self.path_text_inner = urwid.Text([('path_value', self.current_dir)], align='left')
|
|
self.path_text = urwid.Padding(self.path_text_inner, left=1)
|
|
self.path_filler = urwid.Filler(self.path_text, valign='top')
|
|
self.path_frame = urwid.LineBox(self.path_filler, title="Path", title_align='center',
|
|
tlcorner='┌', tline='─', trcorner='┐',
|
|
lline='│', rline='│',
|
|
blcorner='└', bline='─', brcorner='┘')
|
|
self.path_widget = urwid.AttrMap(self.path_frame, 'pink_frame')
|
|
|
|
# Status (без изменений)
|
|
self.status_output = urwid.Text("", align='left')
|
|
self.status_filler = urwid.Filler(self.status_output, valign='top')
|
|
|
|
self.playing = False
|
|
self.paused = False
|
|
self.volume = 0.5
|
|
self.current_audio_duration = 0
|
|
|
|
# Инициализация системной громкости (без изменений)
|
|
try:
|
|
result = subprocess.check_output("amixer get Master | grep -o '[0-9]*%' | uniq", shell=True, text=True).strip()
|
|
percent = int(result.rstrip('%'))
|
|
filled = min(34, percent // 3)
|
|
initial_volume_text = f" {percent}% {'░' * filled + ' ' * (34 - filled)}"
|
|
except subprocess.CalledProcessError:
|
|
initial_volume_text = " --% " + " " * 34
|
|
self.system_volume_bar = urwid.Text(initial_volume_text)
|
|
|
|
# Инициализация прогресс-бара для левого наушника (без изменений)
|
|
try:
|
|
result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Left' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
|
|
percent = int(result.rstrip('%'))
|
|
filled = min(29, percent // 3)
|
|
initial_headphone_left_text = f" {percent}% | {'░' * filled + ' ' * (29 - filled)}"
|
|
except (subprocess.CalledProcessError, ValueError):
|
|
initial_headphone_left_text = " --% | " + " " * 29
|
|
self.headphone_left_bar = urwid.Text(initial_headphone_left_text, align='left')
|
|
|
|
# Инициализация прогресс-бара для правого наушника (без изменений)
|
|
try:
|
|
result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Right' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
|
|
percent = int(result.rstrip('%'))
|
|
filled = min(29, percent // 3)
|
|
initial_headphone_right_text = f" {percent}% | {'░' * filled + ' ' * (29 - filled)}"
|
|
except (subprocess.CalledProcessError, ValueError):
|
|
initial_headphone_right_text = " --% | " + " " * 29
|
|
self.headphone_right_bar = urwid.Text(initial_headphone_right_text, align='left')
|
|
|
|
# Alisa (часы) (без изменений)
|
|
self.alisa_text = urwid.Text("", align='center')
|
|
|
|
super().__init__(self.file_list)
|
|
|
|
self.input_path = input_path
|
|
if not input_path:
|
|
self.refresh_list()
|
|
|
|
self.widget = None
|
|
self.initialize_widget()
|
|
|
|
def start(self):
|
|
if self.input_path:
|
|
if os.path.isdir(self.input_path):
|
|
self.load_and_play_directory(self.input_path)
|
|
elif os.path.isfile(self.input_path):
|
|
self.load_and_play_audio(self.input_path)
|
|
|
|
def update_clock(self, loop=None, data=None):
|
|
SEASONS = {
|
|
1: "Winter", 2: "Winter", 3: "Spring",
|
|
4: "Spring", 5: "Spring", 6: "Summer",
|
|
7: "Summer", 8: "Summer", 9: "Autumn",
|
|
10: "Autumn", 11: "Autumn", 12: "Winter"
|
|
}
|
|
MONTHS = {
|
|
1: "January", 2: "February", 3: "March",
|
|
4: "April", 5: "May", 6: "June",
|
|
7: "July", 8: "August", 9: "September",
|
|
10: "October", 11: "November", 12: "December"
|
|
}
|
|
WEEKDAYS = {
|
|
0: "Monday", 1: "Tuesday", 2: "Wednesday",
|
|
3: "Thursday", 4: "Friday", 5: "Saturday", 6: "Sunday"
|
|
}
|
|
|
|
now = datetime.now()
|
|
year = "2025"
|
|
season = SEASONS[now.month]
|
|
month = MONTHS[now.month]
|
|
weekday = WEEKDAYS[now.weekday()]
|
|
time_str = now.strftime('%H:%M:%S')
|
|
|
|
clock_text = f"{year}\n{season} {month} {weekday}\n{time_str}"
|
|
self.alisa_text.set_text(clock_text)
|
|
|
|
if self.main_loop:
|
|
self.main_loop.set_alarm_in(1 / 30, self.update_clock)
|
|
|
|
def format_time(self, seconds):
|
|
return str(timedelta(seconds=int(seconds))).zfill(8)
|
|
|
|
def update_progress_bar(self, loop=None, data=None):
|
|
if self.playing and not self.paused and pygame.mixer.music.get_busy():
|
|
elapsed = pygame.mixer.music.get_pos() / 1000
|
|
duration = self.current_audio_duration
|
|
if duration > 0:
|
|
progress_percent = min(100, int((elapsed / duration) * 100))
|
|
filled = min(83, int(progress_percent / 1.2048))
|
|
unfilled = 83 - filled
|
|
progress_str = [('path_value', f"{progress_percent:3d}"), ('percent', '%'), (None, f" | {'░' * filled}{' ' * unfilled}")]
|
|
self.progress_bar.set_text(progress_str)
|
|
elapsed_str = self.format_time(elapsed)
|
|
duration_str = self.format_time(duration)
|
|
self.grannik_text.set_text(f" {elapsed_str} / {duration_str}")
|
|
else:
|
|
self.progress_bar.set_text([('path_value', " 0"), ('percent', '%'), (None, " | " + " " * 83)])
|
|
self.grannik_text.set_text(" 00:00:00 / 00:00:00")
|
|
if self.main_loop:
|
|
self.main_loop.set_alarm_in(1, self.update_progress_bar)
|
|
|
|
def initialize_widget(self):
|
|
self.widget = self.wrap_in_three_frames()
|
|
|
|
def wrap_in_three_frames(self):
|
|
term_size = os.get_terminal_size()
|
|
term_width = term_size.columns
|
|
term_height = term_size.lines
|
|
total_border_chars = 6
|
|
|
|
available_width = term_width - total_border_chars
|
|
num_upper_boxes = 3
|
|
base_status_width = (available_width // 3 + available_width // 4) // 2
|
|
status_width = max(15, base_status_width + 3)
|
|
remaining_width = available_width - status_width - 7
|
|
files_width = remaining_width // 2
|
|
metadata_width = available_width - files_width - status_width + 2
|
|
|
|
total_width_so_far = files_width + status_width + metadata_width + 8
|
|
if total_width_so_far < available_width:
|
|
metadata_width += available_width - total_width_so_far
|
|
|
|
total_width = term_width - 1
|
|
left_width = int(total_width * 0.62)
|
|
|
|
combined_widget = urwid.Columns([
|
|
(left_width, self.file_frame),
|
|
('weight', 1, self.metadata_frame),
|
|
], dividechars=1, box_columns=[0, 1])
|
|
height_limited_widget = urwid.Filler(combined_widget, height=3, valign='top')
|
|
|
|
box02_clone = urwid.LineBox(self.system_volume_bar, title='Amixer master volume Level', title_align='center',
|
|
tlcorner='┌', tline='─', trcorner='┐',
|
|
lline='│', rline='│',
|
|
blcorner='└', bline='─', brcorner='┘')
|
|
box02_clone = urwid.AttrMap(box02_clone, 'pink_frame')
|
|
|
|
box02_clone2 = urwid.LineBox(self.headphone_left_bar, title='Amixer headphone left', title_align='center',
|
|
tlcorner='┌', tline='─', trcorner='┐',
|
|
lline='│', rline='│',
|
|
blcorner='└', bline='─', brcorner='┘')
|
|
box02_clone2 = urwid.AttrMap(box02_clone2, 'pink_frame')
|
|
|
|
box02_clone3 = urwid.LineBox(self.headphone_right_bar, title='Amixer headphone right', title_align='center',
|
|
tlcorner='┌', tline='─', trcorner='┐',
|
|
lline='│', rline='│',
|
|
blcorner='└', bline='─', brcorner='┘')
|
|
box02_clone3 = urwid.AttrMap(box02_clone3, 'pink_frame')
|
|
|
|
header_height = 3
|
|
frame_border_height = 2
|
|
available_height = term_height - header_height - frame_border_height
|
|
|
|
upper_boxes_height = 3
|
|
min_footer_height = 8
|
|
|
|
columns_height = max(21, available_height - upper_boxes_height - min_footer_height - 1)
|
|
|
|
new_left_frame = urwid.LineBox(self, title="Files and directories of the Linux OS", title_align='center',
|
|
tlcorner='┌', tline='─', trcorner='┐',
|
|
lline='│', rline='│',
|
|
blcorner='└', bline='─', brcorner='┘')
|
|
new_left_frame = urwid.AttrMap(new_left_frame, 'pink_frame')
|
|
new_left_frame_filler = urwid.Filler(new_left_frame, height=columns_height, valign='top')
|
|
|
|
self.metadata_output = urwid.Text("", align='left')
|
|
self.metadata_filler = urwid.Filler(self.metadata_output, valign='top')
|
|
new_right_frame = urwid.LineBox(self.metadata_filler, title="Info", title_align='center',
|
|
tlcorner='┌', tline='─', trcorner='┐',
|
|
lline='│', rline='│',
|
|
blcorner='└', bline='─', brcorner='┘')
|
|
new_right_frame = urwid.AttrMap(new_right_frame, 'pink_frame')
|
|
new_right_frame_filler = urwid.Filler(new_right_frame, height=columns_height, valign='top')
|
|
|
|
new_frames_widget = urwid.Columns([
|
|
(left_width, new_left_frame_filler),
|
|
('weight', 1, new_right_frame_filler)
|
|
], dividechars=1)
|
|
|
|
footer_width = left_width
|
|
footer_height = available_height - columns_height - upper_boxes_height
|
|
box_height = max(min_footer_height + 1, footer_height)
|
|
|
|
divider_width = 2
|
|
available_footer_width = footer_width - divider_width
|
|
box1_width = available_footer_width // 3
|
|
box2_width = available_footer_width // 3
|
|
box4_width = available_footer_width // 3
|
|
|
|
self.grannik_text = urwid.Text(" 00:00:00 / 00:00:00", align='left')
|
|
box1 = urwid.LineBox(self.grannik_text, title="Playback time", title_align='center',
|
|
tlcorner='┌', tline='─', trcorner='┐',
|
|
lline='│', rline='│',
|
|
blcorner='└', bline='─', brcorner='┘')
|
|
box1 = urwid.AttrMap(box1, 'pink_frame')
|
|
box1_filler = urwid.Filler(box1, height=box_height, valign='middle')
|
|
|
|
box2 = urwid.LineBox(self.alisa_text, title="Current time", title_align='center',
|
|
tlcorner='┌', tline='─', trcorner='┐',
|
|
lline='│', rline='│',
|
|
blcorner='└', bline='─', brcorner='┘')
|
|
box2 = urwid.AttrMap(box2, 'pink_frame')
|
|
box2_filler = urwid.Filler(box2, height=box_height, valign='middle')
|
|
|
|
box4 = urwid.LineBox(self.status_filler, title="Status", title_align='center',
|
|
tlcorner='┌', tline='─', trcorner='┐',
|
|
lline='│', rline='│',
|
|
blcorner='└', bline='─', brcorner='┘')
|
|
box4 = urwid.AttrMap(box4, 'pink_frame')
|
|
box4_filler = urwid.Filler(box4, height=box_height, valign='middle')
|
|
|
|
footer_columns = urwid.Columns([
|
|
(box1_width, box1_filler),
|
|
(box2_width, box2_filler),
|
|
(box4_width, box4_filler),
|
|
], dividechars=1, box_columns=[0, 1, 2])
|
|
|
|
clone_width = metadata_width
|
|
clones_pile = urwid.Pile([
|
|
(3, box02_clone),
|
|
(3, box02_clone2),
|
|
(3, box02_clone3),
|
|
])
|
|
clones_filler = urwid.Filler(clones_pile, height=9, valign='middle')
|
|
|
|
footer_with_clones = urwid.Columns([
|
|
(footer_width, footer_columns),
|
|
(clone_width, clones_filler),
|
|
], dividechars=1, box_columns=[0, 1])
|
|
footer_widget = urwid.Filler(footer_with_clones, height=box_height, valign='middle')
|
|
|
|
body_widget = urwid.Pile([
|
|
(columns_height, new_frames_widget),
|
|
(upper_boxes_height, height_limited_widget),
|
|
(box_height, footer_widget),
|
|
])
|
|
|
|
frame_with_path = urwid.Frame(
|
|
body=body_widget,
|
|
header=self.path_widget,
|
|
)
|
|
return frame_with_path
|
|
|
|
def load_and_play_audio(self, audio_file):
|
|
full_path = os.path.abspath(audio_file)
|
|
self.current_dir = os.path.dirname(full_path)
|
|
file_name = os.path.basename(full_path)
|
|
|
|
self.file_list.clear()
|
|
padded_text = urwid.Padding(urwid.Text(file_name), left=1, right=1)
|
|
self.file_list.append(urwid.AttrMap(padded_text, 'normal', 'selected'))
|
|
self.set_focus(0)
|
|
|
|
self.path_text_inner.set_text([('path_value', self.current_dir)])
|
|
self.play_media(full_path)
|
|
|
|
def load_and_play_directory(self, directory):
|
|
full_path = os.path.abspath(directory)
|
|
self.current_dir = full_path
|
|
self.path_text_inner.set_text([('path_value', self.current_dir)])
|
|
|
|
AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma', 'opus'}
|
|
try:
|
|
all_files = sorted(os.listdir(self.current_dir))
|
|
audio_files = [f for f in all_files
|
|
if not f.startswith('.') and
|
|
f.lower().split('.')[-1] in AUDIO_EXTENSIONS]
|
|
if not audio_files:
|
|
self.file_list.clear()
|
|
self.file_list.append(urwid.AttrMap(urwid.Padding(urwid.Text("(empty)"), left=1, right=1), 'normal', 'selected'))
|
|
return
|
|
|
|
self.file_list.clear()
|
|
self.playlist = [os.path.join(self.current_dir, f) for f in audio_files]
|
|
self.playlist_index = 0
|
|
|
|
for file in audio_files:
|
|
padded_text = urwid.Padding(urwid.Text(file), left=1, right=1)
|
|
self.file_list.append(urwid.AttrMap(padded_text, 'audio_file', 'selected'))
|
|
|
|
self.set_focus(0)
|
|
self.play_media(self.playlist[self.playlist_index])
|
|
except PermissionError:
|
|
self.file_list.clear()
|
|
self.file_list.append(urwid.AttrMap(urwid.Padding(urwid.Text("(access denied)"), left=1, right=1), 'perm_denied', 'selected'))
|
|
|
|
def check_playback_end(self):
|
|
if self.main_loop is not None and self.playing and not pygame.mixer.music.get_busy() and not self.paused:
|
|
self.next_track()
|
|
if self.main_loop is not None:
|
|
self.main_loop.set_alarm_in(0.1, lambda loop, data: self.check_playback_end())
|
|
|
|
def next_track(self):
|
|
if self.playlist and self.playlist_index < len(self.playlist) - 1:
|
|
self.playlist_index += 1
|
|
self.set_focus(self.playlist_index)
|
|
self.play_media(self.playlist[self.playlist_index])
|
|
else:
|
|
pygame.mixer.music.stop()
|
|
self.playing = False
|
|
self.status_output.set_text(" Playlist ended")
|
|
self.metadata_output.set_text("")
|
|
|
|
def update_file_list(self):
|
|
AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma', 'opus'}
|
|
try:
|
|
all_files = sorted(os.listdir(self.current_dir))
|
|
files = [f for f in all_files
|
|
if not f.startswith('.') and
|
|
(os.path.isdir(os.path.join(self.current_dir, f)) or
|
|
f.lower().split('.')[-1] in AUDIO_EXTENSIONS)]
|
|
if not files:
|
|
files = ["(empty)"]
|
|
except PermissionError:
|
|
files = ["(access denied)"]
|
|
|
|
file_items = []
|
|
for file in files:
|
|
full_path = os.path.join(self.current_dir, file)
|
|
if os.path.isdir(full_path):
|
|
attr = 'directory'
|
|
display_name = file + "/"
|
|
elif os.path.isfile(full_path):
|
|
attr = 'audio_file'
|
|
display_name = file
|
|
else:
|
|
attr = 'normal'
|
|
display_name = file
|
|
padded_text = urwid.Padding(urwid.Text(display_name), left=1, right=1)
|
|
file_items.append(urwid.AttrMap(padded_text, attr, 'selected'))
|
|
|
|
return file_items
|
|
|
|
def refresh_list(self):
|
|
old_focus = self.focus_position if self.file_list else 0
|
|
self.file_list[:] = self.update_file_list()
|
|
self.path_text_inner.set_text([('path_value', self.current_dir)])
|
|
if self.file_list:
|
|
self.set_focus(min(old_focus, len(self.file_list) - 1))
|
|
|
|
def get_widget(self):
|
|
return self.widget
|
|
|
|
def cleanup(self):
|
|
if self.playing:
|
|
pygame.mixer.music.stop()
|
|
self.status_output.set_text("")
|
|
self.metadata_output.set_text("")
|
|
self.playing = False
|
|
self.paused = False
|
|
|
|
def show_message(self, message, duration=1):
|
|
if "Permission denied" in message:
|
|
self.status_output.set_text([('perm_denied', f" {message}")])
|
|
duration = 0
|
|
elif "not found" in message or "Error" in message:
|
|
self.status_output.set_text([('error', f" {message}")])
|
|
duration = 2
|
|
else:
|
|
self.status_output.set_text([('normal', f" {message}")])
|
|
self.main_loop.draw_screen()
|
|
if duration > 0:
|
|
def clear_message(loop, data):
|
|
if self.status_output.text in ([('perm_denied', f" {message}"), ('error', f" {message}"), ('normal', f" {message}")]):
|
|
self.status_output.set_text("")
|
|
self.main_loop.draw_screen()
|
|
self.main_loop.set_alarm_in(duration, clear_message)
|
|
|
|
def clear_message(self):
|
|
self.status_output.set_text("")
|
|
self.main_loop.draw_screen()
|
|
|
|
def get_metadata(self, filepath):
|
|
try:
|
|
audio = mutagen.File(filepath)
|
|
if audio is None:
|
|
return " No metadata available"
|
|
metadata = []
|
|
if hasattr(audio, 'info'):
|
|
metadata.append(f" Duration: {audio.info.length:.2f} sec")
|
|
metadata.append(f" Bitrate: {audio.info.bitrate // 1000} kbps")
|
|
metadata.append(f" Channels: {audio.info.channels}")
|
|
metadata.append(f" Sample Rate: {audio.info.sample_rate} Hz")
|
|
if audio.tags:
|
|
for key, value in audio.tags.items():
|
|
value_str = str(value)[:50] + "..." if len(str(value)) > 50 else str(value)
|
|
metadata.append(f" {key}: {value_str}")
|
|
|
|
max_lines = 10
|
|
if len(metadata) > max_lines:
|
|
metadata = metadata[:max_lines - 1] + [" ... (truncated)"]
|
|
return "\n".join(metadata) if metadata else " No metadata available"
|
|
except Exception as e:
|
|
return f" Error reading metadata: {e}"
|
|
|
|
def play_media(self, filepath):
|
|
if not os.path.exists(filepath):
|
|
self.show_message(f"File not found: {filepath}")
|
|
return
|
|
if not os.access(filepath, os.R_OK):
|
|
self.show_message("Permission denied!")
|
|
return
|
|
if self.playing:
|
|
pygame.mixer.music.stop()
|
|
try:
|
|
pygame.mixer.music.load(filepath)
|
|
pygame.mixer.music.set_volume(self.volume)
|
|
pygame.mixer.music.play()
|
|
self.playing = True
|
|
self.paused = False
|
|
self.status_output.set_text([("playing", f" Playing: {os.path.basename(filepath)}")])
|
|
self.metadata_output.set_text(self.get_metadata(filepath))
|
|
# filled = int(self.volume * 40)
|
|
# self.volume_bar.set_text(f" {int(self.volume * 100)}% {'░' * filled + ' ' * (40 - filled)}")
|
|
filled = int(self.volume * 44)
|
|
self.volume_bar.set_text(f" {int(self.volume * 100)}% | {'░' * filled + ' ' * (44 - filled)}")
|
|
audio = mutagen.File(filepath)
|
|
if audio and hasattr(audio, 'info'):
|
|
self.current_audio_duration = audio.info.length
|
|
else:
|
|
sound = pygame.mixer.Sound(filepath)
|
|
self.current_audio_duration = sound.get_length()
|
|
except Exception as e:
|
|
self.show_message(f"Error playing media: {str(e)}")
|
|
|
|
def keypress(self, size, key):
|
|
current_message = self.status_output.text
|
|
is_perm_denied = isinstance(current_message, list) and len(current_message) > 0 and "Permission denied" in current_message[0][1]
|
|
|
|
help_text = [
|
|
('normal,bold', ' left'), ('path_value', ' - Go to parent directory.\n'),
|
|
('normal,bold', ' right'), ('path_value', ' - Go back in directory history.\n'),
|
|
('normal,bold', ' up'), ('path_value', ' - Move focus up in file list.\n'),
|
|
('normal,bold', ' down'), ('path_value', ' - Move focus down in file list.\n'),
|
|
('normal,bold', ' enter'), ('path_value', ' - Open folder or play file.\n'),
|
|
('normal,bold', ' space'), ('path_value', ' - Play directory as playlist.\n'),
|
|
('normal,bold', ' + -'), ('path_value', ' - Increase/Decrease volume (pygame).\n'),
|
|
('normal,bold', ' a'), ('path_value', ' - Увеличить громкость правого наушника\n'),
|
|
('normal,bold', ' b'), ('path_value', ' - Уменьшить громкость правого наушника\n'),
|
|
('normal,bold', ' c'), ('path_value', ' - Увеличить громкость левого наушника\n'),
|
|
('normal,bold', ' d'), ('path_value', ' - Decrease system volume.\n'),
|
|
('normal,bold', ' e'), ('path_value', ' - Увеличить громкость обоих наушников\n'),
|
|
('normal,bold', ' f'), ('path_value', ' - Уменьшить громкость обоих наушников\n'),
|
|
('normal,bold', ' g'), ('path_value', ' - Уменьшить громкость левого наушника\n'),
|
|
('normal,bold', ' p'), ('path_value', ' - Pause or resume playback.\n'),
|
|
('normal,bold', ' s'), ('path_value', ' - Stop playback.\n'),
|
|
('normal,bold', ' r'), ('path_value', ' - Restart current track.\n'),
|
|
('normal,bold', ' i'), ('path_value', ' - Increase system volume.\n'),
|
|
('normal,bold', ' n'), ('path_value', ' - Next track.\n'),
|
|
('normal,bold', ' q or Q'), ('path_value', ' - Quit program.\n'),
|
|
('normal,bold', ' h'), ('path_value', ' - Show help.')
|
|
]
|
|
|
|
if key == 'h' and not self.playing and not self.paused:
|
|
self.metadata_output.set_text(help_text)
|
|
self.main_loop.draw_screen()
|
|
elif key != 'h':
|
|
if self.metadata_output.text == help_text:
|
|
self.metadata_output.set_text("")
|
|
self.main_loop.draw_screen()
|
|
|
|
if key == 'left':
|
|
if self.current_dir != "/":
|
|
try:
|
|
self.dir_history.append(self.current_dir)
|
|
os.chdir("..")
|
|
self.current_dir = os.getcwd()
|
|
self.refresh_list()
|
|
self.clear_message()
|
|
self.main_loop.draw_screen()
|
|
except PermissionError:
|
|
self.show_message("Permission denied!")
|
|
elif key == 'right':
|
|
if self.dir_history:
|
|
try:
|
|
os.chdir(self.dir_history.pop())
|
|
self.current_dir = os.getcwd()
|
|
self.refresh_list()
|
|
self.clear_message()
|
|
self.main_loop.draw_screen()
|
|
except PermissionError:
|
|
self.show_message("Permission denied!")
|
|
elif key == 'up' and self.focus_position > 0:
|
|
self.set_focus(self.focus_position - 1)
|
|
if not is_perm_denied:
|
|
self.clear_message()
|
|
elif key == 'down' and self.focus_position < len(self.file_list) - 1:
|
|
self.set_focus(self.focus_position + 1)
|
|
if not is_perm_denied:
|
|
self.clear_message()
|
|
elif key == 'enter':
|
|
if not self.file_list or self.focus.original_widget.original_widget.text.strip() in ["(empty)", "(access denied)"]:
|
|
return
|
|
selected = self.focus.original_widget.original_widget.text.rstrip('/')
|
|
full_path = os.path.join(self.current_dir, selected)
|
|
try:
|
|
if os.path.isdir(full_path):
|
|
self.dir_history.append(self.current_dir)
|
|
os.chdir(full_path)
|
|
self.current_dir = os.getcwd()
|
|
self.refresh_list()
|
|
self.clear_message()
|
|
self.main_loop.draw_screen()
|
|
elif os.path.isfile(full_path):
|
|
self.play_media(full_path)
|
|
self.playlist = [full_path]
|
|
self.playlist_index = 0
|
|
except Exception as e:
|
|
self.show_message(f"Error: {str(e)}")
|
|
elif key == ' ':
|
|
if self.playing or self.paused:
|
|
pygame.mixer.music.stop()
|
|
self.playing = False
|
|
self.paused = False
|
|
self.file_list.clear()
|
|
self.load_and_play_directory(self.current_dir)
|
|
self.check_playback_end()
|
|
self.main_loop.draw_screen()
|
|
elif key == 'p':
|
|
if self.playing:
|
|
if self.paused:
|
|
pygame.mixer.music.unpause()
|
|
self.paused = False
|
|
filepath = os.path.join(self.current_dir, self.focus.original_widget.original_widget.text.rstrip('/'))
|
|
self.status_output.set_text([("playing", f" Resumed: {os.path.basename(filepath)}")])
|
|
else:
|
|
pygame.mixer.music.pause()
|
|
self.paused = True
|
|
self.status_output.set_text(" Paused")
|
|
elif key == 's':
|
|
if self.playing:
|
|
pygame.mixer.music.stop()
|
|
self.playing = False
|
|
self.paused = False
|
|
self.status_output.set_text(" Stopped")
|
|
self.metadata_output.set_text("")
|
|
elif key == 'r':
|
|
if self.playing or self.paused:
|
|
filepath = os.path.join(self.current_dir, self.focus.original_widget.original_widget.text.rstrip('/'))
|
|
pygame.mixer.music.stop()
|
|
pygame.mixer.music.load(filepath)
|
|
pygame.mixer.music.set_volume(self.volume)
|
|
pygame.mixer.music.play()
|
|
self.playing = True
|
|
self.paused = False
|
|
self.status_output.set_text([("playing", f" Replaying: {os.path.basename(filepath)}")])
|
|
self.metadata_output.set_text(self.get_metadata(filepath))
|
|
elif key == '+':
|
|
self.volume = min(1.0, self.volume + 0.03)
|
|
if self.playing:
|
|
pygame.mixer.music.set_volume(self.volume)
|
|
# filled = int(self.volume * 34)
|
|
filled = int(self.volume * 44)
|
|
# self.volume_bar.set_text(f" {int(self.volume * 100)}% {'░' * filled + ' ' * (34 - filled)}")
|
|
self.volume_bar.set_text(f" {int(self.volume * 100)}% | {'░' * filled + ' ' * (44 - filled)}")
|
|
self.main_loop.draw_screen()
|
|
elif key == '-':
|
|
self.volume = max(0.0, self.volume - 0.03)
|
|
if self.playing:
|
|
pygame.mixer.music.set_volume(self.volume)
|
|
# filled = int(self.volume * 34)
|
|
filled = int(self.volume * 44)
|
|
# self.volume_bar.set_text(f" {int(self.volume * 100)}% {'░' * filled + ' ' * (34 - filled)}")
|
|
self.volume_bar.set_text(f" {int(self.volume * 100)}% | {'░' * filled + ' ' * (44 - filled)}")
|
|
self.main_loop.draw_screen()
|
|
elif key == 'i':
|
|
try:
|
|
result = subprocess.check_output("amixer set Master 3%+ | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
|
|
percent = int(result.rstrip('%'))
|
|
filled = min(34, percent // 3)
|
|
self.system_volume_bar.set_text(f" {percent}% {'░' * filled + ' ' * (34 - filled)}")
|
|
self.main_loop.draw_screen()
|
|
except subprocess.CalledProcessError as e:
|
|
self.show_message(f"Error adjusting system volume: {e}")
|
|
elif key == 'd':
|
|
try:
|
|
result = subprocess.check_output("amixer set Master 3%- | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
|
|
percent = int(result.rstrip('%'))
|
|
filled = min(34, percent // 3)
|
|
self.system_volume_bar.set_text(f" {percent}% {'░' * filled + ' ' * (34 - filled)}")
|
|
self.main_loop.draw_screen()
|
|
except subprocess.CalledProcessError as e:
|
|
self.show_message(f"Error adjusting system volume: {e}")
|
|
elif key == 'a':
|
|
try:
|
|
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()
|
|
percent = int(result.rstrip('%'))
|
|
filled = min(29, percent // 3)
|
|
self.headphone_right_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (29 - filled)}")
|
|
# self.status_output.set_text(f"Headphone Front Right: {result}")
|
|
self.main_loop.draw_screen()
|
|
except subprocess.CalledProcessError as e:
|
|
pass # Пустой оператор, чтобы блок не был пустым
|
|
# self.show_message(f"Error adjusting headphone volume: {e}")
|
|
elif key == 'b':
|
|
try:
|
|
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()
|
|
percent = int(result.rstrip('%'))
|
|
filled = min(29, percent // 3)
|
|
self.headphone_right_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (29 - filled)}")
|
|
# self.status_output.set_text(f"Headphone Front Right: {result}")
|
|
self.main_loop.draw_screen()
|
|
except subprocess.CalledProcessError as e:
|
|
self.show_message(f"Error adjusting headphone volume: {e}")
|
|
elif key == 'c':
|
|
try:
|
|
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()
|
|
percent = int(result.rstrip('%'))
|
|
filled = min(29, percent // 3)
|
|
self.headphone_left_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (29 - filled)}")
|
|
self.status_output.set_text(f"Headphone Front Left: {result}")
|
|
self.main_loop.draw_screen()
|
|
except subprocess.CalledProcessError as e:
|
|
self.show_message(f"Error adjusting headphone volume: {e}")
|
|
elif key == 'g':
|
|
try:
|
|
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()
|
|
percent = int(result.rstrip('%'))
|
|
filled = min(29, percent // 3)
|
|
self.headphone_left_bar.set_text(f" {percent}% | {'░' * filled + ' ' * (29 - filled)}")
|
|
self.status_output.set_text(f"Headphone Front Left: {result}")
|
|
self.main_loop.draw_screen()
|
|
except subprocess.CalledProcessError as e:
|
|
self.show_message(f"Error adjusting headphone volume: {e}")
|
|
elif key == 'e':
|
|
try:
|
|
# Увеличиваем громкость обоих наушников
|
|
subprocess.check_output("amixer sset 'Headphone' 5%+ -q", shell=True, text=True)
|
|
# Получаем значение для левого наушника
|
|
left_result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Left' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
|
|
left_percent = int(left_result.rstrip('%'))
|
|
left_filled = min(29, left_percent // 3)
|
|
self.headphone_left_bar.set_text(f" {left_percent}% | {'░' * left_filled + ' ' * (29 - left_filled)}")
|
|
# Получаем значение для правого наушника
|
|
right_result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Right' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
|
|
right_percent = int(right_result.rstrip('%'))
|
|
right_filled = min(29, right_percent // 3)
|
|
self.headphone_right_bar.set_text(f" {right_percent}% | {'░' * right_filled + ' ' * (29 - right_filled)}")
|
|
self.status_output.set_text(f"Headphone Left Right: {left_result} {right_result}")
|
|
self.main_loop.draw_screen()
|
|
except subprocess.CalledProcessError as e:
|
|
self.show_message(f"Error adjusting headphone volume: {e}")
|
|
elif key == 'f':
|
|
try:
|
|
# Уменьшаем громкость обоих наушников
|
|
subprocess.check_output("amixer sset 'Headphone' 5%- -q", shell=True, text=True)
|
|
# Получаем значение для левого наушника
|
|
left_result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Left' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
|
|
left_percent = int(left_result.rstrip('%'))
|
|
left_filled = min(29, left_percent // 3)
|
|
self.headphone_left_bar.set_text(f" {left_percent}% | {'░' * left_filled + ' ' * (29 - left_filled)}")
|
|
# Получаем значение для правого наушника
|
|
right_result = subprocess.check_output("amixer sget 'Headphone' | grep 'Front Right' | grep -o '[0-9]\+%' | head -1", shell=True, text=True).strip()
|
|
right_percent = int(right_result.rstrip('%'))
|
|
right_filled = min(29, right_percent // 3)
|
|
self.headphone_right_bar.set_text(f" {right_percent}% | {'░' * right_filled + ' ' * (29 - right_filled)}")
|
|
self.status_output.set_text(f"Headphone Left Right: {left_result} {right_result}")
|
|
self.main_loop.draw_screen()
|
|
except subprocess.CalledProcessError as e:
|
|
self.show_message(f"Error adjusting headphone volume: {e}")
|
|
elif key == 'n':
|
|
self.next_track()
|
|
elif key in ('q', 'Q'):
|
|
self.cleanup()
|
|
return 'q'
|
|
else:
|
|
super().keypress(size, key)
|
|
return key
|
|
|
|
def handle_input(self, key):
|
|
if isinstance(key, str):
|
|
return key
|
|
return None
|
|
|
|
class FileManager:
|
|
def __init__(self, input_path=None):
|
|
self.main_loop = None
|
|
self.root_dir = os.path.dirname(os.path.abspath(__file__))
|
|
self.mode = PlaybackMode(None, self.root_dir, input_path)
|
|
initial_widget = self.wrap_mode_widget(self.mode.get_widget())
|
|
self.frame = urwid.Frame(body=initial_widget)
|
|
|
|
def wrap_mode_widget(self, widget):
|
|
framed_widget = urwid.LineBox(
|
|
widget, title="grnManagerTerm", title_align='center',
|
|
tlcorner='╔', tline='═', trcorner='╗',
|
|
lline='║', rline='║',
|
|
blcorner='╚', bline='═', brcorner='╝'
|
|
)
|
|
return urwid.AttrMap(framed_widget, 'header')
|
|
|
|
def unhandled_input(self, key):
|
|
mode_key = self.mode.handle_input(key)
|
|
if mode_key == 'q':
|
|
self.mode.cleanup()
|
|
raise urwid.ExitMainLoop()
|
|
|
|
def run(self):
|
|
os.system('clear')
|
|
self.main_loop = urwid.MainLoop(self.frame, palette=palette, unhandled_input=self.unhandled_input)
|
|
self.mode.main_loop = self.main_loop
|
|
self.mode.start()
|
|
self.mode.check_playback_end()
|
|
self.main_loop.set_alarm_in(0.1, self.mode.update_clock)
|
|
self.main_loop.set_alarm_in(0.1, self.mode.update_progress_bar)
|
|
try:
|
|
self.main_loop.run()
|
|
finally:
|
|
os.system('stty sane')
|
|
os.system('clear')
|
|
|
|
if __name__ == "__main__":
|
|
input_path = None
|
|
if len(sys.argv) > 1:
|
|
input_path = sys.argv[1]
|
|
fm = FileManager(input_path)
|
|
fm.run()
|