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
« 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
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)
12 if path.suffix.lower() == ".ncx":
13 return "application/x-dtbncx+xml"
15 return base_guess_file_type(path)[0]
18class Category(IntEnum):
19 """Broad categories of media types."""
21 IMAGE = auto()
22 AUDIO = auto()
23 STYLE = auto()
24 FONT = auto()
25 OTHER = auto()
26 FOREIGN = auto()
27 SENTINEL = auto()
30class MediaType(Enum):
31 """An EPUB media type, also known as a MIME type."""
33 value: str # type: ignore[reportIncompatibleMethodOverride]
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
42 # Audio
43 AUDIO_MPEG = "audio/mpeg", Category.AUDIO
44 AUDIO_MP4 = "audio/mp4", Category.AUDIO
45 AUDIO_OGG = "audio/ogg", Category.AUDIO
47 # Style
48 CSS = "text/css", Category.STYLE
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
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
67 category: Category
69 def __new__(cls, value: str, category: Category):
70 obj = object.__new__(cls)
71 obj._value_ = value
73 return obj
75 def is_css(self):
76 return self is MediaType.CSS
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 )
85 def __init__(self, value: str, category: Category = Category.SENTINEL) -> None:
86 self.category = category
87 super().__init__()
89 @classmethod
90 def coalesce(cls, value: str | Self):
91 if isinstance(value, cls):
92 return value
94 try:
95 return cls(value)
96 except ValueError:
97 return value
99 @override
100 def __str__(self) -> str:
101 return self.value
103 def _directory_name(self):
104 if self is self.XHTML:
105 return "Text"
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"
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()
127 return "Misc"
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 """
136 guessed = guess_file_type(value)
137 if not guessed:
138 return None
139 return cls.coalesce(guessed)