Skip to content

Commit 14a2d84

Browse files
committed
Implement CustomMimetypes
1 parent 427178a commit 14a2d84

File tree

4 files changed

+2142
-8
lines changed

4 files changed

+2142
-8
lines changed

constants.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import os
88
import socket
9-
import mimetypes
9+
from custom_mimetypes import CustomMimeTypes
1010

1111
# Get hostname, truncate at first dot, then limit to 16 chars max
1212
hostname = socket.gethostname().split(".")[0][:16]
@@ -17,6 +17,9 @@
1717
SERVER_MANUFACTURER = "richstokes"
1818
SERVER_AGENT = f"ZeroConfigDLNA/{SERVER_VERSION} DLNA/1.50 UPnP/1.0"
1919

20+
# Create a global instance of CustomMimeTypes
21+
custom_mimetypes = CustomMimeTypes()
22+
2023

2124
def is_supported_media_file(file_path):
2225
"""
@@ -28,7 +31,7 @@ def is_supported_media_file(file_path):
2831
Returns:
2932
bool: True if the file is a supported media type, False otherwise
3033
"""
31-
mime_type, _ = mimetypes.guess_type(file_path)
34+
mime_type, _ = custom_mimetypes.guess_type(file_path)
3235
return mime_type and (
3336
mime_type.startswith("video/")
3437
or mime_type.startswith("audio/")

custom_mimetypes.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
"""Custom MIME types handler for ZeroConfigDLNA.
2+
3+
This module provides a custom implementation of MIME type detection
4+
that uses a local mime.types file instead of the system default.
5+
"""
6+
7+
import os
8+
import re
9+
from typing import Dict, List, Optional, Tuple
10+
11+
12+
class CustomMimeTypes:
13+
"""
14+
A custom implementation of the mimetypes module that reads from
15+
a local mime.types file in the current directory.
16+
"""
17+
18+
def __init__(self, mime_file_path: Optional[str] = None):
19+
"""
20+
Initialize the CustomMimeTypes object.
21+
22+
Args:
23+
mime_file_path: Path to the mime.types file. If None, looks for
24+
mime.types in the same directory as this module.
25+
"""
26+
self.types_map: Dict[str, str] = {}
27+
self.extensions_map: Dict[str, str] = {}
28+
29+
# If no path provided, use the mime.types file in the same directory as this module
30+
if mime_file_path is None:
31+
mime_file_path = os.path.join(os.path.dirname(__file__), "mime.types")
32+
33+
# Load the mime types from the file
34+
self._load_mime_types(mime_file_path)
35+
36+
def _load_mime_types(self, mime_file_path: str) -> None:
37+
"""
38+
Load MIME types from the specified file.
39+
40+
Args:
41+
mime_file_path: Path to the mime.types file.
42+
"""
43+
try:
44+
with open(mime_file_path, "r", encoding="utf-8") as f:
45+
for line in f:
46+
# Skip comments and empty lines
47+
line = line.strip()
48+
if not line or line.startswith("#"):
49+
continue
50+
51+
# Parse the line: mime_type extension [extension ...]
52+
parts = line.split()
53+
if len(parts) < 2:
54+
continue
55+
56+
mime_type = parts[0].lower()
57+
for ext in parts[1:]:
58+
ext = ext.lower()
59+
if not ext.startswith("."):
60+
ext = "." + ext
61+
62+
# Map extension to MIME type
63+
self.types_map[ext] = mime_type
64+
65+
# Map MIME type to first extension for that type
66+
if mime_type not in self.extensions_map:
67+
self.extensions_map[mime_type] = ext
68+
except Exception as e:
69+
print(f"Error loading mime types from {mime_file_path}: {e}")
70+
# Initialize with basic MIME types if file loading fails
71+
self._init_basic_types()
72+
73+
def _init_basic_types(self) -> None:
74+
"""Initialize with basic MIME types to ensure operation even if file loading fails."""
75+
# Common video formats
76+
for ext in [".mp4", ".mkv", ".avi", ".mov", ".wmv", ".flv", ".webm"]:
77+
self.types_map[ext] = (
78+
"video/mp4"
79+
if ext == ".mp4"
80+
else (
81+
"video/x-matroska"
82+
if ext == ".mkv"
83+
else (
84+
"video/x-msvideo"
85+
if ext == ".avi"
86+
else (
87+
"video/quicktime"
88+
if ext == ".mov"
89+
else (
90+
"video/x-ms-wmv"
91+
if ext == ".wmv"
92+
else "video/x-flv" if ext == ".flv" else "video/webm"
93+
)
94+
)
95+
)
96+
)
97+
)
98+
99+
# Common audio formats
100+
for ext in [".mp3", ".wav", ".ogg", ".aac", ".flac"]:
101+
self.types_map[ext] = (
102+
"audio/mpeg"
103+
if ext == ".mp3"
104+
else (
105+
"audio/wav"
106+
if ext == ".wav"
107+
else (
108+
"audio/ogg"
109+
if ext == ".ogg"
110+
else "audio/aac" if ext == ".aac" else "audio/flac"
111+
)
112+
)
113+
)
114+
115+
# Common image formats
116+
for ext in [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"]:
117+
self.types_map[ext] = (
118+
"image/jpeg"
119+
if ext in [".jpg", ".jpeg"]
120+
else (
121+
"image/png"
122+
if ext == ".png"
123+
else (
124+
"image/gif"
125+
if ext == ".gif"
126+
else "image/bmp" if ext == ".bmp" else "image/webp"
127+
)
128+
)
129+
)
130+
131+
# Create the extensions map
132+
for ext, mime_type in self.types_map.items():
133+
if mime_type not in self.extensions_map:
134+
self.extensions_map[mime_type] = ext
135+
136+
def guess_type(
137+
self, url: str, strict: bool = True
138+
) -> Tuple[Optional[str], Optional[str]]:
139+
"""
140+
Guess the MIME type of a file based on its URL/filename.
141+
142+
Args:
143+
url: URL or filename to guess the MIME type for.
144+
strict: Whether to be strict about the guessing.
145+
If True, only return types found in the mime.types file.
146+
147+
Returns:
148+
A tuple (type, encoding) where type is the MIME type and
149+
encoding is the encoding (always None in this implementation).
150+
"""
151+
# Extract the filename from the URL and convert to lowercase
152+
filename = os.path.basename(url.lower())
153+
154+
# Get the extension
155+
_, ext = os.path.splitext(filename)
156+
157+
# Return the MIME type if found, otherwise None
158+
return (self.types_map.get(ext), None)
159+
160+
def guess_extension(self, mime_type: str, strict: bool = True) -> Optional[str]:
161+
"""
162+
Guess the extension for a given MIME type.
163+
164+
Args:
165+
mime_type: The MIME type to guess the extension for.
166+
strict: Whether to be strict about the guessing.
167+
If True, only return extensions found in the mime.types file.
168+
169+
Returns:
170+
The extension including the leading dot, or None if not found.
171+
"""
172+
# Convert to lowercase
173+
mime_type = mime_type.lower()
174+
175+
# Return the extension if found, otherwise None
176+
return self.extensions_map.get(mime_type)
177+
178+
def add_type(self, mime_type: str, ext: str, strict: bool = True) -> None:
179+
"""
180+
Add a MIME type to the map.
181+
182+
Args:
183+
mime_type: The MIME type to add.
184+
ext: The extension to associate with the MIME type.
185+
strict: Ignored, included for compatibility.
186+
"""
187+
if not ext.startswith("."):
188+
ext = "." + ext
189+
190+
mime_type = mime_type.lower()
191+
ext = ext.lower()
192+
193+
self.types_map[ext] = mime_type
194+
if mime_type not in self.extensions_map:
195+
self.extensions_map[mime_type] = ext
196+
197+
def read(self, filename: str, strict: bool = True) -> None:
198+
"""
199+
Read MIME types from a file.
200+
201+
Args:
202+
filename: Path to the file to read.
203+
strict: Ignored, included for compatibility.
204+
"""
205+
self._load_mime_types(filename)
206+
207+
208+
# Create a singleton instance
209+
mime_types = CustomMimeTypes()
210+
211+
212+
# Provide functions that mimic the standard mimetypes module
213+
def guess_type(url: str, strict: bool = True) -> Tuple[Optional[str], Optional[str]]:
214+
"""
215+
Guess the MIME type of a file based on its URL/filename.
216+
217+
Args:
218+
url: URL or filename to guess the MIME type for.
219+
strict: Whether to be strict about the guessing.
220+
221+
Returns:
222+
A tuple (type, encoding) where type is the MIME type and
223+
encoding is the encoding (always None in this implementation).
224+
"""
225+
return mime_types.guess_type(url, strict)
226+
227+
228+
def guess_extension(mime_type: str, strict: bool = True) -> Optional[str]:
229+
"""
230+
Guess the extension for a given MIME type.
231+
232+
Args:
233+
mime_type: The MIME type to guess the extension for.
234+
strict: Whether to be strict about the guessing.
235+
236+
Returns:
237+
The extension including the leading dot, or None if not found.
238+
"""
239+
return mime_types.guess_extension(mime_type, strict)
240+
241+
242+
def add_type(mime_type: str, ext: str, strict: bool = True) -> None:
243+
"""
244+
Add a MIME type to the map.
245+
246+
Args:
247+
mime_type: The MIME type to add.
248+
ext: The extension to associate with the MIME type.
249+
strict: Ignored, included for compatibility.
250+
"""
251+
mime_types.add_type(mime_type, ext, strict)
252+
253+
254+
def read(filename: str, strict: bool = True) -> None:
255+
"""
256+
Read MIME types from a file.
257+
258+
Args:
259+
filename: Path to the file to read.
260+
strict: Ignored, included for compatibility.
261+
"""
262+
mime_types.read(filename, strict)

dlna.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import uuid
1212
from http.server import BaseHTTPRequestHandler
1313
from urllib.parse import unquote, urlparse, quote
14-
import mimetypes
1514
import html
1615
import traceback
1716
from constants import (
@@ -22,6 +21,10 @@
2221
SERVER_MANUFACTURER,
2322
is_supported_media_file,
2423
)
24+
from custom_mimetypes import CustomMimeTypes
25+
26+
# Create a global instance of CustomMimeTypes
27+
custom_mimetypes = CustomMimeTypes()
2528

2629

2730
def is_safe_path(base_dir, requested_path):
@@ -668,7 +671,7 @@ def send_browse_response(self):
668671
{"name": item_name, "path": relative_path, "is_dir": True}
669672
)
670673
elif os.path.isfile(item_path):
671-
mime_type, _ = mimetypes.guess_type(item_name)
674+
mime_type, _ = custom_mimetypes.guess_type(item_name)
672675
if mime_type and (
673676
mime_type.startswith("video/")
674677
or mime_type.startswith("audio/")
@@ -805,7 +808,7 @@ def serve_media_file(self, filename, head_only=False):
805808
print(f"SECURITY WARNING: Attempted directory traversal to {file_path}")
806809
return
807810

808-
mime_type, _ = mimetypes.guess_type(file_path)
811+
mime_type, _ = custom_mimetypes.guess_type(file_path)
809812
if not mime_type:
810813
mime_type = "application/octet-stream"
811814

@@ -1606,7 +1609,7 @@ def handle_browse_request(self, soap_data):
16061609
}
16071610
)
16081611
elif os.path.isfile(item_path):
1609-
mime_type, _ = mimetypes.guess_type(item_name)
1612+
mime_type, _ = custom_mimetypes.guess_type(item_name)
16101613
if mime_type and (
16111614
mime_type.startswith("video/")
16121615
or mime_type.startswith("audio/")
@@ -1697,7 +1700,7 @@ def handle_browse_request(self, soap_data):
16971700
}
16981701
)
16991702
elif os.path.isfile(item_path):
1700-
mime_type, _ = mimetypes.guess_type(item_name)
1703+
mime_type, _ = custom_mimetypes.guess_type(item_name)
17011704
if mime_type and (
17021705
mime_type.startswith("video/")
17031706
or mime_type.startswith("audio/")
@@ -1814,7 +1817,7 @@ def handle_browse_request(self, soap_data):
18141817
elif os.path.isfile(full_path):
18151818
# File metadata
18161819
file_name = os.path.basename(item_path)
1817-
mime_type, _ = mimetypes.guess_type(file_name)
1820+
mime_type, _ = custom_mimetypes.guess_type(file_name)
18181821

18191822
if mime_type and (
18201823
mime_type.startswith("video/")

0 commit comments

Comments
 (0)