Skip to content

Commit 5980f99

Browse files
committed
batch tagging feature
1 parent 34ed807 commit 5980f99

File tree

5 files changed

+150
-191
lines changed

5 files changed

+150
-191
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/base.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

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

5+
import os
56
import curses
67

78
import stagger
@@ -132,7 +133,7 @@ class ClidEditMeta(npy.ActionFormV2):
132133
133134
Attributes:
134135
files(list): List of files whose tags are being edited.
135-
_label_textbox(ClidTextfield):
136+
_label_textbox(ClidTextfield):
136137
Text box which acts like a label(cannot be edited).
137138
_title_textbox(ClidTextfield):
138139
Text box with a title, to be used as input field for tags.
@@ -149,7 +150,7 @@ def __init__(self, *args, **kwags):
149150
'^Q': self.h_cancel
150151
})
151152
self.in_insert_mode = False
152-
self.files = self.parentApp.current_file
153+
self.files = self.parentApp.current_files
153154

154155
def set_textbox(self):
155156
"""Set the text boxes to be used(with or without vim-bindings).
@@ -163,7 +164,6 @@ def set_textbox(self):
163164
self._label_textbox = ClidTextfield
164165

165166
def create(self):
166-
self.set_textbox()
167167
self.tit = self.add(self._title_textbox, name='Title')
168168
self.nextrely += 1
169169
self.alb = self.add(self._title_textbox, name='Album')
@@ -211,36 +211,46 @@ def h_cancel(self, char):
211211

212212
def on_cancel(self): # char is for handlers
213213
"""Switch to standard view at once without saving"""
214+
self.switch_to_main()
215+
216+
def switch_to_main(self):
214217
self.editing = False
215218
self.parentApp.switchForm("MAIN")
216219

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+
217226
def on_ok(self): # char is for handlers
218227
"""Save and switch to standard view"""
219228
# date format check
220-
m = _const.DATE_PATTERN.match(self.dat.value)
221-
if m is None or m.end() != len(self.dat.value):
229+
match = _const.DATE_PATTERN.match(self.dat.value)
230+
if match is None or match.end() != len(self.dat.value):
222231
npy.notify_confirm(message='Date should be of the form YYYY-MM-DD HH:MM:SS',
223232
title='Invalid Date Format', editw=1)
224233
return None
225234
# track number check
226-
track = self.tno.value or '0' # automatically converted to int by stagger
235+
track = str(self.tno.value) or '0' # automatically converted to int by stagger
227236
if not track.isnumeric():
228237
npy.notify_confirm(message='Track number can only take integer values',
229238
title='Invalid Track Number', editw=1)
230239
return None
231240
# FIXME: values of tags are reset to initial when ok is pressed(no prob with ^S)
232241

242+
main_form = self.parentApp.getForm("MAIN")
243+
tag_fields = self.get_fields_to_save().items()
233244
for mp3 in self.files:
234-
meta = stagger.read_tag(mp3)
235-
for tbox, field in _const.TAG_FIELDS:
236-
# equivalent to `meta.title = self.tit.value`...
237-
tag = getattr(self, tbox).value # get value to be written to file
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
238251
setattr(meta, field, tag)
239-
meta.write()
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)
240255

241-
main_form = self.parentApp.getForm("MAIN")
242-
# show the new tags in the status line
243-
main_form.wMain.set_status(filename=main_form.wMain.get_selected(), force=True)
244-
245-
self.editing = False
246-
self.parentApp.switchForm("MAIN")
256+
return True

clid/database.py

Lines changed: 19 additions & 1 deletion
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,6 +73,24 @@ 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

7795
def parse_meta_for_status(self, filename, force=False):
7896
"""Make a string like 'artist - album - track_number. title' from a filename

clid/editmeta.py

Lines changed: 44 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -12,139 +12,57 @@
1212
from . import _const
1313

1414

15-
class EditMeta(npy.ActionFormV2):
16-
"""Edit the metadata of a track.
17-
18-
Attributes:
19-
in_insert_mode(bool):
20-
Used to decide whether the form is in insert/normal
21-
mode(if vi_keybindings are enabled). This is actually
22-
set as an attribute of the parent form so that all
23-
text boxes in the form are in the same mode.
24-
"""
25-
26-
def __init__(self, *args, **kwags):
27-
super().__init__(*args, **kwags)
28-
self.handlers.update({
29-
'^S': self.h_ok,
30-
'^Q': self.h_cancel
31-
})
32-
33-
self.in_insert_mode = False
34-
15+
class SingleEditMeta(base.ClidEditMeta):
16+
"""Edit the metadata of a *single* track."""
3517
def create(self):
36-
# error if placed in __init__
37-
self.TEXTBOX = base.ClidVimTitleText \
38-
if self.parentApp.settings['vim_mode'] == 'true'\
39-
else base.ClidTitleText # vim keybindings if enabled
40-
41-
self.file = self.parentApp.current_file
18+
self.set_textbox()
19+
file = self.parentApp.current_files[0]
4220
try:
43-
self.meta = stagger.read_tag(self.file)
44-
except stagger.errors.NoTagError:
45-
temp = stagger.Tag23() # create a id3v2.3 tag instance
46-
temp.album = ' ' # so that there is something to write to the file
47-
temp.write(self.parentApp.current_file)
48-
49-
self.meta = stagger.read_tag(self.parentApp.current_file)
50-
self.meta.album = '' # revert what was just done
51-
self.meta.write()
21+
meta = stagger.read_tag(file)
22+
except stagger.NoTagError:
23+
meta = stagger.Tag23() # create a id3v2.3 tag instance
5224

53-
self.filenamebox = self.add(self.TEXTBOX, name='Filename',
54-
value=os.path.basename(self.file).replace('.mp3', ''))
25+
self.filenamebox = self.add(self._title_textbox, name='Filename',
26+
value=os.path.basename(file).replace('.mp3', ''))
5527
self.nextrely += 2
28+
super().create()
5629

57-
self.tit = self.add(self.TEXTBOX, name='Title', value=self.meta.title)
58-
self.nextrely += 1
59-
self.alb = self.add(self.TEXTBOX, name='Album', value=self.meta.album)
60-
self.nextrely += 1
61-
self.art = self.add(self.TEXTBOX, name='Artist', value=self.meta.artist)
62-
self.nextrely += 1
63-
self.ala = self.add(self.TEXTBOX, name='Album Artist', value=self.meta.album_artist)
64-
self.nextrely += 2
65-
66-
self.gen = self.add(self.TEXTBOX, name='Genre', value=self.resolve_genre(self.meta.genre))
67-
self.nextrely += 1
68-
self.dat = self.add(self.TEXTBOX, name='Date/Year', value=self.meta.date)
69-
self.nextrely += 1
70-
self.tno = self.add(self.TEXTBOX, name='Track Number',
71-
value=str(self.meta.track if self.meta.track != 0 else ''))
72-
self.nextrely += 2
73-
self.com = self.add(self.TEXTBOX, name='Comment', value=self.meta.comment)
74-
75-
76-
def resolve_genre(self, num_gen):
77-
"""Convert numerical genre values to readable values. Genre may be
78-
saved as a str of the format '(int)' by applications like EasyTag.
79-
80-
Args:
81-
num_gen (str): str representing the genre.
82-
83-
Returns:
84-
str: Name of the genre (Electronic, Blues, etc). Returns
85-
num_gen itself if it doesn't match the format.
86-
"""
87-
match = _const.GENRE_PAT.findall(num_gen)
30+
for tbox, field in _const.TAG_FIELDS.items(): # show file's tag
31+
getattr(self, tbox).value = str(getattr(meta, field)) # str for track_number
8832

89-
if match:
90-
try:
91-
return _const.GENRES[int(match[0])]
92-
except IndexError:
93-
return ''
94-
else:
95-
return num_gen
33+
def get_fields_to_save(self):
34+
return _const.TAG_FIELDS
9635

97-
def h_ok(self, char):
98-
"""Handler to save the tags"""
99-
self.on_ok()
36+
def on_ok(self):
37+
c = super().on_ok()
38+
if c is None:
39+
return None # some error like invalid date or track number has occured
40+
mp3 = self.files[0]
41+
new_filename = os.path.dirname(mp3) + '/' + self.filenamebox.value + '.mp3'
42+
if mp3 != new_filename: # filename was changed
43+
os.rename(mp3, new_filename)
44+
main_form = self.parentApp.getForm("MAIN")
45+
main_form.value.replace_file(old=mp3, new=new_filename)
46+
main_form.load_files()
10047

101-
def h_cancel(self, char):
102-
"""Handler to cancel the operation"""
103-
self.on_cancel()
48+
self.switch_to_main()
10449

105-
def on_cancel(self): # char is for handlers
106-
"""Switch to standard view at once without saving"""
107-
self.editing = False
108-
self.parentApp.switchForm("MAIN")
10950

110-
def on_ok(self): # char is for handlers
111-
"""Save and switch to standard view"""
112-
try:
113-
self.meta.date = self.dat.value
114-
except ValueError:
115-
npy.notify_confirm(message='Date should be of the form YYYY-MM-DD',
116-
title='Invalid Date Format', editw=1)
117-
return None
118-
119-
track = self.tno.value if self.tno.value != '' else '0' # automatically converted to int by stagger
120-
try:
121-
int(track)
122-
except ValueError:
123-
npy.notify_confirm(message='Track number can only take integer values',
124-
title='Invalid Track Number', editw=1)
125-
return None
126-
else:
127-
self.meta.track = track
128-
# FIXME: values of tags are reset to initial when ok is pressed(no prob with ^S)
129-
130-
self.meta.title = self.tit.value
131-
self.meta.album = self.alb.value
132-
self.meta.genre = self.gen.value
133-
self.meta.artist = self.art.value
134-
self.meta.comment = self.com.value
135-
self.meta.album_artist = self.ala.value
136-
137-
self.meta.write()
138-
139-
new_filename = os.path.dirname(self.file) + '/' + self.filenamebox.value + '.mp3'
140-
if self.file != new_filename: # filename was changed
141-
os.rename(self.file, new_filename)
142-
143-
main_form = self.parentApp.getForm("MAIN")
144-
main_form.value.load_files_and_set_values()
145-
146-
main_form.wMain.set_status(filename=os.path.basename(new_filename)) # show the new tags in the status line
147-
main_form.load_files()
148-
149-
self.editing = False
150-
self.parentApp.switchForm("MAIN")
51+
class MultiEditMeta(base.ClidEditMeta):
52+
def create(self):
53+
self.set_textbox()
54+
self.add(npy.Textfield, color='STANDOUT', editable=False,
55+
value='Batch tagging {} files'.format(len(self.parentApp.current_files)))
56+
self.nextrely += 2
57+
super().create()
58+
59+
def get_fields_to_save(self):
60+
# save only those fields which are not empty, to files
61+
return {tbox: field for tbox, field in _const.TAG_FIELDS.items()\
62+
if getattr(self, tbox).value}
63+
64+
def on_ok(self):
65+
c = super().on_ok()
66+
if c is None:
67+
return None # some error like invalid date or track number has occured
68+
self.switch_to_main()

0 commit comments

Comments
 (0)