def create_thumbnails(self, force=False): """ Checks if every image has an existing thumbnail and generates it if not (or if forced by the user) :param force: Forces generation of thumbnails if set to true """ # Multiply the thumbnail size by the factor to generate larger thumbnails to improve quality on retina displays thumbnail_height = self.gallery_config[ 'thumbnail_height'] * FilesGalleryLogic.THUMBNAIL_SIZE_FACTOR thumbnails_path = self.gallery_config['thumbnails_path'] photos = glob.glob( os.path.join(self.gallery_config['images_path'], '*.*')) if not photos: raise spg_common.SPGException( f'No photos could be found under {self.gallery_config["images_path"]}' ) count_thumbnails_created = 0 for photo in photos: thumbnail_path = get_thumbnail_name(thumbnails_path, photo) # Check if the thumbnail should be generated. This happens if one of the following applies: # - Forced by the user with -f # - No thumbnail for this image # - The thumbnail image size doesn't correspond to the specified size if force or not os.path.exists( thumbnail_path) or not check_correct_thumbnail_size( thumbnail_path, thumbnail_height): spg_media.create_thumbnail(photo, thumbnail_path, thumbnail_height) count_thumbnails_created += 1 spg_common.log(f'New thumbnails generated: {count_thumbnails_created}')
def get_metadata(image, thumbnail_path, public_path): """ Gets the metadata of a media file (image or video) :param image: Path to the media file :param thumbnail_path: Path to the thumbnail image of the media file :param public_path: Path to the public folder of the gallery :return: """ # Paths should be relative to the public folder, because they will directly be used in the HTML image_data = dict(src=os.path.relpath(image, public_path), mtime=os.path.getmtime(image), date=get_image_date(image)) if image.lower().endswith('.jpg') or image.lower().endswith('.jpeg'): image_data['size'] = get_image_size(image) image_data['type'] = 'image' image_data['description'] = get_image_description(image) elif image.lower().endswith('.gif'): image_data['size'] = get_image_size(image) image_data['type'] = 'image' image_data['description'] = '' elif image.lower().endswith('.mp4'): image_data['size'] = get_video_size(image) image_data['type'] = 'video' image_data['description'] = '' thumbnail_path = thumbnail_path.replace('.mp4', '.jpg') else: raise spg_common.SPGException( f'Unsupported file type {os.path.basename(image)}') image_data['thumbnail'] = os.path.relpath(thumbnail_path, public_path) image_data['thumbnail_size'] = get_image_size(thumbnail_path) return image_data
def upload_gallery(self, location, gallery_path): """ Upload the gallery to the specified location :param location: S3 bucket where the gallery should be uploaded :param gallery_path: path to the root of the public files of the gallery """ # Add s3 protocol if needed if not location.startswith('s3://'): location = 's3://' + location # Add trailing / if needed if not location.endswith('/'): location += '/' # Build and execute the AWS S3 sync command aws_command = [ 'aws', 's3', 'sync', gallery_path, location, '--exclude', '.DS_Store' ] spg_common.log(f'Uploading to AWS S3 at {location}') process = subprocess.run(aws_command) if process.returncode != 0: raise spg_common.SPGException('Could not sync with AWS S3') # Compute HTTP URL and display success message url = location.replace('s3://', 'http://') + 'index.html' spg_common.log( f'Upload finished successfully! You can access your gallery at: {url}' )
def get_uploader(hosting_type): """ Factory function that returns an object of a class derived from BaseUploader based on the provided hosting type. Supported uploaders: - AWSUploader - uploader for AWS S3 - Netlify - uploader for Netlify :param hosting_type: name of the hosting provider (aws or netlify) :return: uploader object """ if hosting_type == 'aws': return AWSUploader() elif hosting_type == 'netlify': return NetlifyUploader() else: raise spg_common.SPGException( f"Hosting type not supported: {hosting_type}")
def create_thumbnail(input_path, thumbnail_path, height): """ Creates a thumbnail for a media file (image or video) :param input_path: input media path (image or video) :param thumbnail_path: path to the thumbnail file to be created :param height: height of the thumbnail in pixels """ # Handle JPGs and GIFs if input_path.lower().endswith('.jpg') or input_path.lower().endswith( '.jpeg') or input_path.lower().endswith('.gif'): create_image_thumbnail(input_path, thumbnail_path, height) # Handle MP4s elif input_path.lower().endswith('.mp4'): create_video_thumbnail(input_path, thumbnail_path, height) else: raise spg_common.SPGException( f'Unsupported file type ({os.path.basename(input_path)})')
def get_metadata(image, thumbnail_path, public_path): """ Gets the metadata of a media file (image or video) :param image: Path to the media file :param thumbnail_path: Path to the thumbnail image of the media file :param public_path: Path to the public folder of the gallery :return: """ # Paths should be relative to the public folder, because they will directly be used in the HTML image_data = dict( src=os.path.relpath(image, public_path), mtime=os.path.getmtime(image), date=get_image_date(image), ) if image.lower().endswith(".jpg") or image.lower().endswith(".jpeg"): image_data["size"] = get_image_size(image) image_data["type"] = "image" image_data["description"] = get_image_description(image) elif image.lower().endswith(".gif") or image.lower().endswith(".png"): image_data["size"] = get_image_size(image) image_data["type"] = "image" image_data["description"] = "" elif image.lower().endswith(".mp4"): image_data["size"] = get_video_size(image) image_data["type"] = "video" image_data["description"] = "" thumbnail_path = thumbnail_path.replace(".mp4", ".jpg") else: raise spg_common.SPGException( f"Unsupported file type {os.path.basename(image)}" ) image_data["thumbnail"] = os.path.relpath(thumbnail_path, public_path) image_data["thumbnail_size"] = get_image_size(thumbnail_path) return image_data
def upload_gallery(self, location, gallery_path): """ Upload the gallery to the specified location :param location: Netlify site where the gallery should be uploaded :param gallery_path: path to the root of the public files of the gallery """ # Create a zip file for the gallery spg_common.log('Creating ZIP file of the gallery...') zip_file_path = os.path.join(tempfile.gettempdir(), 'simple_photo_gallery.zip') create_website_zip(gallery_path, zip_file_path) spg_common.log('Gallery ZIP file created!') # Start the HTTP server that handles OAuth authentication at Netlify httpd = SimplePhotoGalleryHTTPServer( ('localhost', 8080), SimplePhotoGalleryHTTPRequestHandler) # Get the authorization token token = self.get_authorization_token(httpd) # Check if the website already exists and get its ID site_id = get_netlify_site_id(location, token) # Deploy the website gallery_url = deploy_to_netlify(zip_file_path, token, site_id) # Delete the zip file os.remove(zip_file_path) # Open the Netlify gallery if successful if gallery_url: spg_common.log(f'Gallery uploaded successfully to:\n{gallery_url}') webbrowser.open(gallery_url) else: raise spg_common.SPGException( f'Something went wrong while uploading to Netlify')
def upload_gallery(self, location, gallery_path): """ Upload the gallery to the specified location :param location: S3 bucket where the gallery should be uploaded :param gallery_path: path to the root of the public files of the gallery """ # Add s3 protocol if needed if not location.startswith("s3://"): location = "s3://" + location # Add trailing / if needed if not location.endswith("/"): location += "/" # Build and execute the AWS S3 sync command aws_command = [ "aws", "s3", "sync", gallery_path, location, "--exclude", ".DS_Store", ] spg_common.log(f"Uploading to AWS S3 at {location}") process = subprocess.run(aws_command) if process.returncode != 0: raise spg_common.SPGException("Could not sync with AWS S3") # Compute HTTP URL and display success message url = location.replace("s3://", "http://") + "index.html" spg_common.log( f"Upload finished successfully! You can access your gallery at: {url}" )
def create_gallery_json(gallery_root, remote_link): """ Creates a new gallery.json file, based on settings specified by the user :param gallery_root: Path to the gallery root :param remote_link: Optional link to a remote shared album containing the photos for the gallery """ spg_common.log('Creating the gallery config...') spg_common.log( 'You can answer the following questions in order to set some important gallery properties. You can ' 'also just press Enter to leave the default and change it later in the gallery.json file.' ) # Initialize the gallery config with the main gallery paths gallery_config = dict( images_data_file=os.path.join(gallery_root, 'images_data.json'), public_path=os.path.join(gallery_root, 'public'), templates_path=os.path.join(gallery_root, 'templates'), images_path=os.path.join(gallery_root, 'public', 'images', 'photos'), thumbnails_path=os.path.join(gallery_root, 'public', 'images', 'thumbnails'), thumbnail_height=160, ) # Initialize remote gallery configuration if remote_link: remote_gallery_type = gallery_logic.get_gallery_type(remote_link) if not remote_gallery_type: raise spg_common.SPGException( 'Cannot initialize remote gallery - please check the provided link.' ) else: gallery_config['remote_gallery_type'] = remote_gallery_type gallery_config['remote_link'] = remote_link # Set configuration defaults default_title = 'My Gallery' default_description = 'Default description of my gallery' # Ask the user for the title gallery_config['title'] = input( f'What is the title of your gallery? (default: "{default_title}")\n' ) or default_title # Ask the user for the description gallery_config['description'] = input( f'What is the description of your gallery? (default: "{default_description}")\n' ) or default_description # Ask the user for the background image gallery_config['background_photo'] = input( f'Which image should be used as background for the header? (default: "")\n' ) # Ask the user for the site URL gallery_config['url'] = input( f'What is your site URL? This is only needed to better show links to your galleries on social media (default: "")\n' ) # Set the default background offset right after the background image gallery_config['background_photo_offset'] = 30 # Save the configuration to a file gallery_config_path = os.path.join(gallery_root, 'gallery.json') with open(gallery_config_path, 'w', encoding='utf-8') as out: json.dump(gallery_config, out, indent=4, separators=(',', ': ')) spg_common.log('Gallery config stored in gallery.json')
def generate_images_data(self, images_data): """ Parse the remote link and extract link to the images and the thumbnails :param images_data: Images data dictionary containing the existing metadata of the images and which will be updated by this function :return updated images data dictionary """ # Get the path to the Firefox webdriver webdriver_path = pkg_resources.resource_filename( 'simplegallery', 'bin/geckodriver') # Configure the driver in headless mode options = Options() options.headless = True spg_common.log(f'Starting Firefox webdriver...') driver = webdriver.Firefox(options=options, executable_path=webdriver_path) # Load the album page spg_common.log( f'Loading album from {self.gallery_config["remote_link"]}...') driver.get(self.gallery_config["remote_link"]) # Wait until the page is fully loaded loading_start = time.time() last_image_count = 0 while True: image_count = len( driver.find_elements_by_class_name('od-ImageTile-image')) if image_count > 1 and image_count == last_image_count: break last_image_count = image_count if (time.time() - loading_start) > 30: raise spg_common.SPGException( 'Loading the page took too long.') time.sleep(5) # Parse all photos spg_common.log('Finding photos...') photos = driver.find_elements_by_class_name('od-ImageTile-image') spg_common.log(f'Photos found: {len(photos)}') current_photo = 1 for photo in photos: photo_url = photo.get_attribute('src') photo_base_url, photo_name = parse_photo_link(photo_url) spg_common.log( f'{current_photo}/{len(photos)}\t\tProcessing photo {photo_name}: {photo_url}' ) current_photo += 1 # Compute photo and thumbnail sizes photo_link_max_size = f'{photo_base_url}?psid=1&width=9999&height=9999' size = spg_media.get_remote_image_size(photo_link_max_size) thumbnail_size = spg_media.get_thumbnail_size( size, self.gallery_config['thumbnail_height']) # Add the photo to the images_data dict images_data[photo_name] = dict( description='', mtime=time.time(), size=size, src=f'{photo_base_url}?psid=1&width={size[0]}&height={size[1]}', thumbnail= f'{photo_base_url}?psid=1&width={thumbnail_size[0]}&height={thumbnail_size[1]}', thumbnail_size=thumbnail_size, type='image', ) spg_common.log(f'All photos processed!') driver.quit() return images_data
def create_gallery_json(gallery_root, remote_link, use_defaults=False): """ Creates a new gallery.json file, based on settings specified by the user :param gallery_root: Path to the gallery root :param remote_link: Optional link to a remote shared album containing the photos for the gallery :param use_defaults: If set to True, there will be no questions asked on the console """ spg_common.log("Creating the gallery config...") spg_common.log( "You can answer the following questions in order to set some important gallery properties. You can " "also just press Enter to leave the default and change it later in the gallery.json file." ) # Initialize the gallery config with the main gallery paths gallery_config = dict( images_data_file=os.path.join(gallery_root, "images_data.json"), public_path=os.path.join(gallery_root, "public"), templates_path=os.path.join(gallery_root, "templates"), images_path=os.path.join(gallery_root, "public", "images", "photos"), thumbnails_path=os.path.join(gallery_root, "public", "images", "thumbnails"), thumbnail_height=160, title="My Gallery", description="Default description of my gallery", background_photo="", url="", background_photo_offset=30, disable_captions=False, ) # Initialize remote gallery configuration if remote_link: remote_gallery_type = gallery_logic.get_gallery_type(remote_link) if not remote_gallery_type: raise spg_common.SPGException( "Cannot initialize remote gallery - please check the provided link." ) else: gallery_config["remote_gallery_type"] = remote_gallery_type gallery_config["remote_link"] = remote_link # Set configuration defaults default_title = "My Gallery" default_description = "Default description of my gallery" # If defaults are not used, ask the user to provide input to some important settings if not use_defaults: # Ask the user for the title gallery_config["title"] = (input( f'What is the title of your gallery? (default: "{default_title}")\n' ) or gallery_config["title"]) # Ask the user for the description gallery_config["description"] = (input( f'What is the description of your gallery? (default: "{default_description}")\n' ) or gallery_config["description"]) # Ask the user for the background image gallery_config["background_photo"] = input( f'Which image should be used as background for the header? (default: "")\n' ) # Ask the user for the site URL gallery_config["url"] = input( f'What is your site URL? This is only needed to better show links to your galleries on social media (default: "")\n' ) # Set the default background offset right after the background image gallery_config["background_photo_offset"] = 30 # Save the configuration to a file gallery_config_path = os.path.join(gallery_root, "gallery.json") with open(gallery_config_path, "w", encoding="utf-8") as out: json.dump(gallery_config, out, indent=4, separators=(",", ": ")) spg_common.log("Gallery config stored in gallery.json")
def generate_images_data(self, images_data): """ Parse the remote link and extract link to the images and the thumbnails :param images_data: Images data dictionary containing the existing metadata of the images and which will be updated by this function :return updated images data dictionary """ # Get the path to the Firefox webdriver webdriver_path = pkg_resources.resource_filename( "simplegallery", "bin/geckodriver") # Configure the driver in headless mode options = Options() options.headless = True spg_common.log(f"Starting Firefox webdriver...") driver = webdriver.Firefox(options=options, executable_path=webdriver_path) # Load the album page spg_common.log( f'Loading album from {self.gallery_config["remote_link"]}...') driver.get(self.gallery_config["remote_link"]) # Wait until the page is fully loaded loading_start = time.time() last_image_count = 0 while True: image_count = len( driver.find_elements_by_xpath("//div[@data-latest-bg]")) if image_count > 1 and image_count == last_image_count: break last_image_count = image_count if (time.time() - loading_start) > 30: raise spg_common.SPGException( "Loading the page took too long.") time.sleep(5) # Parse all photos spg_common.log("Finding photos...") photos = driver.find_elements_by_xpath("//div[@data-latest-bg]") spg_common.log(f"Photos found: {len(photos)}") current_photo = 1 for photo in photos: photo_url = photo.get_attribute("data-latest-bg") photo_base_url, photo_name = parse_photo_link(photo_url) spg_common.log( f"{current_photo}/{len(photos)}\t\tProcessing photo {photo_name}: {photo_url}" ) current_photo += 1 # Compute photo and thumbnail sizes photo_link_max_size = f"{photo_base_url}=w9999-h9999-no" size = spg_media.get_remote_image_size(photo_link_max_size) thumbnail_size = spg_media.get_thumbnail_size( size, self.gallery_config["thumbnail_height"]) # Add the photo to the images_data dict images_data[photo_name] = dict( description="", mtime=time.time(), size=size, src=f"{photo_base_url}=w{size[0]}-h{size[1]}-no", thumbnail= f"{photo_base_url}=w{thumbnail_size[0]}-h{thumbnail_size[1]}-no", thumbnail_size=thumbnail_size, type="image", ) spg_common.log(f"All photos processed!") driver.quit() return images_data