|
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
|
|
|
|