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
« 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
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)
11 if path.suffix.lower() == ".ncx":
12 return "application/x-dtbncx+xml"
14 return base_guess_file_type(path)[0]
17class Category(IntEnum):
18 """Broad categories of media types."""
20 IMAGE = auto()
21 AUDIO = auto()
22 STYLE = auto()
23 FONT = auto()
24 OTHER = auto()
25 FOREIGN = auto()
26 SENTINEL = auto()
29class MediaType(Enum):
30 """An EPUB media type, also known as a MIME type."""
32 value: str # type: ignore[reportIncompatibleMethodOverride]
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
41 # Audio
42 AUDIO_MPEG = "audio/mpeg", Category.AUDIO
43 AUDIO_MP4 = "audio/mp4", Category.AUDIO
44 AUDIO_OGG = "audio/ogg", Category.AUDIO
46 # Style
47 CSS = "text/css", Category.STYLE
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
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
66 category: Category
68 def __new__(cls, value: str, category: Category):
69 obj = object.__new__(cls)
70 obj._value_ = value
72 return obj
74 def is_css(self):
75 return self is MediaType.CSS
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 )
84 def __init__(self, value: str, category: Category = Category.SENTINEL) -> None:
85 self.category = category
86 super().__init__()
88 @classmethod
89 def coalesce(cls, value: str | Self):
90 if isinstance(value, cls):
91 return value
93 try:
94 return cls(value)
95 except ValueError:
96 return value
98 @override
99 def __str__(self) -> str:
100 return self.value
102 def _directory_name(self):
103 if self is self.XHTML:
104 return "Text"
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"
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()
126 return "Misc"
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 """
135 guessed = guess_file_type(value)
136 if not guessed:
137 return None
138 return cls.coalesce(guessed)