def test_resize_height(png_image, jpg_image, tmp_path, fmt): src, dst = get_src_dst(png_image, jpg_image, tmp_path, fmt) width, height = 100, 50 resize_image(png_image, width, height, to=dst, method="height") tw, th = get_image_size(dst) assert th == height
def test_resize_thumbnail(png_image, jpg_image, tmp_path, fmt): src, dst = get_src_dst(png_image, jpg_image, tmp_path, fmt) width, height = 100, 50 resize_image(png_image, width, height, to=dst, method="thumbnail") tw, th = get_image_size(dst) assert tw <= width assert th <= height
def test_resize_contain(png_image, jpg_image, tmp_path, fmt): src, dst = get_src_dst(png_image, jpg_image, tmp_path, fmt) width, height = 5, 50 resize_image(png_image, width, height, to=dst, method="contain") tw, th = get_image_size(dst) assert tw <= width assert th <= height
def check_branding_values(self): """ checks that user-supplied images and colors are valid (so to fail early) Images are checked for existence or downloaded then resized Colors are check for validity """ # skip this step if none of related values were supplied if not sum([ bool(x) for x in ( self.profile_image, self.banner_image, self.main_color, self.secondary_color, ) ]): return logger.info("checking your branding files and values") if self.profile_image: if self.profile_image.startswith("http"): save_file(self.profile_image, self.profile_path) else: if not self.profile_image.exists(): raise IOError( f"--profile image could not be found: {self.profile_image}" ) shutil.move(self.profile_image, self.profile_path) resize_image(self.profile_path, width=100, height=100, method="thumbnail") if self.banner_image: if self.banner_image.startswith("http"): save_file(self.banner_image, self.banner_path) else: if not self.banner_image.exists(): raise IOError( f"--banner image could not be found: {self.banner_image}" ) shutil.move(self.banner_image, self.banner_path) resize_image(self.banner_path, width=1060, height=175, method="thumbnail") if self.main_color and not is_hex_color(self.main_color): raise ValueError( f"--main-color is not a valid hex color: {self.main_color}") if self.secondary_color and not is_hex_color(self.secondary_color): raise ValueError( f"--secondary_color-color is not a valid hex color: {self.secondary_color}" )
def check_branding_values(self): """ checks that user-supplied images and colors are valid (so to fail early) Images are checked for existence or downloaded then resized Colors are check for validity """ # skip this step if none of related values were supplied if not sum([ bool(x) for x in ( self.favicon, self.main_logo, self.secondary_logo, self.main_color, self.secondary_color, self.about, ) ]): return logger.info("checking your branding files and values") images = [ (self.favicon, self.build_dir.joinpath("favicon.png"), 48, 48), (self.main_logo, self.main_logo_path, 300, 65), (self.secondary_logo, self.secondary_logo_path, 300, 65), ] for src, dest, width, height in images: if src: handle_user_provided_file(source=src, dest=dest) resize_image(dest, width=width, height=height, method="thumbnail") if self.main_color and not is_hex_color(self.main_color): raise ValueError( f"--main-color is not a valid hex color: {self.main_color}") if self.secondary_color and not is_hex_color(self.secondary_color): raise ValueError( f"--secondary_color-color is not a valid hex color: {self.secondary_color}" ) if self.about: handle_user_provided_file(source=self.about, dest=self.build_dir / "about.html")
def save_channel_branding(channels_dir, channel_id, save_banner=False): """ download, save and resize profile [and banner] of a channel """ channel_json = get_channel_json(channel_id) thumbnails = channel_json["snippet"]["thumbnails"] for quality in ("medium", "default"): # high:800px, medium:240px, default:88px if quality in thumbnails.keys(): thumnbail = thumbnails[quality]["url"] break profile_path = channels_dir.joinpath(channel_id, "profile.jpg") if not profile_path.exists(): save_file(thumnbail, profile_path) # resize profile as we only use up 100px/80 sq resize_image(profile_path, width=100, height=100) if save_banner: banner = channel_json["brandingSettings"]["image"]["bannerImageUrl"] banner_path = channels_dir.joinpath(channel_id, "banner.jpg") if not banner_path.exists(): save_file(banner, banner_path)
def post_process_video(video_dir, video_id, video_format, low_quality, skip_recompress=False): # apply custom post-processing to downloaded video # - resize thumbnail # - recompress video if incorrect video_format or low_quality requested # find downloaded video from video_dir files = [ p for p in video_dir.iterdir() if p.stem == "video" and p.suffix != ".jpg" ] if len(files) == 0: logger.error(f"Video file missing in {video_dir} for {video_id}") logger.debug(list(video_dir.iterdir())) raise FileNotFoundError(f"Missing video file in {video_dir}") if len(files) > 1: logger.warning( f"Multiple video file candidates for {video_id} in {video_dir}. Picking {files[0]} out of {files}" ) src_path = files[0] # resize thumbnail. we use max width:248x187px in listing resize_image(src_path.parent.joinpath("thumbnail.jpg"), width=248, height=187, method="cover") # don't reencode if not requesting low-quality and received wanted format if skip_recompress or (not low_quality and src_path.suffix[1:] == video_format): return dst_path = src_path.parent.joinpath(f"video.{video_format}") recompress_video(src_path, dst_path, video_format)
def update_metadata(self): # we use title, description, profile and banner of channel/user # or channel of first playlist main_channel_json = get_channel_json(self.main_channel_id) save_channel_branding(self.channels_dir, self.main_channel_id, save_banner=True) # if a single playlist was requested, use if for names; # otherwise, use main_channel's details. auto_title = (self.playlists[0].title if self.is_playlist and len(self.playlists) == 1 else main_channel_json["snippet"]["title"].strip()) auto_description = (clean_text(self.playlists[0].description) if self.is_playlist and len(self.playlists) == 1 else clean_text( main_channel_json["snippet"]["description"])) self.title = self.title or auto_title or "-" self.description = self.description or auto_description or "-" if self.creator is None: if self.is_single_channel: self.creator = _("Youtube Channel “{title}”").format( title=main_channel_json["snippet"]["title"]) else: self.creator = _("Youtube Channels") self.publisher = self.publisher or "Kiwix" self.tags = self.tags or ["youtube"] if "_videos:yes" not in self.tags: self.tags.append("_videos:yes") self.zim_info.update( title=self.title, description=self.description, creator=self.creator, publisher=self.publisher, name=self.name, tags=self.tags, ) # copy our main_channel branding into /(profile|banner).jpg if not supplied if not self.profile_path.exists(): shutil.copy( self.channels_dir.joinpath(self.main_channel_id, "profile.jpg"), self.profile_path, ) if not self.banner_path.exists(): shutil.copy( self.channels_dir.joinpath(self.main_channel_id, "banner.jpg"), self.banner_path, ) # set colors from images if not supplied if self.main_color is None or self.secondary_color is None: profile_main, profile_secondary = get_colors(self.profile_path) self.main_color = self.main_color or profile_main self.secondary_color = self.secondary_color or profile_secondary resize_image( self.profile_path, width=48, height=48, method="thumbnail", to=self.build_dir.joinpath("favicon.jpg"), )
def hook_youtube_dl_ffmpeg(video_format, low_quality, data): """ youtube-dl hook to convert video at end of download - if low_quality was request - if received format is not requested one """ if data.get("status") != "finished": return src_path = pathlib.Path(data["filename"]) tmp_path = src_path.parent.joinpath( "video.tmp.{fmt}".format(src=src_path.name, fmt=video_format) ) dst_path = src_path.parent.joinpath("video.{fmt}".format(fmt=video_format)) # resize thumbnail. we use max width:248x187px in listing # but our posters are 480x270px resize_image( src_path.parent.joinpath("video.jpg"), width=480, height=270, method="cover" ) # don't reencode if not requesting low-quality and received wanted format if not low_quality and src_path.suffix[1:] == video_format: return video_codecs = {"mp4": "h264", "webm": "libvpx"} audio_codecs = {"mp4": "aac", "webm": "libvorbis"} params = {"mp4": ["-movflags", "+faststart"], "webm": []} args = ["ffmpeg", "-y", "-i", "file:{}".format(str(src_path))] if low_quality: args += [ "-codec:v", video_codecs[video_format], "-quality", "best", "-cpu-used", "0", "-b:v", "300k", "-qmin", "30", "-qmax", "42", "-maxrate", "300k", "-bufsize", "1000k", "-threads", "8", "-vf", "scale='480:trunc(ow/a/2)*2'", "-codec:a", audio_codecs[video_format], "-b:a", "128k", ] else: args += [ "-codec:v", video_codecs[video_format], "-quality", "best", "-cpu-used", "0", "-bufsize", "1000k", "-threads", "8", "-codec:a", audio_codecs[video_format], ] args += params[video_format] args += ["file:{}".format(str(tmp_path))] logger.info( " converting {src} into {dst}".format(src=str(src_path), dst=str(dst_path)) ) logger.debug(nicer_args_join(args)) ffmpeg = subprocess.run(args) ffmpeg.check_returncode() # delete original src_path.unlink() # rename temp filename with final one tmp_path.replace(dst_path)