1
|
|
|
"""Car Music Sorter.""" |
2
|
|
|
import gettext |
3
|
|
|
import os |
4
|
|
|
import re |
5
|
|
|
import shutil |
6
|
|
|
import w_settings |
7
|
|
|
|
8
|
|
|
from threading import Thread |
9
|
|
|
from typing import Union |
10
|
|
|
from sys import exit |
11
|
|
|
from tkinter import Tk, PhotoImage, Menu, LabelFrame |
12
|
|
|
from tkinter import Toplevel, messagebox |
13
|
|
|
from tkinter.ttk import Button, Label, Progressbar |
14
|
|
|
from pathlib import Path |
15
|
|
|
from f_getconfig import getconfig |
16
|
|
|
from f_logging import writelog |
17
|
|
|
|
18
|
|
|
import tkinter.filedialog as fd |
19
|
|
|
input_dir: str = '' |
20
|
|
|
output_dir: str = '' |
21
|
|
|
source_file = [] |
22
|
|
|
|
23
|
|
|
|
24
|
|
|
# BEGIN FUNCTIONS # |
25
|
|
|
# FIXME: Вынести по возможности в отдельные файлы |
26
|
|
|
# Определение исходной и целевой директорий |
27
|
|
|
def workdirs(param: str) -> None: |
28
|
|
|
"""Открывает диалог выбора директории.""" |
29
|
|
|
if param == 'indir': |
30
|
|
|
global input_dir |
31
|
|
|
input_dir = fd.askdirectory(title = _('Open source directory')) |
|
|
|
|
32
|
|
|
if input_dir != '': |
33
|
|
|
source_label.config(text = f'...{path_short(input_dir, 2)}') |
34
|
|
|
writelog(_('Input DIR set to: ') + input_dir) |
35
|
|
|
else: |
36
|
|
|
input_dir = '' |
37
|
|
|
elif param == 'outdir': |
38
|
|
|
global output_dir |
39
|
|
|
output_dir = fd.askdirectory(title = _('Set destination directory')) |
40
|
|
|
if output_dir != '': |
41
|
|
|
dest_label.config(text = f'...{path_short(output_dir, 2)}') |
42
|
|
|
writelog(_('Output DIR set to: ') + output_dir) |
43
|
|
|
else: |
44
|
|
|
output_dir = '' |
45
|
|
|
elif param == 'clear': |
46
|
|
|
source_label.config(text = _('Input DIR not defined')) |
47
|
|
|
dest_label.config(text = _('Output DIR not defined')) |
48
|
|
|
main_progressbar['value'] = 0 |
49
|
|
|
input_dir = output_dir = '' |
50
|
|
|
|
51
|
|
|
|
52
|
|
|
def path_short(path_string: str, len: int) -> Union[str, Path]: |
53
|
|
|
"""Сокращает путь для корректного отображения в лейбле.""" |
54
|
|
|
return Path(*Path(path_string).parts[-len:]) |
55
|
|
|
|
56
|
|
|
|
57
|
|
|
# Вызов "О программе" |
58
|
|
|
def popup_about(vers: str) -> None: |
59
|
|
|
"""Открывает окно 'О программе'.""" |
60
|
|
|
# Центровка окна |
61
|
|
|
main_width = 400 |
62
|
|
|
main_height = 150 |
63
|
|
|
center_x_pos = int(window.winfo_screenwidth() / 2) - main_width |
64
|
|
|
center_y_pos = int(window.winfo_screenheight() / 2) - main_height |
65
|
|
|
|
66
|
|
|
popup = Toplevel() |
67
|
|
|
popup.geometry(f'{main_width}x{main_height}+{center_x_pos}+{center_y_pos}') |
68
|
|
|
popup.title(_('About')) |
|
|
|
|
69
|
|
|
imagepath = 'data/imgs/main.png' |
70
|
|
|
img = PhotoImage(file = imagepath) |
71
|
|
|
poplabel1 = Label(popup, image = img) |
72
|
|
|
poplabel1.grid(sticky = 'W', column = 0, row = 0, rowspan = 2) |
73
|
|
|
|
74
|
|
|
name_vers_str = 'Car Music Sorter\n\n' + _('Version: ') + vers |
75
|
|
|
author_github = 'https://github.com/intervisionlord' |
76
|
|
|
prog_author = _('\nAuthor: ') + 'Intervision\nGithub: ' + author_github |
77
|
|
|
poplabel_maindesc = Label(popup, |
78
|
|
|
text = name_vers_str + prog_author, |
79
|
|
|
justify = 'left') |
80
|
|
|
poplabel_maindesc.grid(sticky = 'W', column = 1, row = 0) |
81
|
|
|
# Автор иконок |
82
|
|
|
icons_author = _('Icons: ') + 'icon king1 ' + _('on') + ' freeicons.io' |
83
|
|
|
poplabel_icons = Label(popup, text = icons_author, justify = 'left') |
84
|
|
|
poplabel_icons.grid(sticky = 'W', column = 1, row = 1) |
85
|
|
|
|
86
|
|
|
popup.grab_set() |
87
|
|
|
popup.focus_set() |
88
|
|
|
popup.wait_window() |
89
|
|
|
|
90
|
|
|
|
91
|
|
|
# Основные операции |
92
|
|
|
def check_paths() -> None: |
93
|
|
|
"""Проверяет, что все пути заданы корректно и запускает копирование.""" |
94
|
|
|
if input_dir == '' or output_dir == '': |
95
|
|
|
writelog(_('Input DIR or Output DIR are not defined!')) |
|
|
|
|
96
|
|
|
elif input_dir == output_dir: |
97
|
|
|
writelog(_('Input DIR and Output DIR must be different!')) |
98
|
|
|
return |
99
|
|
|
else: |
100
|
|
|
for path, subdirs, files in os.walk(input_dir): |
101
|
|
|
for file in files: |
102
|
|
|
# Перегоняем MP3 без лайвов и ремиксов в целевую директорию. |
103
|
|
|
filtered = re.search(r'^(?!(.*[Rr]emix.*|.*[Ll]ive.*)).*mp3', |
104
|
|
|
file) |
105
|
|
|
if filtered is not None: |
106
|
|
|
source_file.append(f'{path}/{filtered.group(0)}') |
107
|
|
|
main_progressbar['maximum'] = len(source_file) |
108
|
|
|
for files in source_file: |
109
|
|
|
maincopy(files, Path(output_dir)) |
110
|
|
|
source_file.clear() |
111
|
|
|
|
112
|
|
|
|
113
|
|
|
def proc_thread(): |
114
|
|
|
"""Запускает процесс в отдельном потоке.""" |
115
|
|
|
forked_thread = Thread(target = processing) |
116
|
|
|
forked_thread.start() |
117
|
|
|
|
118
|
|
|
|
119
|
|
|
def processing() -> None: |
120
|
|
|
"""Удаляет ремиксы и лайвы.""" |
121
|
|
|
check_paths() |
122
|
|
|
# Удаление ремиксов и лайвов |
123
|
|
|
# TODO: Depricated |
124
|
|
|
liveregexp = r'.*\(.*[Rr]emix.*\).*|.*\(.*[Ll]ive.*\).*' |
125
|
|
|
for files in os.walk(output_dir): |
126
|
|
|
for file in files[2]: |
127
|
|
|
try: |
128
|
|
|
source_file.append(re.search(liveregexp, file).group(0)) |
129
|
|
|
except Exception: |
130
|
|
|
pass |
131
|
|
|
for file in source_file: |
132
|
|
|
writelog(_('Removing Remix: ') + file) |
|
|
|
|
133
|
|
|
os.remove(f'{output_dir}/{file}') |
134
|
|
|
main_progressbar['value'] = main_progressbar['value'] + 1 |
|
|
|
|
135
|
|
|
window.update_idletasks() |
136
|
|
|
source_file.clear() # Очищаем список |
137
|
|
|
polish_filenames() |
138
|
|
|
|
139
|
|
|
|
140
|
|
|
def polish_filenames() -> None: |
141
|
|
|
"""Удаляет из имен треков мусор.""" |
142
|
|
|
# Готовим список свежепринесенных файлов с вычищенными ремиксами и лайвами |
143
|
|
|
for files in os.walk(output_dir): |
144
|
|
|
for file in files[2]: |
145
|
|
|
try: |
146
|
|
|
source_file.append(file) |
147
|
|
|
except Exception: |
148
|
|
|
pass |
149
|
|
|
|
150
|
|
|
# Убираем из имен файлов мусор (номера треков в различном формате) |
151
|
|
|
main_progressbar['maximum'] = (main_progressbar['maximum'] + |
|
|
|
|
152
|
|
|
len(source_file)) |
153
|
|
|
trashregexp = r'^[\d{1,2}\s\-\.]*' |
154
|
|
|
for file in source_file: |
155
|
|
|
new_file = re.sub(trashregexp, '', file) |
156
|
|
|
shutil.move(f'{output_dir}/{file}', f'{output_dir}/{new_file}') |
157
|
|
|
main_progressbar['value'] = main_progressbar['value'] + 1 |
158
|
|
|
window.update_idletasks() |
159
|
|
|
source_file.clear() |
160
|
|
|
writelog(_('Completed!')) |
|
|
|
|
161
|
|
|
messagebox.showinfo(_('Information'), |
162
|
|
|
_('Completed!')) |
163
|
|
|
|
164
|
|
|
|
165
|
|
|
# Копируем файлы |
166
|
|
|
def maincopy(files: list[str], output_dir: Path) -> None: |
167
|
|
|
"""Копирует файлы.""" |
168
|
|
|
writelog(f'{files}') |
169
|
|
|
filename = str.split(str(files), '/') |
170
|
|
|
writelog(filename[-1]) |
171
|
|
|
shutil.copyfile(f'{files}', f'{output_dir}/{filename[-1]}') |
172
|
|
|
main_progressbar['value'] = main_progressbar['value'] + 1 |
|
|
|
|
173
|
|
|
window.update_idletasks() |
174
|
|
|
# END FUNCTIONS # |
175
|
|
|
|
176
|
|
|
|
177
|
|
|
# Вводим основные переменные |
178
|
|
|
vers = getconfig()['core']['version'] |
179
|
|
|
langcode = getconfig()['settings']['locale'] |
180
|
|
|
# Локализация |
181
|
|
|
gettext.translation('CarMusicSorter', localedir='l10n', |
182
|
|
|
languages=[langcode]).install() |
183
|
|
|
writelog('init') |
184
|
|
|
# Рисуем окно |
185
|
|
|
window = Tk() |
186
|
|
|
window.iconphoto(True, PhotoImage(file = 'data/imgs/main.png')) |
187
|
|
|
window.geometry('370x270') |
188
|
|
|
window.eval('tk::PlaceWindow . center') |
189
|
|
|
window.title('Car Music Sorter') |
190
|
|
|
window.resizable(False, False) |
191
|
|
|
|
192
|
|
|
# Пути к оформлению |
193
|
|
|
sourceicon = PhotoImage(file = 'data/imgs/20source.png') |
194
|
|
|
desticon = PhotoImage(file = 'data/imgs/20dest.png') |
195
|
|
|
launchicon = PhotoImage(file = 'data/imgs/20ok.png') |
196
|
|
|
clearicon = PhotoImage(file = 'data/imgs/20clear.png') |
197
|
|
|
|
198
|
|
|
# Основное меню |
199
|
|
|
menu = Menu(window) |
200
|
|
|
menu_about = Menu(menu, tearoff = 0) |
201
|
|
|
menu_file = Menu(menu, tearoff = 0) |
202
|
|
|
menu.add_cascade(label = _('File'), menu = menu_file) |
|
|
|
|
203
|
|
|
menu.add_cascade(label = _('Info'), menu = menu_about) |
204
|
|
|
|
205
|
|
|
# Элементы меню |
206
|
|
|
menu_about.add_command(label = _('About'), |
207
|
|
|
command = lambda: popup_about(vers), |
208
|
|
|
accelerator = 'F1') |
209
|
|
|
menu_file.add_command(label = _('Input Dir'), |
210
|
|
|
command = lambda: workdirs('indir'), |
211
|
|
|
accelerator = 'CTRL+O') |
212
|
|
|
menu_file.add_command(label = _('Output Dir'), |
213
|
|
|
command = lambda: workdirs('outdirs'), |
214
|
|
|
accelerator = 'CTRL+D') |
215
|
|
|
menu_file.add_command(label = _('Clear'), |
216
|
|
|
command = lambda: workdirs('clear'), |
217
|
|
|
accelerator = 'CTRL+R') |
218
|
|
|
menu_file.add_separator() |
219
|
|
|
menu_file.add_command(label = _('Settings'), |
220
|
|
|
command = w_settings.popup_settings) |
221
|
|
|
menu_file.add_separator() |
222
|
|
|
menu_file.add_command(label = _('Exit'), |
223
|
|
|
command = exit, |
224
|
|
|
accelerator = 'CTRL+E') |
225
|
|
|
|
226
|
|
|
# Биндим хоткеи к функциям |
227
|
|
|
menu_file.bind_all('<Command-o>', lambda event: workdirs('indir')) |
228
|
|
|
menu_file.bind_all('<Command-d>', lambda event: workdirs('outdir')) |
229
|
|
|
menu_file.bind_all('<Command-r>', lambda event: workdirs('clear')) |
230
|
|
|
menu_file.bind_all('<Command-e>', exit) |
231
|
|
|
|
232
|
|
|
menu_about.bind_all('<F1>', lambda event: popup_about(vers)) |
233
|
|
|
window.config(menu = menu) |
234
|
|
|
|
235
|
|
|
# Строим элеметны основного окна и группы |
236
|
|
|
first_group = LabelFrame(window, text = _('IO Directories')) |
237
|
|
|
|
238
|
|
|
first_group.grid(sticky = 'WE', column = 0, row = 0, padx = 5, pady = 10, |
239
|
|
|
ipadx = 2, ipady = 4) |
240
|
|
|
|
241
|
|
|
operation_group = LabelFrame(window, text = _('Operations')) |
242
|
|
|
operation_group.grid(sticky = 'WE', column = 0, row = 3, padx = 5, pady = 5, |
243
|
|
|
ipadx = 5, ipady = 5) |
244
|
|
|
|
245
|
|
|
progress_group = LabelFrame(window, text = _('Progress')) |
246
|
|
|
progress_group.grid(sticky = 'WE', column = 0, row = 1, padx = 5, pady = 5, |
247
|
|
|
ipadx = 0, ipady = 2, rowspan = 2) |
248
|
|
|
|
249
|
|
|
# Прогрессбар |
250
|
|
|
main_progressbar = Progressbar(progress_group, length = 350, value = 0, |
251
|
|
|
orient = 'horizontal', mode = 'determinate') |
252
|
|
|
main_progressbar.grid(pady = 4, column = 0, row = 1) |
253
|
|
|
|
254
|
|
|
# Поясняющие лейблы |
255
|
|
|
source_label_text = _('Input DIR not defined') |
256
|
|
|
dest_label_text = _('Output DIR not defined') |
257
|
|
|
source_label = Label(first_group, text = source_label_text, justify = 'left') |
258
|
|
|
source_label.grid(column = 1, row = 0) |
259
|
|
|
dest_label = Label(first_group, text = dest_label_text, justify = 'left') |
260
|
|
|
dest_label.grid(column = 1, row = 1) |
261
|
|
|
|
262
|
|
|
# Кнопки |
263
|
|
|
source_button = Button(first_group, text = _('Input Dir'), |
264
|
|
|
command = lambda: workdirs('indir'), image = sourceicon, |
265
|
|
|
width = 20, compound = 'left') |
266
|
|
|
source_button.grid(row = 0, ipadx = 2, ipady = 2, padx = 4) |
267
|
|
|
|
268
|
|
|
dest_button = Button(first_group, text = _('Output Dir'), |
269
|
|
|
command = lambda: workdirs('outdir'), image = desticon, |
270
|
|
|
width = 20, compound = 'left') |
271
|
|
|
dest_button.grid(row = 1, ipadx = 2, ipady = 2, padx = 4) |
272
|
|
|
|
273
|
|
|
launch_button = Button(operation_group, text = _('Process'), |
274
|
|
|
command = proc_thread, image = launchicon, |
275
|
|
|
width = 20, compound = 'left') |
276
|
|
|
launch_button.grid(column = 0, row = 2, ipadx = 2, ipady = 2, padx = 12) |
277
|
|
|
|
278
|
|
|
clear_button = Button(operation_group, text = _('Clear'), |
279
|
|
|
command = lambda: workdirs('clear'), image = clearicon, |
280
|
|
|
width = 20, compound = 'left') |
281
|
|
|
|
282
|
|
|
clear_button.grid(column = 1, row = 2, ipadx = 2, ipady = 2, padx = 0) |
283
|
|
|
|
284
|
|
|
if __name__ == '__main__': |
285
|
|
|
window.mainloop() |
286
|
|
|
|