5
\$\begingroup\$

I created a simple script that is used to implement Google's Chromecast wallpaper-images as the wallpaper of my Gnome desktop.

The script extract the images from 2 different sources:

  1. A static webpage containing a collection of the Chromecast images
  2. A dynamic (javscript driven) webpage that shows a different wallpaper at an interval set by Google.

The reasons I use two different sources are:

  1. I already had a working script that used only the static page & wanted to challenge myself
  2. When only using the dynamic page somehow the first image when loading that webpage is always the same (or one from a very minor subset with little variation) and I want to see an immediately different wallpaper each time I run the script.

My current code looks like this:

#!/usr/bin/env python
"""This script runs in the background to download wallpapers used by Google's
chromecast devices and set them as a desktop wallpaper in Linux environments
using Gnome desktop. The wallpaper that is being set will change
automatically within a given interval."""

import io
import os
import subprocess
import time
from random import shuffle
from bs4 import BeautifulSoup
from PIL import Image
from pyvirtualdisplay import Display
import requests
from selenium import webdriver
from selenium.webdriver import DesiredCapabilities


def first_picture():
    """Retrieve a random image from a non-interactive website. This 
    will be the first image to which the Gnome desktop will be set. It 
    will (1) give the user visual feedback that the script is working 
    and (2) prevent the same image (or minor set of images) to be shown
    whenever the script is run."""

    request = requests.get('https://github.com/dconnolly/'\
                           'chromecast-backgrounds')
    content = request.content
    soup = BeautifulSoup(content, features="lxml")
    images = soup.find_all("img")
    sourcelist = []
    current_dir = os.path.dirname(os.path.realpath(__file__))

    for image in images:
        if "camo" in image["src"]:
            sourcelist.append(image["src"])

    # Implemented a try-except because pil_image sometimes raises an error:
    # raise IOError("cannot identify image file %r" % (filename if filename
    # else fp))
    # OSError: cannot identify image file <_io.BytesIO object at
    # 0x66d3bea6d3b0>

    try:

        shuffle(sourcelist)
        raw_image = requests.get(sourcelist[0])
        pil_image = Image.open(io.BytesIO(raw_image.content))

    except IOError:

        first_picture()

    temp_local_image_location = (current_dir + "/interactive_wallpaper."
                                 + pil_image.format)
    pil_image.save(temp_local_image_location)
    subprocess.Popen(["/usr/bin/gsettings", "set",
                      "org.gnome.desktop.background",
                      "picture-uri", "'" + temp_local_image_location + "'"],
                     stdout=subprocess.PIPE)
    time.sleep(40)


def change_desktop(image_source):
    """Tap into the dynamic webpage that is displaying a different
    wallpapers-like images at a given interval."""

    request = requests.get(image_source)
    image = Image.open(io.BytesIO(request.content))
    image_format = image.format
    current_dir = os.path.dirname(os.path.realpath(__file__))
    temp_local_image_location = (current_dir + "/interactive_wallpaper."
                                 + image_format)
    image.save(temp_local_image_location)
    subprocess.Popen(["/usr/bin/gsettings", "set",
                      "org.gnome.desktop.background",
                      "picture-uri", "'" + temp_local_image_location + "'"],
                     stdout=subprocess.PIPE)
    time.sleep(20)

def set_display_and_start_browser():
    """Start virtual display and selenium driver. This is needed
    for the selenium-initiated browser to grab the dynamically
    loaded images dfrom the webpage"""

    display = Display(visible=0, size=(1920, 1080))
    display.start()
    desired_capabilities = DesiredCapabilities.CHROME.copy()
    desired_capabilities['connection'] = "keep-alive"
    browser = webdriver.Chrome(desired_capabilities=desired_capabilities)
    url = "https://clients3.google.com/cast/chromecast/home/"
    browser.get(url)

    first_picture()

    while True:

        element = browser.find_element_by_id("picture-background")
        image_source = element.get_attribute("src")
        change_desktop(image_source)
        time.sleep(20)

Any tips, insights, remarks, opinions, etc. more then welcome!

Curious to learn what you think.

\$\endgroup\$
  • 1
    \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. \$\endgroup\$ – Ethan Bierlein 2 days ago
  • \$\begingroup\$ Ah, thanks for the heads-up, happy to learn. I see you've already corrected it, great! \$\endgroup\$ – Montmons 2 days ago
3
\$\begingroup\$

Restructuring and corrections

Let's start with first_picture function issues.
I'd renamed it at least to get_first_picture.

BeautifulSoup.find_all allows using regular expression to find images tags with src attribute that meets some condition. Thus the condition:

sourcelist = []
...
    if "camo" in image["src"]:
        ...

can be eliminated and images are filtered by images = soup.find_all("img", src=re.compile(r'.*camo.*')).

The except block:

except IOError:
    first_picture()

doesn't provide any "exit" condition. When the control flow will get into this except block and call the current function first_picture() again - that doesn't mean that all subsequent statements below (from previous context) won't be executed.
You would need to either enclose all statements after that block into else: block to make them execute on successful image opening or put return statement right below first_picture() call.

But instead, what should be noticed beforehand is that both get_first_picture and change_desktop functions share the same common behavior that covers the following set of actions:

  • extracting image from remote resource
  • saving image
  • setting desktop background in separate subprocess

The time delay is also has a common amount - 40 (In your set_display_and_start_browser function time.sleep(20) + time.sleep(20), are indirectly added).

That definitely calls for Extract function technique - the common/repeated behavior is extracted into a separate function, say set_desktop_background:

def set_desktop_background(image_source):
    """Tap into the dynamic webpage that is displaying a different
    wallpapers-like images at a given interval."""

    request = requests.get(image_source)
    image = Image.open(io.BytesIO(request.content))
    image_format = image.format
    current_dir = os.path.dirname(os.path.realpath(__file__))
    temp_local_image_location = f'{current_dir}/interactive_wallpaper.{image_format}'
    image.save(temp_local_image_location)
    subprocess.Popen(["/usr/bin/gsettings", "set",
                      "org.gnome.desktop.background",
                      "picture-uri", f"'{temp_local_image_location}'"],
                      stdout=subprocess.PIPE)
    time.sleep(40)

Now change_desktop function is removed in favor of set_desktop_background function.

As for your mentioned periodical error OSError: cannot identify image file ... - I believe that can be fixed separately and is related to whether some particular image type or truncated image or failing to save image; you may find various fixes online.

The get_first_picture function is now shortened to the following:

def get_first_picture():
    """Retrieve a random image from a non-interactive website. This 
    will be the first image to which the Gnome desktop will be set. It 
    will (1) give the user visual feedback that the script is working 
    and (2) prevent the same image (or minor set of images) to be shown
    whenever the script is run."""

    request = requests.get('https://github.com/dconnolly/chromecast-backgrounds')        
    soup = BeautifulSoup(request.content, features="lxml")
    images = soup.find_all("img", src=re.compile(r'.*camo.*'))
    sourcelist = [image["src"] for image in images]

    shuffle(sourcelist)
    set_desktop_background(sourcelist[0])

set_display_and_start_browser is a bad pattern for function naming as it points to excessive responsibility (the function doing too much).
It's better to split it into a separate functions (considering all above optimizations):

start_display function:

def start_display():
    display = Display(visible=0, size=(1920, 1080))
    display.start()

start_browser function:

def start_browser():
    """Start selenium driver. This is needed
       for the selenium-initiated browser to grab the dynamically
       loaded images from the webpage.
    """
    desired_capabilities = DesiredCapabilities.CHROME.copy()
    desired_capabilities['connection'] = "keep-alive"
    browser = webdriver.Chrome(desired_capabilities=desired_capabilities)
    url = "https://clients3.google.com/cast/chromecast/home/"
    browser.get(url)
    return browser

and load_images_to_desktop function (as the main supervisor function):

def load_images_to_desktop():
    start_display()
    browser = start_browser()
    get_first_picture()

    while True:
        element = browser.find_element_by_id("picture-background")
        image_source = element.get_attribute("src")
        set_desktop_background(image_source)
\$\endgroup\$
  • \$\begingroup\$ Wow, thanks for your very extended and clear answer! Not only did it help me gain some extra insights into my own piece of code & some of the concepts you explicitly mentioned in your answer. But it also sneaked in some other interesting things I didn't yet know about like Python f-strings \$\endgroup\$ – Montmons 2 days ago
  • \$\begingroup\$ @Montmons, great, I'm glad you liked it. BTW, I noticed a lot of beautiful pictures on that repository :) \$\endgroup\$ – RomanPerekhrest 2 days ago

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.