Skip to content

Commit 12d55fd

Browse files
committed
Merge branch 'batch_tagging'
2 parents 1722dad + 5980f99 commit 12d55fd

File tree

6 files changed

+328
-192
lines changed

6 files changed

+328
-192
lines changed

TODO.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ TODO & BUGS
33

44
BUGS
55
----
6-
#1 [ ] Status bar doesn't change preview at top 2 tracks when `ArrowUp` or `PageUp` is pressed.
6+
#1 [x] Status bar doesn't change preview at top 2 tracks when `ArrowUp` or `PageUp` is pressed.
77
#2 [ ] Track number is shown as `0` in preview if track number is not set for the track.
8+
#3 [ ] Status line doesn't change when switching dirs

clid/_const.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,26 @@
213213
}
214214

215215
FORMAT_PAT = re.compile(r'%.')
216+
217+
TAG_FIELDS = {
218+
'tit': 'title',
219+
'alb': 'album',
220+
'gen': 'genre',
221+
'tno': 'track',
222+
'art': 'artist',
223+
'com': 'comment',
224+
'ala': 'album_artist'
225+
}
226+
227+
228+
DATE_PATTERN = re.compile(r"""(?x)\s*
229+
((?P<year>[0-9]{4}) # YYYY
230+
(-(?P<month>[01][0-9]) # -MM
231+
(-(?P<day>[0-3][0-9]) # -DD
232+
)?)?)?
233+
[ T]?
234+
((?P<hour>[0-2][0-9]) # HH
235+
(:(?P<min>[0-6][0-9]) # :MM
236+
(:(?P<sec>[0-6][0-9]) # :SS
237+
)?)?)?\s*
238+
""")

clid/base.py

Lines changed: 143 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
"""Base classes to be used by clid"""
44

5+
import os
56
import curses
67

8+
import stagger
79
import npyscreen as npy
810

11+
from . import _const
12+
913
class ClidActionController(npy.ActionControllerSimple):
1014
"""Base class for the command line at the bottom of the screen"""
1115

@@ -30,10 +34,10 @@ def set_up_handlers(self):
3034
self.handlers[curses.KEY_END] = self.h_end
3135
self.handlers[curses.KEY_HOME] = self.h_home
3236

33-
def h_home(self, input):
37+
def h_home(self, char):
3438
self.cursor_position = 0
3539

36-
def h_end(self, input):
40+
def h_end(self, char):
3741
self.cursor_position = len(self.value)
3842

3943

@@ -75,30 +79,30 @@ def set_up_handlers(self):
7579
self.vim_add_handlers()
7680
self.handlers[curses.ascii.ESC] = self.h_vim_normal_mode # is a bit slow
7781

78-
def h_addch(self, input):
82+
def h_addch(self, char):
7983
if self.parent.in_insert_mode: # add characters only if in insert mode
80-
super().h_addch(input)
84+
super().h_addch(char)
8185

82-
def h_vim_insert_mode(self, input):
86+
def h_vim_insert_mode(self, char):
8387
"""Enter insert mode"""
8488
self.parent.in_insert_mode = True
8589
self.vim_remove_handlers() # else `k`, j`, etc will not be added to text(will still act as keybindings)
8690

87-
def h_vim_normal_mode(self, input):
91+
def h_vim_normal_mode(self, char):
8892
"""Exit insert mode by pressing Esc"""
8993
self.parent.in_insert_mode = False
9094
self.cursor_position -= 1 # just like in vim
9195
self.vim_add_handlers() # removed earlier when going to insert mode
9296

93-
def h_vim_append_char(self, input):
97+
def h_vim_append_char(self, char):
9498
"""Append characters, like `a` in vim"""
95-
self.h_vim_insert_mode(input)
99+
self.h_vim_insert_mode(char)
96100
self.cursor_position += 1
97101

98-
def h_vim_append_char_at_end(self, input):
102+
def h_vim_append_char_at_end(self, char):
99103
"""Add characters to the end of the line, like `A` in vim"""
100-
self.h_vim_insert_mode(input)
101-
self.h_end(input) # go to the end
104+
self.h_vim_insert_mode(char)
105+
self.h_end(char) # go to the end
102106

103107
class ClidVimTitleText(npy.TitleText):
104108
_entry_type = ClidVimTextfield
@@ -122,3 +126,131 @@ class ClidCommandLine(npy.fmFormMuttActive.TextCommandBoxTraditional, ClidTextfi
122126
# # self.color = 'DEFAULT'
123127
# # self.show_bold = False
124128
pass
129+
130+
131+
class ClidEditMeta(npy.ActionFormV2):
132+
"""Edit the metadata of a track.
133+
134+
Attributes:
135+
files(list): List of files whose tags are being edited.
136+
_label_textbox(ClidTextfield):
137+
Text box which acts like a label(cannot be edited).
138+
_title_textbox(ClidTextfield):
139+
Text box with a title, to be used as input field for tags.
140+
in_insert_mode(bool):
141+
Used to decide whether the form is in insert/normal
142+
mode(if vi_keybindings are enabled). This is actually
143+
set as an attribute of the parent form so that all
144+
text boxes in the form are in the same mode.
145+
"""
146+
def __init__(self, *args, **kwags):
147+
super().__init__(*args, **kwags)
148+
self.handlers.update({
149+
'^S': self.h_ok,
150+
'^Q': self.h_cancel
151+
})
152+
self.in_insert_mode = False
153+
self.files = self.parentApp.current_files
154+
155+
def set_textbox(self):
156+
"""Set the text boxes to be used(with or without vim-bindings).
157+
Called by child classes.
158+
"""
159+
if self.parentApp.settings['vim_mode'] == 'true':
160+
self._title_textbox = ClidVimTitleText # vim keybindings if enabled
161+
self._label_textbox = ClidVimTextfield
162+
else:
163+
self._title_textbox = ClidTitleText
164+
self._label_textbox = ClidTextfield
165+
166+
def create(self):
167+
self.tit = self.add(self._title_textbox, name='Title')
168+
self.nextrely += 1
169+
self.alb = self.add(self._title_textbox, name='Album')
170+
self.nextrely += 1
171+
self.art = self.add(self._title_textbox, name='Artist')
172+
self.nextrely += 1
173+
self.ala = self.add(self._title_textbox, name='Album Artist')
174+
self.nextrely += 2
175+
self.gen = self.add(self._title_textbox, name='Genre')
176+
self.nextrely += 1
177+
self.dat = self.add(self._title_textbox, name='Date/Year')
178+
self.nextrely += 1
179+
self.tno = self.add(self._title_textbox, name='Track Number')
180+
self.nextrely += 2
181+
self.com = self.add(self._title_textbox, name='Comment')
182+
183+
def resolve_genre(self, num_gen):
184+
"""Convert numerical genre values to readable values. Genre may be
185+
saved as a str of the format '(int)' by applications like EasyTag.
186+
187+
Args:
188+
num_gen (str): str representing the genre.
189+
190+
Returns:
191+
str: Name of the genre (Electronic, Blues, etc). Returns
192+
num_gen itself if it doesn't match the format.
193+
"""
194+
match = _const.GENRE_PAT.findall(num_gen)
195+
196+
if match:
197+
try:
198+
return _const.GENRES[int(match[0])]
199+
except IndexError:
200+
return ''
201+
else:
202+
return num_gen
203+
204+
def h_ok(self, char):
205+
"""Handler to save the tags"""
206+
self.on_ok()
207+
208+
def h_cancel(self, char):
209+
"""Handler to cancel the operation"""
210+
self.on_cancel()
211+
212+
def on_cancel(self): # char is for handlers
213+
"""Switch to standard view at once without saving"""
214+
self.switch_to_main()
215+
216+
def switch_to_main(self):
217+
self.editing = False
218+
self.parentApp.switchForm("MAIN")
219+
220+
def get_fields_to_save(self):
221+
"""Return a modified version of _const.TAG_FIELDS. Only tag fields in
222+
returned dict will be saved to file; used by children
223+
"""
224+
pass
225+
226+
def on_ok(self): # char is for handlers
227+
"""Save and switch to standard view"""
228+
# date format check
229+
match = _const.DATE_PATTERN.match(self.dat.value)
230+
if match is None or match.end() != len(self.dat.value):
231+
npy.notify_confirm(message='Date should be of the form YYYY-MM-DD HH:MM:SS',
232+
title='Invalid Date Format', editw=1)
233+
return None
234+
# track number check
235+
track = str(self.tno.value) or '0' # automatically converted to int by stagger
236+
if not track.isnumeric():
237+
npy.notify_confirm(message='Track number can only take integer values',
238+
title='Invalid Track Number', editw=1)
239+
return None
240+
# FIXME: values of tags are reset to initial when ok is pressed(no prob with ^S)
241+
242+
main_form = self.parentApp.getForm("MAIN")
243+
tag_fields = self.get_fields_to_save().items()
244+
for mp3 in self.files:
245+
try:
246+
meta = stagger.read_tag(mp3)
247+
except stagger.NoTagError:
248+
meta = stagger.Tag23() # create an ID3v2.3 instance
249+
for tbox, field in tag_fields: # equivalent to `meta.title = self.tit.value`...
250+
tag = track if field == 'track' else getattr(self, tbox).value # get value to be written to file
251+
setattr(meta, field, tag)
252+
meta.write(mp3)
253+
# show the new tags in the status line
254+
main_form.wMain.set_status(filename=os.path.basename(mp3), force=True)
255+
256+
return True

clid/database.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class Mp3DataBase(npyscreen.NPSFilteredDataBase):
3131
list of format specifiers in pre_format
3232
meta_cache(dict):
3333
cache which holds the metadata of files as they are selected.
34-
34+
filename as key and metadata as key
3535
"""
3636
def __init__(self):
3737
super().__init__()
@@ -73,16 +73,35 @@ def load_files_and_set_values(self):
7373
self._values = tuple(sorted(self.file_dict.keys())) # sorted tuple of filenames
7474
self.meta_cache = dict()
7575

76+
def replace_file(self, old, new):
77+
"""Replace a filename with another one in _values, file_dict and meta_cache.
78+
Used externally when a file is renamed.
79+
80+
Args:
81+
old(str): abs path to old name of file
82+
new(str): abs path to new name of file
83+
"""
84+
del self.file_dict[os.path.basename(old)]
85+
self.file_dict[os.path.basename(new)] = new
86+
with open('sdf', 'w') as f:
87+
f.write(os.path.basename(new)+'\n'+new)
88+
89+
self._values = tuple(sorted(self.file_dict.keys()))
90+
91+
status_string = self.meta_cache[os.path.basename(old)]
92+
del self.meta_cache[os.path.basename(old)]
93+
self.meta_cache[os.path.basename(new)] = status_string
7694

77-
def parse_meta_for_status(self, filename):
95+
def parse_meta_for_status(self, filename, force=False):
7896
"""Make a string like 'artist - album - track_number. title' from a filename
7997
(using file_dict and data[attributes])
8098
8199
Args:
82100
filename: the filename(*not* the absolute path)
101+
force: reconstruct the string even if it has already been made
83102
"""
84103
temp = self.pre_format # make a copy of format and replace specifiers with tags
85-
if not filename in self.meta_cache:
104+
if not filename in self.meta_cache or force:
86105
try:
87106
meta = stagger.read_tag(self.file_dict[filename])
88107
for spec in self.specifiers: # str to convert track number to str if given

0 commit comments

Comments
 (0)