common: split MetadataFetcher into its own file
[nephilim.git] / nephilim / plugins / Library.py
blob3ff62422861359cc48a23a966a1d9ee9a9e1e341
2 # Copyright (C) 2009 Anton Khirnov <wyskas@gmail.com>
4 # Nephilim is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # Nephilim is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Nephilim. If not, see <http://www.gnu.org/licenses/>.
18 from PyQt4 import QtGui, QtCore
19 from PyQt4.QtCore import pyqtSlot as Slot
21 from ..plugin import Plugin
22 from ..common import MIMETYPES, SongsMimeData
24 class Library(Plugin):
25 # public, const
26 info = 'Display MPD database as a tree.'
28 # public, read-only
29 o = None
31 # private
32 DEFAULTS = {'foldings' : ['albumartist/album'], 'cur_folding' : 0}
34 def _load(self):
35 self.o = LibraryWidget(self)
36 def _unload(self):
37 self.o = None
39 def _get_dock_widget(self):
40 return self._create_dock(self.o)
42 def fill_library(self):
43 if not self.o:
44 return
45 self.o.fill_library()
47 class LibraryWidget(QtGui.QWidget):
48 library_view = None
49 library_model = None
50 search_txt = None
51 settings = None
52 plugin = None
53 logger = None
54 filtered_items = None
55 filter = None
59 def __init__(self, plugin):
60 QtGui.QWidget.__init__(self)
61 self.plugin = plugin
62 self.logger = plugin.logger
63 self.settings = QtCore.QSettings()
64 self.filter = ''
65 self.filtered_items = []
66 self.settings.beginGroup(self.plugin.name)
68 # folding widgets
69 self.foldings = LibraryFolding(self.plugin, self)
70 self.foldings.activated.connect(self.fill_library)
71 del_folding = QtGui.QPushButton(QtGui.QIcon(':icons/delete.png'), '')
72 del_folding.setToolTip('Delete current folding pattern.')
73 del_folding.clicked.connect(lambda :self.foldings.removeItem(self.foldings.currentIndex()))
74 folding_layout = QtGui.QHBoxLayout()
75 folding_layout.addWidget(self.foldings, stretch = 1)
76 folding_layout.addWidget(del_folding)
78 self.search_txt = QtGui.QLineEdit()
79 self.search_txt.setToolTip('Filter library')
80 self.search_txt.textChanged.connect(self.filter_library)
81 self.search_txt.returnPressed.connect(self.add_filtered)
83 #construct the library
84 self.library_model = LibraryModel()
85 self.fill_library()
87 self.library_view = LibraryView()
88 self.library_view.setModel(self.library_model)
89 self.library_view.activated.connect(lambda : self.add_indices(self.library_view.selectedIndexes()))
91 self.setLayout(QtGui.QVBoxLayout())
92 self.layout().setSpacing(2)
93 self.layout().setMargin(0)
94 self.layout().addLayout(folding_layout)
95 self.layout().addWidget(self.search_txt)
96 self.layout().addWidget(self.library_view)
98 self.plugin.mpclient.db_updated.connect(self.fill_library)
100 def fill_library(self):
101 self.logger.info('Refreshing library.')
102 self.plugin.mpclient.database(lambda songs: self.library_model.fill(songs, self.foldings.currentText().split('/')))
104 @Slot(unicode)
105 def filter_library(self, text):
106 """Hide all items that don't contain text."""
107 to_hide = []
108 to_show = []
109 filtered_items = []
110 text = text.lower()
111 if not text: # show all items
112 to_show = self.library_model.findItems('*', QtCore.Qt.MatchWildcard|QtCore.Qt.MatchRecursive)
113 elif self.filter and self.filter in text:
114 for item in self.filtered_items:
115 if text in item.text().lower():
116 filtered_items.append(item)
117 while item:
118 to_show.append(item)
119 item = item.parent()
120 else:
121 while item:
122 to_hide.append(item)
123 item = item.parent()
124 else:
125 for item in self.library_model.findItems('*', QtCore.Qt.MatchWildcard|QtCore.Qt.MatchRecursive):
126 if text in item.text().lower():
127 filtered_items.append(item)
128 while item:
129 to_show.append(item)
130 item = item.parent()
131 else:
132 while item:
133 to_hide.append(item)
134 item = item.parent()
135 for item in to_hide:
136 self.library_view.setRowHidden(item.row(), self.library_model.indexFromItem(item.parent()), True)
137 for item in to_show:
138 self.library_view.setRowHidden(item.row(), self.library_model.indexFromItem(item.parent()), False)
140 if len(filtered_items) < 5:
141 for item in filtered_items:
142 while item:
143 item = item.parent()
144 self.library_view.setExpanded(self.library_model.indexFromItem(item), True)
146 self.filtered_items = filtered_items
147 self.filter = text
149 @Slot()
150 def add_filtered(self):
151 self.add_indices([self.library_model.indexFromItem(index) for index in self.filtered_items])
152 self.search_txt.clear()
154 def add_indices(self, indices):
155 paths = []
156 for song in self.library_model.walk_tree(indices):
157 paths.append(song)
158 self.plugin.mpclient.add(paths)
160 class LibrarySongItem(QtGui.QStandardItem):
161 # public
162 "Song path"
163 path = None
165 class LibraryModel(QtGui.QStandardItemModel):
166 def fill(self, songs, folding):
167 self.clear()
169 tree = [{},self.invisibleRootItem()]
170 for song in songs:
171 cur_item = tree
172 for part in folding:
173 try:
174 tag = song[part]
175 except KeyError:
176 tag = 'Unknown'
177 if tag in cur_item[0]:
178 cur_item = cur_item[0][tag]
179 else:
180 it = QtGui.QStandardItem(tag)
181 it.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled)
182 cur_item[1].appendRow(it)
183 cur_item[0][tag] = [{}, it]
184 cur_item = cur_item[0][tag]
185 it = LibrarySongItem('%s%02d %s'%(song['disc'] + '/' if 'disc' in song else '',
186 song['tracknum'] if 'tracknum' in song else 0,
187 song['?title']))
188 it.path = song['?file']
189 it.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled)
190 cur_item[1].appendRow(it)
192 self.sort(0, QtCore.Qt.AscendingOrder)
194 def walk_tree(self, indices):
195 """Returns a generator over all songs that are children of indices."""
196 for index in indices:
197 if self.hasChildren(index):
198 for song in self.walk_tree([self.index(i, 0, index) for i in range(self.rowCount(index))]):
199 yield song
200 else:
201 yield self.itemFromIndex(index).path
203 def flags(self, index):
204 return (QtCore.Qt.ItemIsDragEnabled if index.isValid() else 0) | QtGui.QStandardItemModel.flags(self, index)
206 def mimeTypes(self):
207 return MIMETYPES['songs']
209 def mimeData(self, indices):
210 data = SongsMimeData()
212 songs = []
213 for song in self.walk_tree(indices):
214 songs.append(song)
216 data.set_songs(songs)
217 return data
219 class LibraryView(QtGui.QTreeView):
220 def __init__(self):
221 QtGui.QTreeView.__init__(self)
223 self.setAlternatingRowColors(True)
224 self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
225 self.setUniformRowHeights(True)
226 self.setHeaderHidden(True)
227 self.setDragEnabled(True)
229 class LibraryFolding(QtGui.QComboBox):
231 #### PRIVATE ####
232 _plugin = None
233 _settings = None
235 #### PUBLIC ####
237 def __init__(self, plugin, parent):
238 QtGui.QComboBox.__init__(self, parent)
239 self.setEditable(True)
240 self.setToolTip('Current folding pattern.')
242 self._plugin = plugin
244 # fill with stored folding patterns, ensure at least an empty string
245 self._settings = QtCore.QSettings()
246 self._settings.beginGroup(self._plugin.name)
247 for item in self._settings.value('foldings'):
248 self.addItem(item)
249 if not self.count():
250 self.addItem('')
251 self.setCurrentIndex(int(self._settings.value('cur_folding')))
253 # connect signals to slots
254 self.currentIndexChanged.connect( self._update_foldings)
255 self.model().rowsInserted.connect(self._update_foldings)
256 self.model().rowsRemoved.connect( self._update_foldings)
257 self.model().rowsMoved.connect( self._update_foldings)
259 #### PRIVATE ####
260 @Slot()
261 def _update_foldings(self):
262 foldings = []
263 for i in xrange(self.count()):
264 foldings.append(self.itemText(i))
265 self._settings.setValue('foldings', foldings)
266 self._settings.setValue('cur_folding', self.currentIndex())