Karan Adhikari commited on
Commit
580ed85
·
unverified ·
1 Parent(s): ef88983

Combined file Support

Browse files

Support combined episode files under a special season for Stremio

Combined episode files (e.g. "S01 [E04-06]") and whole-season
"Combined" files are now indexed under a special season, so they
appear and play in Stremio.

Recommended setup — give this addon priority over Cinemeta:
- Stremio: arrange the Telegram Stremio addon above the Cinemeta
addon using https://addon-manager.dontwanttos.top/
- Nuvio: in the Addons section, move Telegram Stremio to the top
(before the Cinemeta addon).

Limitation -
Combined files doesn't work in global search.

Backend/helper/metadata.py CHANGED
@@ -10,7 +10,7 @@ from Backend.logger import LOGGER
10
  from Backend.helper.imdb import get_detail, get_season, search_title, search_title_multi
11
  from Backend.helper.settings_manager import SettingsManager
12
  from Backend.helper.encrypt import encode_string
13
- from Backend.helper.split_files import parse_split_info
14
  from themoviedb import aioTMDb
15
  from rapidfuzz import fuzz
16
  from guessit import guessit as _guessit
@@ -47,6 +47,21 @@ API_SEMAPHORE = asyncio.Semaphore(12)
47
 
48
  _MULTIPART_RE = re.compile(r"(?:part|cd|disc|disk)[s._-]*\d+(?=\.\w+$)", re.IGNORECASE)
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  _tmdb_client: aioTMDb | None = None
51
  _tmdb_client_key: str | None = None
52
 
@@ -531,12 +546,14 @@ async def metadata(filename: str, channel: int, msg_id, override_id: str = None)
531
  LOGGER.info(f"Skipping {filename}: split video file not meant to be combined in Stremio")
532
  return None
533
 
 
 
534
  excess = parsed.get("excess")
535
- if excess and any("combined" in item.lower() for item in excess):
536
  LOGGER.info(f"Skipping {filename}: contains 'combined'")
537
  return None
538
 
539
- split_info = parse_split_info(filename)
540
  part_number = split_info[1] if split_info else None
541
 
542
  title = parsed.get("title")
@@ -545,7 +562,9 @@ async def metadata(filename: str, channel: int, msg_id, override_id: str = None)
545
  year = parsed.get("year")
546
  quality = parsed.get("quality")
547
 
548
- if isinstance(season, list) or isinstance(episode, list):
 
 
549
  LOGGER.warning(f"Invalid season/episode format for {filename}: {parsed}")
550
  return None
551
  if season and not episode:
@@ -571,6 +590,8 @@ async def metadata(filename: str, channel: int, msg_id, override_id: str = None)
571
  if season and episode:
572
  LOGGER.info(f"Fetching TV metadata: {title} S{season:02d}E{episode:02d} (year={year})")
573
  result = await fetch_tv_metadata(title, season, episode, encoded_string, year, quality, default_id)
 
 
574
  else:
575
  LOGGER.info(f"Fetching Movie metadata: {title} (year={year})")
576
  result = await fetch_movie_metadata(title, encoded_string, year, quality, default_id)
 
10
  from Backend.helper.imdb import get_detail, get_season, search_title, search_title_multi
11
  from Backend.helper.settings_manager import SettingsManager
12
  from Backend.helper.encrypt import encode_string
13
+ from Backend.helper.split_files import parse_split_info, parse_combined_episodes
14
  from themoviedb import aioTMDb
15
  from rapidfuzz import fuzz
16
  from guessit import guessit as _guessit
 
47
 
48
  _MULTIPART_RE = re.compile(r"(?:part|cd|disc|disk)[s._-]*\d+(?=\.\w+$)", re.IGNORECASE)
49
 
50
+ # Combined episode files (e.g. "S01 [E04-06]" or "S01 Combined") are grouped under a special season.
51
+ COMBINED_SEASON = 0
52
+
53
+
54
+ # Re-file a combined entry under the special season with a descriptive title.
55
+ def _apply_combined_override(payload: dict, combined: dict) -> None:
56
+ season, start, end = combined["season"], combined["start"], combined["end"]
57
+ payload["season_number"] = COMBINED_SEASON
58
+ if start is None:
59
+ payload["episode_number"] = season * 1000
60
+ payload["episode_title"] = f"S{season:02d} Complete (Combined)"
61
+ else:
62
+ payload["episode_number"] = season * 1000 + start
63
+ payload["episode_title"] = f"S{season:02d} E{start:02d}-E{end:02d} (Combined)"
64
+
65
  _tmdb_client: aioTMDb | None = None
66
  _tmdb_client_key: str | None = None
67
 
 
546
  LOGGER.info(f"Skipping {filename}: split video file not meant to be combined in Stremio")
547
  return None
548
 
549
+ combined = parse_combined_episodes(filename)
550
+
551
  excess = parsed.get("excess")
552
+ if not combined and excess and any("combined" in item.lower() for item in excess):
553
  LOGGER.info(f"Skipping {filename}: contains 'combined'")
554
  return None
555
 
556
+ split_info = None if combined else parse_split_info(filename)
557
  part_number = split_info[1] if split_info else None
558
 
559
  title = parsed.get("title")
 
562
  year = parsed.get("year")
563
  quality = parsed.get("quality")
564
 
565
+ if combined:
566
+ season, episode = combined["season"], combined["start"] or 1
567
+ elif isinstance(season, list) or isinstance(episode, list):
568
  LOGGER.warning(f"Invalid season/episode format for {filename}: {parsed}")
569
  return None
570
  if season and not episode:
 
590
  if season and episode:
591
  LOGGER.info(f"Fetching TV metadata: {title} S{season:02d}E{episode:02d} (year={year})")
592
  result = await fetch_tv_metadata(title, season, episode, encoded_string, year, quality, default_id)
593
+ if result is not None and combined:
594
+ _apply_combined_override(result, combined)
595
  else:
596
  LOGGER.info(f"Fetching Movie metadata: {title} (year={year})")
597
  result = await fetch_movie_metadata(title, encoded_string, year, quality, default_id)
Backend/helper/split_files.py CHANGED
@@ -39,6 +39,48 @@ def parse_split_info(filename: str) -> Optional[Tuple[str, int]]:
39
  return _normalize(remainder), part_num
40
 
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  def strip_part_suffix(filename: str) -> str:
43
  if not filename:
44
  return filename
 
39
  return _normalize(remainder), part_num
40
 
41
 
42
+ _COMBINED_EPISODES_RE = re.compile(
43
+ r"E(?:P|PISODE)?[\s._-]*0*(\d{1,4})[\s._-]*(?:-|–|~|to)+[\s._-]*(?:E(?:P|PISODE)?[\s._-]*)?0*(\d{1,4})(?=\D|$)",
44
+ re.IGNORECASE,
45
+ )
46
+ _COMBINED_SEASON_RE = re.compile(r"S(?:EASON)?[\s._-]*0*(\d{1,3})", re.IGNORECASE)
47
+ _COMBINED_KEYWORD_RE = re.compile(r"\bcombined\b", re.IGNORECASE)
48
+
49
+
50
+ def _combined_season(name: str) -> Optional[int]:
51
+ match = _COMBINED_SEASON_RE.search(name)
52
+ return int(match.group(1)) if match else None
53
+
54
+
55
+ # Detect a combined file: an episode range ("S01 E04-06") or a whole-season
56
+ # "Combined" file. Returns {season, start, end} (start/end None for whole season).
57
+ def parse_combined_episodes(filename: str) -> Optional[dict]:
58
+ if not filename:
59
+ return None
60
+
61
+ match = _COMBINED_EPISODES_RE.search(filename)
62
+ if match:
63
+ start, end = int(match.group(1)), int(match.group(2))
64
+ if 1 <= start < end <= 99:
65
+ return {"season": _combined_season(filename) or 1, "start": start, "end": end}
66
+
67
+ if _COMBINED_KEYWORD_RE.search(filename):
68
+ season = _combined_season(filename)
69
+ if season is not None:
70
+ return {"season": season, "start": None, "end": None}
71
+
72
+ return None
73
+
74
+
75
+ # True when a combined file covers the requested season/episode.
76
+ def combined_covers(combined: dict, season: Optional[int], episode: Optional[int]) -> bool:
77
+ if season is not None and combined["season"] != int(season):
78
+ return False
79
+ if episode is None or combined["start"] is None:
80
+ return True
81
+ return combined["start"] <= int(episode) <= combined["end"]
82
+
83
+
84
  def strip_part_suffix(filename: str) -> str:
85
  if not filename:
86
  return filename