Release v1.0.10: Jellyfin multi-episode file naming (S01E01-E02).

Match TheTVDB combined-order titles and rename to official aired episode ranges for files that contain two episodes in one video.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Bulk Renamer
2026-07-03 18:23:06 -05:00
co-authored by Cursor
parent a65ae73bcc
commit a6339467d4
5 changed files with 270 additions and 38 deletions
+56 -8
View File
@@ -339,6 +339,7 @@ class _TvdbMatchWorker(QThread):
season_type: str,
filenames: list[str],
all_seasons: bool,
multi_episode: bool,
):
super().__init__()
self.series_id = series_id
@@ -346,30 +347,60 @@ class _TvdbMatchWorker(QThread):
self.season_type = season_type
self.filenames = filenames
self.all_seasons = all_seasons
self.multi_episode = multi_episode
def run(self):
try:
client = TvdbClient(TVDB_API_KEY)
if self.all_seasons:
official = client.get_all_episodes(self.series_id, season_type="official")
season_filter = 0
else:
official = client.get_season_episodes(
self.series_id,
self.season,
season_type="official",
)
season_filter = self.season
if self.all_seasons:
episodes = client.get_all_episodes(
self.series_id,
season_type=self.season_type,
)
season_filter = 0
else:
episodes = client.get_season_episodes(
self.series_id,
self.season,
season_type=self.season_type,
)
season_filter = self.season
if not episodes:
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",
)
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:
@@ -434,6 +465,15 @@ class TvdbEpisodeRenumberRuleWidget(QWidget):
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)
@@ -443,7 +483,8 @@ class TvdbEpisodeRenumberRuleWidget(QWidget):
layout.addRow(self.status)
info = QLabel(
"Reads titles from S01E05 - Episode Title or Show 04x01 Episode Title filenames. "
"Reads titles from S01E05 - Episode Title, S01E05-E06 (multi-episode), "
"or Show 04x01 Episode Title filenames. "
"Use All seasons to match across every season and fix wrong season numbers too."
)
info.setWordWrap(True)
@@ -519,6 +560,7 @@ class TvdbEpisodeRenumberRuleWidget(QWidget):
self._order_value(),
self._file_names,
all_seasons,
self.multi_episode_cb.isChecked(),
)
self._match_worker.finished.connect(self._on_match_finished)
self._match_worker.failed.connect(self._on_match_failed)
@@ -542,11 +584,17 @@ class TvdbEpisodeRenumberRuleWidget(QWidget):
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={
k: (v.season, v.episode) if hasattr(v, "season") else v
for k, v in self._episode_mapping.items()
},
episode_mapping=mapping,
padding=self.padding.value(),
)
r.enabled = self.enabled_cb.isChecked()