def test_types(self): from quodlibet.util.path import normalize_path assert isinstance(normalize_path(fsnative(u"foo"), False), fsnative) assert isinstance(normalize_path("foo", False), fsnative) assert isinstance(normalize_path(fsnative(u"foo"), True), fsnative) assert isinstance(normalize_path("foo", True), fsnative)
def remove_roots(self, old_roots: Iterable[str]) -> Generator[None, None, None]: """Remove library roots (scandirs) entirely, and all their songs""" old_paths = [ Path(normalize_path(root, canonicalise=True)).expanduser() for root in old_roots ] total = len(self) removed = set() print_d( f"Removing library roots {old_roots} from {self._name} library") yield with Task(_("Library"), _("Removing library files")) as task: for i, song in enumerate(list(self.values())): task.update(i / total) key = normalize_path(song.key) song_path = Path(key) if any(path in song_path.parents for path in old_paths): removed.add(song) if not i % 100: yield if removed: self.remove(removed) else: print_d(f"No tracks in {old_roots} to remove from {self._name}")
def move_root(self, old_root: str, new_root: fsnative, write_files: bool = True) -> Generator[None, None, None]: """ Move the root for all songs in a given (scan) directory. We avoid dereferencing the destination, to allow users things like: 1. Symlink new_path -> old_root 2. Move QL root to new_path 3. Remove symlink 4. Move audio files: old_root -> new_path """ # TODO: only move primary library old_path = Path(normalize_path(old_root, canonicalise=True)).expanduser() new_path = Path(normalize_path(new_root)).expanduser() if not old_path.is_dir(): print_w(f"Source dir {str(old_path)!r} doesn't exist, assuming that's OK", self._name) if not new_path.is_dir(): raise ValueError(f"Destination {new_path!r} is not a directory") print_d(f"Checking entire library for {str(old_path)!r}", self._name) missing: Set[AudioFile] = set() changed = set() total = len(self) if not total: return with Task(_("Library"), _("Moving library files")) as task: yield for i, song in enumerate(list(self.values())): task.update(i / total) key = normalize_path(song.key) path = Path(key) if old_path in path.parents: # TODO: more Pathlib-friendly dir replacement... new_key = key.replace(str(old_path), str(new_path), 1) new_key = normalize_path(new_key, canonicalise=False) if new_key == key: print_w(f"Substitution failed for {key!r}", self._name) # We need to update ~filename and ~mountpoint song.sanitize() if write_files: song.write() if self.move_song(song, new_key): changed.add(song) else: missing.add(song) elif not (i % 1000): print_d(f"Not moved, for example: {key!r}", self._name) if not i % 100: yield self.changed(changed) if missing: print_w(f"Couldn't find {len(list(missing))} files: {missing}", self._name) yield self.save() print_d(f"Done moving {len(changed)} track(s) (of {total}) " f"to {str(new_path)!r}.", self._name)
def test_move_root(self): # TODO: mountpoint tests too self.library.filename = "moving" root = Path(normalize_path(mkdtemp(), True)) other_root = Path(normalize_path(mkdtemp(), True)) new_root = Path(normalize_path(mkdtemp(), True)) in_song = FakeAudioFile(str(root / "in file.mp3")) in_song.sanitize() out_song = FakeAudioFile(str(other_root / "out file.mp3")) # Make sure they exists in_song.sanitize() out_song.sanitize() assert Path(in_song("~dirname")) == root, "test setup wrong" assert Path(out_song("~dirname")) == other_root, "test setup wrong" self.library.add([out_song, in_song]) # Run it by draining the generator list(self.library.move_root(root, str(new_root))) msg = f"Dir wasn't updated in {root!r} -> {new_root!r} for {in_song.key}" assert Path(in_song("~dirname")) == new_root, msg assert Path(in_song("~filename")) == (new_root / "in file.mp3") assert Path(out_song( "~dirname")) == other_root, f"{out_song} was wrongly moved" assert in_song._written, "Song wasn't written to disk" assert not out_song._written, "Excluded songs was written!"
def test_move_root_gone_source_dir(self): # See #3967 self.library.filename = "moving" gone_root = Path(normalize_path("/gone", True)) new_root = Path(normalize_path(mkdtemp(), True)) song = FakeAudioFile(str(gone_root / "in file.mp3")) assert Path(song("~dirname")) == gone_root, "test setup wrong" self.library.add([song]) # Run it by draining the generator list(self.library.move_root(gone_root, str(new_root))) assert Path(song("~dirname")) == new_root assert song._written, "Song wasn't written to disk"
def __popup_menu(self, view, fs): # get all songs for the selection filenames = [ normalize_path(f, canonicalise=True) for f in fs.get_selected_paths() ] maybe_songs = [self.__library.get(f) for f in filenames] songs = [s for s in maybe_songs if s] if songs: menu = self.pm.Menu(self.__library, songs) if menu is None: menu = Gtk.Menu() else: menu.prepend(SeparatorMenuItem()) else: menu = Gtk.Menu() b = TrashMenuItem() b.connect('activate', self.__delete, filenames, fs) menu.prepend(b) def selection_done_cb(menu): menu.destroy() menu.connect('selection-done', selection_done_cb) menu.show_all() return view.popup_menu(menu, 0, Gtk.get_current_event_time())
def temp_filename(*args, as_path=False, **kwargs): """ Creates an empty file, returning the normalized path to it, and removes it when done. with temp_filename() as filename: with open(filename, 'w') as h: h.write("foo") do_stuff(filename) """ from tests import mkstemp try: del kwargs["as_path"] except KeyError: pass fd, filename = mkstemp(*args, **kwargs) os.close(fd) normalized = normalize_path(filename) yield Path(normalized) if as_path else normalized try: os.remove(filename) except OSError as e: if e.errno != errno.ENOENT: raise
def __popup_menu(self, view, fs): # get all songs for the selection filenames = [normalize_path(f, canonicalise=True) for f in fs.get_selected_paths()] maybe_songs = [self.__library.get(f) for f in filenames] songs = [s for s in maybe_songs if s] if songs: menu = self.pm.Menu(self.__library, songs) if menu is None: menu = Gtk.Menu() else: menu.prepend(SeparatorMenuItem()) else: menu = Gtk.Menu() b = TrashMenuItem() b.connect('activate', self.__delete, filenames, fs) menu.prepend(b) def selection_done_cb(menu): menu.destroy() menu.connect('selection-done', selection_done_cb) menu.show_all() return view.popup_menu(menu, 0, Gtk.get_current_event_time())
def show_files_win32(path, files): """Takes a path to a directory and a list of filenames in that directory to display. Returns True on success. """ assert os.name == "nt" import pywintypes from win32com.shell import shell assert is_fsnative(path) assert all(is_fsnative(f) for f in files) normalized_files = map(normalize_path, files) try: folder_pidl = shell.SHILCreateFromPath(path, 0)[0] desktop = shell.SHGetDesktopFolder() shell_folder = desktop.BindToObject( folder_pidl, None, shell.IID_IShellFolder) items = [] for item in shell_folder: name = desktop.GetDisplayNameOf(item, 0) if normalize_path(name) in normalized_files: items.append(item) shell.SHOpenFolderAndSelectItems(folder_pidl, items, 0) except pywintypes.com_error: return False else: return True
def __find_songs(self, selection): model, rows = selection.get_selected_rows() dirs = [model[row][0] for row in rows] songs = [] to_add = [] for dir in dirs: try: for file in filter(formats.filter, sorted(os.listdir(dir))): raw_path = os.path.join(dir, file) fn = normalize_path(raw_path, canonicalise=True) if fn in self.__glibrary: songs.append(self.__glibrary[fn]) elif fn not in self.__library: song = formats.MusicFile(fn) if song: to_add.append(song) songs.append(song) yield songs if fn in self.__library: song = self.__library[fn] if not song.valid(): self.__library.reload(song) if song in self.__library: songs.append(song) except OSError: pass self.__library.add(to_add) yield songs
def test_file_encoding(self): if os.name == "nt": return f = self.add_file(bytes2fsn(b"\xff\xff\xff\xff - cover.jpg", None)) self.assertTrue(isinstance(self.song("album"), text_type)) h = self._find_cover(self.song) self.assertEqual(h.name, normalize_path(f))
def test_file_encoding(self): if os.name == "nt": return f = self.add_file("\xff\xff\xff\xff - cover.jpg") self.assertTrue(isinstance(self.song("album"), unicode)) h = self._find_cover(self.song) self.assertEqual(h.name, normalize_path(f))
def test_file_encoding(self): if os.name == "nt": return f = self.add_file("\xff\xff\xff\xff - cover.jpg") self.assertTrue(isinstance(quux("album"), unicode)) h = self._find_cover(quux) self.assertEqual(h.name, normalize_path(f))
def setUp(self): config.RATINGS = config.HardCodedRatingsPrefs() fd, filename = mkstemp() os.close(fd) self.quux = AudioFile({ "~filename": normalize_path(filename, True), "album": u"Quuxly" })
def _file_changed(self, _monitor, main_file: Gio.File, other_file: Optional[Gio.File], event_type: Gio.FileMonitorEvent) -> None: file_path = main_file.get_path() other_path = (Path(normalize_path(other_file.get_path(), True)) if other_file else None) print_d(f"Got event {event_type} on {file_path}->{other_path}") self.changed.append((event_type, file_path))
def sanitize(self, filename=None): """Fill in metadata defaults. Find ~mountpoint, ~#mtime, ~#filesize and ~#added. Check for null bytes in tags. Does not raise. """ # Replace nulls with newlines, trimming zero-length segments for key, val in listitems(self): self[key] = val if isinstance(val, string_types) and '\0' in val: self[key] = '\n'.join(filter(lambda s: s, val.split('\0'))) # Remove unnecessary defaults if key in NUMERIC_ZERO_DEFAULT and val == 0: del self[key] if filename: self["~filename"] = filename elif "~filename" not in self: raise ValueError("Unknown filename!") assert isinstance(self["~filename"], fsnative) if self.is_file: self["~filename"] = normalize_path( self["~filename"], canonicalise=True) # Find mount point (terminating at "/" if necessary) head = self["~filename"] while "~mountpoint" not in self: head, tail = os.path.split(head) # Prevent infinite loop without a fully-qualified filename # (the unit tests use these). head = head or fsnative(u"/") if ismount(head): self["~mountpoint"] = head else: self["~mountpoint"] = fsnative(u"/") # Fill in necessary values. self.setdefault("~#added", int(time.time())) # For efficiency, do a single stat here. See Issue 504 try: stat = os.stat(self['~filename']) self["~#mtime"] = stat.st_mtime self["~#filesize"] = stat.st_size # Issue 342. This is a horrible approximation (due to headers) but # on FLACs, the most common case, this should be close enough if "~#bitrate" not in self: try: # kbps = bytes * 8 / seconds / 1000 self["~#bitrate"] = int(stat.st_size / (self["~#length"] * (1000 / 8))) except (KeyError, ZeroDivisionError): pass except OSError: self["~#mtime"] = 0
def sanitize(self, filename=None): """Fill in metadata defaults. Find ~mountpoint, ~#mtime, ~#filesize and ~#added. Check for null bytes in tags. Does not raise. """ # Replace nulls with newlines, trimming zero-length segments for key, val in list(self.items()): self[key] = val if isinstance(val, str) and '\0' in val: self[key] = '\n'.join(filter(lambda s: s, val.split('\0'))) # Remove unnecessary defaults if key in NUMERIC_ZERO_DEFAULT and val == 0: del self[key] if filename: self["~filename"] = filename elif "~filename" not in self: raise ValueError("Unknown filename!") assert isinstance(self["~filename"], fsnative) if self.is_file: self["~filename"] = normalize_path(self["~filename"], canonicalise=True) # Find mount point (terminating at "/" if necessary) head = self["~filename"] while "~mountpoint" not in self: head, tail = os.path.split(head) # Prevent infinite loop without a fully-qualified filename # (the unit tests use these). head = head or fsnative(u"/") if ismount(head): self["~mountpoint"] = head else: self["~mountpoint"] = fsnative(u"/") # Fill in necessary values. self.setdefault("~#added", int(time.time())) # For efficiency, do a single stat here. See Issue 504 try: stat = os.stat(self['~filename']) self["~#mtime"] = stat.st_mtime self["~#filesize"] = stat.st_size # Issue 342. This is a horrible approximation (due to headers) but # on FLACs, the most common case, this should be close enough if "~#bitrate" not in self: try: # kbps = bytes * 8 / seconds / 1000 self["~#bitrate"] = int(stat.st_size / (self["~#length"] * (1000 / 8))) except (KeyError, ZeroDivisionError): pass except OSError: self["~#mtime"] = 0
def test_remove_roots(self): self.library.filename = "removing" root = Path(normalize_path(mkdtemp(), True)) other_root = Path(normalize_path(mkdtemp(), True)) out_song = FakeAudioFile(str(other_root / "out file.mp3")) in_song = FakeAudioFile(str(root / "in file.mp3")) in_song.sanitize() out_song.sanitize() self.library.add([in_song, out_song]) assert in_song in self.library, "test seems broken" # Run it by draining the generator list(self.library.remove_roots([root])) assert in_song not in self.library assert out_song in self.library, "removed too many files" assert self.removed == [in_song], "didn't signal the song removal" assert not self.changed, "shouldn't have changed any tracks"
def read(self, db): """Iterate through the database and import data for songs found in the library """ # use the Row class for extracting rows db.row_factory = sqlite3.Row # iterate over all songs in the database # throws sqlite3.OperationalError if CoreTracks is not found for row in db.execute("SELECT * FROM CoreTracks"): try: filename = uri2fsn(row["Uri"]) except ValueError: continue song = self._library.get(normalize_path(filename)) if not song: continue has_changed = False # rating is stored as integer from 0 to 5 b_rating = row["Rating"] / 5.0 if b_rating != song("~#rating"): song["~#rating"] = b_rating has_changed = True # play count is stored as integer from 0 if row["PlayCount"] != song("~#playcount"): # summing play counts would break on multiple imports song["~#playcount"] = row["PlayCount"] has_changed = True # skip count is stored as integer from 0 if row["SkipCount"] != song("~#skipcount"): song["~#skipcount"] = row["SkipCount"] has_changed = True # timestamp is stored as integer or None if row["LastPlayedStamp"] is not None: value = row["LastPlayedStamp"] # keep timestamp if it is newer than what we had if value > song("~#lastplayed", 0): song["~#lastplayed"] = value has_changed = True if row["DateAddedStamp"] is not None: value = row["DateAddedStamp"] # keep timestamp if it is older than what we had if value < song("~#added", 0): song["~#added"] = value has_changed = True if has_changed: self._changed_songs.append(song)
def a_dummy_song(): """Looks like the real thing""" fd, filename = mkstemp() os.close(fd) return AudioFile({ '~#length': 234, '~filename': normalize_path(filename, True), 'artist': AN_ARTIST, 'album': 'An Example Album', 'title': A_TITLE, 'tracknumber': 1, 'date': '2010-12-31', })
def test_file_encoding(self): if os.name == "nt": return f = self.full_path("\xff\xff\xff\xff - cover.jpg") file(f, "w").close() self.files.append(f) self.assertTrue(isinstance(quux("album"), unicode)) h = self._find_cover(quux) self.assertEqual(h.name, normalize_path(f))
def setUp(self): # Need the playlists library now init_fake_app() config.RATINGS = config.HardCodedRatingsPrefs() fd, filename = mkstemp() os.close(fd) self.quux = AudioFile({ "~filename": normalize_path(filename, True), "album": u"Quuxly" })
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
def endElement(self, name): self._tag = None if name == "entry" and self._current is not None: current = self._current self._current = None if len(current) > 1: uri = current.pop("location", "") try: filename = uri2fsn(uri) except ValueError: return self._process_song(normalize_path(filename), current)
def rename(self, newname): """Rename a file. Errors are not handled. This shouldn't be used directly; use library.rename instead.""" if os.path.isabs(newname): mkdir(os.path.dirname(newname)) else: newname = os.path.join(self('~dirname'), newname) if not os.path.exists(newname): shutil.move(self['~filename'], newname) elif normalize_path(newname, canonicalise=True) != self['~filename']: raise ValueError self.sanitize(newname)
def select(model, path, iter_, paths_): (paths, first) = paths_ value = model.get_value(iter_) if value is None: return not bool(paths) value = normalize_path(value) if value in paths: self.get_child().get_selection().select_path(path) paths.remove(value) if not first: self.get_child().set_cursor(path) # copy treepath, gets invalid after the callback first.append(path.copy()) else: for fpath in paths: if fpath.startswith(value): self.get_child().expand_row(path, False) return not bool(paths)
def __select_paths(self, paths): # AudioFile uses normalized paths, DirectoryTree doesn't paths = map(normalize_path, paths) def select(model, path, iter_, (paths, first)): value = model.get_value(iter_) if value is None: return not bool(paths) value = normalize_path(value) if value in paths: self.get_child().get_selection().select_path(path) paths.remove(value) if not first: self.get_child().set_cursor(path) # copy treepath, gets invalid after the callback first.append(path.copy()) else: for fpath in paths: if fpath.startswith(value): self.get_child().expand_row(path, False) return not bool(paths)
def test_watched_moving_song(self): with temp_filename(dir=self.temp_path, suffix=".flac", as_path=True) as path: shutil.copy(Path(get_data_path("silence-44-s.flac")), path) sleep(0.2) assert path.exists() run_gtk_loop() assert str(path) in self.library, f"New path {path!s} didn't get added" assert len(self.added) == 1 assert self.added[0]("~basename") == path.name self.added.clear() # Now move it... new_path = path.parent / f"moved-{path.name}" path.rename(new_path) sleep(0.2) assert not path.exists(), "test should have removed old file" assert new_path.exists(), "test should have renamed file" print_d(f"New test file at {new_path}") run_gtk_loop() p = normalize_path(str(new_path), True) assert p in self.library, f"New path {new_path} not in library [{self.fns}]" msg = "Inconsistent events: should be (added and removed) or nothing at all" assert not (bool(self.added) ^ bool(self.removed)), msg
def add_filename(self, filename, add=True): """Add a song to the library based on filename. If 'add' is true, the song will be added and the 'added' signal may be fired. Example (add=False): load many songs and call Library.add(songs) to add all in one go. The song is returned if it is in the library after this call. Otherwise, None is returned. """ key = normalize_path(filename, True) song = None if key not in self._contents: song = MusicFile(filename) if song and add: self.add([song]) else: print_d("Already got file %r." % filename) song = self._contents[key] return song
def test_get_link_target(self): path = get_data_path("test.lnk") d = windows.get_link_target(path) self.assertTrue(isinstance(d, text_type)) self.assertEqual( normalize_path(d), normalize_path(u"C:\\Windows\\explorer.exe"))
def dummy_path(path): path = fsnative(path) if os.name == "nt": return normalize_path(u"z:\\" + path.replace(u"/", u"\\")) return path
def get_filename(self, filename): key = normalize_path(filename, True) return self._contents.get(key)
def contains_filename(self, filename): key = normalize_path(filename, True) return key in self._contents
def test_get_link_target(self): path = os.path.join(DATA_DIR, "test.lnk") d = windows.get_link_target(path) self.assertTrue(isinstance(d, unicode)) self.assertEqual( normalize_path(d), normalize_path(u"C:\Windows\explorer.exe"))
def test_file_encoding(self): f = self.add_file(fsnative(u"öäü - cover.jpg")) self.assertTrue(isinstance(self.song("album"), text_type)) h = self._find_cover(self.song) self.assertEqual(normalize_path(h.name), normalize_path(f))
def test_get_link_target(self): path = get_data_path("test.lnk") d = windows.get_link_target(path) self.assertTrue(isinstance(d, text_type)) self.assertEqual(normalize_path(d), normalize_path(u"C:\Windows\explorer.exe"))