Coverage for src/epublib/media_type.py: 100%

75 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-06 16:00 -0300

1from enum import IntEnum, StrEnum, auto 

2from mimetypes import guess_file_type as base_guess_file_type 

3from pathlib import Path 

4from typing import Self, override 

5 

6 

7def guess_file_type(path: str | Path) -> str | None: 

8 """Guess the media type of a file based on its filename or path.""" 

9 path = Path(path) 

10 

11 if path.suffix.lower() == ".ncx": 

12 return "application/x-dtbncx+xml" 

13 

14 return base_guess_file_type(path)[0] 

15 

16 

17class Category(IntEnum): 

18 """Broad categories of media types.""" 

19 

20 IMAGE = auto() 

21 AUDIO = auto() 

22 STYLE = auto() 

23 FONT = auto() 

24 OTHER = auto() 

25 FOREIGN = auto() 

26 SENTINEL = auto() 

27 

28 

29class MediaType(StrEnum): 

30 """An EPUB media type, also known as a MIME type.""" 

31 

32 category: Category 

33 

34 # Images 

35 IMAGE_GIF = "image/gif", Category.IMAGE 

36 IMAGE_JPEG = "image/jpeg", Category.IMAGE 

37 IMAGE_PNG = "image/png", Category.IMAGE 

38 IMAGE_SVG = "image/svg+xml", Category.IMAGE 

39 IMAGE_WEBP = "image/webp", Category.IMAGE 

40 

41 # Audio 

42 AUDIO_MPEG = "audio/mpeg", Category.AUDIO 

43 AUDIO_MP4 = "audio/mp4", Category.AUDIO 

44 AUDIO_OGG = "audio/ogg", Category.AUDIO 

45 

46 # Style 

47 CSS = "text/css", Category.STYLE 

48 

49 # Fonts 

50 FONT_TTF = "font/ttf", Category.FONT 

51 FONT_SFNT = "application/font-sfnt", Category.FONT 

52 FONT_OTF = "font/otf", Category.FONT 

53 VND_MS_OPENTYPE = "application/vnd.ms-opentype", Category.FONT 

54 FONT_WOFF = "font/woff", Category.FONT 

55 APPLICATION_FONT_WOFF = "application/font-woff", Category.FONT 

56 FONT_WOFF2 = "font/woff2", Category.FONT 

57 

58 # Other 

59 XHTML = "application/xhtml+xml", Category.OTHER 

60 JAVASCRIPT = "application/javascript", Category.OTHER 

61 ECMASCRIPT = "application/ecmascript", Category.OTHER 

62 TEXT_JAVASCRIPT = "text/javascript", Category.OTHER 

63 NCX = "application/x-dtbncx+xml", Category.OTHER 

64 SMIL_XML = "application/smil+xml", Category.OTHER 

65 

66 def __new__(cls, value: str, category: Category): 

67 obj = str.__new__(cls, value) 

68 obj._value_ = value 

69 

70 return obj 

71 

72 def __init__(self, value: str, category: Category = Category.SENTINEL) -> None: 

73 self.category = category 

74 super().__init__() 

75 

76 @classmethod 

77 @override 

78 def _missing_(cls, value: object): 

79 if value and isinstance(value, str): 

80 obj = str.__new__(cls, value) 

81 obj._value_ = value 

82 obj._name_ = "FOREIGN" 

83 obj.category = Category.FOREIGN 

84 cls._value2member_map_[value] = obj 

85 

86 return obj 

87 

88 raise ValueError(f"{value} is not a valid {cls.__name__}") 

89 

90 @classmethod 

91 def from_filename(cls, value: str | Path) -> Self | None: 

92 """ 

93 Detect media type from filename or path. If a mimetype for the 

94 path is found, but is not supported by MediaType, return it as a string. 

95 """ 

96 

97 guessed = guess_file_type(value) 

98 if not guessed: 

99 return None 

100 instance = cls(guessed) 

101 return instance 

102 

103 @override 

104 def __str__(self) -> str: 

105 return self.value 

106 

107 def is_css(self): 

108 return self is self.CSS 

109 

110 def is_js(self): 

111 return ( 

112 self is self.JAVASCRIPT 

113 or self is self.ECMASCRIPT 

114 or self is self.TEXT_JAVASCRIPT 

115 ) 

116 

117 def is_video(self): 

118 return self.startswith("video/")