class TDirectoryTree(TestCase): if os.name == "nt": ROOTS = [get_home_dir(), u"C:\\"] else: ROOTS = [get_home_dir(), "/"] def setUp(self): quodlibet.config.init() def tearDown(self): quodlibet.config.quit() def test_initial(self): paths = ["/", get_home_dir(), "/usr/bin"] if os.name == "nt": paths = [u"C:\\", get_home_dir()] for path in paths: dirlist = DirectoryTree(path, folders=self.ROOTS) model, rows = dirlist.get_selection().get_selected_rows() selected = [model[row][0] for row in rows] dirlist.destroy() self.failUnlessEqual([path], selected) def test_bad_initial(self): invalid = os.path.join("bin", "file", "does", "not", "exist") for path in self.ROOTS: newpath = os.path.join(path, invalid) dirlist = DirectoryTree(newpath, folders=self.ROOTS) selected = dirlist.get_selected_paths() dirlist.destroy() # select the last valid parent directory self.assertEqual(len(selected), 1) self.assertTrue(selected[0].startswith(path)) def test_bad_go_to(self): newpath = fsnative(u"/woooooo/bar/fun/broken") dirlist = DirectoryTree(fsnative(u"/"), folders=self.ROOTS) dirlist.go_to(newpath) dirlist.destroy() def test_main(self): folders = ["/"] if os.name == "nt": folders = [u"C:\\"] main = MainDirectoryTree(folders=folders) self.assertTrue(len(main.get_model())) main = MainDirectoryTree() self.assertTrue(len(main.get_model())) def test_get_drives(self): for path in get_drives(): self.assertTrue(is_fsnative(path))
def test_initial(self): paths = ["/", get_home_dir(), "/usr/bin"] if os.name == "nt": paths = [u"C:\\", get_home_dir()] for path in paths: dirlist = DirectoryTree(path, folders=self.ROOTS) model, rows = dirlist.get_selection().get_selected_rows() selected = [model[row][0] for row in rows] dirlist.destroy() self.failUnlessEqual([path], selected)
def test_initial(self): if os.name == "nt": paths = [u"C:\\", get_home_dir()] else: paths = ["/", get_home_dir(), sys.prefix] for path in paths: dirlist = DirectoryTree(path, folders=self.ROOTS) model, rows = dirlist.get_selection().get_selected_rows() selected = [model[row][0] for row in rows] dirlist.destroy() self.failUnlessEqual([os.path.normpath(path)], selected)
def get_init_select_dir(): scandirs = get_scan_dirs() if scandirs and os.path.isdir(scandirs[-1]): # start with last added directory return scandirs[-1] else: return get_home_dir()
def __get_themes(self): # deprecated, but there is no public replacement with warnings.catch_warnings(): warnings.simplefilter("ignore") theme_dir = Gtk.rc_get_theme_dir() theme_dirs = [theme_dir, os.path.join(get_home_dir(), ".themes")] themes = set() for theme_dir in theme_dirs: try: subdirs = os.listdir(theme_dir) except OSError: continue for dir_ in subdirs: gtk_dir = os.path.join(theme_dir, dir_, "gtk-3.0") if os.path.isdir(gtk_dir): themes.add(dir_) try: resource_themes = Gio.resources_enumerate_children( "/org/gtk/libgtk/theme", 0) except GLib.GError: pass else: themes.update([t.rstrip("/") for t in resource_themes]) return themes
def test_get_exclude_dirs(self): some_path = os.path.join(get_home_dir(), "foo") if os.name != "nt": some_path = unexpand(some_path) config.set('library', 'exclude', some_path) assert expanduser(some_path) in get_exclude_dirs() assert all([isinstance(p, fsnative) for p in get_exclude_dirs()])
def test_get_scan_dirs(self): some_path = os.path.join(get_home_dir(), "foo") if os.name != "nt": some_path = unexpand(some_path) config.set('settings', 'scan', some_path) assert expanduser(some_path) in get_scan_dirs() assert all([isinstance(p, fsnative) for p in get_scan_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 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 get_init_select_dir(): """Returns a path which might be a good starting point when browsing for a path for library scanning. Returns: fsnative """ scandirs = get_scan_dirs() if scandirs and os.path.isdir(scandirs[-1]): # start with last added directory return scandirs[-1] else: return get_home_dir()
def __get_themes(self): # deprecated, but there is no public replacement with warnings.catch_warnings(): warnings.simplefilter("ignore") theme_dir = Gtk.rc_get_theme_dir() theme_dirs = [theme_dir, os.path.join(get_home_dir(), ".themes")] themes = set() for theme_dir in theme_dirs: try: subdirs = os.listdir(theme_dir) except OSError: continue for dir_ in subdirs: gtk_dir = os.path.join(theme_dir, dir_, "gtk-3.0") if os.path.isdir(gtk_dir): themes.add(dir_) return themes
def get_favorites(): """A list of paths of commonly used folders (Desktop,..) Paths don't have to exist. """ if os.name == "nt": return _get_win_favorites() else: paths = [get_home_dir()] xfg_user_dirs = xdg_get_user_dirs() for key in ["XDG_DESKTOP_DIR", "XDG_DOWNLOAD_DIR", "XDG_MUSIC_DIR"]: if key in xfg_user_dirs: path = xfg_user_dirs[key] if path not in paths: paths.append(path) return paths
def __import(self, activator, library): filt = lambda fn: fn.endswith(".pls") or fn.endswith(".m3u") from quodlibet.qltk.chooser import FileChooser chooser = FileChooser(self, _("Import Playlist"), filt, get_home_dir()) files = chooser.run() chooser.destroy() for filename in files: if filename.endswith(".m3u"): playlist = parse_m3u(filename, library=library) elif filename.endswith(".pls"): playlist = parse_pls(filename, library=library) else: qltk.ErrorMessage( qltk.get_top_parent(self), _("Unable to import playlist"), _("Quod Libet can only import playlists in the M3U " "and PLS formats.")).run() return self.changed(playlist) library.add(playlist)
def get_gtk_bookmarks(): """A list of paths from the GTK+ bookmarks. The paths don't have to exist. Returns: List[fsnative] """ if os.name == "nt": return [] path = os.path.join(get_home_dir(), ".gtk-bookmarks") folders = [] try: with open(path, "rb") as f: folders = parse_gtk_bookmarks(f.read()) except (EnvironmentError, ValueError): pass return folders
def test_lyric_filename_search_order_priority(self): """test custom lyrics order priority""" with self.lyric_filename_test_setup() as ts: root2 = os.path.join(get_home_dir(), ".lyrics") # built-in default fp2 = os.path.join(root2, ts["artist"] + " - " + ts["title"] + ".lyric") p2 = os.path.dirname(fp2) mkdir(p2) with io.open(fp2, "w", encoding='utf-8') as f: f.write(u"") fp = os.path.join(ts.root, ts["artist"] + " - " + ts["title"] + ".lyric") with io.open(fp, "w", encoding='utf-8') as f: f.write(u"") mkdir(p2) search = ts.lyric_filename os.remove(fp2) os.rmdir(p2) os.remove(fp) self.assertEqual(search, fp)
def __import(self, activator, library): filt = lambda fn: fn.endswith(".pls") or fn.endswith(".m3u") from quodlibet.qltk.chooser import FileChooser chooser = FileChooser(self, _("Import Playlist"), filt, get_home_dir()) files = chooser.run() chooser.destroy() for filename in files: if filename.endswith(".m3u"): playlist = parse_m3u(filename, library=library) elif filename.endswith(".pls"): playlist = parse_pls(filename, library=library) else: qltk.ErrorMessage( qltk.get_top_parent(self), _("Unable to import playlist"), _("Quod Libet can only import playlists in the M3U " "and PLS formats.")).run() return PlaylistsBrowser.changed(playlist) library.add(playlist)
def __get_themes(self): # deprecated, but there is no public replacement with warnings.catch_warnings(): warnings.simplefilter("ignore") theme_dir = Gtk.rc_get_theme_dir() theme_dirs = [theme_dir, os.path.join(get_home_dir(), ".themes")] theme_dirs += [ os.path.join(d, "themes") for d in xdg_get_system_data_dirs() ] def is_valid_teme_dir(path): """If the path contains a theme for the running gtk version""" major = qltk.gtk_version[0] minor = qltk.gtk_version[1] names = ["gtk-%d.%d" % (major, m) for m in range(minor, -1, -1)] for name in names: if os.path.isdir(os.path.join(path, name)): return True return False themes = set() for theme_dir in set(theme_dirs): try: subdirs = os.listdir(theme_dir) except OSError: continue for dir_ in subdirs: if is_valid_teme_dir(os.path.join(theme_dir, dir_)): themes.add(dir_) try: resource_themes = Gio.resources_enumerate_children( "/org/gtk/libgtk/theme", 0) except GLib.GError: pass else: themes.update([t.rstrip("/") for t in resource_themes]) return themes
def get_current_dir(): """Returns the currently active chooser directory path. The path might not actually exist. Returns: fsnative """ data = config.getbytes("memory", "chooser_dir", b"") try: path = bytes2fsn(data, "utf-8") or None except ValueError: path = None # the last user dir might not be there any more, try showing a parent # instead if path is not None: path = find_nearest_dir(path) if path is None: path = get_home_dir() return path
def get_gtk_bookmarks(): """A list of paths from the GTK+ bookmarks. The paths don't have to exist. """ if os.name == "nt": return [] path = os.path.join(get_home_dir(), ".gtk-bookmarks") folders = [] try: with open(path, "rb") as f: for line in f.readlines(): parts = line.split() if not parts: continue folder_url = parts[0] folders.append(urlparse.urlsplit(folder_url)[2]) except EnvironmentError: pass return folders
def get_gtk_bookmarks(): """A list of paths from the GTK+ bookmarks. The paths don't have to exist. """ if os.name == "nt": return [] path = os.path.join(get_home_dir(), ".gtk-bookmarks") folders = [] try: with open(path, "rb") as f: for line in f.readlines(): parts = line.split() if not parts: continue folder_url = parts[0] folders.append(urlsplit(folder_url)[2]) except EnvironmentError: pass return folders
def test_get_home_dir(self): self.assertTrue(isinstance(get_home_dir(), fsnative)) self.assertTrue(os.path.isabs(get_home_dir()))
def test_get_exclude_dirs(self): some_path = os.path.join(unexpand(get_home_dir()), "foo") config.set('library', 'exclude', some_path) assert expanduser(some_path) in get_exclude_dirs() assert all([isinstance(p, fsnative) for p in get_exclude_dirs()])
def lyric_filename(self): """Returns the validated, or default, lyrics filename for this file. User defined '[memory] lyric_rootpaths' and '[memory] lyric_filenames' matches take precedence""" from quodlibet.pattern \ import ArbitraryExtensionFileFromPattern as expand_patterns rx_params = re.compile(r'[^\\]<[^' + re.escape(os.sep) + r']*[^\\]>') def expand_pathfile(rpf): """Return the expanded RootPathFile""" expanded = [] root = expanduser(rpf.root) pathfile = expanduser(rpf.pathfile) if rx_params.search(pathfile): root = expand_patterns(root).format(self) pathfile = expand_patterns(pathfile).format(self) rpf = RootPathFile(root, pathfile) expanded.append(rpf) if not os.path.exists(pathfile) and is_windows(): # prioritise a special character encoded version # # most 'alien' chars are supported for 'nix fs paths, and we # only pass the proposed path through 'escape_filename' (which # apparently doesn't respect case) if we don't care about case! # # FIX: assumes 'nix build used on a case-sensitive fs, nt case # insensitive. clearly this is not biting anyone though (yet!) pathfile = os.path.sep.join([rpf.root, rpf.end_escaped]) rpf = RootPathFile(rpf.root, pathfile) expanded.insert(len(expanded) - 1, rpf) return expanded def sanitise(sep, parts): """Return a santisied version of a path's parts""" return sep.join(part.replace(os.path.sep, u'')[:128] for part in parts) # setup defaults (user-defined take precedence) # root search paths lyric_paths = \ config.getstringlist("memory", "lyric_rootpaths", []) # ensure default paths lyric_paths.append(os.path.join(get_home_dir(), ".lyrics")) lyric_paths.append( os.path.join(os.path.dirname(self.comma('~filename')))) # search pathfile names lyric_filenames = \ config.getstringlist("memory", "lyric_filenames", []) # ensure some default pathfile names lyric_filenames.append( sanitise(os.sep, [(self.comma("lyricist") or self.comma("artist")), self.comma("title")]) + u'.lyric') lyric_filenames.append( sanitise(' - ', [(self.comma("lyricist") or self.comma("artist")), self.comma("title")]) + u'.lyric') # generate all potential paths (unresolved/unexpanded) pathfiles = OrderedDict() for r in lyric_paths: for f in lyric_filenames: pathfile = os.path.join(r, os.path.dirname(f), fsnative(os.path.basename(f))) rpf = RootPathFile(r, pathfile) if not pathfile in pathfiles: pathfiles[pathfile] = rpf #print_d("searching for lyrics in:\n%s" % '\n'.join(pathfiles.keys())) # expand each raw pathfile in turn and test for existence match_ = "" pathfiles_expanded = OrderedDict() for pf, rpf in pathfiles.items(): for rpf in expand_pathfile(rpf): # resolved as late as possible pathfile = rpf.pathfile pathfiles_expanded[pathfile] = rpf if os.path.exists(pathfile): match_ = pathfile break if match_ != "": break if not match_: # search even harder! lyric_extensions = ['lyric', 'lyrics', '', 'txt'] #print_d("extending search to extensions: %s" % lyric_extensions) def generate_mod_ext_paths(pathfile): # separate pathfile's extension (if any) ext = os.path.splitext(pathfile)[1][1:] path = pathfile[:-1 * len(ext)].strip('.') if ext else pathfile # skip the proposed lyric extension if it is the same as # the original for a given search pathfile stub - it has # already been tested without success! extra_extensions = [x for x in lyric_extensions if x != ext] # join valid new extensions to pathfile stub and return return ['.'.join([path, ext]) if ext else path for ext in extra_extensions] # look for a match by modifying the extension for each of the # (now fully resolved) 'pathfiles_expanded' search items for pathfile in pathfiles_expanded.keys(): # get alternatives for existence testing paths_mod_ext = generate_mod_ext_paths(pathfile) for path_ext in paths_mod_ext: if os.path.exists(path_ext): # persistence has paid off! #print_d("extended search match!") match_ = path_ext break if match_: break if not match_: # default match_ = list(pathfiles_expanded.keys())[0] return match_
class TDirectoryTree(TestCase): if os.name == "nt": ROOTS = [get_home_dir(), u"C:\\"] else: ROOTS = [get_home_dir(), "/"] def setUp(self): quodlibet.config.init() def tearDown(self): quodlibet.config.quit() def test_initial(self): if os.name == "nt": paths = [u"C:\\", get_home_dir()] else: paths = ["/", get_home_dir(), sys.prefix] for path in paths: dirlist = DirectoryTree(path, folders=self.ROOTS) model, rows = dirlist.get_selection().get_selected_rows() selected = [model[row][0] for row in rows] dirlist.destroy() self.failUnlessEqual([os.path.normpath(path)], selected) def test_bad_initial(self): invalid = os.path.join("bin", "file", "does", "not", "exist") for path in self.ROOTS: newpath = os.path.join(path, invalid) dirlist = DirectoryTree(newpath, folders=self.ROOTS) selected = dirlist.get_selected_paths() dirlist.destroy() # select the last valid parent directory self.assertEqual(len(selected), 1) self.assertTrue(selected[0].startswith(path)) def test_bad_go_to(self): newpath = fsnative(u"/woooooo/bar/fun/broken") dirlist = DirectoryTree(fsnative(u"/"), folders=self.ROOTS) dirlist.go_to(newpath) dirlist.destroy() def test_main(self): folders = ["/"] if os.name == "nt": folders = [u"C:\\"] main = MainDirectoryTree(folders=folders) self.assertTrue(len(main.get_model())) main = MainDirectoryTree() self.assertTrue(len(main.get_model())) def test_get_drives(self): for path in get_drives(): self.assertTrue(isinstance(path, fsnative)) def test_popup(self): dt = DirectoryTree(None, folders=self.ROOTS) menu = dt._create_menu() dt._popup_menu(menu) children = menu.get_children() self.failUnlessEqual(len(children), 4) delete = children[1] self.failUnlessEqual(delete.get_label(), __("_Delete")) self.failUnless(delete.get_sensitive()) def test_multiple_selections(self): dt = DirectoryTree(None, folders=self.ROOTS) menu = dt._create_menu() dt._popup_menu(menu) children = menu.get_children() select_sub = children[3] self.failUnless("sub-folders" in select_sub.get_label().lower()) self.failUnless(select_sub.get_sensitive()) sel = dt.get_selection() model = dt.get_model() for it, pth in model.iterrows(None): sel.select_iter(it) self.failUnless(select_sub.get_sensitive(), msg="Select All should work for multiple") self.failIf(children[0].get_sensitive(), msg="New Folder should be disabled for multiple") self.failUnless(children[3].get_sensitive(), msg="Refresh should be enabled for multiple")
def __init__(self, library, player, headless=False, restore_cb=None): super(QuodLibetWindow, self).__init__(dialog=False) self.last_dir = get_home_dir() self.__destroyed = False self.__update_title(player) self.set_default_size(550, 450) main_box = Gtk.VBox() self.add(main_box) # create main menubar, load/restore accelerator groups self.__library = library ui = self.__create_menu(player, library) accel_group = ui.get_accel_group() self.add_accel_group(accel_group) def scroll_and_jump(*args): self.__jump_to_current(True, True) keyval, mod = Gtk.accelerator_parse("<control><shift>J") accel_group.connect(keyval, mod, 0, scroll_and_jump) # dbus app menu # Unity puts the app menu next to our menu bar. Since it only contains # menu items also available in the menu bar itself, don't add it. if not util.is_unity(): AppMenu(self, ui.get_action_groups()[0]) # custom accel map accel_fn = os.path.join(quodlibet.get_user_dir(), "accels") Gtk.AccelMap.load(accel_fn) # save right away so we fill the file with example comments of all # accels Gtk.AccelMap.save(accel_fn) menubar = ui.get_widget("/Menu") # Since https://git.gnome.org/browse/gtk+/commit/?id=b44df22895c79 # toplevel menu items show an empty 16x16 image. While we don't # need image items there UIManager creates them by default. # Work around by removing the empty GtkImages for child in menubar.get_children(): if isinstance(child, Gtk.ImageMenuItem): child.set_image(None) main_box.pack_start(menubar, False, True, 0) # get the playlist up before other stuff self.songlist = MainSongList(library, player) self.songlist.show_all() self.songlist.connect("key-press-event", self.__songlist_key_press) self.songlist.connect_after('drag-data-received', self.__songlist_drag_data_recv) self.song_scroller = SongListScroller( ui.get_widget("/Menu/View/SongList")) self.song_scroller.add(self.songlist) self.qexpander = QueueExpander(ui.get_widget("/Menu/View/Queue"), library, player) self.playlist = PlaylistMux(player, self.qexpander.model, self.songlist.model) top_bar = TopBar(self, player, library) main_box.pack_start(top_bar, False, True, 0) self.top_bar = top_bar self.__browserbox = Align(bottom=3) main_box.pack_start(self.__browserbox, True, True, 0) statusbox = StatusBarBox(self.songlist.model, player) self.order = statusbox.order self.repeat = statusbox.repeat self.statusbar = statusbox.statusbar main_box.pack_start(Align(statusbox, border=3, top=-3, right=3), False, True, 0) self.songpane = ConfigRVPaned("memory", "queue_position", 0.75) self.songpane.pack1(self.song_scroller, resize=True, shrink=False) self.songpane.pack2(self.qexpander, resize=True, shrink=False) self.__handle_position = self.songpane.get_property("position") def songpane_button_press_cb(pane, event): """If we start to drag the pane handle while the queue expander is unexpanded, expand it and move the handle to the bottom, so we can 'drag' the queue out """ if event.window != pane.get_handle_window(): return False if not self.qexpander.get_expanded(): self.qexpander.set_expanded(True) pane.set_relative(1.0) return False self.songpane.connect("button-press-event", songpane_button_press_cb) self.song_scroller.connect('notify::visible', self.__show_or) self.qexpander.connect('notify::visible', self.__show_or) self.qexpander.connect('notify::expanded', self.__expand_or) self.qexpander.connect('draw', self.__qex_size_allocate) self.songpane.connect('notify', self.__moved_pane_handle) try: orders = [] for e in config.getstringlist('memory', 'sortby', []): orders.append((e[1:], int(e[0]))) except ValueError: pass else: self.songlist.set_sort_orders(orders) self.browser = None self.ui = ui main_box.show_all() self._playback_error_dialog = None connect_destroy(player, 'song-started', self.__song_started) connect_destroy(player, 'paused', self.__update_paused, True) connect_destroy(player, 'unpaused', self.__update_paused, False) # make sure we redraw all error indicators before opening # a dialog (blocking the main loop), so connect after default handlers connect_after_destroy(player, 'error', self.__player_error) # connect after to let SongTracker update stats connect_after_destroy(player, "song-ended", self.__song_ended) # set at least the playlist. the song should be restored # after the browser emits the song list player.setup(self.playlist, None, 0) self.__restore_cb = restore_cb self.__first_browser_set = True restore_browser = not headless try: self.select_browser(self, config.get("memory", "browser"), library, player, restore_browser) except: config.set("memory", "browser", browsers.name(0)) config.save() raise self.showhide_playlist(ui.get_widget("/Menu/View/SongList")) self.showhide_playqueue(ui.get_widget("/Menu/View/Queue")) self.songlist.connect('popup-menu', self.__songs_popup_menu) self.songlist.connect('columns-changed', self.__cols_changed) self.songlist.connect('columns-changed', self.__hide_headers) self.songlist.info.connect("changed", self.__set_time) lib = library.librarian connect_destroy(lib, 'changed', self.__song_changed, player) targets = [("text/uri-list", Gtk.TargetFlags.OTHER_APP, DND_URI_LIST)] targets = [Gtk.TargetEntry.new(*t) for t in targets] self.drag_dest_set(Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY) self.connect('drag-data-received', self.__drag_data_received) if not headless: on_first_map(self, self.__configure_scan_dirs, library) if config.getboolean('library', 'refresh_on_start'): self.__rebuild(None, False) self.connect("key-press-event", self.__key_pressed, player) self.connect("destroy", self.__destroy) self.enable_window_tracking("quodlibet")
def test_get_home_dir(self): self.assertTrue(is_fsnative(get_home_dir())) self.assertTrue(os.path.isabs(get_home_dir()))
def lyric_filename(self): """Returns the validated, or default, lyrics filename for this file. User defined '[memory] lyric_rootpaths' and '[memory] lyric_filenames' matches take precedence""" from quodlibet.pattern \ import ArbitraryExtensionFileFromPattern as expand_patterns rx_params = re.compile(r'[^\\]<[^' + re.escape(os.sep) + r']*[^\\]>') def expand_pathfile(rpf): """Return the expanded RootPathFile""" expanded = [] root = expanduser(rpf.root) pathfile = expanduser(rpf.pathfile) if rx_params.search(pathfile): root = expand_patterns(root).format(self) pathfile = expand_patterns(pathfile).format(self) rpf = RootPathFile(root, pathfile) expanded.append(rpf) if not os.path.exists(pathfile) and is_windows(): # prioritise a special character encoded version # # most 'alien' chars are supported for 'nix fs paths, and we # only pass the proposed path through 'escape_filename' (which # apparently doesn't respect case) if we don't care about case! # # FIX: assumes 'nix build used on a case-sensitive fs, nt case # insensitive. clearly this is not biting anyone though (yet!) pathfile = os.path.sep.join([rpf.root, rpf.end_escaped]) rpf = RootPathFile(rpf.root, pathfile) expanded.insert(len(expanded) - 1, rpf) return expanded def sanitise(sep, parts): """Return a santisied version of a path's parts""" return sep.join( part.replace(os.path.sep, u'')[:128] for part in parts) # setup defaults (user-defined take precedence) # root search paths lyric_paths = \ config.getstringlist("memory", "lyric_rootpaths", []) # ensure default paths lyric_paths.append(os.path.join(get_home_dir(), ".lyrics")) lyric_paths.append( os.path.join(os.path.dirname(self.comma('~filename')))) # search pathfile names lyric_filenames = \ config.getstringlist("memory", "lyric_filenames", []) # ensure some default pathfile names lyric_filenames.append( sanitise(os.sep, [(self.comma("lyricist") or self.comma("artist")), self.comma("title")]) + u'.lyric') lyric_filenames.append( sanitise(' - ', [(self.comma("lyricist") or self.comma("artist")), self.comma("title")]) + u'.lyric') # generate all potential paths (unresolved/unexpanded) pathfiles = OrderedDict() for r in lyric_paths: for f in lyric_filenames: pathfile = os.path.join(r, os.path.dirname(f), fsnative(os.path.basename(f))) rpf = RootPathFile(r, pathfile) if not pathfile in pathfiles: pathfiles[pathfile] = rpf #print_d("searching for lyrics in:\n%s" % '\n'.join(pathfiles.keys())) # expand each raw pathfile in turn and test for existence match_ = "" pathfiles_expanded = OrderedDict() for pf, rpf in pathfiles.items(): for rpf in expand_pathfile(rpf): # resolved as late as possible pathfile = rpf.pathfile pathfiles_expanded[pathfile] = rpf if os.path.exists(pathfile): match_ = pathfile break if match_ != "": break if not match_: # search even harder! lyric_extensions = ['lyric', 'lyrics', '', 'txt'] #print_d("extending search to extensions: %s" % lyric_extensions) def generate_mod_ext_paths(pathfile): # separate pathfile's extension (if any) ext = os.path.splitext(pathfile)[1][1:] path = pathfile[:-1 * len(ext)].strip('.') if ext else pathfile # skip the proposed lyric extension if it is the same as # the original for a given search pathfile stub - it has # already been tested without success! extra_extensions = [x for x in lyric_extensions if x != ext] # join valid new extensions to pathfile stub and return return [ '.'.join([path, ext]) if ext else path for ext in extra_extensions ] # look for a match by modifying the extension for each of the # (now fully resolved) 'pathfiles_expanded' search items for pathfile in pathfiles_expanded.keys(): # get alternatives for existence testing paths_mod_ext = generate_mod_ext_paths(pathfile) for path_ext in paths_mod_ext: if os.path.exists(path_ext): # persistence has paid off! #print_d("extended search match!") match_ = path_ext break if match_: break if not match_: # default match_ = list(pathfiles_expanded.keys())[0] return match_
class ExportSavedSearches(EventPlugin, PluginConfigMixin): PLUGIN_ID = 'ExportSavedSearches' PLUGIN_NAME = _('Export saved searches') PLUGIN_DESC = _('Exports saved searches to M3U playlists.') PLUGIN_ICON = Icons.DOCUMENT_SAVE_AS REQUIRES_ACTION = True lastfolder = get_home_dir() @classmethod def PluginPreferences(self, parent): vbox = Gtk.VBox(spacing=6) queries = {} query_path = os.path.join(get_user_dir(), 'lists', 'queries.saved') with open(query_path, 'rU', encoding='utf-8') as query_file: for query_string in query_file: name = next(query_file).strip() queries[name] = Query(query_string.strip()) for query_name, query in queries.items(): check_button = ConfigCheckButton((query_name), "plugins", self._config_key(query_name)) check_button.set_active(self.config_get_bool(query_name)) vbox.pack_start(check_button, False, True, 0) chooserButton = Gtk.FileChooserButton(_('Select destination folder')) chooserButton.set_current_folder(self.lastfolder) chooserButton.set_action(Gtk.FileChooserAction.SELECT_FOLDER) # https://stackoverflow.com/a/14742779/109813 def get_actual_filename(name): # Do nothing except on Windows if os.name != 'nt': return name dirs = name.split('\\') # disk letter test_name = [dirs[0].upper()] for d in dirs[1:]: test_name += ["%s[%s]" % (d[:-1], d[-1])] res = glob.glob('\\'.join(test_name)) if not res: # File not found, return the input return name return res[0] def __file_error(file_path): qltk.ErrorMessage( None, _("Unable to export playlist"), _("Writing to <b>%s</b> failed.") % util.escape(file_path)).run() def __m3u_export(dir_path, query_name, songs): file_path = os.path.join(dir_path, query_name + '.m3u') try: fhandler = open(file_path, "wb") except IOError: __file_error(file_path) else: text = "#EXTM3U\n" for song in songs: title = "%s - %s" % (song('~people').replace( "\n", ", "), song('~title~version')) path = song('~filename') path = get_actual_filename(path) try: path = relpath(path, dir_path) except ValueError: # Keep absolute path pass text += "#EXTINF:%d,%s\n" % (song('~#length'), title) text += path + "\n" fhandler.write(text.encode("utf-8")) fhandler.close() def __start(button): target_folder = chooserButton.get_filename() songs = app.library.get_content() for query_name, query in queries.items(): if self.config_get_bool(query_name): # Query is enabled songs_for_query = query.filter(songs) __m3u_export(target_folder, query_name, songs_for_query) message = qltk.Message(Gtk.MessageType.INFO, app.window, _("Done"), _("Export finished.")) message.run() start_button = Gtk.Button(label=("Export")) start_button.connect('clicked', __start) vbox.pack_start(chooserButton, True, True, 0) vbox.pack_start(start_button, True, True, 0) label = Gtk.Label( label= "Quod Libet may become unresponsive. You will get a message when finished." ) vbox.pack_start(label, True, True, 0) return qltk.Frame(("Select the saved searches to copy:"), child=vbox)
def test_get_scan_dirs(self): some_path = os.path.join(unexpand(get_home_dir()), "foo") config.set('settings', 'scan', some_path) assert expanduser(some_path) in get_scan_dirs() assert all([isinstance(p, fsnative) for p in get_scan_dirs()])
def __init__(self, library, player, headless=False, restore_cb=None): super(QuodLibetWindow, self).__init__(dialog=False) self.last_dir = get_home_dir() self.__destroyed = False self.__update_title(player) self.set_default_size(550, 450) main_box = Gtk.VBox() self.add(main_box) # create main menubar, load/restore accelerator groups self.__library = library ui = self.__create_menu(player, library) accel_group = ui.get_accel_group() self.add_accel_group(accel_group) def scroll_and_jump(*args): self.__jump_to_current(True, True) keyval, mod = Gtk.accelerator_parse("<control><shift>J") accel_group.connect(keyval, mod, 0, scroll_and_jump) # dbus app menu # Unity puts the app menu next to our menu bar. Since it only contains # menu items also available in the menu bar itself, don't add it. if not util.is_unity(): AppMenu(self, ui.get_action_groups()[0]) # custom accel map accel_fn = os.path.join(quodlibet.get_user_dir(), "accels") Gtk.AccelMap.load(accel_fn) # save right away so we fill the file with example comments of all # accels Gtk.AccelMap.save(accel_fn) menubar = ui.get_widget("/Menu") # Since https://git.gnome.org/browse/gtk+/commit/?id=b44df22895c79 # toplevel menu items show an empty 16x16 image. While we don't # need image items there UIManager creates them by default. # Work around by removing the empty GtkImages for child in menubar.get_children(): if isinstance(child, Gtk.ImageMenuItem): child.set_image(None) main_box.pack_start(menubar, False, True, 0) # get the playlist up before other stuff self.songlist = MainSongList(library, player) self.songlist.show_all() self.songlist.connect("key-press-event", self.__songlist_key_press) self.songlist.connect_after( 'drag-data-received', self.__songlist_drag_data_recv) self.song_scroller = SongListScroller( ui.get_widget("/Menu/View/SongList")) self.song_scroller.add(self.songlist) self.qexpander = QueueExpander( ui.get_widget("/Menu/View/Queue"), library, player) self.playlist = PlaylistMux( player, self.qexpander.model, self.songlist.model) top_bar = TopBar(self, player, library) main_box.pack_start(top_bar, False, True, 0) self.top_bar = top_bar self.__browserbox = Align(bottom=3) main_box.pack_start(self.__browserbox, True, True, 0) statusbox = StatusBarBox(self.songlist.model, player) self.order = statusbox.order self.repeat = statusbox.repeat self.statusbar = statusbox.statusbar main_box.pack_start( Align(statusbox, border=3, top=-3, right=3), False, True, 0) self.songpane = ConfigRVPaned("memory", "queue_position", 0.75) self.songpane.pack1(self.song_scroller, resize=True, shrink=False) self.songpane.pack2(self.qexpander, resize=True, shrink=False) self.__handle_position = self.songpane.get_property("position") def songpane_button_press_cb(pane, event): """If we start to drag the pane handle while the queue expander is unexpanded, expand it and move the handle to the bottom, so we can 'drag' the queue out """ if event.window != pane.get_handle_window(): return False if not self.qexpander.get_expanded(): self.qexpander.set_expanded(True) pane.set_relative(1.0) return False self.songpane.connect("button-press-event", songpane_button_press_cb) self.song_scroller.connect('notify::visible', self.__show_or) self.qexpander.connect('notify::visible', self.__show_or) self.qexpander.connect('notify::expanded', self.__expand_or) self.qexpander.connect('draw', self.__qex_size_allocate) self.songpane.connect('notify', self.__moved_pane_handle) try: orders = [] for e in config.getstringlist('memory', 'sortby', []): orders.append((e[1:], int(e[0]))) except ValueError: pass else: self.songlist.set_sort_orders(orders) self.browser = None self.ui = ui main_box.show_all() self._playback_error_dialog = None connect_destroy(player, 'song-started', self.__song_started) connect_destroy(player, 'paused', self.__update_paused, True) connect_destroy(player, 'unpaused', self.__update_paused, False) # make sure we redraw all error indicators before opening # a dialog (blocking the main loop), so connect after default handlers connect_after_destroy(player, 'error', self.__player_error) # connect after to let SongTracker update stats connect_after_destroy(player, "song-ended", self.__song_ended) # set at least the playlist. the song should be restored # after the browser emits the song list player.setup(self.playlist, None, 0) self.__restore_cb = restore_cb self.__first_browser_set = True restore_browser = not headless try: self.select_browser( self, config.get("memory", "browser"), library, player, restore_browser) except: config.set("memory", "browser", browsers.name(0)) config.save() raise self.showhide_playlist(ui.get_widget("/Menu/View/SongList")) self.showhide_playqueue(ui.get_widget("/Menu/View/Queue")) self.songlist.connect('popup-menu', self.__songs_popup_menu) self.songlist.connect('columns-changed', self.__cols_changed) self.songlist.connect('columns-changed', self.__hide_headers) self.songlist.info.connect("changed", self.__set_time) lib = library.librarian connect_destroy(lib, 'changed', self.__song_changed, player) targets = [("text/uri-list", Gtk.TargetFlags.OTHER_APP, DND_URI_LIST)] targets = [Gtk.TargetEntry.new(*t) for t in targets] self.drag_dest_set( Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY) self.connect('drag-data-received', self.__drag_data_received) if not headless: on_first_map(self, self.__configure_scan_dirs, library) if config.getboolean('library', 'refresh_on_start'): self.__rebuild(None, False) self.connect("key-press-event", self.__key_pressed, player) self.connect("destroy", self.__destroy) self.enable_window_tracking("quodlibet")
from senf import fsn2bytes, extsep from quodlibet import _ from quodlibet import app from quodlibet.plugins.songshelpers import each_song, is_writable, is_a_file, \ is_finite from quodlibet.qltk import ErrorMessage, Icons from quodlibet.util.path import get_home_dir from quodlibet.plugins.songsmenu import SongsMenuPlugin from quodlibet.compat import iteritems __all__ = ['Export', 'Import'] lastfolder = get_home_dir() def filechooser(save, title): chooser = Gtk.FileChooserDialog( title=(save and "Export %s Metadata to ..." or "Import %s Metadata from ...") % title, action=(save and Gtk.FileChooserAction.SAVE or Gtk.FileChooserAction.OPEN)) chooser.add_button(_("_OK"), Gtk.ResponseType.ACCEPT) chooser.add_button(_("_Cancel"), Gtk.ResponseType.REJECT) for name, pattern in [('Tag files (*.tags)', '*.tags'), ('All Files', '*')]: filter = Gtk.FileFilter()
class AudioFeeds(Browser): __feeds = Gtk.ListStore(object) # unread headers = ("title artist performer ~people album date website language " "copyright organization license contact").split() name = _("Audio Feeds") accelerated_name = _("_Audio Feeds") keys = ["AudioFeeds"] priority = 20 uses_main_library = False __last_folder = get_home_dir() def pack(self, songpane): container = qltk.ConfigRHPaned("browsers", "audiofeeds_pos", 0.4) self.show() container.pack1(self, True, False) container.pack2(songpane, True, False) return container def unpack(self, container, songpane): container.remove(songpane) container.remove(self) @staticmethod def cell_data(col, render, model, iter, data): if model[iter][0].changed: render.markup = "<b>%s</b>" % util.escape(model[iter][0].name) else: render.markup = util.escape(model[iter][0].name) render.set_property('markup', render.markup) @classmethod def changed(klass, feeds): for row in klass.__feeds: if row[0] in feeds: row[0].changed = True row[0] = row[0] AudioFeeds.write() @classmethod def write(klass): feeds = [row[0] for row in klass.__feeds] with open(FEEDS, "wb") as f: pickle_dump(feeds, f, 2) @classmethod def init(klass, library): uris = set() try: with open(FEEDS, "rb") as fileobj: feeds = pickle_load(fileobj) except (PickleError, EnvironmentError): pass else: for feed in feeds: if feed.uri in uris: continue klass.__feeds.append(row=[feed]) uris.add(feed.uri) GLib.idle_add(klass.__do_check) @classmethod def reload(klass, library): klass.__feeds = Gtk.ListStore(object) # unread klass.init(library) @classmethod def __do_check(klass): thread = threading.Thread(target=klass.__check, args=()) thread.setDaemon(True) thread.start() @classmethod def __check(klass): for row in klass.__feeds: feed = row[0] if feed.get_age() < 2 * 60 * 60: continue elif feed.parse(): feed.changed = True row[0] = feed klass.write() GLib.timeout_add(60 * 60 * 1000, klass.__do_check) def Menu(self, songs, library, items): if len(songs) == 1: item = qltk.MenuItem(_(u"_Download…"), Icons.NETWORK_WORKGROUP) item.connect('activate', self.__download, songs[0]("~uri")) item.set_sensitive(not songs[0].is_file) else: uris = [song("~uri") for song in songs if not song.is_file] item = qltk.MenuItem(_(u"_Download…"), Icons.NETWORK_WORKGROUP) item.connect('activate', self.__download_many, uris) item.set_sensitive(bool(uris)) items.append([item]) menu = SongsMenu(library, songs, items=items) return menu def __download_many(self, activator, sources): chooser = Gtk.FileChooserDialog( title=_("Download Files"), parent=qltk.get_top_parent(self), action=Gtk.FileChooserAction.CREATE_FOLDER) chooser.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL) chooser.add_button(_("_Save"), Gtk.ResponseType.OK) chooser.set_current_folder(self.__last_folder) resp = chooser.run() if resp == Gtk.ResponseType.OK: target = chooser.get_filename() if target: type(self).__last_folder = os.path.dirname(target) for i, source in enumerate(sources): base = os.path.basename(source) if not base: base = ("file%d" % i) + ( os.path.splitext(source)[1] or ".audio") fulltarget = os.path.join(target, base) DownloadWindow.download(source, fulltarget, self) chooser.destroy() def __download(self, activator, source): chooser = Gtk.FileChooserDialog( title=_("Download File"), parent=qltk.get_top_parent(self), action=Gtk.FileChooserAction.SAVE) chooser.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL) chooser.add_button(_("_Save"), Gtk.ResponseType.OK) chooser.set_current_folder(self.__last_folder) name = os.path.basename(source) if name: chooser.set_current_name(name) resp = chooser.run() if resp == Gtk.ResponseType.OK: target = chooser.get_filename() if target: type(self).__last_folder = os.path.dirname(target) DownloadWindow.download(source, target, self) chooser.destroy() def __init__(self, library): super(AudioFeeds, self).__init__(spacing=6) self.set_orientation(Gtk.Orientation.VERTICAL) self.__view = view = AllTreeView() self.__render = render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) col = Gtk.TreeViewColumn("Audio Feeds", render) col.set_cell_data_func(render, AudioFeeds.cell_data) view.append_column(col) view.set_model(self.__feeds) view.set_rules_hint(True) view.set_headers_visible(False) swin = ScrolledWindow() swin.set_shadow_type(Gtk.ShadowType.IN) swin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) swin.add(view) self.pack_start(swin, True, True, 0) new = Button(_("_New"), Icons.LIST_ADD, Gtk.IconSize.MENU) new.connect('clicked', self.__new_feed) view.get_selection().connect('changed', self.__changed) view.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) view.connect('popup-menu', self.__popup_menu) targets = [ ("text/uri-list", 0, DND_URI_LIST), ("text/x-moz-url", 0, DND_MOZ_URL) ] targets = [Gtk.TargetEntry.new(*t) for t in targets] view.drag_dest_set(Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY) view.connect('drag-data-received', self.__drag_data_received) view.connect('drag-motion', self.__drag_motion) view.connect('drag-leave', self.__drag_leave) connect_obj(self, 'destroy', self.__save, view) self.pack_start(Align(new, left=3, bottom=3), False, True, 0) for child in self.get_children(): child.show_all() def __drag_motion(self, view, ctx, x, y, time): targets = [t.name() for t in ctx.list_targets()] if "text/x-quodlibet-songs" not in targets: view.get_parent().drag_highlight() return True return False def __drag_leave(self, view, ctx, time): view.get_parent().drag_unhighlight() def __drag_data_received(self, view, ctx, x, y, sel, tid, etime): view.emit_stop_by_name('drag-data-received') targets = [ ("text/uri-list", 0, DND_URI_LIST), ("text/x-moz-url", 0, DND_MOZ_URL) ] targets = [Gtk.TargetEntry.new(*t) for t in targets] view.drag_dest_set(Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY) if tid == DND_URI_LIST: uri = sel.get_uris()[0] elif tid == DND_MOZ_URL: uri = sel.data.decode('utf16', 'replace').split('\n')[0] else: ctx.finish(False, False, etime) return ctx.finish(True, False, etime) feed = Feed(uri.encode("ascii", "replace")) feed.changed = feed.parse() if feed: self.__feeds.append(row=[feed]) AudioFeeds.write() else: ErrorMessage( self, _("Unable to add feed"), _("%s could not be added. The server may be down, " "or the location may not be an audio feed.") % util.bold(util.escape(feed.uri))).run() def __popup_menu(self, view): model, paths = view.get_selection().get_selected_rows() menu = Gtk.Menu() refresh = MenuItem(_("_Refresh"), Icons.VIEW_REFRESH) delete = MenuItem(_("_Delete"), Icons.EDIT_DELETE) connect_obj(refresh, 'activate', self.__refresh, [model[p][0] for p in paths]) connect_obj(delete, 'activate', map, model.remove, map(model.get_iter, paths)) menu.append(refresh) menu.append(delete) menu.show_all() menu.connect('selection-done', lambda m: m.destroy()) # XXX: keep the menu around self.__menu = menu return view.popup_menu(menu, 0, Gtk.get_current_event_time()) def __save(self, view): AudioFeeds.write() def __refresh(self, feeds): changed = listfilter(Feed.parse, feeds) AudioFeeds.changed(changed) def activate(self): self.__changed(self.__view.get_selection()) def __changed(self, selection): model, paths = selection.get_selected_rows() if model and paths: songs = [] for path in paths: model[path][0].changed = False songs.extend(model[path][0]) self.songs_selected(songs, True) config.set("browsers", "audiofeeds", "\t".join([model[path][0].name for path in paths])) def __new_feed(self, activator): feed = AddFeedDialog(self).run() if feed is not None: feed.changed = feed.parse() if feed: self.__feeds.append(row=[feed]) AudioFeeds.write() else: ErrorMessage( self, _("Unable to add feed"), _("%s could not be added. The server may be down, " "or the location may not be an audio feed.") % util.bold(util.escape(feed.uri))).run() def restore(self): try: names = config.get("browsers", "audiofeeds").split("\t") except: pass else: self.__view.select_by_func(lambda r: r[0].name in names)
# This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gtk from os.path import splitext, extsep, dirname from quodlibet import app from quodlibet.qltk import ErrorMessage from quodlibet.util.path import get_home_dir from quodlibet.plugins.songsmenu import SongsMenuPlugin __all__ = ['Export', 'Import'] lastfolder = get_home_dir() def filechooser(save, title): chooser = Gtk.FileChooserDialog( title=(save and "Export %s Metadata to ..." or "Import %s Metadata from ...") % title, action=(save and Gtk.FileChooserAction.SAVE or Gtk.FileChooserAction.OPEN), buttons=(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT, Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT)) for name, pattern in [('Tag files (*.tags)', '*.tags'), ('All Files', '*')]: filter = Gtk.FileFilter() filter.set_name(name)