22
33"""Base classes to be used by clid"""
44
5+ import os
56import curses
67
8+ import stagger
79import npyscreen as npy
810
11+ from . import _const
12+
913class 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
103107class 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
0 commit comments