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
« 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
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(StrEnum):
30 """An EPUB media type, also known as a MIME type."""
32 category: Category
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 def __new__(cls, value: str, category: Category):
67 obj = str.__new__(cls, value)
68 obj._value_ = value
70 return obj
72 def __init__(self, value: str, category: Category = Category.SENTINEL) -> None:
73 self.category = category
74 super().__init__()
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
86 return obj
88 raise ValueError(f"{value} is not a valid {cls.__name__}")
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 """
97 guessed = guess_file_type(value)
98 if not guessed:
99 return None
100 instance = cls(guessed)
101 return instance
103 @override
104 def __str__(self) -> str:
105 return self.value
107 def is_css(self):
108 return self is self.CSS
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 )
117 def is_video(self):
118 return self.startswith("video/")