Anime and other non-English shows returned Japanese default titles while filenames use English; add Episode titles language control defaulting to English. Co-authored-by: Cursor <[email protected]>
708 lines
25 KiB
Python
708 lines
25 KiB
Python
"""
|
|
Rule configuration widgets: each rule type has a small form that updates the Rule model.
|
|
"""
|
|
from PyQt6.QtWidgets import (
|
|
QWidget,
|
|
QVBoxLayout,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QLineEdit,
|
|
QSpinBox,
|
|
QComboBox,
|
|
QCheckBox,
|
|
QGroupBox,
|
|
QFormLayout,
|
|
QStackedWidget,
|
|
QPushButton,
|
|
QFileDialog,
|
|
QMessageBox,
|
|
)
|
|
from PyQt6.QtCore import pyqtSignal, QThread
|
|
|
|
from engine.rules import (
|
|
ReplaceRule,
|
|
InsertRule,
|
|
RemoveRule,
|
|
CaseRule,
|
|
NumberingRule,
|
|
EpisodeRenumberRule,
|
|
TvdbEpisodeRenumberRule,
|
|
RegexRule,
|
|
PrefixSuffixRule,
|
|
CsvMappingRule,
|
|
)
|
|
from engine.tvdb_client import TvdbClient, TvdbError, TVDB_API_KEY, SEASON_TYPE_CHOICES, LANGUAGE_CHOICES
|
|
from engine.episode_match import match_filenames_to_episodes
|
|
|
|
|
|
class ReplaceRuleWidget(QWidget):
|
|
ruleChanged = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
layout = QFormLayout(self)
|
|
self.enabled_cb = QCheckBox("Use this rule")
|
|
self.enabled_cb.setChecked(False)
|
|
self.enabled_cb.toggled.connect(self._emit)
|
|
layout.addRow(self.enabled_cb)
|
|
self.find = QLineEdit()
|
|
self.find.setPlaceholderText("Text to find")
|
|
self.find.textChanged.connect(self._emit)
|
|
self.replace = QLineEdit()
|
|
self.replace.setPlaceholderText("Replace with")
|
|
self.replace.textChanged.connect(self._emit)
|
|
self.caseSensitive = QCheckBox("Case sensitive")
|
|
self.caseSensitive.toggled.connect(self._emit)
|
|
self.wholeWord = QCheckBox("Whole word only")
|
|
self.wholeWord.toggled.connect(self._emit)
|
|
layout.addRow("Find:", self.find)
|
|
layout.addRow("Replace with:", self.replace)
|
|
layout.addRow(self.caseSensitive)
|
|
layout.addRow(self.wholeWord)
|
|
|
|
def _emit(self):
|
|
self.ruleChanged.emit()
|
|
|
|
def getRule(self) -> ReplaceRule:
|
|
r = ReplaceRule(
|
|
find=self.find.text(),
|
|
replace=self.replace.text(),
|
|
case_sensitive=self.caseSensitive.isChecked(),
|
|
whole_word=self.wholeWord.isChecked(),
|
|
)
|
|
r.enabled = self.enabled_cb.isChecked()
|
|
return r
|
|
|
|
|
|
class RegexRuleWidget(QWidget):
|
|
ruleChanged = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
layout = QFormLayout(self)
|
|
self.enabled_cb = QCheckBox("Use this rule")
|
|
self.enabled_cb.setChecked(False)
|
|
self.enabled_cb.toggled.connect(self._emit)
|
|
layout.addRow(self.enabled_cb)
|
|
self.pattern = QLineEdit()
|
|
self.pattern.setPlaceholderText(r"e.g. S(\d+)E(\d+)")
|
|
self.pattern.textChanged.connect(self._emit)
|
|
self.replacement = QLineEdit()
|
|
self.replacement.setPlaceholderText(r"e.g. S\1E\2")
|
|
self.replacement.textChanged.connect(self._emit)
|
|
layout.addRow("Regex pattern:", self.pattern)
|
|
layout.addRow("Replacement:", self.replacement)
|
|
|
|
def _emit(self):
|
|
self.ruleChanged.emit()
|
|
|
|
def getRule(self) -> RegexRule:
|
|
r = RegexRule(pattern=self.pattern.text(), replacement=self.replacement.text())
|
|
r.enabled = self.enabled_cb.isChecked()
|
|
return r
|
|
|
|
|
|
class InsertRuleWidget(QWidget):
|
|
ruleChanged = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
layout = QFormLayout(self)
|
|
self.enabled_cb = QCheckBox("Use this rule")
|
|
self.enabled_cb.setChecked(False)
|
|
self.enabled_cb.toggled.connect(self._emit)
|
|
layout.addRow(self.enabled_cb)
|
|
self.text = QLineEdit()
|
|
self.text.setPlaceholderText("Text to insert")
|
|
self.text.textChanged.connect(self._emit)
|
|
self.position = QComboBox()
|
|
self.position.addItems(["At start", "At end", "At position..."])
|
|
self.position.setCurrentIndex(0)
|
|
self.position.currentIndexChanged.connect(self._emit)
|
|
self.positionSpin = QSpinBox()
|
|
self.positionSpin.setMinimum(0)
|
|
self.positionSpin.setMaximum(9999)
|
|
self.positionSpin.valueChanged.connect(self._emit)
|
|
layout.addRow("Text:", self.text)
|
|
layout.addRow("Position:", self.position)
|
|
layout.addRow("Index:", self.positionSpin)
|
|
|
|
def _emit(self):
|
|
self.ruleChanged.emit()
|
|
|
|
def getRule(self) -> InsertRule:
|
|
idx = self.position.currentIndex()
|
|
if idx == 0:
|
|
pos = 0
|
|
elif idx == 1:
|
|
pos = -1
|
|
else:
|
|
pos = self.positionSpin.value()
|
|
r = InsertRule(text=self.text.text(), position=pos)
|
|
r.enabled = self.enabled_cb.isChecked()
|
|
return r
|
|
|
|
|
|
class RemoveRuleWidget(QWidget):
|
|
ruleChanged = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
layout = QFormLayout(self)
|
|
self.enabled_cb = QCheckBox("Use this rule")
|
|
self.enabled_cb.setChecked(False)
|
|
self.enabled_cb.toggled.connect(self._emit)
|
|
layout.addRow(self.enabled_cb)
|
|
self.removeType = QComboBox()
|
|
self.removeType.addItems([
|
|
"Remove text",
|
|
"Remove all digits",
|
|
"Remove first N characters",
|
|
"Remove last N characters",
|
|
])
|
|
self.removeType.currentIndexChanged.connect(self._emit)
|
|
self.value = QLineEdit()
|
|
self.value.setPlaceholderText("Text or number")
|
|
self.value.textChanged.connect(self._emit)
|
|
layout.addRow("Type:", self.removeType)
|
|
layout.addRow("Value:", self.value)
|
|
|
|
def _emit(self):
|
|
self.ruleChanged.emit()
|
|
|
|
def getRule(self) -> RemoveRule:
|
|
idx = self.removeType.currentIndex()
|
|
types = ["chars", "digits", "first_n", "last_n"]
|
|
r = RemoveRule(remove_type=types[idx], value=self.value.text())
|
|
r.enabled = self.enabled_cb.isChecked()
|
|
return r
|
|
|
|
|
|
class CaseRuleWidget(QWidget):
|
|
ruleChanged = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
layout = QFormLayout(self)
|
|
self.enabled_cb = QCheckBox("Use this rule")
|
|
self.enabled_cb.setChecked(False)
|
|
self.enabled_cb.toggled.connect(self._emit)
|
|
layout.addRow(self.enabled_cb)
|
|
self.caseType = QComboBox()
|
|
self.caseType.addItems(["Title Case", "UPPER", "lower", "Sentence case"])
|
|
self.caseType.currentIndexChanged.connect(self._emit)
|
|
layout.addRow("Case:", self.caseType)
|
|
|
|
def _emit(self):
|
|
self.ruleChanged.emit()
|
|
|
|
def getRule(self) -> CaseRule:
|
|
types = ["title", "upper", "lower", "sentence"]
|
|
r = CaseRule(case_type=types[self.caseType.currentIndex()])
|
|
r.enabled = self.enabled_cb.isChecked()
|
|
return r
|
|
|
|
|
|
class NumberingRuleWidget(QWidget):
|
|
ruleChanged = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
layout = QFormLayout(self)
|
|
self.enabled_cb = QCheckBox("Use this rule")
|
|
self.enabled_cb.setChecked(False)
|
|
self.enabled_cb.toggled.connect(self._emit)
|
|
layout.addRow(self.enabled_cb)
|
|
self.start = QSpinBox()
|
|
self.start.setMinimum(-9999)
|
|
self.start.setMaximum(99999)
|
|
self.start.setValue(1)
|
|
self.start.valueChanged.connect(self._emit)
|
|
self.step = QSpinBox()
|
|
self.step.setMinimum(1)
|
|
self.step.setMaximum(100)
|
|
self.step.setValue(1)
|
|
self.step.valueChanged.connect(self._emit)
|
|
self.padding = QSpinBox()
|
|
self.padding.setMinimum(1)
|
|
self.padding.setMaximum(10)
|
|
self.padding.setValue(2)
|
|
self.padding.valueChanged.connect(self._emit)
|
|
self.where = QComboBox()
|
|
self.where.addItems(["Prefix", "Suffix", "Insert at index"])
|
|
self.where.currentIndexChanged.connect(self._emit)
|
|
self.separator = QLineEdit()
|
|
self.separator.setText(" ")
|
|
self.separator.textChanged.connect(self._emit)
|
|
self.insertAt = QSpinBox()
|
|
self.insertAt.setMinimum(0)
|
|
self.insertAt.setMaximum(9999)
|
|
self.insertAt.valueChanged.connect(self._emit)
|
|
layout.addRow("Start:", self.start)
|
|
layout.addRow("Step:", self.step)
|
|
layout.addRow("Padding:", self.padding)
|
|
layout.addRow("Where:", self.where)
|
|
layout.addRow("Separator:", self.separator)
|
|
layout.addRow("Insert at index:", self.insertAt)
|
|
|
|
def _emit(self):
|
|
self.ruleChanged.emit()
|
|
|
|
def getRule(self) -> NumberingRule:
|
|
where_map = ["prefix", "suffix", "insert_at"]
|
|
r = NumberingRule(
|
|
start=self.start.value(),
|
|
step=self.step.value(),
|
|
padding=self.padding.value(),
|
|
where=where_map[self.where.currentIndex()],
|
|
insert_at=self.insertAt.value(),
|
|
separator=self.separator.text() or " ",
|
|
)
|
|
r.enabled = self.enabled_cb.isChecked()
|
|
return r
|
|
|
|
|
|
class EpisodeRenumberRuleWidget(QWidget):
|
|
ruleChanged = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
layout = QFormLayout(self)
|
|
self.enabled_cb = QCheckBox("Use this rule")
|
|
self.enabled_cb.setChecked(False)
|
|
self.enabled_cb.toggled.connect(self._emit)
|
|
layout.addRow(self.enabled_cb)
|
|
self.start = QSpinBox()
|
|
self.start.setMinimum(1)
|
|
self.start.setMaximum(9999)
|
|
self.start.setValue(1)
|
|
self.start.valueChanged.connect(self._emit)
|
|
self.step = QSpinBox()
|
|
self.step.setMinimum(1)
|
|
self.step.setValue(1)
|
|
self.step.valueChanged.connect(self._emit)
|
|
self.padding = QSpinBox()
|
|
self.padding.setMinimum(1)
|
|
self.padding.setMaximum(3)
|
|
self.padding.setValue(2)
|
|
self.padding.valueChanged.connect(self._emit)
|
|
layout.addRow("First episode number:", self.start)
|
|
layout.addRow("Step:", self.step)
|
|
layout.addRow("Zero-pad width:", self.padding)
|
|
info = QLabel(
|
|
"Matches S01E05 - Title (single) or S01E05-E06 - Title (two-part). "
|
|
"Ranges keep their length: S01E01-E02, S01E03-E04, … Order follows the file list / table sort."
|
|
)
|
|
info.setWordWrap(True)
|
|
layout.addRow(info)
|
|
|
|
def _emit(self):
|
|
self.ruleChanged.emit()
|
|
|
|
def getRule(self) -> EpisodeRenumberRule:
|
|
r = EpisodeRenumberRule(
|
|
start=self.start.value(),
|
|
step=self.step.value(),
|
|
padding=self.padding.value(),
|
|
)
|
|
r.enabled = self.enabled_cb.isChecked()
|
|
return r
|
|
|
|
|
|
class _TvdbSearchWorker(QThread):
|
|
finished = pyqtSignal(list)
|
|
failed = pyqtSignal(str)
|
|
|
|
def __init__(self, query: str):
|
|
super().__init__()
|
|
self.query = query
|
|
|
|
def run(self):
|
|
try:
|
|
client = TvdbClient(TVDB_API_KEY)
|
|
results = client.search_series(self.query)
|
|
self.finished.emit(results)
|
|
except TvdbError as e:
|
|
self.failed.emit(str(e))
|
|
except Exception as e:
|
|
self.failed.emit(str(e))
|
|
|
|
|
|
class _TvdbMatchWorker(QThread):
|
|
finished = pyqtSignal(dict, list, list)
|
|
failed = pyqtSignal(str)
|
|
|
|
def __init__(
|
|
self,
|
|
series_id: int,
|
|
season: int,
|
|
season_type: str,
|
|
filenames: list[str],
|
|
all_seasons: bool,
|
|
multi_episode: bool,
|
|
language: str,
|
|
):
|
|
super().__init__()
|
|
self.series_id = series_id
|
|
self.season = season
|
|
self.season_type = season_type
|
|
self.filenames = filenames
|
|
self.all_seasons = all_seasons
|
|
self.multi_episode = multi_episode
|
|
self.language = language
|
|
|
|
def run(self):
|
|
try:
|
|
client = TvdbClient(TVDB_API_KEY)
|
|
lang = self.language
|
|
if self.all_seasons:
|
|
official = client.get_all_episodes(
|
|
self.series_id, season_type="official", language=lang,
|
|
)
|
|
season_filter = 0
|
|
else:
|
|
official = client.get_season_episodes(
|
|
self.series_id,
|
|
self.season,
|
|
season_type="official",
|
|
language=lang,
|
|
)
|
|
season_filter = self.season
|
|
|
|
if self.all_seasons:
|
|
episodes = client.get_all_episodes(
|
|
self.series_id,
|
|
season_type=self.season_type,
|
|
language=lang,
|
|
)
|
|
else:
|
|
episodes = client.get_season_episodes(
|
|
self.series_id,
|
|
self.season,
|
|
season_type=self.season_type,
|
|
language=lang,
|
|
)
|
|
|
|
combined = None
|
|
if self.multi_episode or self.season_type == "alternate":
|
|
if self.all_seasons:
|
|
combined = client.get_all_episodes(
|
|
self.series_id,
|
|
season_type="alternate",
|
|
language=lang,
|
|
)
|
|
else:
|
|
combined = client.get_season_episodes(
|
|
self.series_id,
|
|
self.season,
|
|
season_type="alternate",
|
|
language=lang,
|
|
)
|
|
if not combined and lang:
|
|
# Combined order may lack translations; fall back to show language.
|
|
if self.all_seasons:
|
|
combined = client.get_all_episodes(
|
|
self.series_id, season_type="alternate",
|
|
)
|
|
else:
|
|
combined = client.get_season_episodes(
|
|
self.series_id, self.season, season_type="alternate",
|
|
)
|
|
|
|
if self.season_type == "alternate":
|
|
episodes = official
|
|
|
|
if not episodes and not combined:
|
|
label = "all seasons" if self.all_seasons else f"season {self.season}"
|
|
raise TvdbError(f"No episodes found for {label}")
|
|
mapping, unmatched, notes = match_filenames_to_episodes(
|
|
self.filenames,
|
|
episodes,
|
|
season_filter=season_filter,
|
|
official_episodes=official,
|
|
combined_episodes=combined,
|
|
)
|
|
self.finished.emit(mapping, unmatched, notes)
|
|
except TvdbError as e:
|
|
self.failed.emit(str(e))
|
|
except Exception as e:
|
|
self.failed.emit(str(e))
|
|
|
|
|
|
class TvdbEpisodeRenumberRuleWidget(QWidget):
|
|
ruleChanged = pyqtSignal()
|
|
matchCompleted = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._file_names: list[str] = []
|
|
self._episode_mapping: dict[str, int] = {}
|
|
self._search_results: list = []
|
|
self._search_worker: _TvdbSearchWorker | None = None
|
|
self._match_worker: _TvdbMatchWorker | None = None
|
|
|
|
layout = QFormLayout(self)
|
|
self.enabled_cb = QCheckBox("Use this rule")
|
|
self.enabled_cb.setChecked(False)
|
|
self.enabled_cb.toggled.connect(self._emit)
|
|
layout.addRow(self.enabled_cb)
|
|
|
|
layout.addRow("Source:", QLabel("TheTVDB.com"))
|
|
|
|
search_row = QHBoxLayout()
|
|
self.search_query = QLineEdit()
|
|
self.search_query.setPlaceholderText("Show name to search")
|
|
self.search_query.returnPressed.connect(self._search_series)
|
|
search_btn = QPushButton("Search")
|
|
search_btn.clicked.connect(self._search_series)
|
|
search_row.addWidget(self.search_query, 1)
|
|
search_row.addWidget(search_btn)
|
|
layout.addRow("Show:", search_row)
|
|
|
|
self.series_combo = QComboBox()
|
|
self.series_combo.setEnabled(False)
|
|
self.series_combo.currentIndexChanged.connect(self._emit)
|
|
layout.addRow("Pick show:", self.series_combo)
|
|
|
|
self.order_type = QComboBox()
|
|
for label, value in SEASON_TYPE_CHOICES:
|
|
self.order_type.addItem(label, value)
|
|
self.order_type.currentIndexChanged.connect(self._emit)
|
|
layout.addRow("Episode order:", self.order_type)
|
|
|
|
self.title_language = QComboBox()
|
|
for label, value in LANGUAGE_CHOICES:
|
|
self.title_language.addItem(label, value)
|
|
self.title_language.setCurrentIndex(1) # English — most filenames use it
|
|
self.title_language.currentIndexChanged.connect(self._emit)
|
|
layout.addRow("Episode titles:", self.title_language)
|
|
|
|
self.season = QSpinBox()
|
|
self.season.setMinimum(0)
|
|
self.season.setMaximum(99)
|
|
self.season.setValue(0)
|
|
self.season.setSpecialValueText("All seasons")
|
|
self.season.valueChanged.connect(self._emit)
|
|
layout.addRow("Season:", self.season)
|
|
|
|
self.padding = QSpinBox()
|
|
self.padding.setMinimum(1)
|
|
self.padding.setMaximum(3)
|
|
self.padding.setValue(2)
|
|
self.padding.valueChanged.connect(self._emit)
|
|
layout.addRow("Zero-pad width:", self.padding)
|
|
|
|
self.multi_episode_cb = QCheckBox("Multi-episode files (Jellyfin S01E01-E02)")
|
|
self.multi_episode_cb.setChecked(True)
|
|
self.multi_episode_cb.setToolTip(
|
|
"Match combined-order titles from TheTVDB and rename using aired episode ranges, "
|
|
"e.g. two episodes in one file becomes S01E01-E02."
|
|
)
|
|
self.multi_episode_cb.toggled.connect(self._emit)
|
|
layout.addRow(self.multi_episode_cb)
|
|
|
|
self.match_btn = QPushButton("Match titles from file list")
|
|
self.match_btn.clicked.connect(self._match_titles)
|
|
layout.addRow(self.match_btn)
|
|
|
|
self.status = QLabel("Search for a show, choose episode order, then match titles (0 = all seasons).")
|
|
self.status.setWordWrap(True)
|
|
layout.addRow(self.status)
|
|
|
|
info = QLabel(
|
|
"Reads titles from S01E05 - Episode Title, S01E05-E06 (multi-episode), "
|
|
"or Show 04x01 Episode Title filenames. "
|
|
"For non-English shows, set Episode titles to English if your filenames use English. "
|
|
"Use All seasons to match across every season and fix wrong season numbers too."
|
|
)
|
|
info.setWordWrap(True)
|
|
layout.addRow(info)
|
|
|
|
def set_file_names(self, names: list[str]) -> None:
|
|
self._file_names = list(names)
|
|
|
|
def _order_label(self) -> str:
|
|
return self.order_type.currentText()
|
|
|
|
def _order_value(self) -> str:
|
|
value = self.order_type.currentData()
|
|
return value if value else "default"
|
|
|
|
def _language_value(self) -> str:
|
|
value = self.title_language.currentData()
|
|
return value if value else ""
|
|
|
|
def _language_label(self) -> str:
|
|
return self.title_language.currentText()
|
|
|
|
def _emit(self):
|
|
self.ruleChanged.emit()
|
|
|
|
def _search_series(self):
|
|
query = self.search_query.text().strip()
|
|
if not query:
|
|
QMessageBox.warning(self, "Search", "Enter a show name to search.")
|
|
return
|
|
self.match_btn.setEnabled(False)
|
|
self.series_combo.setEnabled(False)
|
|
self.status.setText("Searching TheTVDB…")
|
|
self._search_worker = _TvdbSearchWorker(query)
|
|
self._search_worker.finished.connect(self._on_search_finished)
|
|
self._search_worker.failed.connect(self._on_search_failed)
|
|
self._search_worker.start()
|
|
|
|
def _on_search_finished(self, results):
|
|
self._search_results = results
|
|
self.series_combo.clear()
|
|
if not results:
|
|
self.status.setText("No series found. Try a different search.")
|
|
self.match_btn.setEnabled(True)
|
|
return
|
|
for s in results:
|
|
label = s.name
|
|
if s.year:
|
|
label = f"{label} ({s.year})"
|
|
self.series_combo.addItem(label, s.id)
|
|
self.series_combo.setEnabled(True)
|
|
self.match_btn.setEnabled(True)
|
|
self.status.setText(f"Found {len(results)} series. Pick one and match titles.")
|
|
self._emit()
|
|
|
|
def _on_search_failed(self, message: str):
|
|
self.match_btn.setEnabled(True)
|
|
self.series_combo.setEnabled(self.series_combo.count() > 0)
|
|
self.status.setText(f"Search failed: {message}")
|
|
|
|
def _match_titles(self):
|
|
if self.series_combo.count() == 0:
|
|
QMessageBox.warning(self, "Match", "Search and select a show first.")
|
|
return
|
|
if not self._file_names:
|
|
QMessageBox.warning(self, "Match", "Load a folder with files first.")
|
|
return
|
|
series_id = self.series_combo.currentData()
|
|
if series_id is None:
|
|
return
|
|
self.match_btn.setEnabled(False)
|
|
all_seasons = self.season.value() == 0
|
|
self.status.setText(
|
|
f"Fetching episodes ({self._order_label()}, {self._language_label()}, "
|
|
f"{'all seasons' if all_seasons else f'season {self.season.value()}'}…) and matching titles…"
|
|
)
|
|
self._match_worker = _TvdbMatchWorker(
|
|
int(series_id),
|
|
self.season.value(),
|
|
self._order_value(),
|
|
self._file_names,
|
|
all_seasons,
|
|
self.multi_episode_cb.isChecked(),
|
|
self._language_value(),
|
|
)
|
|
self._match_worker.finished.connect(self._on_match_finished)
|
|
self._match_worker.failed.connect(self._on_match_failed)
|
|
self._match_worker.start()
|
|
|
|
def _on_match_finished(self, mapping: dict, unmatched: list, notes: list):
|
|
self._episode_mapping = mapping
|
|
self.match_btn.setEnabled(True)
|
|
matched = len(mapping)
|
|
total = len(self._file_names)
|
|
scope = "all seasons" if self.season.value() == 0 else f"season {self.season.value()}"
|
|
msg = f"Matched {matched} of {total} file(s) ({scope}, {self._order_label()}, {self._language_label()})."
|
|
if unmatched:
|
|
msg += f" {len(unmatched)} file(s) unmatched."
|
|
self.status.setText(msg)
|
|
self.matchCompleted.emit()
|
|
self._emit()
|
|
|
|
def _on_match_failed(self, message: str):
|
|
self.match_btn.setEnabled(True)
|
|
self.status.setText(f"Match failed: {message}")
|
|
|
|
def getRule(self) -> TvdbEpisodeRenumberRule:
|
|
mapping: dict = {}
|
|
for k, v in self._episode_mapping.items():
|
|
if hasattr(v, "season"):
|
|
if getattr(v, "episode_end", None) and v.episode_end > v.episode:
|
|
mapping[k] = (v.season, v.episode, v.episode_end)
|
|
else:
|
|
mapping[k] = (v.season, v.episode)
|
|
else:
|
|
mapping[k] = v
|
|
r = TvdbEpisodeRenumberRule(
|
|
episode_mapping=mapping,
|
|
padding=self.padding.value(),
|
|
)
|
|
r.enabled = self.enabled_cb.isChecked()
|
|
return r
|
|
|
|
|
|
class PrefixSuffixRuleWidget(QWidget):
|
|
ruleChanged = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
layout = QFormLayout(self)
|
|
self.enabled_cb = QCheckBox("Use this rule")
|
|
self.enabled_cb.setChecked(False)
|
|
self.enabled_cb.toggled.connect(self._emit)
|
|
layout.addRow(self.enabled_cb)
|
|
self.prefix = QLineEdit()
|
|
self.prefix.setPlaceholderText("Prefix")
|
|
self.prefix.textChanged.connect(self._emit)
|
|
self.suffix = QLineEdit()
|
|
self.suffix.setPlaceholderText("Suffix")
|
|
self.suffix.textChanged.connect(self._emit)
|
|
layout.addRow("Prefix:", self.prefix)
|
|
layout.addRow("Suffix:", self.suffix)
|
|
|
|
def _emit(self):
|
|
self.ruleChanged.emit()
|
|
|
|
def getRule(self) -> PrefixSuffixRule:
|
|
r = PrefixSuffixRule(prefix=self.prefix.text(), suffix=self.suffix.text())
|
|
r.enabled = self.enabled_cb.isChecked()
|
|
return r
|
|
|
|
|
|
class CsvMappingRuleWidget(QWidget):
|
|
ruleChanged = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
layout = QFormLayout(self)
|
|
self.enabled_cb = QCheckBox("Use this rule")
|
|
self.enabled_cb.setChecked(False)
|
|
self.enabled_cb.toggled.connect(self._emit)
|
|
layout.addRow(self.enabled_cb)
|
|
path_row = QHBoxLayout()
|
|
self.path_edit = QLineEdit()
|
|
self.path_edit.setPlaceholderText("Path to CSV file…")
|
|
self.path_edit.textChanged.connect(self._emit)
|
|
browse_btn = QPushButton("Browse…")
|
|
browse_btn.clicked.connect(self._browse)
|
|
path_row.addWidget(self.path_edit, 1)
|
|
path_row.addWidget(browse_btn)
|
|
layout.addRow("CSV file:", path_row)
|
|
info = QLabel('CSV must have columns "Original Name" and "Target Name". Lookup is by current filename.')
|
|
info.setWordWrap(True)
|
|
layout.addRow(info)
|
|
|
|
def _browse(self):
|
|
path, _ = QFileDialog.getOpenFileName(
|
|
self,
|
|
"Select CSV",
|
|
"",
|
|
"CSV (*.csv);;All files (*)",
|
|
)
|
|
if path:
|
|
self.path_edit.setText(path)
|
|
|
|
def _emit(self):
|
|
self.ruleChanged.emit()
|
|
|
|
def getRule(self) -> CsvMappingRule:
|
|
r = CsvMappingRule(csv_path=self.path_edit.text().strip())
|
|
r.enabled = self.enabled_cb.isChecked()
|
|
return r
|