1# Copyright 2006-2009 Scott Horowitz <stonecrest@gmail.com> 2# Copyright 2009-2014 Jonathan Ballet <jon@multani.info> 3# 4# This file is part of Sonata. 5# 6# Sonata is free software: you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation, either version 3 of the License, or 9# (at your option) any later version. 10# 11# Sonata is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with Sonata. If not, see <http://www.gnu.org/licenses/>. 18 19import os 20import re 21import gettext 22import locale 23import threading # libsearchfilter_toggle starts thread libsearchfilter_loop 24import operator 25 26from gi.repository import Gtk, Gdk, GdkPixbuf, GObject, GLib, Pango 27 28from sonata import ui, misc, consts, formatting, breadcrumbs, mpdhelper as mpdh 29from sonata.artwork import get_multicd_album_root_dir 30from sonata.song import SongRecord 31 32 33VARIOUS_ARTISTS = _("Various Artists") 34 35 36def list_mark_various_artists_albums(albums): 37 for i in range(len(albums)): 38 if i + consts.NUM_ARTISTS_FOR_VA - 1 > len(albums)-1: 39 break 40 VA = False 41 for j in range(1, consts.NUM_ARTISTS_FOR_VA): 42 if albums[i].album.lower() != albums[i + j].album.lower() or \ 43 albums[i].year != albums[i + j].year or \ 44 albums[i].path != albums[i + j].path: 45 break 46 if albums[i].artist == albums[i + j].artist: 47 albums.pop(i + j) 48 break 49 if j == consts.NUM_ARTISTS_FOR_VA - 1: 50 VA = True 51 if VA: 52 album = albums[i]._asdict() 53 album['artist'] = VARIOUS_ARTISTS 54 albums[i] = SongRecord(**album) 55 j = 1 56 while i + j <= len(albums) - 1: 57 if albums[i].album.lower() == albums[i + j].album.lower() \ 58 and albums[i].year == albums[i + j].year: 59 albums.pop(i + j) 60 else: 61 break 62 return albums 63 64 65class Library: 66 def __init__(self, config, mpd, artwork, TAB_LIBRARY, settings_save, 67 filter_key_pressed, on_add_item, connected, 68 on_library_button_press, add_tab): 69 self.artwork = artwork 70 self.config = config 71 self.mpd = mpd 72 self.librarymenu = None # cyclic dependency, set later 73 self.settings_save = settings_save 74 self.filter_key_pressed = filter_key_pressed 75 self.on_add_item = on_add_item 76 self.connected = connected 77 self.on_library_button_press = on_library_button_press 78 79 self.NOTAG = _("Untagged") 80 self.search_terms = [_('Artist'), _('Title'), _('Album'), _('Genre'), 81 _('Filename'), _('Everything')] 82 self.search_terms_mpd = ['artist', 'title', 'album', 'genre', 'file', 83 'any'] 84 85 self.libfilterbox_cmd_buf = None 86 self.libfilterbox_cond = None 87 self.libfilterbox_source = None 88 89 self.prevlibtodo_base = None 90 self.prevlibtodo_base_results = None 91 self.prevlibtodo = None 92 93 self.save_timeout = None 94 self.libsearch_last_tooltip = None 95 96 self.lib_view_filesystem_cache = None 97 self.lib_view_artist_cache = None 98 self.lib_view_genre_cache = None 99 self.lib_view_album_cache = None 100 self.lib_list_genres = None 101 self.lib_list_artists = None 102 self.lib_list_albums = None 103 self.lib_list_years = None 104 self.view_caches_reset() 105 106 # Library tab 107 self.builder = ui.builder('library') 108 self.css_provider = ui.css_provider('library') 109 110 self.libraryvbox = self.builder.get_object('library_page_v_box') 111 self.library = self.builder.get_object('library_page_treeview') 112 self.library_selection = self.library.get_selection() 113 self.breadcrumbs = self.builder.get_object('library_crumbs_box') 114 self.crumb_section = self.builder.get_object( 115 'library_crumb_section_togglebutton') 116 self.crumb_section_image = self.builder.get_object( 117 'library_crumb_section_image') 118 self.crumb_break = self.builder.get_object( 119 'library_crumb_break_box') 120 self.breadcrumbs.set_crumb_break(self.crumb_break) 121 self.crumb_section_handler = None 122 expanderwindow2 = self.builder.get_object('library_page_scrolledwindow') 123 self.searchbox = self.builder.get_object('library_page_searchbox') 124 self.searchcombo = self.builder.get_object('library_page_searchbox_combo') 125 self.searchtext = self.builder.get_object('library_page_searchbox_entry') 126 self.searchbutton = self.builder.get_object('library_page_searchbox_button') 127 self.searchbutton.hide() 128 self.libraryview = self.builder.get_object('library_crumb_button') 129 self.tab_label_widget = self.builder.get_object('library_tab_eventbox') 130 tab_label = self.builder.get_object('library_tab_label') 131 tab_label.set_text(TAB_LIBRARY) 132 133 self.tab = add_tab(self.libraryvbox, self.tab_label_widget, 134 TAB_LIBRARY, self.library) 135 136 # Assign some pixbufs for use in self.library 137 self.openpb2 = self.library.render_icon(Gtk.STOCK_OPEN, 138 Gtk.IconSize.LARGE_TOOLBAR) 139 self.harddiskpb2 = self.library.render_icon(Gtk.STOCK_HARDDISK, 140 Gtk.IconSize.LARGE_TOOLBAR) 141 self.openpb = self.library.render_icon(Gtk.STOCK_OPEN, 142 Gtk.IconSize.MENU) 143 self.harddiskpb = self.library.render_icon(Gtk.STOCK_HARDDISK, 144 Gtk.IconSize.MENU) 145 self.albumpb = self.library.render_icon('sonata-album', 146 Gtk.IconSize.LARGE_TOOLBAR) 147 self.genrepb = self.library.render_icon('gtk-orientation-portrait', 148 Gtk.IconSize.LARGE_TOOLBAR) 149 self.artistpb = self.library.render_icon('sonata-artist', 150 Gtk.IconSize.LARGE_TOOLBAR) 151 self.sonatapb = self.library.render_icon('sonata', 152 Gtk.IconSize.LARGE_TOOLBAR) 153 154 # list of the library views: (id, name, icon name, label) 155 self.VIEWS = [ 156 (consts.VIEW_FILESYSTEM, 'filesystem', 157 Gtk.STOCK_HARDDISK, _("Filesystem")), 158 (consts.VIEW_ALBUM, 'album', 159 'sonata-album', _("Albums")), 160 (consts.VIEW_ARTIST, 'artist', 161 'sonata-artist', _("Artists")), 162 (consts.VIEW_GENRE, 'genre', 163 Gtk.STOCK_ORIENTATION_PORTRAIT, _("Genres")), 164 ] 165 166 self.library.connect('row_activated', self.on_library_row_activated) 167 self.library.connect('button_press_event', 168 self.on_library_button_press) 169 self.library.connect('key-press-event', self.on_library_key_press) 170 self.library.connect('query-tooltip', self.on_library_query_tooltip) 171 expanderwindow2.connect('scroll-event', self.on_library_scrolled) 172 self.libraryview.connect('clicked', self.library_view_popup) 173 self.searchtext.connect('key-press-event', 174 self.libsearchfilter_key_pressed) 175 self.searchtext.connect('activate', self.libsearchfilter_on_enter) 176 self.searchbutton.connect('clicked', self.on_search_end) 177 178 self.libfilter_changed_handler = self.searchtext.connect( 179 'changed', self.libsearchfilter_feed_loop) 180 searchcombo_changed_handler = self.searchcombo.connect( 181 'changed', self.on_library_search_combo_change) 182 183 # Initialize library data and widget 184 self.libraryposition = {} 185 self.libraryselectedpath = {} 186 self.searchcombo.handler_block(searchcombo_changed_handler) 187 self.searchcombo.set_active(self.config.last_search_num) 188 self.searchcombo.handler_unblock(searchcombo_changed_handler) 189 self.librarydata = Gtk.ListStore(GdkPixbuf.Pixbuf, 190 GObject.TYPE_PYOBJECT, str) 191 self.library.set_model(self.librarydata) 192 self.library.set_search_column(2) 193 self.librarycell = Gtk.CellRendererText() 194 self.librarycell.set_property("ellipsize", Pango.EllipsizeMode.END) 195 self.libraryimg = Gtk.CellRendererPixbuf() 196 self.librarycolumn = Gtk.TreeViewColumn() 197 self.librarycolumn.pack_start(self.libraryimg, False) 198 self.librarycolumn.pack_start(self.librarycell, True) 199 self.librarycolumn.add_attribute(self.libraryimg, 'pixbuf', 0) 200 self.librarycolumn.add_attribute(self.librarycell, 'markup', 2) 201 self.librarycolumn.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) 202 self.library.append_column(self.librarycolumn) 203 self.library_selection.set_mode(Gtk.SelectionMode.MULTIPLE) 204 205 def get_libraryactions(self): 206 return [(name + 'view', icon, label, 207 None, None, self.on_libraryview_chosen) 208 for _view, name, icon, label in self.VIEWS] 209 210 def get_model(self): 211 return self.librarydata 212 213 def get_widgets(self): 214 return self.libraryvbox 215 216 def get_treeview(self): 217 return self.library 218 219 def get_selection(self): 220 return self.library_selection 221 222 def set_librarymenu(self, librarymenu): 223 self.librarymenu = librarymenu 224 self.librarymenu.attach_to_widget(self.libraryview, None) 225 226 def library_view_popup(self, button): 227 self.librarymenu.popup(None, None, self.library_view_position_menu, 228 button, 1, 0) 229 230 def library_view_position_menu(self, _menu, button): 231 alloc = button.get_allocation() 232 return (self.config.x + alloc.x, 233 self.config.y + alloc.y + alloc.height, 234 True) 235 236 def on_libraryview_chosen(self, action): 237 if self.search_visible(): 238 self.on_search_end(None) 239 if action.get_name() == 'filesystemview': 240 self.config.lib_view = consts.VIEW_FILESYSTEM 241 elif action.get_name() == 'artistview': 242 self.config.lib_view = consts.VIEW_ARTIST 243 elif action.get_name() == 'genreview': 244 self.config.lib_view = consts.VIEW_GENRE 245 elif action.get_name() == 'albumview': 246 self.config.lib_view = consts.VIEW_ALBUM 247 self.library.grab_focus() 248 self.libraryposition = {} 249 self.libraryselectedpath = {} 250 self.library_browse(root=SongRecord(path="/")) 251 try: 252 if len(self.librarydata) > 0: 253 first = Gtk.TreePath.new_first() 254 to = Gtk.TreePath.new() 255 to.append_index(len(self.librarydata) - 1) 256 self.library_selection.unselect_range(first, to) 257 except Exception as e: 258 # XXX import logger here in the future 259 raise e 260 GLib.idle_add(self.library.scroll_to_point, 0, 0) 261 262 def view_caches_reset(self): 263 # We should call this on first load and whenever mpd is 264 # updated. 265 self.lib_view_filesystem_cache = None 266 self.lib_view_artist_cache = None 267 self.lib_view_genre_cache = None 268 self.lib_view_album_cache = None 269 self.lib_list_genres = None 270 self.lib_list_artists = None 271 self.lib_list_albums = None 272 self.lib_list_years = None 273 274 def on_library_scrolled(self, _widget, _event): 275 try: 276 # Use GLib.idle_add so that we can get the visible 277 # state of the treeview 278 GLib.idle_add(self._on_library_scrolled) 279 except: 280 pass 281 282 def _on_library_scrolled(self): 283 if not self.config.show_covers: 284 return 285 286 # This avoids a warning about a NULL node in get_visible_range 287 if not self.library.props.visible: 288 return 289 290 visible_range = self.library.get_visible_range() 291 292 if visible_range is None: 293 return 294 else: 295 start_row, end_row = visible_range 296 297 self.artwork.library_artwork_update(self.librarydata, start_row, 298 end_row, self.albumpb) 299 300 def library_browse(self, _widget=None, root=None): 301 # Populates the library list with entries 302 if not self.connected(): 303 return 304 305 if root is None or (self.config.lib_view == consts.VIEW_FILESYSTEM \ 306 and root.path is None): 307 root = SongRecord(path="/") 308 if self.config.wd is None or (self.config.lib_view == \ 309 consts.VIEW_FILESYSTEM and \ 310 self.config.wd.path is None): 311 self.config.wd = SongRecord(path="/") 312 313 prev_selection = [] 314 prev_selection_root = False 315 prev_selection_parent = False 316 if root == self.config.wd: 317 # This will happen when the database is updated. So, lets save 318 # the current selection in order to try to re-select it after 319 # the update is over. 320 model, selected = self.library_selection.get_selected_rows() 321 for path in selected: 322 prev_selection.append(model.get_value(model.get_iter(path), 1)) 323 self.libraryposition[self.config.wd] = \ 324 self.library.get_visible_rect().width 325 path_updated = True 326 else: 327 path_updated = False 328 329 new_level = self.library_get_data_level(root) 330 curr_level = self.library_get_data_level(self.config.wd) 331 # The logic below is more consistent with, e.g., thunar. 332 if new_level > curr_level: 333 # Save position and row for where we just were if we've 334 # navigated into a sub-directory: 335 self.libraryposition[self.config.wd] = \ 336 self.library.get_visible_rect().width 337 model, rows = self.library_selection.get_selected_rows() 338 if len(rows) > 0: 339 data = self.librarydata.get_value( 340 self.librarydata.get_iter(rows[0]), 2) 341 self.libraryselectedpath[self.config.wd] = rows[0] 342 elif (self.config.lib_view == consts.VIEW_FILESYSTEM and \ 343 root != self.config.wd) \ 344 or (self.config.lib_view != consts.VIEW_FILESYSTEM and new_level != \ 345 curr_level): 346 # If we've navigated to a parent directory, don't save 347 # anything so that the user will enter that subdirectory 348 # again at the top position with nothing selected 349 self.libraryposition[self.config.wd] = 0 350 self.libraryselectedpath[self.config.wd] = None 351 352 # In case sonata is killed or crashes, we'll save the library state 353 # in 5 seconds (first removing any current settings_save timeouts) 354 if self.config.wd != root: 355 try: 356 GLib.source_remove(self.save_timeout) 357 except: 358 pass 359 self.save_timeout = GLib.timeout_add(5000, self.settings_save) 360 361 self.config.wd = root 362 self.library.freeze_child_notify() 363 self.library.set_model(None) 364 self.librarydata.clear() 365 366 # Populate treeview with data: 367 bd = [] 368 wd = self.config.wd 369 while len(bd) == 0: 370 if self.config.lib_view == consts.VIEW_FILESYSTEM: 371 bd = self.library_populate_filesystem_data(wd.path) 372 elif self.config.lib_view == consts.VIEW_ALBUM: 373 if wd.album is not None: 374 bd = self.library_populate_data(artist=wd.artist, 375 album=wd.album, 376 year=wd.year) 377 else: 378 bd = self.library_populate_toplevel_data(albumview=True) 379 elif self.config.lib_view == consts.VIEW_ARTIST: 380 if wd.artist is not None and wd.album is not None: 381 bd = self.library_populate_data(artist=wd.artist, 382 album=wd.album, 383 year=wd.year) 384 elif self.config.wd.artist is not None: 385 bd = self.library_populate_data(artist=wd.artist) 386 else: 387 bd = self.library_populate_toplevel_data(artistview=True) 388 elif self.config.lib_view == consts.VIEW_GENRE: 389 if wd.genre is not None and \ 390 wd.artist is not None and \ 391 wd.album is not None: 392 bd = self.library_populate_data(genre=wd.genre, 393 artist=wd.artist, 394 album=wd.album, 395 year=wd.year) 396 elif wd.genre is not None: 397 bd = self.library_populate_data(genre=wd.genre, 398 artist=wd.artist) 399 else: 400 bd = self.library_populate_toplevel_data(genreview=True) 401 402 if len(bd) == 0: 403 # Nothing found; go up a level until we reach the top level 404 # or results are found 405 last_wd = self.config.wd 406 self.config.wd = self.library_get_parent() 407 if self.config.wd == last_wd: 408 break 409 410 for _sort, path in bd: 411 self.librarydata.append(path) 412 413 self.library.set_model(self.librarydata) 414 self.library.thaw_child_notify() 415 416 # Scroll back to set view for current dir: 417 self.library.realize() 418 GLib.idle_add(self.library_set_view, not path_updated) 419 if len(prev_selection) > 0 or prev_selection_root or \ 420 prev_selection_parent: 421 # Retain pre-update selection: 422 self.library_retain_selection(prev_selection, prev_selection_root, 423 prev_selection_parent) 424 425 self.update_breadcrumbs() 426 427 def update_breadcrumbs(self): 428 # remove previous buttons 429 for b in self.breadcrumbs: 430 self.breadcrumbs.remove(b) 431 432 # find info for current view 433 view, _name, icon, label = [v for v in self.VIEWS 434 if v[0] == self.config.lib_view][0] 435 436 # the first crumb is the root of the current view 437 self.crumb_section.set_label(label) 438 self.crumb_section_image.set_from_stock(icon, Gtk.IconSize.MENU) 439 self.crumb_section.set_tooltip_text(label) 440 if self.crumb_section_handler: 441 self.crumb_section.disconnect(self.crumb_section_handler) 442 443 444 crumbs = [] 445 # crumbs are specific to the view 446 if view == consts.VIEW_FILESYSTEM: 447 if self.config.wd.path and self.config.wd.path != '/': 448 parts = self.config.wd.path.split('/') 449 else: 450 parts = [] # no crumbs for / 451 # append a crumb for each part 452 for i, part in enumerate(parts): 453 partpath = '/'.join(parts[:i + 1]) 454 target = SongRecord(path=partpath) 455 crumbs.append((part, Gtk.STOCK_OPEN, None, target)) 456 else: 457 parts = () 458 if view == consts.VIEW_ALBUM: 459 # We don't want to show an artist button in album view 460 keys = 'genre', 'album' 461 nkeys = 2 462 parts = (self.config.wd.genre, self.config.wd.album) 463 else: 464 keys = 'genre', 'artist', 'album' 465 nkeys = 3 466 parts = (self.config.wd.genre, self.config.wd.artist, 467 self.config.wd.album) 468 # append a crumb for each part 469 for i, key, part in zip(range(nkeys), keys, parts): 470 if part is None: 471 continue 472 partdata = dict(list(zip(keys, parts))[:i + 1]) 473 target = SongRecord(**partdata) 474 pb, icon = None, None 475 if key == 'album': 476 # Album artwork, with self.alumbpb as a backup: 477 cache_data = SongRecord(artist=self.config.wd.artist, 478 album=self.config.wd.album, 479 path=self.config.wd.path) 480 pb = self.artwork.cache.get_pixbuf(cache_data, 481 consts.LIB_COVER_SIZE) 482 if pb is None: 483 icon = 'album' 484 elif key == 'artist': 485 icon = 'sonata-artist' 486 else: 487 icon = Gtk.STOCK_ORIENTATION_PORTRAIT 488 crumbs.append((part, icon, pb, target)) 489 490 if not len(crumbs): 491 self.crumb_section.set_active(True) 492 context = self.crumb_section.get_style_context() 493 context.add_class('last_crumb') 494 else: 495 self.crumb_section.set_active(False) 496 context = self.crumb_section.get_style_context() 497 context.remove_class('last_crumb') 498 499 self.crumb_section_handler = self.crumb_section.connect('toggled', 500 self.library_browse, SongRecord(path='/')) 501 502 # add a button for each crumb 503 for crumb in crumbs: 504 text, icon, pb, target = crumb 505 text = misc.escape_html(text) 506 label = Gtk.Label(text, use_markup=True) 507 508 if icon: 509 image = Gtk.Image.new_from_stock(icon, Gtk.IconSize.MENU) 510 elif pb: 511 pb = pb.scale_simple(16, 16, GdkPixbuf.InterpType.HYPER) 512 image = Gtk.Image.new_from_pixbuf(pb) 513 514 b = breadcrumbs.CrumbButton(image, label) 515 516 if crumb is crumbs[-1]: 517 # FIXME makes the button request minimal space: 518 b.set_active(True) 519 context = b.get_style_context() 520 context.add_class('last_crumb') 521 522 b.set_tooltip_text(label.get_label()) 523 b.connect('toggled', self.library_browse, target) 524 self.breadcrumbs.pack_start(b, False, False, 0) 525 b.show_all() 526 527 def library_populate_filesystem_data(self, path): 528 # List all dirs/files at path 529 bd = [] 530 if path == '/' and self.lib_view_filesystem_cache is not None: 531 # Use cache if possible... 532 bd = self.lib_view_filesystem_cache 533 else: 534 for item in self.mpd.lsinfo(path): 535 if 'directory' in item: 536 name = os.path.basename(item['directory']) 537 data = SongRecord(path=item["directory"]) 538 bd += [('d' + str(name).lower(), [self.openpb, data, 539 misc.escape_html(name)])] 540 elif 'file' in item: 541 data = SongRecord(path=item['file']) 542 bd += [('f' + item['file'].lower(), 543 [self.sonatapb, data, 544 formatting.parse(self.config.libraryformat, item, 545 True)])] 546 bd.sort(key=operator.itemgetter(0)) 547 return bd 548 549 def library_get_toplevel_cache(self, genreview=False, artistview=False, 550 albumview=False): 551 if genreview and self.lib_view_genre_cache is not None: 552 bd = self.lib_view_genre_cache 553 elif artistview and self.lib_view_artist_cache is not None: 554 bd = self.lib_view_artist_cache 555 elif albumview and self.lib_view_album_cache is not None: 556 bd = self.lib_view_album_cache 557 else: 558 return None 559 # Check if we can update any artwork: 560 for _sort, info in bd: 561 pb = info[0] 562 if pb == self.albumpb: 563 key = SongRecord(path=info[1].path, artist=info[1].artist, 564 album=info[1].album) 565 pb2 = self.artwork.cache.get_pixbuf(key, 566 consts.LIB_COVER_SIZE) 567 if pb2 is not None: 568 info[0] = pb2 569 return bd 570 571 def library_populate_toplevel_data(self, genreview=False, artistview=False, 572 albumview=False): 573 bd = self.library_get_toplevel_cache(genreview, artistview, albumview) 574 if bd is not None: 575 # We have our cached data, woot. 576 return bd 577 bd = [] 578 if genreview or artistview: 579 # Only for artist/genre views, album view is handled differently 580 # since multiple artists can have the same album name 581 if genreview: 582 items = self.library_return_list_items('genre') 583 pb = self.genrepb 584 else: 585 items = self.library_return_list_items('artist') 586 pb = self.artistpb 587 if not (self.NOTAG in items): 588 items.append(self.NOTAG) 589 for item in items: 590 if genreview: 591 playtime, num_songs = self.library_return_count(genre=item) 592 data = SongRecord(genre=item) 593 else: 594 playtime, num_songs = self.library_return_count( 595 artist=item) 596 data = SongRecord(artist=item) 597 if num_songs > 0: 598 display = misc.escape_html(item) 599 display += self.add_display_info(num_songs, playtime) 600 bd += [(misc.lower_no_the(item), [pb, data, display])] 601 elif albumview: 602 albums = [] 603 untagged_found = False 604 for item in self.mpd.listallinfo('/'): 605 if 'file' in item and 'album' in item: 606 album = item['album'] 607 artist = item.get('artist', self.NOTAG) 608 year = item.get('date', self.NOTAG) 609 path = get_multicd_album_root_dir( 610 os.path.dirname(item['file'])) 611 data = SongRecord(album=album, artist=artist, 612 year=year, path=path) 613 albums.append(data) 614 if album == self.NOTAG: 615 untagged_found = True 616 if not untagged_found: 617 albums.append(SongRecord(album=self.NOTAG)) 618 albums = misc.remove_list_duplicates(albums, case=False) 619 albums = list_mark_various_artists_albums(albums) 620 for item in albums: 621 album, artist, _genre, year, path = item 622 playtime, num_songs = self.library_return_count(artist=artist, 623 album=album, 624 year=year) 625 if num_songs > 0: 626 data = SongRecord(artist=artist, album=album, 627 year=year, path=path) 628 display = misc.escape_html(album) 629 if artist and year and len(artist) > 0 and len(year) > 0 \ 630 and artist != self.NOTAG and year != self.NOTAG: 631 display += " <span weight='light'>(%s, %s)</span>" \ 632 % (misc.escape_html(artist), 633 misc.escape_html(year)) 634 elif artist and len(artist) > 0 and artist != self.NOTAG: 635 display += " <span weight='light'>(%s)</span>" \ 636 % misc.escape_html(artist) 637 elif year and len(year) > 0 and year != self.NOTAG: 638 display += " <span weight='light'>(%s)</span>" \ 639 % misc.escape_html(year) 640 display += self.add_display_info(num_songs, playtime) 641 bd += [(misc.lower_no_the(album), [self.albumpb, data, 642 display])] 643 bd.sort(key=lambda key: locale.strxfrm(key[0])) 644 if genreview: 645 self.lib_view_genre_cache = bd 646 elif artistview: 647 self.lib_view_artist_cache = bd 648 elif albumview: 649 self.lib_view_album_cache = bd 650 return bd 651 652 653 def library_populate_data(self, genre=None, artist=None, album=None, 654 year=None): 655 # Create treeview model info 656 bd = [] 657 if genre is not None and artist is None and album is None: 658 # Artists within a genre 659 artists = self.library_return_list_items('artist', genre=genre) 660 if len(artists) > 0: 661 if not self.NOTAG in artists: 662 artists.append(self.NOTAG) 663 for artist in artists: 664 playtime, num_songs = self.library_return_count( 665 genre=genre, artist=artist) 666 if num_songs > 0: 667 display = misc.escape_html(artist) 668 display += self.add_display_info(num_songs, playtime) 669 data = SongRecord(genre=genre, artist=artist) 670 bd += [(misc.lower_no_the(artist), 671 [self.artistpb, data, display])] 672 elif artist is not None and album is None: 673 # Albums/songs within an artist and possibly genre 674 # Albums first: 675 if genre is not None: 676 albums = self.library_return_list_items('album', genre=genre, 677 artist=artist) 678 else: 679 albums = self.library_return_list_items('album', artist=artist) 680 for album in albums: 681 if genre is not None: 682 years = self.library_return_list_items('date', genre=genre, 683 artist=artist, 684 album=album) 685 else: 686 years = self.library_return_list_items('date', 687 artist=artist, 688 album=album) 689 if not self.NOTAG in years: 690 years.append(self.NOTAG) 691 for year in years: 692 if genre is not None: 693 playtime, num_songs = self.library_return_count( 694 genre=genre, artist=artist, album=album, year=year) 695 if num_songs > 0: 696 files = self.library_return_list_items( 697 'file', genre=genre, artist=artist, 698 album=album, year=year) 699 path = os.path.dirname(files[0]) 700 data = SongRecord(genre=genre, artist=artist, 701 album=album, year=year, path=path) 702 else: 703 playtime, num_songs = self.library_return_count( 704 artist=artist, album=album, year=year) 705 if num_songs > 0: 706 files = self.library_return_list_items( 707 'file', artist=artist, album=album, year=year) 708 path = os.path.dirname(files[0]) 709 cache_data = SongRecord(artist=artist, album=album, 710 path=path) 711 data = SongRecord(artist=artist, album=album, 712 year=year, path=path) 713 if num_songs > 0: 714 cache_data = SongRecord(artist=artist, album=album, path=path) 715 display = misc.escape_html(album) 716 if year and len(year) > 0 and year != self.NOTAG: 717 display += " <span weight='light'>(%s)</span>" \ 718 % misc.escape_html(year) 719 display += self.add_display_info(num_songs, playtime) 720 ordered_year = year 721 if ordered_year == self.NOTAG: 722 ordered_year = '9999' 723 pb = self.artwork.cache.get_pixbuf( 724 cache_data, consts.LIB_COVER_SIZE, 725 self.albumpb) 726 bd += [(ordered_year + misc.lower_no_the(album), 727 [pb, data, display])] 728 # Now, songs not in albums: 729 bd += self.library_populate_data_songs(genre, artist, self.NOTAG, 730 None) 731 else: 732 # Songs within an album, artist, year, and possibly genre 733 bd += self.library_populate_data_songs(genre, artist, album, year) 734 bd.sort(key=lambda key: locale.strxfrm(key[0])) 735 return bd 736 737 def library_populate_data_songs(self, genre, artist, album, year): 738 bd = [] 739 if genre is not None: 740 songs, _playtime, _num_songs = \ 741 self.library_return_search_items(genre=genre, artist=artist, 742 album=album, year=year) 743 else: 744 songs, _playtime, _num_songs = self.library_return_search_items( 745 artist=artist, album=album, year=year) 746 for song in songs: 747 data = SongRecord(path=song.file) 748 track = str(song.get('track', 99)).zfill(2) 749 disc = str(song.get('disc', 99)).zfill(2) 750 try: 751 bd += [('f' + disc + track + misc.lower_no_the(song.title), 752 [self.sonatapb, data, formatting.parse( 753 self.config.libraryformat, song, True)])] 754 except: 755 bd += [('f' + disc + track + song.file.lower(), 756 [self.sonatapb, data, 757 formatting.parse(self.config.libraryformat, song, 758 True)])] 759 return bd 760 761 def library_return_list_items(self, itemtype, genre=None, artist=None, 762 album=None, year=None, ignore_case=True): 763 # Returns all items of tag 'itemtype', in alphabetical order, 764 # using mpd's 'list'. If searchtype is passed, use 765 # a case insensitive search, via additional 'list' 766 # queries, since using a single 'list' call will be 767 # case sensitive. 768 results = [] 769 searches = self.library_compose_list_count_searchlist(genre, artist, 770 album, year) 771 if len(searches) > 0: 772 for s in searches: 773 # If we have untagged tags (''), use search instead 774 # of list because list will not return anything. 775 if '' in s: 776 items = [] 777 songs, playtime, num_songs = \ 778 self.library_return_search_items(genre, artist, 779 album, year) 780 for song in songs: 781 items.append(song.get(itemtype)) 782 else: 783 items = self.mpd.list(itemtype, *s) 784 for item in items: 785 if len(item) > 0: 786 results.append(item) 787 else: 788 if genre is None and artist is None and album is None and year \ 789 is None: 790 for item in self.mpd.list(itemtype): 791 if len(item) > 0: 792 results.append(item) 793 if ignore_case: 794 results = misc.remove_list_duplicates(results, case=False) 795 results.sort(key=locale.strxfrm) 796 return results 797 798 def library_return_count(self, genre=None, artist=None, album=None, 799 year=None): 800 # Because mpd's 'count' is case sensitive, we have to 801 # determine all equivalent items (case insensitive) and 802 # call 'count' for each of them. Using 'list' + 'count' 803 # involves much less data to be transferred back and 804 # forth than to use 'search' and count manually. 805 searches = self.library_compose_list_count_searchlist(genre, artist, 806 album, year) 807 playtime = 0 808 num_songs = 0 809 for s in searches: 810 count = self.mpd.count(*s) 811 playtime += count.playtime 812 num_songs += count.songs 813 814 return (playtime, num_songs) 815 816 def library_compose_list_count_searchlist_single(self, search, typename, 817 cached_list, searchlist): 818 s = [] 819 skip_type = (typename == 'artist' and search == VARIOUS_ARTISTS) 820 if search is not None and not skip_type: 821 if search == self.NOTAG: 822 itemlist = [search, ''] 823 else: 824 itemlist = [] 825 if cached_list is None: 826 cached_list = self.library_return_list_items(typename, 827 ignore_case=False) 828 # This allows us to match untagged items 829 cached_list.append('') 830 for item in cached_list: 831 if str(item).lower() == str(search).lower(): 832 itemlist.append(item) 833 if len(itemlist) == 0: 834 # There should be no results! 835 return None, cached_list 836 for item in itemlist: 837 if len(searchlist) > 0: 838 for item2 in searchlist: 839 s.append(item2 + (typename, item)) 840 else: 841 s.append((typename, item)) 842 else: 843 s = searchlist 844 return s, cached_list 845 846 def library_compose_list_count_searchlist(self, genre=None, artist=None, 847 album=None, year=None): 848 s = [] 849 s, self.lib_list_genres = \ 850 self.library_compose_list_count_searchlist_single( 851 genre, 'genre', self.lib_list_genres, s) 852 if s is None: 853 return [] 854 s, self.lib_list_artists = \ 855 self.library_compose_list_count_searchlist_single( 856 artist, 'artist', self.lib_list_artists, s) 857 if s is None: 858 return [] 859 s, self.lib_list_albums = \ 860 self.library_compose_list_count_searchlist_single( 861 album, 'album', self.lib_list_albums, s) 862 if s is None: 863 return [] 864 s, self.lib_list_years = \ 865 self.library_compose_list_count_searchlist_single( 866 year, 'date', self.lib_list_years, s) 867 if s is None: 868 return [] 869 return s 870 871 def library_compose_search_searchlist_single(self, search, typename, 872 searchlist): 873 s = [] 874 skip_type = (typename == 'artist' and search == VARIOUS_ARTISTS) 875 if search is not None and not skip_type: 876 if search == self.NOTAG: 877 itemlist = [search, ''] 878 else: 879 itemlist = [search] 880 for item in itemlist: 881 if len(searchlist) > 0: 882 for item2 in searchlist: 883 s.append(item2 + (typename, item)) 884 else: 885 s.append((typename, item)) 886 else: 887 s = searchlist 888 return s 889 890 def library_compose_search_searchlist(self, genre=None, artist=None, 891 album=None, year=None): 892 s = [] 893 s = self.library_compose_search_searchlist_single(genre, 'genre', s) 894 s = self.library_compose_search_searchlist_single(album, 'album', s) 895 s = self.library_compose_search_searchlist_single(artist, 'artist', s) 896 s = self.library_compose_search_searchlist_single(year, 'date', s) 897 return s 898 899 def library_return_search_items(self, genre=None, artist=None, album=None, 900 year=None): 901 # Returns all mpd items, using mpd's 'search', along with 902 # playtime and num_songs. 903 searches = self.library_compose_search_searchlist(genre, artist, album, 904 year) 905 for s in searches: 906 args_tuple = tuple(map(str, s)) 907 playtime = 0 908 num_songs = 0 909 results = [] 910 strip_type = None 911 912 if len(args_tuple) == 0: 913 return None, 0, 0 914 915 items = self.mpd.search(*args_tuple) 916 if items is not None: 917 for item in items: 918 if strip_type is None or (strip_type is not None and not \ 919 strip_type in item.keys()): 920 match = True 921 pos = 0 922 # Ensure that if, e.g., "foo" is searched, 923 # "foobar" isn't returned too 924 for arg in args_tuple[::2]: 925 if arg in item and \ 926 str(item.get(arg, '')).upper() != \ 927 str(args_tuple[pos + 1]).upper(): 928 match = False 929 break 930 pos += 2 931 if match: 932 results.append(item) 933 num_songs += 1 934 playtime += item.time 935 return (results, int(playtime), num_songs) 936 937 def add_display_info(self, num_songs, playtime): 938 seconds = int(playtime) 939 hours = seconds // 3600 940 seconds -= 3600 * hours 941 minutes = seconds // 60 942 seconds -= 60 * minutes 943 songs_text = ngettext('{count} song', '{count} songs', 944 num_songs).format(count=num_songs) 945 seconds_text = ngettext('{count} second', '{count} seconds', 946 seconds).format(count=seconds) 947 minutes_text = ngettext('{count} minute', '{count} minutes', 948 minutes).format(count=minutes) 949 hours_text = ngettext('{count} hour', '{count} hours', 950 hours).format(count=hours) 951 time_parts = [songs_text] 952 if hours > 0: 953 time_parts.extend([hours_text, minutes_text]) 954 elif minutes > 0: 955 time_parts.extend([minutes_text, seconds_text]) 956 else: 957 time_parts.extend([seconds_text]) 958 display_markup = "\n<small><span weight='light'>{}</span></small>" 959 display_text = ', '.join(time_parts) 960 return display_markup.format(display_text) 961 962 def library_retain_selection(self, prev_selection, prev_selection_root, 963 prev_selection_parent): 964 # Unselect everything: 965 if len(self.librarydata) > 0: 966 first = Gtk.TreePath.new_first() 967 to = Gtk.TreePath.new() 968 to.append_index(len(self.librarydata) - 1) 969 self.library_selection.unselect_range(first, to) 970 # Now attempt to retain the selection from before the update: 971 for value in prev_selection: 972 for row in self.librarydata: 973 if value == row[1]: 974 self.library_selection.select_path(row.path) 975 break 976 if prev_selection_root: 977 self.library_selection.select_path((0,)) 978 if prev_selection_parent: 979 self.library_selection.select_path((1,)) 980 981 def library_set_view(self, select_items=True): 982 # select_items should be false if the same directory has merely 983 # been refreshed (updated) 984 try: 985 if self.config.wd in self.libraryposition: 986 self.library.scroll_to_point( 987 -1, self.libraryposition[self.config.wd]) 988 else: 989 self.library.scroll_to_point(0, 0) 990 except: 991 self.library.scroll_to_point(0, 0) 992 993 # Select and focus previously selected item 994 if select_items: 995 if self.config.wd in self.libraryselectedpath: 996 try: 997 if self.libraryselectedpath[self.config.wd]: 998 self.library_selection.select_path( 999 self.libraryselectedpath[self.config.wd]) 1000 self.library.grab_focus() 1001 except: 1002 pass 1003 1004 def library_get_data_level(self, data): 1005 if self.config.lib_view == consts.VIEW_FILESYSTEM: 1006 # Returns the number of directories down: 1007 if data.path == '/': 1008 # Every other path doesn't start with "/", so 1009 # start the level numbering at -1 1010 return -1 1011 else: 1012 return data.path.count("/") 1013 else: 1014 # Returns the number of items stored in data, excluding 1015 # the path: 1016 level = 0 1017 for item in data: 1018 if item is not None: 1019 level += 1 1020 return level 1021 1022 def on_library_key_press(self, widget, event): 1023 if event.keyval == Gdk.keyval_from_name('Return'): 1024 self.on_library_row_activated(widget, widget.get_cursor()[0]) 1025 return True 1026 1027 def on_library_query_tooltip(self, widget, x, y, keyboard_mode, tooltip): 1028 if keyboard_mode or not self.search_visible(): 1029 widget.set_tooltip_text("") 1030 return False 1031 1032 bin_x, bin_y = widget.convert_widget_to_bin_window_coords(x, y) 1033 1034 pathinfo = widget.get_path_at_pos(bin_x, bin_y) 1035 if not pathinfo: 1036 widget.set_tooltip_text("") 1037 # If the user hovers over an empty row and then back to 1038 # a row with a search result, this will ensure the tooltip 1039 # shows up again: 1040 GLib.idle_add(self.library_search_tooltips_enable, widget, x, y, 1041 keyboard_mode, None) 1042 return False 1043 treepath, _col, _x2, _y2 = pathinfo 1044 1045 i = self.librarydata.get_iter(treepath.get_indices()[0]) 1046 path = misc.escape_html(self.librarydata.get_value(i, 1).path) 1047 song = self.librarydata.get_value(i, 2) 1048 new_tooltip = "<b>%s:</b> %s\n<b>%s:</b> %s" \ 1049 % (_("Song"), song, _("Path"), path) 1050 1051 if new_tooltip != self.libsearch_last_tooltip: 1052 self.libsearch_last_tooltip = new_tooltip 1053 self.library.set_property('has-tooltip', False) 1054 GLib.idle_add(self.library_search_tooltips_enable, widget, x, y, 1055 keyboard_mode, tooltip) 1056 GLib.idle_add(widget.set_tooltip_markup, new_tooltip) 1057 return 1058 1059 self.libsearch_last_tooltip = new_tooltip 1060 1061 return False #api says we should return True, but this doesn't work? 1062 1063 def library_search_tooltips_enable(self, widget, x, y, keyboard_mode, 1064 tooltip): 1065 self.library.set_property('has-tooltip', True) 1066 if tooltip is not None: 1067 self.on_library_query_tooltip(widget, x, y, keyboard_mode, tooltip) 1068 1069 def on_library_row_activated(self, _widget, path, _column=0): 1070 if path is None: 1071 # Default to last item in selection: 1072 _model, selected = self.library_selection.get_selected_rows() 1073 if len(selected) >= 1: 1074 path = selected[0] 1075 else: 1076 return 1077 value = self.librarydata.get_value(self.librarydata.get_iter(path), 1) 1078 icon = self.librarydata.get_value(self.librarydata.get_iter(path), 0) 1079 if icon == self.sonatapb: 1080 # Song found, add item 1081 self.on_add_item(self.library) 1082 elif value.path == "..": 1083 self.library_browse_parent(None) 1084 else: 1085 self.library_browse(None, value) 1086 1087 def library_get_parent(self): 1088 wd = self.config.wd 1089 if self.config.lib_view == consts.VIEW_ALBUM: 1090 value = SongRecord(path="/") 1091 elif self.config.lib_view == consts.VIEW_ARTIST: 1092 if wd.album is None: 1093 value = SongRecord(path="/") 1094 else: 1095 value = SongRecord(artist = wd.artist) 1096 elif self.config.lib_view == consts.VIEW_GENRE: 1097 if wd.album is not None: 1098 value = SongRecord(genre=wd.genre, 1099 artist=wd.artist) 1100 elif wd.artist is not None: 1101 value = SongRecord(genre=wd.genre) 1102 else: 1103 value = SongRecord(path="/") 1104 else: 1105 newvalue = '/'.join(wd.path.split('/')[:-1]) or '/' 1106 value = SongRecord(path=newvalue) 1107 return value 1108 1109 def library_browse_parent(self, _action): 1110 if not self.search_visible(): 1111 if self.library.is_focus(): 1112 value = self.library_get_parent() 1113 self.library_browse(None, value) 1114 return True 1115 1116 def not_parent_is_selected(self): 1117 # Returns True if something is selected and it's not 1118 # ".." or "/": 1119 model, rows = self.library_selection.get_selected_rows() 1120 for path in rows: 1121 i = model.get_iter(path) 1122 value = model.get_value(i, 2) 1123 if value != ".." and value != "/": 1124 return True 1125 return False 1126 1127 def get_path_child_filenames(self, return_root, selected_only=True): 1128 # If return_root=True, return main directories whenever possible 1129 # instead of individual songs in order to reduce the number of 1130 # mpd calls we need to make. We won't want this behavior in some 1131 # instances, like when we want all end files for editing tags 1132 items = [] 1133 if selected_only: 1134 model, rows = self.library_selection.get_selected_rows() 1135 else: 1136 model = self.librarydata 1137 rows = [(i,) for i in range(len(model))] 1138 for path in rows: 1139 i = model.get_iter(path) 1140 pb = model.get_value(i, 0) 1141 data = model.get_value(i, 1) 1142 value = model.get_value(i, 2) 1143 if value != ".." and value != "/": 1144 if data.path is not None and data.album is None and data.artist is None and \ 1145 data.year is None and data.genre is None: 1146 if pb == self.sonatapb: 1147 # File 1148 items.append(data.path) 1149 else: 1150 # Directory 1151 if not return_root: 1152 items += self.library_get_path_files_recursive( 1153 data.path) 1154 else: 1155 items.append(data.path) 1156 else: 1157 results, _playtime, _num_songs = \ 1158 self.library_return_search_items( 1159 genre=data.genre, artist=data.artist, album=data.album, 1160 year=data.year) 1161 for item in results: 1162 items.append(item.file) 1163 # Make sure we don't have any EXACT duplicates: 1164 items = misc.remove_list_duplicates(items, case=True) 1165 return items 1166 1167 def library_get_path_files_recursive(self, path): 1168 results = [] 1169 for item in self.mpd.lsinfo(path): 1170 if 'directory' in item: 1171 results = results + self.library_get_path_files_recursive( 1172 item['directory']) 1173 elif 'file' in item: 1174 results.append(item['file']) 1175 return results 1176 1177 def on_library_search_combo_change(self, _combo=None): 1178 self.config.last_search_num = self.searchcombo.get_active() 1179 if not self.search_visible(): 1180 return 1181 self.prevlibtodo = "" 1182 self.prevlibtodo_base = "__" 1183 self.libsearchfilter_feed_loop(self.searchtext) 1184 1185 def on_search_end(self, _button, move_focus=True): 1186 if self.search_visible(): 1187 self.libsearchfilter_toggle(move_focus) 1188 1189 def search_visible(self): 1190 return self.searchbutton.get_property('visible') 1191 1192 def libsearchfilter_toggle(self, move_focus): 1193 if not self.search_visible() and self.connected(): 1194 self.library.set_property('has-tooltip', True) 1195 ui.show(self.searchbutton) 1196 self.prevlibtodo = 'foo' 1197 self.prevlibtodo_base = "__" 1198 self.prevlibtodo_base_results = [] 1199 # extra thread for background search work, 1200 # synchronized with a condition and its internal mutex 1201 self.libfilterbox_cond = threading.Condition() 1202 self.libfilterbox_cmd_buf = self.searchtext.get_text() 1203 qsearch_thread = threading.Thread(target=self.libsearchfilter_loop) 1204 qsearch_thread.name = "LibraryFilter" 1205 qsearch_thread.daemon = True 1206 qsearch_thread.start() 1207 elif self.search_visible(): 1208 ui.hide(self.searchbutton) 1209 self.searchtext.handler_block(self.libfilter_changed_handler) 1210 self.searchtext.set_text("") 1211 self.searchtext.handler_unblock(self.libfilter_changed_handler) 1212 self.libsearchfilter_stop_loop() 1213 # call library_browse from the main thread to avoid corruption 1214 # of treeview, fixes #1959 1215 GLib.idle_add(self.library_browse, None, self.config.wd) 1216 if move_focus: 1217 self.library.grab_focus() 1218 1219 def libsearchfilter_feed_loop(self, editable): 1220 if not self.search_visible(): 1221 self.libsearchfilter_toggle(None) 1222 # Lets only trigger the searchfilter_loop if 200ms pass 1223 # without a change in Gtk.Entry 1224 try: 1225 GLib.source_remove(self.libfilterbox_source) 1226 except: 1227 pass 1228 self.libfilterbox_source = GLib.timeout_add( 1229 300, self.libsearchfilter_start_loop, editable) 1230 1231 def libsearchfilter_start_loop(self, editable): 1232 self.libfilterbox_cond.acquire() 1233 self.libfilterbox_cmd_buf = editable.get_text() 1234 self.libfilterbox_cond.notifyAll() 1235 self.libfilterbox_cond.release() 1236 1237 def libsearchfilter_stop_loop(self): 1238 self.libfilterbox_cond.acquire() 1239 self.libfilterbox_cmd_buf = '$$$QUIT###' 1240 self.libfilterbox_cond.notifyAll() 1241 self.libfilterbox_cond.release() 1242 1243 def libsearchfilter_loop(self): 1244 while True: 1245 # copy the last command or pattern safely 1246 self.libfilterbox_cond.acquire() 1247 try: 1248 while(self.libfilterbox_cmd_buf == '$$$DONE###'): 1249 self.libfilterbox_cond.wait() 1250 todo = self.libfilterbox_cmd_buf 1251 self.libfilterbox_cond.release() 1252 except: 1253 todo = self.libfilterbox_cmd_buf 1254 searchby = self.search_terms_mpd[self.config.last_search_num] 1255 if self.prevlibtodo != todo: 1256 if todo == '$$$QUIT###': 1257 GLib.idle_add(ui.reset_entry_marking, self.searchtext) 1258 return 1259 elif len(todo) > 1: 1260 GLib.idle_add(self.libsearchfilter_do_search, searchby, 1261 todo) 1262 elif len(todo) == 0: 1263 GLib.idle_add(ui.reset_entry_marking, self.searchtext) 1264 self.libsearchfilter_toggle(False) 1265 else: 1266 GLib.idle_add(ui.reset_entry_marking, self.searchtext) 1267 self.libfilterbox_cond.acquire() 1268 self.libfilterbox_cmd_buf = '$$$DONE###' 1269 try: 1270 self.libfilterbox_cond.release() 1271 except Exception as e: 1272 # XXX add logger here in the future! 1273 raise e 1274 self.prevlibtodo = todo 1275 1276 def libsearchfilter_do_search(self, searchby, todo): 1277 if not self.prevlibtodo_base in todo: 1278 # Do library search based on first two letters: 1279 self.prevlibtodo_base = todo[:2] 1280 self.prevlibtodo_base_results = self.mpd.search(searchby, 1281 self.prevlibtodo_base) 1282 subsearch = False 1283 else: 1284 subsearch = True 1285 1286 # Now, use filtering similar to playlist filtering: 1287 # this make take some seconds... and we'll escape the search text 1288 # because we'll be searching for a match in items that are also escaped 1289 # 1290 # Note that the searching is not order specific. That is, "foo bar" 1291 # will match on "fools bar" and "barstool foo". 1292 1293 todos = todo.split(" ") 1294 regexps = [] 1295 for i in range(len(todos)): 1296 todos[i] = misc.escape_html(todos[i]) 1297 todos[i] = re.escape(todos[i]) 1298 todos[i] = '.*' + todos[i].lower() 1299 regexps.append(re.compile(todos[i])) 1300 matches = [] 1301 if searchby != 'any': 1302 for row in self.prevlibtodo_base_results: 1303 is_match = True 1304 for regexp in regexps: 1305 if not regexp.match(row.get(searchby, '').lower()): 1306 is_match = False 1307 break 1308 if is_match: 1309 matches.append(row) 1310 else: 1311 for row in self.prevlibtodo_base_results: 1312 allstr = " ".join(row.values()) 1313 is_match = True 1314 for regexp in regexps: 1315 if not regexp.match(str(allstr).lower()): 1316 is_match = False 1317 break 1318 if is_match: 1319 matches.append(row) 1320 if subsearch and len(matches) == len(self.librarydata): 1321 # nothing changed.. 1322 return 1323 self.library.freeze_child_notify() 1324 currlen = len(self.librarydata) 1325 bd = [(self.sonatapb, 1326 SongRecord(path=item['file']), 1327 formatting.parse(self.config.libraryformat, item, True)) 1328 for item in matches if 'file' in item] 1329 bd.sort(key=lambda key: locale.strxfrm(key[2])) 1330 for i, item in enumerate(bd): 1331 if i < currlen: 1332 j = self.librarydata.get_iter((i, )) 1333 for index in range(len(item)): 1334 if item[index] != self.librarydata.get_value(j, index): 1335 self.librarydata.set_value(j, index, item[index]) 1336 else: 1337 self.librarydata.append(item) 1338 # Remove excess items... 1339 newlen = len(bd) 1340 if newlen == 0: 1341 self.librarydata.clear() 1342 else: 1343 for i in range(currlen - newlen): 1344 j = self.librarydata.get_iter((currlen - 1 - i,)) 1345 self.librarydata.remove(j) 1346 self.library.thaw_child_notify() 1347 if len(matches) == 0: 1348 GLib.idle_add(ui.set_entry_invalid, self.searchtext) 1349 else: 1350 GLib.idle_add(self.library.set_cursor, Gtk.TreePath.new_first(), 1351 None, False) 1352 GLib.idle_add(ui.reset_entry_marking, self.searchtext) 1353 1354 def libsearchfilter_key_pressed(self, widget, event): 1355 self.filter_key_pressed(widget, event, self.library) 1356 1357 def libsearchfilter_on_enter(self, _entry): 1358 self.on_library_row_activated(None, None) 1359 1360 def libsearchfilter_set_focus(self): 1361 GLib.idle_add(self.searchtext.grab_focus) 1362 1363 def libsearchfilter_get_style(self): 1364 return self.searchtext.get_style() 1365