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

92 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-18 16:07 -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 print(path) 

11 

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

13 return "application/x-dtbncx+xml" 

14 

15 return base_guess_file_type(path)[0] 

16 

17 

18class Category(IntEnum): 

19 """Broad categories of media types.""" 

20 

21 IMAGE = auto() 

22 AUDIO = auto() 

23 STYLE = auto() 

24 FONT = auto() 

25 OTHER = auto() 

26 FOREIGN = auto() 

27 SENTINEL = auto() 

28 

29 

30class MediaType(Enum): 

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

32 

33 value: str # type: ignore[reportIncompatibleMethodOverride] 

34 

35 # Images 

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

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

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

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

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

41 

42 # Audio 

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

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

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

46 

47 # Style 

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

49 

50 # Fonts 

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

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

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

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

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

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

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

58 

59 # Other 

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

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

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

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

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

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

66 

67 category: Category 

68 

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

70 obj = object.__new__(cls) 

71 obj._value_ = value 

72 

73 return obj 

74 

75 def is_css(self): 

76 return self is MediaType.CSS 

77 

78 def is_js(self): 

79 return ( 

80 self is self.JAVASCRIPT 

81 or self is self.ECMASCRIPT 

82 or self is self.TEXT_JAVASCRIPT 

83 ) 

84 

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

86 self.category = category 

87 super().__init__() 

88 

89 @classmethod 

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

91 if isinstance(value, cls): 

92 return value 

93 

94 try: 

95 return cls(value) 

96 except ValueError: 

97 return value 

98 

99 @override 

100 def __str__(self) -> str: 

101 return self.value 

102 

103 def _directory_name(self): 

104 if self is self.XHTML: 

105 return "Text" 

106 

107 match self.category: 

108 case Category.IMAGE: 

109 return "Images" 

110 case Category.AUDIO: 

111 return "Audio" 

112 case Category.STYLE: 

113 return "Styles" 

114 case Category.FONT: 

115 return "Fonts" 

116 case Category.OTHER: 

117 return "Fonts" 

118 case _: 

119 return "Misc" 

120 

121 @classmethod 

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

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

124 if isinstance(value, cls): 

125 return value._directory_name() 

126 

127 return "Misc" 

128 

129 @classmethod 

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

131 """ 

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

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

134 """ 

135 

136 guessed = guess_file_type(value) 

137 if not guessed: 

138 return None 

139 return cls.coalesce(guessed)