diff --git a/unshackle/core/manifests/dash.py b/unshackle/core/manifests/dash.py index fd51557..3e7913b 100644 --- a/unshackle/core/manifests/dash.py +++ b/unshackle/core/manifests/dash.py @@ -151,6 +151,11 @@ class DASH: if not track_fps and segment_base is not None: track_fps = segment_base.get("timescale") + scan_type = None + scan_type_str = get("scanType") + if scan_type_str and scan_type_str.lower() == "interlaced": + scan_type = Video.ScanType.INTERLACED + track_args = dict( range_=self.get_video_range( codecs, findall("SupplementalProperty"), findall("EssentialProperty") @@ -159,6 +164,7 @@ class DASH: width=get("width") or 0, height=get("height") or 0, fps=track_fps or None, + scan_type=scan_type, ) elif content_type == "audio": track_type = Audio diff --git a/unshackle/core/titles/episode.py b/unshackle/core/titles/episode.py index 2217ea1..b80afe8 100644 --- a/unshackle/core/titles/episode.py +++ b/unshackle/core/titles/episode.py @@ -153,7 +153,12 @@ class Episode(Title): # likely a movie or HD source, so it's most likely widescreen so # 16:9 canvas makes the most sense. resolution = int(primary_video_track.width * (9 / 16)) - name += f" {resolution}p" + # Determine scan type suffix - default to "p", use "i" only if explicitly interlaced + scan_suffix = "p" + scan_type = getattr(primary_video_track, 'scan_type', None) + if scan_type and str(scan_type).lower() == "interlaced": + scan_suffix = "i" + name += f" {resolution}{scan_suffix}" # Service (use track source if available) if show_service: diff --git a/unshackle/core/titles/movie.py b/unshackle/core/titles/movie.py index 4e1d02c..2346a82 100644 --- a/unshackle/core/titles/movie.py +++ b/unshackle/core/titles/movie.py @@ -88,7 +88,12 @@ class Movie(Title): # likely a movie or HD source, so it's most likely widescreen so # 16:9 canvas makes the most sense. resolution = int(primary_video_track.width * (9 / 16)) - name += f" {resolution}p" + # Determine scan type suffix - default to "p", use "i" only if explicitly interlaced + scan_suffix = "p" + scan_type = getattr(primary_video_track, 'scan_type', None) + if scan_type and str(scan_type).lower() == "interlaced": + scan_suffix = "i" + name += f" {resolution}{scan_suffix}" # Service (use track source if available) if show_service: diff --git a/unshackle/core/tracks/video.py b/unshackle/core/tracks/video.py index 5c54631..79fbb92 100644 --- a/unshackle/core/tracks/video.py +++ b/unshackle/core/tracks/video.py @@ -186,6 +186,10 @@ class Video(Track): # for some reason there's no Dolby Vision info tag raise ValueError(f"The M3U Range Tag '{tag}' is not a supported Video Range") + class ScanType(str, Enum): + PROGRESSIVE = "progressive" + INTERLACED = "interlaced" + def __init__( self, *args: Any, @@ -195,6 +199,7 @@ class Video(Track): width: Optional[int] = None, height: Optional[int] = None, fps: Optional[Union[str, int, float]] = None, + scan_type: Optional[Video.ScanType] = None, **kwargs: Any, ) -> None: """ @@ -232,6 +237,8 @@ class Video(Track): raise TypeError(f"Expected height to be a {int}, not {height!r}") if not isinstance(fps, (str, int, float, type(None))): raise TypeError(f"Expected fps to be a {str}, {int}, or {float}, not {fps!r}") + if not isinstance(scan_type, (Video.ScanType, type(None))): + raise TypeError(f"Expected scan_type to be a {Video.ScanType}, not {scan_type!r}") self.codec = codec self.range = range_ or Video.Range.SDR @@ -256,6 +263,7 @@ class Video(Track): except Exception as e: raise ValueError("Expected fps to be a number, float, or a string as numerator/denominator form, " + str(e)) + self.scan_type = scan_type self.needs_duration_fix = False def __str__(self) -> str: