import re from PySide6 import QtCore, QtGui, QtWidgets from tidalapi import Album, Mix, Playlist, Track, UserPlaylist, Video from tidalapi.artist import Artist from tidalapi.media import Quality def get_table_data( item: QtWidgets.QTreeWidgetItem, column: int ) -> Track | Video | Album | Artist | Mix | Playlist | UserPlaylist: result: Track | Video | Album | Artist = item.data(column, QtCore.Qt.ItemDataRole.UserRole) return result def get_table_text(item: QtWidgets.QTreeWidgetItem, column: int) -> str: result: str = item.text(column) return result def get_results_media_item( index: QtCore.QModelIndex, proxy: QtCore.QSortFilterProxyModel, model: QtGui.QStandardItemModel ) -> Track | Video | Album | Artist | Playlist | Mix: # Switch column to "obj" column and map proxy data to our model. item: QtGui.QStandardItem = model.itemFromIndex(proxy.mapToSource(index.siblingAtColumn(1))) result: Track | Video | Album | Artist = item.data(QtCore.Qt.ItemDataRole.UserRole) return result def get_user_list_media_item(item: QtWidgets.QTreeWidgetItem) -> Mix | Playlist | UserPlaylist: result: Mix | Playlist | UserPlaylist = get_table_data(item, 1) return result def get_queue_download_media( item: QtWidgets.QTreeWidgetItem, ) -> Mix | Playlist | UserPlaylist | Track | Video | Album | Artist: result: Mix | Playlist | UserPlaylist | Track | Video | Album | Artist = get_table_data(item, 1) return result def get_queue_download_quality( item: QtWidgets.QTreeWidgetItem, ) -> Quality: result: Quality = get_table_text(item, 4) return result def set_table_data( item: QtWidgets.QTreeWidgetItem, data: Track | Video | Album | Artist | Mix | Playlist | UserPlaylist, column: int ): item.setData(column, QtCore.Qt.ItemDataRole.UserRole, data) def set_results_media(item: QtWidgets.QTreeWidgetItem, media: Track | Video | Album | Artist): set_table_data(item, media, 1) def set_user_list_media( item: QtWidgets.QTreeWidgetItem, media: Track | Video | Album | Artist | Mix | Playlist | UserPlaylist ): set_table_data(item, media, 1) def set_queue_download_media( item: QtWidgets.QTreeWidgetItem, media: Mix | Playlist | UserPlaylist | Track | Video | Album | Artist ): set_table_data(item, media, 1) class FilterHeader(QtWidgets.QHeaderView): filter_activated = QtCore.Signal() def __init__(self, parent): super().__init__(QtCore.Qt.Horizontal, parent) self._editors = [] self._padding = 4 self.setCascadingSectionResizes(True) self.setSectionResizeMode(QtWidgets.QHeaderView.Interactive) self.setStretchLastSection(True) self.setDefaultAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) self.setSortIndicatorShown(False) self.setSectionsMovable(True) self.sectionResized.connect(self.adjust_positions) parent.horizontalScrollBar().valueChanged.connect(self.adjust_positions) def set_filter_boxes(self, count): while self._editors: editor = self._editors.pop() editor.deleteLater() for _ in range(count): editor = QtWidgets.QLineEdit(self.parent()) editor.setPlaceholderText("Filter") editor.setClearButtonEnabled(True) editor.returnPressed.connect(self.filter_activated.emit) self._editors.append(editor) self.adjust_positions() def sizeHint(self): size = super().sizeHint() if self._editors: height = self._editors[0].sizeHint().height() size.setHeight(size.height() + height + self._padding) return size def updateGeometries(self): if self._editors: height = self._editors[0].sizeHint().height() self.setViewportMargins(0, 0, 0, height + self._padding) else: self.setViewportMargins(0, 0, 0, 0) super().updateGeometries() self.adjust_positions() def adjust_positions(self): for index, editor in enumerate(self._editors): height = editor.sizeHint().height() editor.move(self.sectionPosition(index) - self.offset() + 2, height + (self._padding // 2)) editor.resize(self.sectionSize(index), height) def filter_text(self, index) -> str: if 0 <= index < len(self._editors): return self._editors[index].text() return "" def set_filter_text(self, index, text): if 0 <= index < len(self._editors): self._editors[index].setText(text) def clear_filters(self): for editor in self._editors: editor.clear() class HumanProxyModel(QtCore.QSortFilterProxyModel): def _human_key(self, key): parts = re.split(r"(\d*\.\d+|\d+)", key) return tuple((e.swapcase() if i % 2 == 0 else float(e)) for i, e in enumerate(parts)) def lessThan(self, source_left, source_right): data_left = source_left.data() data_right = source_right.data() if isinstance(data_left, str) and isinstance(data_right, str): return self._human_key(data_left) < self._human_key(data_right) return super().lessThan(source_left, source_right) @property def filters(self): if not hasattr(self, "_filters"): self._filters = [] return self._filters @filters.setter def filters(self, filters): self._filters = filters self.invalidateFilter() def filterAcceptsRow(self, source_row: int, source_parent: QtCore.QModelIndex) -> bool: model = self.sourceModel() source_index = model.index(source_row, 0, source_parent) result: [bool] = [] # Show top level children for child_row in range(model.rowCount(source_index)): if self.filterAcceptsRow(child_row, source_index): return True # Filter for actual needle for i, text in self.filters: if 0 <= i < self.columnCount(): ix = self.sourceModel().index(source_row, i, source_parent) data = ix.data() # Append results to list to enable an AND operator for filtering. result.append(bool(re.search(rf"{text}", data, re.MULTILINE | re.IGNORECASE)) if data else False) # If no filter set, just set the result to True. if not result: result.append(True) return all(result)