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}')
예제 #2
0
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
예제 #3
0
    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}'
        )
예제 #4
0
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}")
예제 #5
0
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)})')
예제 #6
0
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')
예제 #8
0
    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}"
        )
예제 #9
0
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')
예제 #10
0
    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
예제 #11
0
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")
예제 #12
0
    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