def __changed_and_signal_library(self, entry, section, name): config.set(section, name, str(entry.get_value())) print_d("Signalling \"changed\" to entire library. Hold tight...") # Cache over clicks self._songs = self._songs or app.library.values() copool.add(emit_signal, self._songs, funcid="library changed", name=_("Updating for new ratings"))
def test_step(self): copool.add(self.__set_buffer, funcid="test") copool.pause("test") self.assertTrue(copool.step("test")) self.go = False self.assertFalse(copool.step("test")) self.assertRaises(ValueError, copool.step, "test")
def test_timeout(self): copool.add(self.__set_buffer, funcid="test", timeout=100) copool.pause("test") copool.resume("test") copool.remove("test") with pytest.raises(ValueError): copool.step("test")
def __drag_data_received(self, ctx, x, y, sel, tid, etime): if tid == 1: uris = sel.get_uris() if tid == 2: uri = sel.data.decode('utf16', 'replace').split('\n')[0] uris = [uri.encode('ascii', 'replace')] dirs = [] error = False for uri in uris: try: uri = URI(uri) except ValueError: continue if uri.is_filename: loc = os.path.normpath(uri.filename) if os.path.isdir(loc): dirs.append(loc) else: loc = os.path.realpath(loc) if loc not in self.__library: self.__library.add_filename(loc) elif player.can_play_uri(uri): if uri not in self.__library: self.__library.add([RemoteFile(uri)]) else: error = True break ctx.finish(not error, False, etime) if error: ErrorMessage( self, _("Unable to add songs"), _("<b>%s</b> uses an unsupported protocol.") % uri).run() else: if dirs: copool.add( self.__library.scan, dirs, self.__status.bar.progress, cofuncid="library", funcid="library")
def plugin_playlist(self, playlist): # TODO - only get coordinator nodes, somehow self.device: SoCo = soco.discovery.any_soco() device = self.device if not device: qltk.ErrorMessage( app.window, _("Error finding Sonos device(s)"), _("Error finding Sonos. Please check settings")).run() else: sonos_pls = device.get_sonos_playlists() pl_id_to_name = {pl.item_id: pl.title for pl in sonos_pls} print_d("Sonos playlists: %s" % pl_id_to_name) ret = GetSonosPlaylistDialog(pl_id_to_name).run(playlist.name) if ret: spl_id, name = ret if spl_id: spl: DidlPlaylistContainer = next(s for s in sonos_pls if s.item_id == spl_id) print_w(f"Replacing existing Sonos playlist {spl!r}") device.remove_sonos_playlist(spl) print_d(f"Creating new playlist {name!r}") spl = device.create_sonos_playlist(name) task = Task("Sonos", _("Export to Sonos playlist"), stop=self.__cancel_add) copool.add(self.__add_songs, task, playlist.songs, spl, funcid="sonos-playlist-save")
def __drag_data_received(self, widget, ctx, x, y, sel, tid, etime): assert tid == DND_URI_LIST uris = sel.get_uris() dirs = [] error = False for uri in uris: try: uri = URI(uri) except ValueError: continue if uri.is_filename: loc = os.path.normpath(uri.filename) if os.path.isdir(loc): dirs.append(loc) else: loc = os.path.realpath(loc) if loc not in self.__library: self.__library.add_filename(loc) elif app.player.can_play_uri(uri): if uri not in self.__library: self.__library.add([RemoteFile(uri)]) else: error = True break Gtk.drag_finish(ctx, not error, False, etime) if error: ErrorMessage(self, _("Unable to add songs"), _("%s uses an unsupported protocol.") % util.bold(uri)).run() else: if dirs: copool.add(self.__library.scan, dirs, cofuncid="library", funcid="library")
def refresh_cb(button): from quodlibet.library import library from quodlibet.util import copool paths = util.split_scan_dirs(config.get("settings", "scan")) exclude = config.get("library", "exclude").split(":") copool.add(library.rebuild, paths, False, exclude, cofuncid="library", funcid="library")
def _on_songs_received(self, client, songs): print_d(f"Got {len(songs)} songs") self.add(songs) # Can't have multiple requests cancel each other's copools funcid = hash("".join(s["~uri"] for s in songs)) # Rate limit a little to avoid 429s copool.add(self._get_stream_urls, songs, timeout=100, funcid=funcid)
def start_watching(self, paths: Iterable[fsnative]): print_d(f"Setting up file watches on {paths}...", self._name) exclude_dirs = [e for e in get_exclude_dirs() if e] def watching_producer(): # TODO: integrate this better with scanning. for fullpath in paths: desc = _("Adding watches for %s") % (fsn2text(unexpand(fullpath))) with Task(_("Library"), desc) as task: normalised = Path(normalize_path(fullpath, True)).expanduser() if any(Path(exclude) in normalised.parents for exclude in exclude_dirs): continue unpulsed = 0 self.monitor_dir(normalised) for path, dirs, files in os.walk(normalised): normalised = Path(normalize_path(path, True)) for d in dirs: self.monitor_dir(normalised / d) unpulsed += len(dirs) if unpulsed > 50: task.pulse() unpulsed = 0 yield copool.add(watching_producer, funcid="watch_library")
def test_pause_all(self): self.buffer = None copool.add(self.__set_buffer, funcid="test") self._assert_eventually(True) copool.pause_all() self.buffer = None self._assert_never(True)
def test_step(self): copool.add(self.__set_buffer, funcid="test") copool.pause("test") assert copool.step("test") self.go = False assert not copool.step("test") with pytest.raises(ValueError): copool.step("test")
def __destroy(self): config.save() new_dirs = set(get_scan_dirs()) gone_dirs = set(self.current_scan_dirs) - new_dirs if new_dirs - set(self.current_scan_dirs): print_d("Library paths have been added, re-scanning...") scan_library(app.library, force=False) elif gone_dirs: copool.add(app.librarian.remove_roots, gone_dirs)
def open_chooser(self, action): last_dir = self.last_dir if not os.path.exists(last_dir): last_dir = get_home_dir() class MusicFolderChooser(FolderChooser): def __init__(self, parent, init_dir): super(MusicFolderChooser, self).__init__(parent, _("Add Music"), init_dir) cb = Gtk.CheckButton(_("Watch this folder for new songs")) # enable if no folders are being watched cb.set_active(not get_scan_dirs()) cb.show() self.set_extra_widget(cb) def run(self): fns = super(MusicFolderChooser, self).run() cb = self.get_extra_widget() return fns, cb.get_active() class MusicFileChooser(FileChooser): def __init__(self, parent, init_dir): super(MusicFileChooser, self).__init__(parent, _("Add Music"), formats.filter, init_dir) if action.get_name() == "AddFolders": dialog = MusicFolderChooser(self, last_dir) fns, do_watch = dialog.run() dialog.destroy() if fns: fns = map(glib2fsnative, fns) # scan them self.last_dir = fns[0] copool.add(self.__library.scan, fns, cofuncid="library", funcid="library") # add them as library scan directory if do_watch: dirs = get_scan_dirs() for fn in fns: if fn not in dirs: dirs.append(fn) set_scan_dirs(dirs) else: dialog = MusicFileChooser(self, last_dir) fns = dialog.run() dialog.destroy() if fns: fns = map(glib2fsnative, fns) self.last_dir = os.path.dirname(fns[0]) for filename in map(os.path.realpath, fns): self.__library.add_filename(filename)
def test_add_remove_with_funcid(self): copool.add(self.__set_buffer, funcid="test") Gtk.main_iteration_do(False) Gtk.main_iteration_do(False) self.assertEquals(self.buffer, True) copool.remove("test") self.buffer = None Gtk.main_iteration_do(False) Gtk.main_iteration_do(False) self.assertEquals(self.buffer, None)
def test_add_remove(self): copool.add(self.__set_buffer) gtk.main_iteration(block=False) gtk.main_iteration(block=False) self.assertEquals(self.buffer, True) copool.remove(self.__set_buffer) self.buffer = None gtk.main_iteration(block=False) gtk.main_iteration(block=False) self.assertEquals(self.buffer, None)
def test_add_remove_with_funcid(self): copool.add(self.__set_buffer, funcid="test") gtk.main_iteration(block=False) gtk.main_iteration(block=False) self.assertEquals(self.buffer, True) copool.remove("test") self.buffer = None gtk.main_iteration(block=False) gtk.main_iteration(block=False) self.assertEquals(self.buffer, None)
def test_pause_all(self): copool.add(self.__set_buffer, funcid="test") Gtk.main_iteration_do(False) Gtk.main_iteration_do(False) self.failUnless(self.buffer) copool.pause_all() self.buffer = None Gtk.main_iteration_do(False) Gtk.main_iteration_do(False) self.failIf(self.buffer)
def test_add_remove(self): copool.add(self.__set_buffer) Gtk.main_iteration_do(False) Gtk.main_iteration_do(False) self.assertEquals(self.buffer, True) copool.remove(self.__set_buffer) self.buffer = None Gtk.main_iteration_do(False) Gtk.main_iteration_do(False) self.assertEquals(self.buffer, None)
def _add_location(app, value): if os.path.isfile(value): ret = app.library.add_filename(value) if not ret: print_e("Couldn't add file to library") elif os.path.isdir(value): copool.add(app.library.scan, [value], cofuncid="library", funcid="library") else: print_e("Invalid location")
def set_tag(self, tag, library): if not config.getboolean("settings", "eager_search"): return elif tag is None: return elif tag in ("bpm date discnumber isrc originaldate recordingdate " "tracknumber title").split() + MACHINE_TAGS: return elif tag in formats.PEOPLE: tag = "~people" copool.add(self.__fill_tag, tag, library)
def scan_library(library, force): """Start the global library re-scan Args: library (Library) force (bool): if True, reload all existing valid items """ paths = get_scan_dirs() exclude = get_exclude_dirs() copool.add(library.rebuild, paths, force, exclude, cofuncid="library", funcid="library")
def scan_library(library, force): """Start the global library re-scan If `force` is True, reload all existing valid items. """ paths = get_scan_dirs() exclude = split_scan_dirs(config.get("library", "exclude")) exclude = [bytes2fsnative(e) for e in exclude] copool.add(library.rebuild, paths, force, exclude, cofuncid="library", funcid="library")
def __init__(self, library): super(LibraryTagCompletion, self).__init__() try: model = self.__model except AttributeError: model = type(self).__model = gtk.ListStore(str) library.connect('changed', self.__update_song, model) library.connect('added', self.__update_song, model) library.connect('removed', self.__update_song, model) copool.add(self.__build_model, library, model) self.set_model(model) self.set_text_column(0)
def test_pause_resume(self): copool.add(self.__set_buffer) self._assert_eventually(True) copool.pause(self.__set_buffer) self.buffer = None self._assert_never(True) copool.resume(self.__set_buffer) self._assert_eventually(True) copool.remove(self.__set_buffer) self.buffer = None self._assert_never(True)
def plugin_songs(self, songs): def check_songs(): with Task(_("Refresh songs"), _("%d songs") % len(songs)) as task: task.copool(check_songs) for i, song in enumerate(songs): song = song._song if song in app.library: app.library.reload(song) task.update((float(i) + 1) / len(songs)) yield copool.add(check_songs)
def __init__(self, library): super(LibraryTagCompletion, self).__init__() try: model = self.__model except AttributeError: model = type(self).__model = Gtk.ListStore(str) library.connect('changed', self.__update_song, model) library.connect('added', self.__update_song, model) library.connect('removed', self.__update_song, model) copool.add(self.__build_model, library, model) self.set_model(model) self.set_text_column(0)
def open_chooser(self, action): last_dir = self.last_dir if not os.path.exists(last_dir): last_dir = get_home_dir() class MusicFolderChooser(FolderChooser): def __init__(self, parent, init_dir): super(MusicFolderChooser, self).__init__( parent, _("Add Music"), init_dir) cb = Gtk.CheckButton(_("Watch this folder for new songs")) # enable if no folders are being watched cb.set_active(not get_scan_dirs()) cb.show() self.set_extra_widget(cb) def run(self): fns = super(MusicFolderChooser, self).run() cb = self.get_extra_widget() return fns, cb.get_active() class MusicFileChooser(FileChooser): def __init__(self, parent, init_dir): super(MusicFileChooser, self).__init__( parent, _("Add Music"), formats.filter, init_dir) if action.get_name() == "AddFolders": dialog = MusicFolderChooser(self, last_dir) fns, do_watch = dialog.run() dialog.destroy() if fns: fns = map(glib2fsnative, fns) # scan them self.last_dir = fns[0] copool.add(self.__library.scan, fns, cofuncid="library", funcid="library") # add them as library scan directory if do_watch: dirs = get_scan_dirs() for fn in fns: if fn not in dirs: dirs.append(fn) set_scan_dirs(dirs) else: dialog = MusicFileChooser(self, last_dir) fns = dialog.run() dialog.destroy() if fns: fns = map(glib2fsnative, fns) self.last_dir = os.path.dirname(fns[0]) for filename in map(os.path.realpath, fns): self.__library.add_filename(filename)
def test_pause_resume_with_funcid(self): self.buffer = None copool.add(self.__set_buffer, funcid="test") self._assert_eventually(True) copool.pause("test") self.buffer = None self._assert_never(True) copool.resume("test") copool.resume("test") self._assert_eventually(True) copool.remove("test") self.buffer = None self._assert_never(True)
def plugin_songs(self, songs): def check_songs(): desc = numeric_phrase("%d song", "%d songs", len(songs)) with Task(_("Rescan songs"), desc) as task: task.copool(check_songs) for i, song in enumerate(songs): song = song._song if song in app.library: app.library.reload(song) task.update((float(i) + 1) / len(songs)) yield copool.add(check_songs)
def plugin_songs(self, songs): def check_songs(): desc = ngettext("%d song", "%d songs", len(songs)) % len(songs) with Task(_("Refresh songs"), desc) as task: task.copool(check_songs) for i, song in enumerate(songs): song = song._song if song in app.library: app.library.reload(song) task.update((float(i) + 1) / len(songs)) yield copool.add(check_songs)
def __update_visible_rows(self, view, preload): self.__scan_timeout = None vrange = view.get_visible_range() if vrange is None: return model_filter = view.get_model() model = model_filter.get_model() #generate a path list so that cover scanning starts in the middle #of the visible area and alternately moves up and down start, end = vrange # pygtk2.12 sometimes returns empty tuples if not start or not end: return start = start.get_indices()[0] - preload - 1 end = end.get_indices()[0] + preload vlist = range(end, start, -1) top = vlist[:len(vlist) / 2] bottom = vlist[len(vlist) / 2:] top.reverse() vlist_new = [] for i in vlist: if top: vlist_new.append(top.pop()) if bottom: vlist_new.append(bottom.pop()) vlist_new = filter(lambda s: s >= 0, vlist_new) vlist_new = map(Gtk.TreePath, vlist_new) visible_paths = [] for path in vlist_new: model_path = model_filter.convert_path_to_child_path(path) try: row = model[model_path] except TypeError: pass else: if self._row_needs_update(row): visible_paths.append([model, model_path]) if not self.__pending_paths and visible_paths: copool.add(self.__scan_paths) self.__pending_paths = visible_paths
def plugin_playlist(self, playlist): pattern_text = CONFIG.default_pattern dialog = ExportToFolderDialog(self.plugin_window, pattern_text) if dialog.run() == Gtk.ResponseType.OK: directory = dialog.directory_chooser.get_filename() pattern = FileFromPattern(dialog.pattern_entry.get_text()) task = Task("Export", _("Export Playlist to Folder"), stop=self.__cancel_copy) copool.add(self.__copy_songs, task, playlist.songs, directory, pattern, funcid="export-playlist-folder") dialog.destroy()
def open_chooser(self, action): if action.get_name() == "AddFolders": fns = choose_folders(self, _("Add Music"), _("_Add Folders")) if fns: # scan them copool.add(self.__library.scan, fns, cofuncid="library", funcid="library") else: patterns = ["*" + path2fsn(k) for k in formats.loaders.keys()] choose_filter = create_chooser_filter(_("Music Files"), patterns) fns = choose_files( self, _("Add Music"), _("_Add Files"), choose_filter) if fns: for filename in fns: self.__library.add_filename(filename)
def plugin_playlist(self, playlist): self.init_server() if not self.server.is_connected: qltk.ErrorMessage( app.window, _("Error finding Squeezebox server"), _("Error finding %s. Please check settings") % self.server.config ).run() else: name = self.__get_playlist_name(name=playlist.name) if name: task = Task("Squeezebox", _("Export to Squeezebox playlist"), stop=self.__cancel_add) copool.add(self.__add_songs, task, playlist.songs, name, funcid="squeezebox-playlist-save")
def plugin_playlist(self, playlist): self.init_server() if not self.server.is_connected: qltk.ErrorMessage( None, _("Error finding Squeezebox server"), _("Error finding %s. Please check settings") % self.server.config ).run() else: name = self.__get_playlist_name(name=playlist.name) if name: task = Task("Squeezebox", _("Export to Squeezebox playlist"), stop=self.__cancel_add) copool.add(self.__add_songs, task, playlist.songs, name, funcid="squeezebox-playlist-save")
def scan_library(library, force): """Start the global library re-scan Args: library (Library) force (bool): if True, reload all existing valid items TODO: consider storing scan_dirs in Library instead of passing around always """ paths = get_scan_dirs() exclude = get_exclude_dirs() copool.add(library.rebuild, paths, force, exclude, cofuncid="library", funcid="library")
def __update_visible_rows(self, view, preload): vrange = view.get_visible_range() if vrange is None: return model = view.get_model() # Generate a path list so that cover scanning starts in the middle # of the visible area and alternately moves up and down. start, end = vrange # pygtk2.12 sometimes returns empty tuples if not start or not end: return start = start.get_indices()[0] - preload - 1 end = end.get_indices()[0] + preload vlist = range(end, start, -1) top = vlist[:len(vlist) / 2] bottom = vlist[len(vlist) / 2:] top.reverse() vlist_new = [] for i in vlist: if top: vlist_new.append(top.pop()) if bottom: vlist_new.append(bottom.pop()) vlist_new = filter(lambda s: s >= 0, vlist_new) vlist_new = map(Gtk.TreePath, vlist_new) visible_paths = [] for path in vlist_new: try: iter_ = model.get_iter(path) except ValueError: continue if self._row_needs_update(model, iter_): visible_paths.append((model, path)) if not self.__pending_paths and visible_paths: copool.add(self.__scan_paths) self.__pending_paths = visible_paths
def __update_visible_rows(self, view, preload): vrange = view.get_visible_range() if vrange is None: return model = view.get_model() # Generate a path list so that cover scanning starts in the middle # of the visible area and alternately moves up and down. start, end = vrange # pygtk2.12 sometimes returns empty tuples if not start or not end: return start = start.get_indices()[0] - preload - 1 end = end.get_indices()[0] + preload vlist = list(range(end, start, -1)) top = vlist[:len(vlist) // 2] bottom = vlist[len(vlist) // 2:] top.reverse() vlist_new = [] for i in vlist: if top: vlist_new.append(top.pop()) if bottom: vlist_new.append(bottom.pop()) vlist_new = filter(lambda s: s >= 0, vlist_new) vlist_new = map(Gtk.TreePath, vlist_new) visible_paths = [] for path in vlist_new: try: iter_ = model.get_iter(path) except ValueError: continue if self._row_needs_update(model, iter_): visible_paths.append((model, path)) if not self.__pending_paths and visible_paths: copool.add(self.__scan_paths) self.__pending_paths = visible_paths
def open_chooser(self, action): if not os.path.exists(self.last_dir): self.last_dir = const.HOME if action.get_name() == "AddFolders": chooser = FolderChooser(self, _("Add Music"), self.last_dir) cb = gtk.CheckButton(_("Watch this folder for new songs")) cb.set_active(not config.get("settings", "scan")) cb.show() chooser.set_extra_widget(cb) else: chooser = FileChooser( self, _("Add Music"), formats.filter, self.last_dir) cb = None fns = chooser.run() chooser.destroy() if fns: if action.get_name() == "AddFolders": self.last_dir = fns[0] copool.add(self.__library.scan, fns, funcid="library") else: self.last_dir = os.path.basename(fns[0]) for filename in map(os.path.realpath, map(util.fsnative, fns)): if filename in self.__library: continue song = self.__library.add_filename(filename) if not song: from traceback import format_exception_only as feo tb = feo(sys.last_type, sys.last_value) msg = _("%s could not be added to your library.\n\n") msg %= util.escape(util.fsdecode( os.path.basename(filename))) msg += util.escape("".join(tb).decode( const.ENCODING, "replace")) d = ErrorMessage(self, _("Unable to add song"), msg) d.label.set_selectable(True) d.run() continue if cb and cb.get_active(): dirs = util.split_scan_dirs(config.get("settings", "scan")) for fn in fns: if fn not in dirs: dirs.append(fn) dirs = ":".join(dirs) config.set("settings", "scan", dirs)
def test_pause_resume_with_funcid(self): copool.add(self.__set_buffer, funcid="test") Gtk.main_iteration_do(False) Gtk.main_iteration_do(False) copool.pause("test") self.buffer = None Gtk.main_iteration_do(False) Gtk.main_iteration_do(False) self.assertEquals(self.buffer, None) copool.resume("test") copool.resume("test") Gtk.main_iteration_do(False) Gtk.main_iteration_do(False) self.assertEquals(self.buffer, True) copool.remove("test") self.buffer = None Gtk.main_iteration_do(False) Gtk.main_iteration_do(False)
def test_pause_restart_pause(self): copool.add(self.__set_buffer, funcid="test") gtk.main_iteration(block=False) gtk.main_iteration(block=False) self.failUnless(self.buffer) copool.pause("test") self.buffer = None gtk.main_iteration(block=False) gtk.main_iteration(block=False) self.failIf(self.buffer) copool.add(self.__set_buffer, funcid="test") gtk.main_iteration(block=False) gtk.main_iteration(block=False) self.failUnless(self.buffer) copool.pause("test") self.buffer = None gtk.main_iteration(block=False) gtk.main_iteration(block=False) self.failIf(self.buffer)
def __update_visible_rows(self, view, preload): vrange = view.get_visible_range() if vrange is None: return model_filter = view.get_model() model = model_filter.get_model() #generate a path list so that cover scanning starts in the middle #of the visible area and alternately moves up and down start, end = vrange # pygtk2.12 sometimes returns empty tuples if not start or not end: return start = start[0] - preload - 1 end = end[0] + preload vlist = range(end, start, -1) top = vlist[:len(vlist)/2] bottom = vlist[len(vlist)/2:] top.reverse() vlist_new = [] for i in vlist: if top: vlist_new.append(top.pop()) if bottom: vlist_new.append(bottom.pop()) vlist_new = filter(lambda s: s >= 0, vlist_new) visible_paths = [] for path in vlist_new: model_path = model_filter.convert_path_to_child_path(path) try: row = model[model_path] except TypeError: pass else: if self._row_needs_update(row): visible_paths.append([model, model_path]) if not self.__pending_paths and visible_paths: copool.add(self.__scan_paths) self.__pending_paths = visible_paths
def __move(self, widget): selection = self.view.get_selection() model, paths = selection.get_selected_rows() rows = [model[p] for p in paths or []] if len(rows) > 1: print_w("Can't do multiple moves at once") return elif not rows: return base_dir = rows[0][0] chooser = _get_chooser(_("Select This Directory"), _("_Cancel")) chooser.set_title( _("Select Actual / New Directory for {dir!r}").format( dir=base_dir)) chooser.set_action(Gtk.FileChooserAction.SELECT_FOLDER) chooser.set_local_only(True) chooser.set_select_multiple(False) results = _run_chooser(self, chooser) if not results: return new_dir = results[0] desc = (_("This will move QL metadata:\n\n" "{old!r} -> {new!r}\n\n" "The audio files themselves are not moved by this.\n" "Nonetheless, a backup is recommended " "(including the Quodlibet 'songs' file)").format( old=base_dir, new=new_dir)) title = _("Move scan root {dir!r}?").format(dir=base_dir) value = ConfirmationPrompt(self, title=title, description=desc, ok_button_text=_("OK, move it!")).run() if value != ConfirmationPrompt.RESPONSE_INVOKE: print_d("User aborted") return print_d(f"Migrate from {base_dir} -> {new_dir}") copool.add(app.librarian.move_root, base_dir, new_dir) path = paths[0] self.model[path] = [new_dir] self.model.row_changed(path, rows[0].iter) self.__save()
def download_cb(menu_item): songs = relevant total = len(songs) msg = ngettext("Download {name!r} to", "Download {total} files to", total) msg = msg.format( name=next(iter(songs))("title")[:99] if total else "?", total=total) chooser = folder_chooser or choose_folders paths = chooser(None, msg, _("Download here"), allow_multiple=False) if not paths: print_d("Cancelling download") return path = paths[0] progress = DownloadProgress(songs) progress.connect('finished', _finished) copool.add(progress.download_songs, path)
def enable_periodic_save(save_library): import quodlibet.library from quodlibet.util import copool from quodlibet import config timeout = 5 * 60 * 1000 # 5 minutes def periodic_config_save(): while 1: config.save(quodlibet.const.CONFIG) yield copool.add(periodic_config_save, timeout=timeout) if not save_library: return def periodic_library_save(): while 1: quodlibet.library.save() yield copool.add(periodic_library_save, timeout=timeout)