Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28ec7746b8 |
+2
-6
@@ -39,12 +39,8 @@ if [[ -f "packaging/release-stamp-${VERSION}.txt" ]]; then
|
|||||||
fi
|
fi
|
||||||
# Gear Lever (Forgejo) treats same-size AppImages as "no update". Version-scoped
|
# Gear Lever (Forgejo) treats same-size AppImages as "no update". Version-scoped
|
||||||
# padding keeps each release a unique download size without affecting the app.
|
# padding keeps each release a unique download size without affecting the app.
|
||||||
VER_MAJOR="${VERSION%%.*}"
|
IFS=. read -r VER_MAJOR VER_MINOR VER_PATCH <<< "${VERSION}.0.0"
|
||||||
VER_REST="${VERSION#*.}"
|
PAD_KB=$((300 + VER_MAJOR * 100 + VER_MINOR * 20 + VER_PATCH * 10))
|
||||||
VER_MINOR="${VER_REST%%.*}"
|
|
||||||
VER_PATCH="${VER_REST#*.}"
|
|
||||||
VER_PATCH="${VER_PATCH%%.*}"
|
|
||||||
PAD_KB=$((300 + 10#${VER_MAJOR:-0} * 100 + 10#${VER_MINOR:-0} * 20 + 10#${VER_PATCH:-0} * 10))
|
|
||||||
dd if=/dev/urandom of="$APPDIR/usr/share/hsrename/.release-pad" bs=1024 count="$PAD_KB" status=none
|
dd if=/dev/urandom of="$APPDIR/usr/share/hsrename/.release-pad" bs=1024 count="$PAD_KB" status=none
|
||||||
echo "Release padding: ${PAD_KB} KiB (Gear Lever update detection)"
|
echo "Release padding: ${PAD_KB} KiB (Gear Lever update detection)"
|
||||||
|
|
||||||
|
|||||||
+8
-19
@@ -6,14 +6,13 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .tvdb_client import TvdbEpisode
|
from .tvdb_client import TvdbEpisode
|
||||||
|
|
||||||
# S01E05 - Title or S01E05-E06 - Title
|
# S01E05 - Title or S01E05-E06 - Title
|
||||||
PATTERN_SXXEXX = re.compile(
|
PATTERN_SXXEXX = re.compile(
|
||||||
r"^(.*?)([Ss])(\d+)([Ee])(\d+)(-[Ee](\d+))?(.*)$",
|
r"^(.*?[Ss])(\d+)([Ee])(\d+)(-[Ee](\d+))?(.*)$",
|
||||||
)
|
)
|
||||||
# Show Name 04x01 Title, 4x01 - Title, etc.
|
# Show Name 04x01 Title, 4x01 - Title, etc.
|
||||||
PATTERN_NXNN = re.compile(
|
PATTERN_NXNN = re.compile(
|
||||||
@@ -60,7 +59,7 @@ def parse_episode_stem(stem: str, pattern: str = DEFAULT_EPISODE_PATTERN) -> Opt
|
|||||||
|
|
||||||
m = PATTERN_SXXEXX.match(stem)
|
m = PATTERN_SXXEXX.match(stem)
|
||||||
if m:
|
if m:
|
||||||
prefix, s_letter, season_s, e_letter, ep_s, _range_block, ep2_s, rest = m.groups()
|
prefix_before_s, season_s, _e, ep_s, _dash, ep2_s, rest = m.groups()
|
||||||
try:
|
try:
|
||||||
season = int(season_s)
|
season = int(season_s)
|
||||||
old_first = int(ep_s)
|
old_first = int(ep_s)
|
||||||
@@ -78,9 +77,7 @@ def parse_episode_stem(stem: str, pattern: str = DEFAULT_EPISODE_PATTERN) -> Opt
|
|||||||
title = _clean_title(rest)
|
title = _clean_title(rest)
|
||||||
return {
|
return {
|
||||||
"format": "sxxexx",
|
"format": "sxxexx",
|
||||||
"prefix": prefix,
|
"prefix_before_s": prefix_before_s,
|
||||||
"s_letter": s_letter,
|
|
||||||
"e_letter": e_letter,
|
|
||||||
"season": season,
|
"season": season,
|
||||||
"season_pad": len(season_s),
|
"season_pad": len(season_s),
|
||||||
"episode_pad": len(ep_s),
|
"episode_pad": len(ep_s),
|
||||||
@@ -145,14 +142,11 @@ def rewrite_episode_stem(
|
|||||||
s_pad = max(parsed["season_pad"], len(str(new_season)))
|
s_pad = max(parsed["season_pad"], len(str(new_season)))
|
||||||
e_pad = max(parsed["episode_pad"], pad, len(str(new_ep)))
|
e_pad = max(parsed["episode_pad"], pad, len(str(new_ep)))
|
||||||
e1 = str(new_ep).zfill(e_pad)
|
e1 = str(new_ep).zfill(e_pad)
|
||||||
head = (
|
prefix = f"{parsed['prefix_before_s']}S{new_season:0{s_pad}d}E"
|
||||||
f"{parsed['prefix']}{parsed['s_letter']}{new_season:0{s_pad}d}{parsed['e_letter']}"
|
|
||||||
)
|
|
||||||
if span <= 1:
|
if span <= 1:
|
||||||
return f"{head}{e1}{parsed['suffix']}"
|
return f"{prefix}{e1}{parsed['suffix']}"
|
||||||
e2 = str(new_ep + span - 1).zfill(e_pad)
|
e2 = str(new_ep + span - 1).zfill(e_pad)
|
||||||
range_prefix = f"-{parsed['e_letter']}"
|
return f"{prefix}{e1}-E{e2}{parsed['suffix']}"
|
||||||
return f"{head}{e1}{range_prefix}{e2}{parsed['suffix']}"
|
|
||||||
|
|
||||||
|
|
||||||
def rewrite_episode_number(
|
def rewrite_episode_number(
|
||||||
@@ -207,8 +201,7 @@ def match_filenames_to_episodes(
|
|||||||
|
|
||||||
file_entries: list[tuple[str, str, str, dict]] = []
|
file_entries: list[tuple[str, str, str, dict]] = []
|
||||||
for name in filenames:
|
for name in filenames:
|
||||||
base = Path(name).name
|
stem = name.rsplit(".", 1)[0] if "." in name and not name.startswith(".") else name
|
||||||
stem = base.rsplit(".", 1)[0] if "." in base and not base.startswith(".") else base
|
|
||||||
parsed = parse_episode_stem(stem, pattern)
|
parsed = parse_episode_stem(stem, pattern)
|
||||||
if not parsed or not parsed["title"]:
|
if not parsed or not parsed["title"]:
|
||||||
continue
|
continue
|
||||||
@@ -254,11 +247,7 @@ def match_filenames_to_episodes(
|
|||||||
name for name in filenames
|
name for name in filenames
|
||||||
if name not in mapping
|
if name not in mapping
|
||||||
and parse_episode_stem(
|
and parse_episode_stem(
|
||||||
(
|
name.rsplit(".", 1)[0] if "." in name and not name.startswith(".") else name,
|
||||||
Path(name).name.rsplit(".", 1)[0]
|
|
||||||
if "." in Path(name).name and not Path(name).name.startswith(".")
|
|
||||||
else Path(name).name
|
|
||||||
),
|
|
||||||
pattern,
|
pattern,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
"""Scan folders for rename candidates with optional recursion and extension filters."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from .pipeline import UNDO_FILENAME
|
|
||||||
|
|
||||||
FILE_TYPE_PRESETS: dict[str, str] = {
|
|
||||||
"All files": "",
|
|
||||||
"Video": ".mkv,.mp4,.avi,.webm,.m4v,.mov",
|
|
||||||
"Audio": ".mp3,.flac,.wav,.ogg,.m4a,.aac",
|
|
||||||
"Images": ".jpg,.jpeg,.png,.gif,.webp,.bmp",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_extension_filter(spec: str) -> frozenset[str] | None:
|
|
||||||
"""
|
|
||||||
Parse a user extension filter into a set of lowercase extensions with a leading dot.
|
|
||||||
Returns None when empty (match all files).
|
|
||||||
"""
|
|
||||||
spec = spec.strip()
|
|
||||||
if not spec:
|
|
||||||
return None
|
|
||||||
exts: set[str] = set()
|
|
||||||
for part in re.split(r"[,;\s]+", spec):
|
|
||||||
part = part.strip().lower()
|
|
||||||
if not part:
|
|
||||||
continue
|
|
||||||
if part.startswith("*."):
|
|
||||||
part = part[1:]
|
|
||||||
if not part.startswith("."):
|
|
||||||
part = "." + part
|
|
||||||
exts.add(part)
|
|
||||||
return frozenset(exts) if exts else None
|
|
||||||
|
|
||||||
|
|
||||||
def file_extension(rel_path: str) -> str:
|
|
||||||
"""Return the lowercase extension (with dot) of the basename, or '' if none."""
|
|
||||||
name = Path(rel_path).name
|
|
||||||
if "." in name and not name.startswith("."):
|
|
||||||
return "." + name.rsplit(".", 1)[-1].lower()
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def matches_extension(rel_path: str, extensions: frozenset[str] | None) -> bool:
|
|
||||||
if extensions is None:
|
|
||||||
return True
|
|
||||||
return file_extension(rel_path) in extensions
|
|
||||||
|
|
||||||
|
|
||||||
def list_files(
|
|
||||||
base_dir: str,
|
|
||||||
*,
|
|
||||||
recursive: bool = False,
|
|
||||||
extensions: frozenset[str] | None = None,
|
|
||||||
) -> list[str]:
|
|
||||||
"""Return sorted paths relative to base_dir."""
|
|
||||||
base = Path(base_dir)
|
|
||||||
if not base.is_dir():
|
|
||||||
return []
|
|
||||||
|
|
||||||
found: list[str] = []
|
|
||||||
|
|
||||||
def consider(path: Path) -> None:
|
|
||||||
if not path.is_file():
|
|
||||||
return
|
|
||||||
if path.name.startswith("."):
|
|
||||||
return
|
|
||||||
if path.name == UNDO_FILENAME:
|
|
||||||
return
|
|
||||||
rel = path.relative_to(base).as_posix()
|
|
||||||
if matches_extension(rel, extensions):
|
|
||||||
found.append(rel)
|
|
||||||
|
|
||||||
if recursive:
|
|
||||||
for dirpath, dirnames, filenames in os.walk(base):
|
|
||||||
dirnames[:] = sorted(d for d in dirnames if not d.startswith("."))
|
|
||||||
for fn in sorted(filenames):
|
|
||||||
consider(Path(dirpath) / fn)
|
|
||||||
else:
|
|
||||||
for entry in sorted(base.iterdir(), key=lambda p: p.name.lower()):
|
|
||||||
consider(entry)
|
|
||||||
|
|
||||||
return sorted(found, key=str.lower)
|
|
||||||
+7
-19
@@ -25,24 +25,13 @@ def apply_pipeline(
|
|||||||
index: int,
|
index: int,
|
||||||
total: int,
|
total: int,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Apply all enabled rules to a filename or relative path. Returns new name/path."""
|
"""Apply all enabled rules to a single filename (no path). Returns new filename."""
|
||||||
rel = Path(name)
|
stem, ext = _split_name(name)
|
||||||
if len(rel.parts) > 1:
|
|
||||||
parent = rel.parent
|
|
||||||
file_name = rel.name
|
|
||||||
else:
|
|
||||||
parent = None
|
|
||||||
file_name = name
|
|
||||||
|
|
||||||
stem, ext = _split_name(file_name)
|
|
||||||
for r in rules:
|
for r in rules:
|
||||||
if not r.enabled:
|
if not r.enabled:
|
||||||
continue
|
continue
|
||||||
stem, ext = r.apply(stem, ext, index, total, original_name=name)
|
stem, ext = r.apply(stem, ext, index, total, original_name=name)
|
||||||
new_name = stem + ext
|
return stem + ext
|
||||||
if parent is not None:
|
|
||||||
return str(parent / new_name)
|
|
||||||
return new_name
|
|
||||||
|
|
||||||
|
|
||||||
def compute_preview(
|
def compute_preview(
|
||||||
@@ -88,12 +77,11 @@ def perform_renames(
|
|||||||
final_names = {n for _, n in step1}
|
final_names = {n for _, n in step1}
|
||||||
temp_map = []
|
temp_map = []
|
||||||
for i, (old_path, new_name) in enumerate(step1):
|
for i, (old_path, new_name) in enumerate(step1):
|
||||||
new_path = base / new_name
|
temp_name = f"__temp_{i}_{old_path.name}{temp_suffix}"
|
||||||
temp_path = old_path.parent / f"__temp_{i}_{old_path.name}{temp_suffix}"
|
while temp_name in final_names or (base / temp_name).exists():
|
||||||
while temp_path.name in final_names or temp_path.exists() or temp_path == new_path:
|
|
||||||
i += 1
|
i += 1
|
||||||
temp_path = old_path.parent / f"__temp_{i}_{old_path.name}{temp_suffix}"
|
temp_name = f"__temp_{i}_{old_path.name}{temp_suffix}"
|
||||||
temp_map.append((old_path, temp_path, new_path))
|
temp_map.append((old_path, base / temp_name, base / new_name))
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
for old_p, temp_p, new_p in temp_map:
|
for old_p, temp_p, new_p in temp_map:
|
||||||
|
|||||||
+11
-80
@@ -22,14 +22,11 @@ from PyQt6.QtWidgets import (
|
|||||||
QHeaderView,
|
QHeaderView,
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QFrame,
|
QFrame,
|
||||||
QCheckBox,
|
|
||||||
QComboBox,
|
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QDir, QItemSelectionModel, QSettings
|
from PyQt6.QtCore import Qt, QDir, QItemSelectionModel
|
||||||
from PyQt6.QtGui import QFont, QColor
|
from PyQt6.QtGui import QFont, QColor
|
||||||
|
|
||||||
from engine.pipeline import compute_preview, perform_renames, save_undo_log, load_undo_log, perform_undo
|
from engine.pipeline import compute_preview, perform_renames, save_undo_log, load_undo_log, perform_undo
|
||||||
from engine.file_scan import FILE_TYPE_PRESETS, list_files, parse_extension_filter, file_extension
|
|
||||||
from engine.rules import Rule
|
from engine.rules import Rule
|
||||||
from .rule_widgets import (
|
from .rule_widgets import (
|
||||||
ReplaceRuleWidget,
|
ReplaceRuleWidget,
|
||||||
@@ -53,9 +50,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._base_dir = ""
|
self._base_dir = ""
|
||||||
self._file_names: list[str] = []
|
self._file_names: list[str] = []
|
||||||
self._preview_by_orig: dict[str, str] = {} # original name -> new name for selection preview
|
self._preview_by_orig: dict[str, str] = {} # original name -> new name for selection preview
|
||||||
self._split: QSplitter | None = None
|
|
||||||
self._setup_ui()
|
self._setup_ui()
|
||||||
self._restore_layout()
|
|
||||||
self._refresh_preview()
|
self._refresh_preview()
|
||||||
|
|
||||||
def _setup_ui(self):
|
def _setup_ui(self):
|
||||||
@@ -75,22 +70,6 @@ class MainWindow(QMainWindow):
|
|||||||
dir_layout.addWidget(browse_btn)
|
dir_layout.addWidget(browse_btn)
|
||||||
layout.addLayout(dir_layout)
|
layout.addLayout(dir_layout)
|
||||||
|
|
||||||
filter_layout = QHBoxLayout()
|
|
||||||
self.recursive_cb = QCheckBox("Include subfolders")
|
|
||||||
self.recursive_cb.toggled.connect(self._reload_files)
|
|
||||||
filter_layout.addWidget(self.recursive_cb)
|
|
||||||
filter_layout.addWidget(QLabel("File types:"))
|
|
||||||
self.types_preset = QComboBox()
|
|
||||||
self.types_preset.addItems(list(FILE_TYPE_PRESETS.keys()) + ["Custom"])
|
|
||||||
self.types_preset.currentTextChanged.connect(self._on_types_preset_changed)
|
|
||||||
filter_layout.addWidget(self.types_preset)
|
|
||||||
self.types_edit = QLineEdit()
|
|
||||||
self.types_edit.setPlaceholderText("e.g. .mkv,.mp4 or *.avi (empty = all types)")
|
|
||||||
self.types_edit.setReadOnly(True)
|
|
||||||
self.types_edit.editingFinished.connect(self._reload_files)
|
|
||||||
filter_layout.addWidget(self.types_edit, 1)
|
|
||||||
layout.addLayout(filter_layout)
|
|
||||||
|
|
||||||
# Split: left = rules, right = file list + preview
|
# Split: left = rules, right = file list + preview
|
||||||
split = QSplitter(Qt.Orientation.Horizontal)
|
split = QSplitter(Qt.Orientation.Horizontal)
|
||||||
|
|
||||||
@@ -174,53 +153,13 @@ class MainWindow(QMainWindow):
|
|||||||
split.addWidget(right)
|
split.addWidget(right)
|
||||||
|
|
||||||
split.setSizes([320, 680])
|
split.setSizes([320, 680])
|
||||||
self._split = split
|
|
||||||
layout.addWidget(split, 1)
|
layout.addWidget(split, 1)
|
||||||
|
|
||||||
def _settings(self) -> QSettings:
|
|
||||||
return QSettings()
|
|
||||||
|
|
||||||
def _restore_layout(self) -> None:
|
|
||||||
settings = self._settings()
|
|
||||||
geometry = settings.value("window/geometry")
|
|
||||||
if geometry is not None:
|
|
||||||
self.restoreGeometry(geometry)
|
|
||||||
if self._split is not None:
|
|
||||||
split_state = settings.value("splitter/state")
|
|
||||||
if split_state is not None:
|
|
||||||
self._split.restoreState(split_state)
|
|
||||||
header = self.preview_table.horizontalHeader()
|
|
||||||
header_state = settings.value("preview_table/header")
|
|
||||||
if header_state is not None:
|
|
||||||
header.restoreState(header_state)
|
|
||||||
|
|
||||||
def _save_layout(self) -> None:
|
|
||||||
settings = self._settings()
|
|
||||||
settings.setValue("window/geometry", self.saveGeometry())
|
|
||||||
if self._split is not None:
|
|
||||||
settings.setValue("splitter/state", self._split.saveState())
|
|
||||||
settings.setValue(
|
|
||||||
"preview_table/header",
|
|
||||||
self.preview_table.horizontalHeader().saveState(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def closeEvent(self, event) -> None:
|
|
||||||
self._save_layout()
|
|
||||||
super().closeEvent(event)
|
|
||||||
|
|
||||||
def _browse(self):
|
def _browse(self):
|
||||||
path = QFileDialog.getExistingDirectory(self, "Select folder")
|
path = QFileDialog.getExistingDirectory(self, "Select folder")
|
||||||
if path:
|
if path:
|
||||||
self.dir_edit.setText(path)
|
self.dir_edit.setText(path)
|
||||||
|
|
||||||
def _on_types_preset_changed(self, preset: str):
|
|
||||||
if preset != "Custom":
|
|
||||||
self.types_edit.setText(FILE_TYPE_PRESETS.get(preset, ""))
|
|
||||||
self.types_edit.setReadOnly(True)
|
|
||||||
else:
|
|
||||||
self.types_edit.setReadOnly(False)
|
|
||||||
self._reload_files()
|
|
||||||
|
|
||||||
def _on_dir_changed(self):
|
def _on_dir_changed(self):
|
||||||
path = self.dir_edit.text().strip()
|
path = self.dir_edit.text().strip()
|
||||||
if not path or not os.path.isdir(path):
|
if not path or not os.path.isdir(path):
|
||||||
@@ -228,19 +167,10 @@ class MainWindow(QMainWindow):
|
|||||||
self._file_names = []
|
self._file_names = []
|
||||||
else:
|
else:
|
||||||
self._base_dir = path
|
self._base_dir = path
|
||||||
self._reload_files()
|
self._file_names = sorted(
|
||||||
return
|
f for f in os.listdir(path)
|
||||||
self._refresh_preview()
|
if os.path.isfile(os.path.join(path, f)) and not f.startswith(".")
|
||||||
|
)
|
||||||
def _reload_files(self):
|
|
||||||
if not self._base_dir:
|
|
||||||
return
|
|
||||||
extensions = parse_extension_filter(self.types_edit.text())
|
|
||||||
self._file_names = list_files(
|
|
||||||
self._base_dir,
|
|
||||||
recursive=self.recursive_cb.isChecked(),
|
|
||||||
extensions=extensions,
|
|
||||||
)
|
|
||||||
self._refresh_preview()
|
self._refresh_preview()
|
||||||
|
|
||||||
def _on_rule_changed(self):
|
def _on_rule_changed(self):
|
||||||
@@ -259,7 +189,9 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _ext_for(self, filename: str) -> str:
|
def _ext_for(self, filename: str) -> str:
|
||||||
"""Return file extension with leading dot, or empty string if none."""
|
"""Return file extension with leading dot, or empty string if none."""
|
||||||
return file_extension(filename)
|
if "." in filename and not filename.startswith("."):
|
||||||
|
return "." + filename.rsplit(".", 1)[-1].lower()
|
||||||
|
return ""
|
||||||
|
|
||||||
def _on_preview_selection_changed(self):
|
def _on_preview_selection_changed(self):
|
||||||
"""Show preview (new name) only in selected rows; others show original."""
|
"""Show preview (new name) only in selected rows; others show original."""
|
||||||
@@ -331,8 +263,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.preview_status.setText("Warning: some new names are duplicated. Fix rules to avoid overwriting.")
|
self.preview_status.setText("Warning: some new names are duplicated. Fix rules to avoid overwriting.")
|
||||||
self.preview_status.setStyleSheet("color: #c00;")
|
self.preview_status.setStyleSheet("color: #c00;")
|
||||||
else:
|
else:
|
||||||
scope = "including subfolders" if self.recursive_cb.isChecked() else "top folder only"
|
self.preview_status.setText(f"{len(preview)} file(s). Ready to rename.")
|
||||||
self.preview_status.setText(f"{len(preview)} file(s) ({scope}). Ready to rename.")
|
|
||||||
self.preview_status.setStyleSheet("")
|
self.preview_status.setStyleSheet("")
|
||||||
|
|
||||||
def _apply_renames(self):
|
def _apply_renames(self):
|
||||||
@@ -386,7 +317,7 @@ class MainWindow(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
save_undo_log(self._base_dir, renames)
|
save_undo_log(self._base_dir, renames)
|
||||||
QMessageBox.information(self, "Done", f"Renamed {len(results)} file(s).")
|
QMessageBox.information(self, "Done", f"Renamed {len(results)} file(s).")
|
||||||
self._reload_files()
|
self._on_dir_changed()
|
||||||
|
|
||||||
def _undo_renames(self):
|
def _undo_renames(self):
|
||||||
if not self._base_dir:
|
if not self._base_dir:
|
||||||
@@ -418,4 +349,4 @@ class MainWindow(QMainWindow):
|
|||||||
QMessageBox.warning(self, "Undo errors", msg)
|
QMessageBox.warning(self, "Undo errors", msg)
|
||||||
else:
|
else:
|
||||||
QMessageBox.information(self, "Undone", f"Reverted {len(results)} file(s).")
|
QMessageBox.information(self, "Undone", f"Reverted {len(results)} file(s).")
|
||||||
self._reload_files()
|
self._on_dir_changed()
|
||||||
|
|||||||
@@ -21,11 +21,7 @@ def main():
|
|||||||
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
||||||
)
|
)
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
app.setOrganizationName("HSRename")
|
|
||||||
app.setOrganizationDomain("hisora.dev")
|
|
||||||
app.setApplicationName("HSRename")
|
app.setApplicationName("HSRename")
|
||||||
version_file = _root / "VERSION"
|
|
||||||
app.setApplicationVersion(version_file.read_text().strip() if version_file.is_file() else "")
|
|
||||||
win = MainWindow()
|
win = MainWindow()
|
||||||
win.show()
|
win.show()
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
HSRename 1.0.8
|
|
||||||
- Include subfolders option for recursive file listing
|
|
||||||
- File type filter with Video, Audio, Images presets and custom extensions
|
|
||||||
- Rename rules preserve subfolder paths; nested apply/undo supported
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
HSRename 1.0.9
|
|
||||||
- Remember window size, splitter position, and preview column widths between sessions
|
|
||||||
- Fix double S in SxxExx renames (SS02E33) when show name ends before the episode code
|
|
||||||
Reference in New Issue
Block a user