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