How to create a PySimpleGUI program

lollipop2iphone_banner

I wrote a program a few weeks ago that moves playlists from Linux to iPhone. It works but I wanted something a little nicer. I was going to write it in GTK but I always write GTK. Then I remember writing some simple program in a library called EasyGUI years ago. Anyone remember AnyGUI? I was surprised to find EasyGUI still out here. I started writing code and thought it seemed a bit too simple. Then I remembered another one, PySimpleGUI. It’s still out here too. Well, it won mainly because it can be themed easily.

Base setup and libraries

I would suggest creating a virtual environment if you want to keep this project separate from your system Python but it is up to you. This code is for Linux running Gnome. I am running Pop! OS 22.04 running Gnome 42.3 and will write this in Python 3.x.

First thing you need are the packages. You will need PyGObject and PySimpleGUI. Everything else is included in Python.

Install both with PIP:

$ pip install pysimplegui

$ pip install pygobject

I ran into problems with PyGObject, installing these two packages should get you running.

$ sudo apt install libcairo2-dev

$ sudo apt install libgirepository1.0-dev

Try to pip install pygobject again. If it fails just look at the messages as you will need to identify what library you are missing and install it.

The program

Here is the full program. I have documented it throughout to make it clear what is going on. There Cookbook for PySimpleGUI… Now to convert to classes and make it really nice.

#!/usr/bin/env python3
from gi.repository import Gio # https://pypi.org/project/PyGObject/
import PySimpleGUI as sg      # https://pypi.org/project/PySimpleGUI/
import urllib.parse
import shutil
import os
import sqlite3
import getpass
import threading

'''
PySimpleGUI example that pushes Lollipop playlists to FlacBox on iPhone.
Author: C. Nichols, Oct. 2022

What you will learn:
    How to create a simple PySImpleGUI application
    Basic layout.
    Basic app theming.
    How to update lists within the application.
    How to thread a process and pump message back to the user.
    How to create a popup.
    How to get the path to the iPhone document folder via Gio.
    SQLite access.
''' 
def notify(appname, message):
    cmd = 'zenity --name="{0}" --notification --text="{1}" "$THREAT\n$STRING"'.format(appname, message)
    os.system(cmd)
    
def get_flacbox_path():
    '''Uses GTK Gio to access Gnome virtual filesystem and determine the 
    path to iPhone Documments. Return a path we can use to push files 
    over.''' 
    flac_path = 'iPhone not connected, most likely.'
    try:
        vm = Gio.VolumeMonitor.get()
        for volume in vm.get_volumes():
            activation_root = volume.get_activation_root()
            if activation_root:
                if activation_root.get_uri_scheme() == 'afc':
                    gvfs_path = "/run/user/1000/gvfs/afc:host={0},port=3".format(volume.get_uuid())
        
        flac_path = os.path.join(gvfs_path,"com.leshko.flap","_downloads")
    except:
        # iPhone not connected.
        pass
        
    if os.path.exists(flac_path):
        return flac_path 

def get_playlists(user):
    '''Collects all Lollipop playlists from the database, if any.'''
    playlist_data = {}
    # Connect to the Lollipop database and fetch the tracks for each playlist.
    lollipop_db = '/home/{0}/.local/share/lollypop/playlists.db'.format(user)
    #if not os.path.exists(lollipop_db): return 0
    try:
        conn = sqlite3.connect(lollipop_db)
        
        cursor = conn.execute("SELECT * from playlists p JOIN tracks t ON p.id = t.playlist_id")

        for row in cursor:
            playlist_name = row[1].strip().upper()
            if playlist_name not in playlist_data:
                playlist_data[playlist_name]=[]
            
            if "popular tracks" in playlist_name: continue # This is a default Lollipop playlist and cannot be removed.

            song_path = urllib.parse.unquote(row[8].strip()).replace('file://','').replace('\n','')
            playlist_data[playlist_name].append(song_path)

        conn.close()
    except:
        return 0

    return playlist_data

def push_to_iphone(window, pl_songs, selected_lists, base_flacbox_path):
    '''Copies music to iPhone.'''
    messages = []
    try:
        for playlist_name in selected_lists:
            for song_path in pl_songs[playlist_name.strip()]:
                song_only_path = os.path.split(song_path)[0]
                song_name = os.path.split(song_path)[-1]

                phone_playlist_path = os.path.join(base_flacbox_path, playlist_name)
                phone_playlist_file = os.path.join(base_flacbox_path, playlist_name, song_name)
                
                if not os.path.exists(phone_playlist_path):
                    os.mkdir(phone_playlist_path)
                    
                if os.path.exists(song_path):
                    try:
                        shutil.copyfile(os.path.realpath(song_path), os.path.realpath(phone_playlist_file))
                        msg = 'Copied: {0} - {1}'.format(playlist_name,song_name)
                    except Exception as err:
                        msg = 'Error: "{0} - {1} -> {2}'.format(playlist_name,song_name,err)
                else:
                    msg = 'Not Found: {0} - {1}'.format(playlist_name,song_name)
                
                # Update app message window.
                print(msg) # Gets piped to output in app.
                #window.write_event_value('-THREAD PROGRESS-', msg)
    except Exception as err:
        msg = err           
        # Update app message window.
        print(msg)
            
    # Let our app know we are done and notify user.
    window.write_event_value('-UPLOAD_COMPLETED-', '')

def threaded_copy(window, pdict, upload_list, doc_path):
    '''Called from upload button to process push_to_iphone in thread.'''
    threading.Thread(target=push_to_iphone, args=(window, pdict, upload_list, doc_path,), daemon=True).start()

# ===========================================
#                   MAIN
# ===========================================
if __name__ == "__main__":

    # Set theme, more at PySimpleGui web-site.
    sg.theme('Default1')
    
    # Get the current logged on user's name.
    my_name = getpass.getuser()
    
    # Check for iPhone
    doc_path = ''
    iphone_available = False
    doc_path = get_flacbox_path()
    if doc_path: 
        iphone_available = True
        conn = sg.Text('iPhone Connected', text_color='#037100', enable_events=True, key="-CONNECT-")
    else:
        conn = sg.Text('Connect iPhone', text_color='#ff0000', enable_events=True, key="-CONNECT-")
    
    # Grab the playlists from Lollipop. 
    pdict = get_playlists(my_name)
    pnames = list(pdict.keys()) 
    pnames.sort()

    upload_list = [] # Define the upload list.

    # Define layout into columns and add the form components.
    col_right = [[sg.Button('Upload')]]
    col_left  = [[sg.Button('Quit')]]

    col_center = [
                    [sg.Text('Select Playlist(s)')],
                    [sg.Listbox(values=pnames, size=(70, 4), enable_events=True, key='-ADD-')],
                    [sg.Text('Playlist(s) to upload:')],
                    [sg.Listbox(values=upload_list, size=(70, 4), enable_events=True, key='-REMOVE-')],
                    [sg.Text('Messages:')],
                    [sg.Output(size=(70,10))]
                 ]

    gui_rows = [
                 [sg.Column(col_center, element_justification='center', expand_x=True)],
                 [sg.Column(col_left, element_justification='left', expand_x=True), 
                  conn,
                  sg.Column(col_right, element_justification='right', expand_x=True)]
               ]

    # Create window.
    window = sg.Window('Lollipop2iPhone', gui_rows, element_justification='center')
    
    # *** Start the application loop ***
    while (True):
        # This is the code that reads and updates your window
        event, values = window.read()

        # [TEXT ACTION GET IPHONE PATH]        
        if '-CONNECT-' in event:
            if not iphone_available:
                doc_path = get_flacbox_path()
                if doc_path: 
                    iphone_available = True
                    window['-CONNECT-'].update('iPhone Connected', text_color = '#037100') 
                else:
                    iphone_available = False
                    window['-CONNECT-'].update('Connect iPhone', text_color = '#ff0000')
                    sg.popup('iPhone not found.')
                    
        # [LIST ADD TO UPLOADs]
        if '-ADD-' in event:
            if values['-ADD-']:
                upload_list.append(values['-ADD-'][0])
                pnames.remove(values['-ADD-'][0])
                
                # Update physical lists.
                window['-ADD-'].update(pnames)
                window['-REMOVE-'].update(upload_list)
                
        # [LIST REMOVE FROM UPLOADS]
        if '-REMOVE-' in event:
            if values['-REMOVE-']:
                #INDEX = int(''.join(map(str, window["remove"].get_indexes())))
                upload_list.remove(values['-REMOVE-'][0])
                
                #pnames.insert(INDEX, values['remove'][0])
                pnames.append(values['-REMOVE-'][0])
                pnames.sort()
                
                window['-REMOVE-'].update(upload_list)
                window['-ADD-'].update(pnames)
                
        # [BUTTON UPLOAD]        
        if 'Upload' in event:
            #window.perform_long_operation(lambda: push_to_iphone(pdict, upload_list, doc_path), 'UPLOAD_COMPLETED')
            threaded_copy(window, pdict, upload_list, doc_path)
            
        # [UPLOAD FINISHED POPUP]
        if '-UPLOAD_COMPLETED-' in event:
            
            # Notify the uploads are finished.
            sg.popup('Uploads Finished.')
            
            # Reset the list boxes...
            for i in upload_list:
                pnames.append(i)
            upload_list = []
            pnames.sort()
                
            window['-ADD-'].update(pnames)
            window['-REMOVE-'].update(upload_list)
            
            # Send notification to Gnome.
            notify("PySimpleGUI", "Playlists uploaded to Flacbox.")
        
        # [BUTTON QUIT]
        if event in ('Quit', sg.WIN_CLOSED):
            break

    window.close()
lollipop2phone screenshot