f976ce616ec399d70cfb6b1f7231776162e3ea33
[minwii.git] / src / minwii / widgets / songfilebrowser.py
1 # -*- coding: utf-8 -*-
2 """
3 Boîte de dialogue pour sélection des chansons.
4
5 $Id$
6 $URL$
7 """
8
9 from pgu.gui import FileDialog
10 import pgu.gui.basic as basic
11 import pgu.gui.input as input
12 import pgu.gui.button as button
13 import pgu.gui.pguglobals as pguglobals
14 import pgu.gui.table as table
15 import pgu.gui.area as area
16 from pgu.gui.const import *
17 from pgu.gui.dialog import Dialog
18
19 import os
20 import tempfile
21 from xml.etree import ElementTree
22 from minwii.musicxml import musicXml2Song
23
24 INDEX_TXT = 'index.txt'
25
26 class FileOpenDialog(FileDialog):
27
28
29
30 def __init__(self, path):
31 cls1 = 'filedialog'
32 if not path: self.curdir = os.getcwd()
33 else: self.curdir = path
34 self.dir_img = basic.Image(
35 pguglobals.app.theme.get(cls1+'.folder', '', 'image'))
36 td_style = {'padding_left': 4,
37 'padding_right': 4,
38 'padding_top': 2,
39 'padding_bottom': 2}
40 self.title = basic.Label("Ouvrir un chanson", cls="dialog.title.label")
41 self.body = table.Table()
42 self.list = area.List(width=700, height=250)
43 self.input_dir = input.Input()
44 self.input_file = input.Input()
45 self._current_sort = 'alpha'
46 self._list_dir_()
47 self.button_ok = button.Button("Ouvrir")
48 self.button_sort_alpha = button.Button("A-Z")
49 self.button_sort_alpha.connect(CLICK, self._set_current_sort_, 'alpha')
50 self.button_sort_num = button.Button("0-9")
51 self.button_sort_num.connect(CLICK, self._set_current_sort_, 'num')
52 self.body.tr()
53 self.body.td(basic.Label("Dossier"), style=td_style, align=-1)
54 self.body.td(self.input_dir, style=td_style)
55 self.body.td(self.button_sort_alpha)
56 self.body.td(self.button_sort_num)
57 self.body.tr()
58 self.body.td(self.list, colspan=4, style=td_style)
59 self.list.connect(CHANGE, self._item_select_changed_, None)
60 self.button_ok.connect(CLICK, self._button_okay_clicked_, None)
61 self.body.tr()
62 self.body.td(basic.Label("Fichier"), style=td_style, align=-1)
63 self.body.td(self.input_file, style=td_style)
64 self.body.td(self.button_ok, style=td_style, colspan=2)
65 self.value = None
66 Dialog.__init__(self, self.title, self.body)
67
68 # FileDialog.__init__(self,
69 # title_txt="Ouvrir une chanson",
70 # button_txt="Ouvrir",
71 # path=path,
72 # )
73 # self.list.style.width = 700
74 # self.list.style.height = 250
75
76 def _list_dir_(self):
77 self.input_dir.value = self.curdir
78 self.input_dir.pos = len(self.curdir)
79 self.input_dir.vpos = 0
80 dirs = []
81 files = []
82 try:
83 for i in os.listdir(self.curdir):
84 if i.startswith('.') : continue
85 if os.path.isdir(os.path.join(self.curdir, i)): dirs.append(i)
86 else: files.append(i)
87 except:
88 self.input_file.value = "Dossier innacessible !"
89
90 dirs.sort()
91 dirs.insert(0, '..')
92
93 files.sort()
94 for i in dirs:
95 self.list.add(i, image=self.dir_img, value=i)
96
97 xmlFiles = []
98 for i in files:
99 if not i.endswith('.xml') :
100 continue
101 filepath = os.path.join(self.curdir, i)
102 xmlFiles.append(filepath)
103 # self.list.add(FileOpenDialog.getSongTitle(filepath), value=i)
104
105 if xmlFiles :
106 printableLines = self.getPrintableLines(xmlFiles)
107 for l in printableLines :
108 self.list.add(l[0], value = l[1])
109
110 self.list.set_vertical_scroll(0)
111
112 def getPrintableLines(self, xmlFiles) :
113 index = self.getUpdatedIndex(xmlFiles)
114
115 printableLines = []
116 for l in index :
117 l = l.strip()
118 l = l.split('\t')
119 printableLines.append(('%s - %s / %s' % (l[2], l[3], l[4]), l[0]))
120
121 return printableLines
122
123
124 @staticmethod
125 def getSongTitle(file) :
126 it = ElementTree.iterparse(file, ['start', 'end'])
127 creditFound = False
128 title = os.path.basename(file)
129
130 for evt, el in it :
131 if el.tag == 'credit' :
132 creditFound = True
133 if el.tag == 'credit-words' and creditFound:
134 title = el.text
135 break
136 if el.tag == 'part-list' :
137 # au delà de ce tag : aucune chance de trouver un titre
138 break
139 return title
140
141 @staticmethod
142 def getSongMetadata(file) :
143 metadata = {}
144 metadata['title'] = FileOpenDialog.getSongTitle(file).encode('iso-8859-1')
145 metadata['mtime'] = str(os.stat(file).st_mtime)
146 metadata['file'] = os.path.basename(file)
147 song = musicXml2Song(file)
148 metadata['distinctNotes'] = len(song.distinctNotes)
149
150 histo = song.intervalsHistogram
151 coeffInter = reduce(lambda a, b : a + b,
152 [abs(k) * v for k, v in histo.items()])
153
154 totInter = reduce(lambda a, b: a+b, histo.values())
155 totInter = totInter - histo.get(0, 0)
156 difficulty = int(round(float(coeffInter) / totInter, 0))
157 metadata['difficulty'] = difficulty
158
159 return metadata
160
161 def getUpdatedIndex(self, xmlFiles) :
162 indexTxtPath = os.path.join(self.curdir, INDEX_TXT)
163 index = []
164
165 if not os.path.exists(indexTxtPath) :
166 musicXmlFound = False
167 tmp = tempfile.TemporaryFile(mode='r+')
168 for file in xmlFiles :
169 try :
170 metadata = FileOpenDialog.getSongMetadata(file)
171 musicXmlFound = True
172 except ValueError, e :
173 print e
174 if e.args and e.args[0] == 'not a musicxml file' :
175 continue
176
177 line = '%(file)s\t%(mtime)s\t%(title)s\t%(distinctNotes)d\t%(difficulty)d\n' % metadata
178 index.append(line)
179 tmp.write(line)
180
181 if musicXmlFound :
182 tmp.seek(0)
183 indexFile = open(indexTxtPath, 'w')
184 indexFile.write(tmp.read())
185 indexFile.close()
186 tmp.close()
187 else :
188 indexedFiles = {}
189 indexTxt = open(indexTxtPath, 'r')
190
191 # check if index is up to date, and update entries if so.
192 for l in filter(None, indexTxt.readlines()) :
193 parts = l.split('\t')
194 fileBaseName, modificationTime = parts[0], parts[1]
195 filePath = os.path.join(self.curdir, fileBaseName)
196
197 if not os.path.exists(filePath) :
198 continue
199
200 indexedFiles[fileBaseName] = l
201 currentMtime = str(os.stat(filePath).st_mtime)
202
203 # check modification time missmatch
204 if currentMtime != modificationTime :
205 try :
206 metadata = FileOpenDialog.getSongMetadata(filePath)
207 musicXmlFound = True
208 except ValueError, e :
209 print e
210 if e.args and e.args[0] == 'not a musicxml file' :
211 continue
212
213 metadata = FileOpenDialog.getSongMetadata(filePath)
214 line = '%(file)s\t%(mtime)s\t%(title)s\t%(distinctNotes)d\t%(difficulty)d\n' % metadata
215 indexedFiles[fileBaseName] = line
216
217 # check for new files.
218 for file in xmlFiles :
219 fileBaseName = os.path.basename(file)
220 if not indexedFiles.has_key(fileBaseName) :
221 try :
222 metadata = FileOpenDialog.getSongMetadata(filePath)
223 musicXmlFound = True
224 except ValueError, e :
225 print e
226 if e.args and e.args[0] == 'not a musicxml file' :
227 continue
228
229 metadata = FileOpenDialog.getSongMetadata(file)
230 line = '%(file)s\t%(mtime)s\t%(title)s\t%(distinctNotes)d\t%(difficulty)d\n' % metadata
231 indexedFiles[fileBaseName] = line
232
233 # ok, the index is up to date !
234
235 index = indexedFiles.values()
236
237
238 if self._current_sort == 'alpha' :
239 def s(a, b) :
240 da = desacc(a.split('\t')[2]).lower()
241 db = desacc(b.split('\t')[2]).lower()
242 return cmp(da, db)
243
244 elif self._current_sort == 'num' :
245 def s(a, b) :
246 da = int(a.split('\t')[3])
247 db = int(b.split('\t')[3])
248 return cmp(da, db)
249 else :
250 s = cmp
251
252 index.sort(s)
253 return index
254
255 def _set_current_sort_(self, arg) :
256 self._current_sort = arg
257 self.list.clear()
258 self._list_dir_()
259
260 # utils
261 from unicodedata import decomposition
262 from string import printable
263 _printable = dict([(c, True) for c in printable])
264 isPrintable = _printable.has_key
265
266 def _recurseDecomposition(uc):
267 deco = decomposition(uc).split()
268 fullDeco = []
269 if deco :
270 while (deco) :
271 code = deco.pop()
272 if code.startswith('<') :
273 continue
274 c = unichr(int(code, 16))
275 subDeco = decomposition(c).split()
276 if subDeco :
277 deco.extend(subDeco)
278 else :
279 fullDeco.append(c)
280 fullDeco.reverse()
281 else :
282 fullDeco.append(uc)
283
284 fullDeco = u''.join(filter(lambda c : isPrintable(c), fullDeco))
285 return fullDeco
286
287 def desacc(s) :
288 us = s.decode('utf-8', 'ignore')
289 ret = []
290 for uc in us :
291 ret.append(_recurseDecomposition(uc))
292 return u''.join(ret)