@@ -113,22 +113,28 @@ def read(self, name: str) -> bytes:
113113 return factory .get (name ).read ()
114114
115115
116- class _TarFile ( tarfile . TarFile ) :
116+ class _TarFile :
117117 """Wrapper around tarfile.TarFile to mimic zipfile.ZipFile's API."""
118118
119119 def __init__ (self , filepath : Path , mode : Literal ["r" ]) -> None :
120- super ().__init__ (filepath , mode )
120+ self .tar : tarfile .TarFile
121+ self .filepath = filepath
122+ self .mode = mode
121123
122124 def namelist (self ) -> list [str ]:
123- return self .getnames ()
125+ return self .tar . getnames ()
124126
125127 def read (self , name : str ) -> bytes :
126- return unwrap (self .extractfile (name )).read ()
128+ return unwrap (self .tar .extractfile (name )).read ()
129+
130+ def __enter__ (self ) -> "_TarFile" :
131+ self .tar = tarfile .open (self .filepath , self .mode ).__enter__ ()
132+ return self
133+
134+ def __exit__ (self , * args ) -> None :
135+ self .tar .__exit__ (* args )
127136
128137
129- type _Archive_T = (
130- type [zipfile .ZipFile ] | type [rarfile .RarFile ] | type [_SevenZipFile ] | type [_TarFile ]
131- )
132138type _Archive = zipfile .ZipFile | rarfile .RarFile | _SevenZipFile | _TarFile
133139
134140
@@ -910,15 +916,7 @@ def _epub_cover(filepath: Path, ext: str) -> Image.Image | None:
910916 """
911917 im : Image .Image | None = None
912918 try :
913- archiver : _Archive_T = zipfile .ZipFile
914- if ext == ".cb7" :
915- archiver = _SevenZipFile
916- elif ext == ".cbr" :
917- archiver = rarfile .RarFile
918- elif ext == ".cbt" :
919- archiver = _TarFile
920-
921- with archiver (filepath , "r" ) as archive :
919+ with ThumbRenderer .__open_archive (filepath , ext ) as archive :
922920 if "ComicInfo.xml" in archive .namelist ():
923921 comic_info = ET .fromstring (archive .read ("ComicInfo.xml" ))
924922 im = ThumbRenderer .__cover_from_comic_info (archive , comic_info , "FrontCover" )
@@ -928,13 +926,7 @@ def _epub_cover(filepath: Path, ext: str) -> Image.Image | None:
928926 )
929927
930928 if not im :
931- for file_name in archive .namelist ():
932- if file_name .lower ().endswith (
933- (".png" , ".jpg" , ".jpeg" , ".gif" , ".bmp" , ".svg" )
934- ):
935- image_data = archive .read (file_name )
936- im = Image .open (BytesIO (image_data ))
937- break
929+ im = ThumbRenderer .__first_image (archive )
938930 except Exception as e :
939931 logger .error ("Couldn't render thumbnail" , filepath = filepath , error = type (e ).__name__ )
940932
@@ -966,6 +958,63 @@ def __cover_from_comic_info(
966958
967959 return im
968960
961+ @staticmethod
962+ def _archive_thumb (filepath : Path , ext : str ) -> Image .Image | None :
963+ """Extract the first image found in the archive.
964+
965+ Args:
966+ filepath (Path): The path to the archive.
967+ ext (str): The file extension.
968+
969+ Returns:
970+ Image: The first image found in the archive.
971+ """
972+ im : Image .Image | None = None
973+ try :
974+ with ThumbRenderer .__open_archive (filepath , ext ) as archive :
975+ im = ThumbRenderer .__first_image (archive )
976+ except Exception as e :
977+ logger .error ("Couldn't render thumbnail" , filepath = filepath , error = type (e ).__name__ )
978+
979+ return im
980+
981+ @staticmethod
982+ def __open_archive (filepath : Path , ext : str ) -> _Archive :
983+ """Open an archive with its corresponding archiver.
984+
985+ Args:
986+ filepath (Path): The path to the archive.
987+ ext (str): The file extension.
988+
989+ Returns:
990+ _Archive: The opened archive.
991+ """
992+ archiver : type [_Archive ] = zipfile .ZipFile
993+ if ext in {".7z" , ".cb7" , ".s7z" }:
994+ archiver = _SevenZipFile
995+ elif ext in {".cbr" , ".rar" }:
996+ archiver = rarfile .RarFile
997+ elif ext in {".cbt" , ".tar" , ".tgz" }:
998+ archiver = _TarFile
999+ return archiver (filepath , "r" )
1000+
1001+ @staticmethod
1002+ def __first_image (archive : _Archive ) -> Image .Image | None :
1003+ """Find and extract the first renderable image in the archive.
1004+
1005+ Args:
1006+ archive (_Archive): The current archive.
1007+
1008+ Returns:
1009+ Image: The first renderable image in the archive.
1010+ """
1011+ for file_name in archive .namelist ():
1012+ if file_name .lower ().endswith ((".png" , ".jpg" , ".jpeg" , ".gif" , ".bmp" , ".svg" )):
1013+ image_data = archive .read (file_name )
1014+ return Image .open (BytesIO (image_data ))
1015+
1016+ return None
1017+
9691018 def _font_short_thumb (self , filepath : Path , size : int ) -> Image .Image | None :
9701019 """Render a small font preview ("Aa") thumbnail from a font file.
9711020
@@ -1819,6 +1868,9 @@ def _render(
18191868 ext , MediaCategories .PDF_TYPES , mime_fallback = True
18201869 ):
18211870 image = self ._pdf_thumb (_filepath , adj_size )
1871+ # Archives =====================================================
1872+ elif MediaCategories .is_ext_in_category (ext , MediaCategories .ARCHIVE_TYPES ):
1873+ image = self ._archive_thumb (_filepath , ext )
18221874 # MDIPACK ======================================================
18231875 elif MediaCategories .is_ext_in_category (ext , MediaCategories .MDIPACK_TYPES ):
18241876 image = self ._mdp_thumb (_filepath )
0 commit comments