Skip to content

Commit eb5367c

Browse files
authored
refactor/fix/perf(ui): MVC split ItemThumb + ThumbGrid, fixes and performance (#1539)
* refactor(ui): clean up flow_layout.py * refactor: centralize entry tag badge state * refactor: MVC split ItemThumb and absorb ThumbButton * refactor: MVC split ThumbGridLayout * fix(ui): make browsing history track pages and scroll positions * perf(ui): optimize waveform thumbs, drop pydub * perf(ui): fix GIL forking ffmpeg command on macOS * fix(ui): scroll to top of page when redoing same query * perf(ui): add delay to duration label processing * fix(ui): fix clicking badges causing scroll jumps
1 parent e959f16 commit eb5367c

35 files changed

Lines changed: 1493 additions & 4399 deletions

‎nix/package/default.nix‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ python3Packages.buildPythonApplication {
5454
pillow-heif
5555
py7zr
5656
pydantic
57-
pydub
5857
pygments
5958
pyside6
6059
rarfile

‎pyproject.toml‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ dependencies = [
2626
"Pillow>=10.2,<12",
2727
"py7zr~=1.1.3",
2828
"pydantic~=2.10",
29-
"pydub~=0.25",
3029
"Pygments~=2.20",
3130
"PySide6==6.11.2",
3231
"rarfile==4.2",
@@ -101,7 +100,6 @@ filterwarnings = [
101100
ignore = [
102101
".venv/**",
103102
"src/tagstudio/core/library/json/",
104-
"src/tagstudio/previews/vendored/pydub/",
105103
]
106104
include = ["src/tagstudio", "tests"]
107105
# Reference for the settings here: https://github.com/microsoft/pyright/blob/main/docs/configuration.md
@@ -122,7 +120,6 @@ reportUnannotatedClassAttribute = false
122120
reportUninitializedInstanceVariable = false
123121

124122
[tool.ruff]
125-
exclude = ["home_ui.py", "resources.py", "resources_rc.py"]
126123
line-length = 100
127124

128125
[tool.ruff.lint]

‎src/tagstudio/core/library/alchemy/enums.py‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import random
77
from dataclasses import dataclass, field, replace
88
from pathlib import Path
9+
from typing import Any
910

1011
import structlog
1112

@@ -82,17 +83,15 @@ class SortingModeEnum(enum.Enum):
8283

8384
@dataclass
8485
class BrowsingState:
85-
"""Represent a state of the Library grid view."""
86+
"""Represent a browsing history state for a library."""
8687

87-
page_index: int = 0
88-
page_positions: dict[int, int] = field(default_factory=dict)
89-
sorting_mode: SortingModeEnum = SortingModeEnum.DATE_ADDED
9088
ascending: bool = False
89+
first_visible_entry_id: int | None = field(default=None, compare=False)
90+
page_index: int = 0
91+
query: str | None = None
9192
random_seed: float = 0
92-
9393
show_hidden_entries: bool = False
94-
95-
query: str | None = None
94+
sorting_mode: SortingModeEnum = SortingModeEnum.DATE_ADDED
9695

9796
# Abstract Syntax Tree Of the current Search Query
9897
@property
@@ -141,19 +140,23 @@ def from_tag_name(cls, tag_name: str) -> BrowsingState:
141140
return cls(query=f'tag:"{tag_name}"')
142141

143142
def with_page_index(self, index: int) -> BrowsingState:
144-
return replace(self, page_index=index)
143+
return replace(self, page_index=index, first_visible_entry_id=None)
145144

146145
def with_sorting_mode(self, mode: SortingModeEnum) -> BrowsingState:
147146
seed = self.random_seed
148147
if mode == SortingModeEnum.RANDOM:
149148
seed = random.random()
150-
return replace(self, sorting_mode=mode, random_seed=seed)
149+
return self._with_new_results(sorting_mode=mode, random_seed=seed)
151150

152151
def with_sorting_direction(self, ascending: bool) -> BrowsingState:
153-
return replace(self, ascending=ascending)
152+
return self._with_new_results(ascending=ascending)
154153

155154
def with_search_query(self, search_query: str) -> BrowsingState:
156-
return replace(self, query=search_query)
155+
return self._with_new_results(query=search_query)
157156

158157
def with_show_hidden_entries(self, show_hidden_entries: bool) -> BrowsingState:
159-
return replace(self, show_hidden_entries=show_hidden_entries)
158+
return self._with_new_results(show_hidden_entries=show_hidden_entries)
159+
160+
def _with_new_results(self, **changes: Any) -> BrowsingState:
161+
"""Copy this state with changes to its results, starting back on the first page."""
162+
return replace(self, page_index=0, first_visible_entry_id=None, **changes)

‎src/tagstudio/core/library/alchemy/library.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,7 @@ def get_entries(self, entry_ids: Iterable[int]) -> list[Entry]:
600600
with Session(self.engine) as session:
601601
statement = select(Entry).where(Entry.id.in_(entry_ids))
602602
entries = dict((e.id, e) for e in session.scalars(statement))
603-
return [entries[id] for id in entry_ids]
603+
return [entries[id] for id in entry_ids if id in entries]
604604

605605
def get_entries_full(self, entry_ids: list[int] | set[int]) -> Iterator[Entry]:
606606
"""Load entry and join with all joins and all tags."""

‎src/tagstudio/previews/renderers/audio.py‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# SPDX-License-Identifier: MIT
33

44

5+
import subprocess
56
from io import BytesIO
67
from pathlib import Path
78
from typing import override
@@ -17,15 +18,17 @@
1718

1819
from tagstudio.core.enums import Theme
1920
from tagstudio.core.media_types import MediaTypes
21+
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus
22+
from tagstudio.core.utils.silent_subprocess import silent_run
23+
from tagstudio.core.utils.types import unwrap
2024
from tagstudio.previews.base_preview import RENDER, BasePreview
2125
from tagstudio.previews.effects import apply_overlay_color
22-
from tagstudio.previews.vendored.pydub.audio_segment import (
23-
_AudioSegment as AudioSegment, # pyright: ignore[reportPrivateUsage]
24-
)
2526
from tagstudio.qt.views.styles.palette import UiColor
2627

2728
logger = structlog.get_logger(__name__)
2829

30+
_WAVEFORM_SAMPLE_RATE = 8000 # Good enough for the waveforms while still being small bits of data
31+
2932

3033
class AudioPreview(BasePreview):
3134
media_type_name = "audio"
@@ -120,8 +123,13 @@ def audio_waveform_thumb(filepath: Path, theme: Theme, size: tuple[int, int]) ->
120123

121124
try:
122125
bar_count: int = 32
123-
audio = AudioSegment.from_file(filepath, filepath.suffix.lower()[1:]) # pyright: ignore[reportUnknownVariableType]
124-
data = np.frombuffer(buffer=audio._data, dtype=f"<i{audio.sample_width}")
126+
command = [unwrap(FfmpegStatus.which()), "-nostdin", "-v", "error", "-i", str(filepath)]
127+
command += ["-vn", "-ac", "1", "-ar", str(_WAVEFORM_SAMPLE_RATE), "-f", "s16le", "-"]
128+
ffmpeg = silent_run(
129+
command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, close_fds=False
130+
)
131+
ffmpeg.check_returncode()
132+
data = np.frombuffer(buffer=ffmpeg.stdout, dtype="<i2")
125133
# Bars are twice as wide as the gaps between them, with double gaps at the edges
126134
bar_margin: float = size_scaled / (bar_count * 3 + 3)
127135
line_width: float = bar_margin * 2
@@ -162,7 +170,7 @@ def audio_waveform_thumb(filepath: Path, theme: Theme, size: tuple[int, int]) ->
162170

163171
current_x = current_x + line_width + bar_margin
164172

165-
im.resize(size, Resampling.BILINEAR)
173+
im = im.resize(size, Resampling.BILINEAR)
166174
im = apply_overlay_color(im, UiColor.GREEN, theme)
167175

168176
except Exception as e:

‎src/tagstudio/previews/vendored/probe.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def probe(filename: Path | str, timeout: int | None = None, **kwargs: ...):
3535
args += [filename] # pyright: ignore
3636

3737
# PATCHED
38-
p = silent_popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
38+
p = silent_popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
3939
communicate_kwargs = {}
4040
if timeout is not None:
4141
communicate_kwargs["timeout"] = timeout

0 commit comments

Comments
 (0)