Coverage for src/epublib/mediatype.py: 78%

91 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-09-30 17:43 -0300

1from enum import Enum, IntEnum, 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(Enum): 

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

31 

32 value: str # type: ignore[reportIncompatibleMethodOverride] 

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 category: Category 

67 

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

69 obj = object.__new__(cls) 

70 obj._value_ = value 

71 

72 return obj 

73 

74 def is_css(self): 

75 return self is MediaType.CSS 

76 

77 def is_js(self): 

78 return ( 

79 self is self.JAVASCRIPT 

80 or self is self.ECMASCRIPT 

81 or self is self.TEXT_JAVASCRIPT 

82 ) 

83 

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

85 self.category = category 

86 super().__init__() 

87 

88 @classmethod 

89 def coalesce(cls, value: str | Self): 

90 if isinstance(value, cls): 

91 return value 

92 

93 try: 

94 return cls(value) 

95 except ValueError: 

96 return value 

97 

98 @override 

99 def __str__(self) -> str: 

100 return self.value 

101 

102 def _directory_name(self): 

103 if self is self.XHTML: 

104 return "Text" 

105 

106 match self.category: 

107 case Category.IMAGE: 

108 return "Images" 

109 case Category.AUDIO: 

110 return "Audio" 

111 case Category.STYLE: 

112 return "Styles" 

113 case Category.FONT: 

114 return "Fonts" 

115 case Category.OTHER: 

116 return "Fonts" 

117 case _: 

118 return "Misc" 

119 

120 @classmethod 

121 def directory_name(cls, value: "MediaType | str | None"): 

122 """Default directory name for each category of file. Follows Sigil's defaults""" 

123 if isinstance(value, cls): 

124 return value._directory_name() 

125 

126 return "Misc" 

127 

128 @classmethod 

129 def from_filename(cls, value: str | Path): 

130 """ 

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

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

133 """ 

134 

135 guessed = guess_file_type(value) 

136 if not guessed: 

137 return None 

138 return cls.coalesce(guessed)