Copy playlists from Lollipop music player on Linux to iPhone

playlist to iphone banner

A quick and dirty Python script to copy Lollipop playlists and music files to FlacBox music player running on iPhone or iPad. Instructions are in code if you need them. I tested this on Pop OS and Manjaro Gnome flavors. Fedora 37 failed to mount the iPhone Document section, although everything was there that is needed. I have not tested this in KDE or other DEs. I am considering wrapping this into a GTK GUI and maybe making a plugin for Rhythmbox.

Update: I wrote a PySimpleGUI version if you prefer a GUI app.

A new and improved iPhone connection watcher.

#!/usr/bin/env python3
from gi.repository import Gio
import urllib.parse
import shutil
import os
import sqlite3
import getpass
import time
'''
Author: C. Nichols <mohawke@gmail.com>
Date: Oct. 1, 2022

This code was written on Pop! OS 22.04. Your paths may differ from mine.
This code transfers playlists created in Lollipop to FlacBox player on iPhone.
If you have the path on you phone for another player it should work as long as 
long as it's a file path with write access. This should work on Android as well.

This script is for my personal automation, because I got tired of manually managing shit.
I release this code as is without any warrently under public domain. Do what thou
wilt shall be the whole of the law. 

Tested on Manjaro Gnome =- latest.
Failed on Fedora 37 beta. iPhone doc location not created.
I have not tested with KDE or other DEs.

Instructions:

Note: fF you cannot get your user id using the getpass lib simply hardcode it.
Install FlacBox on your iPhone and open the app at least once to create the file system.
Connect your phone via USB and give it access. In Files (default Gnome file manager) you 
should see two entries for iPhone, you want the path for "Documents On iPhone" not DCIM (images). 
Once connected this should just run fine but if not you will need to diagnose why the iPhone won't mount.
Create a playlist in Lollipop if you don't have one.
Run the script. If it fails verify getpass can actually get your user name and verify the database is 
at the path I use. That's all the help I can give... 

Once the script is finished and the files landed without issues open FlacBox and create the play lists 
then nav to Files and press and hold then add each playlist folder to your FlacBox playlists. I have not
tried to access the FlacBox DB to do this automatically. Maybe another time.

To run this:

Copy the code to a text editor and save as something.py
Then open a terminal and type: python3 whateveryounamedit.py

Cheers, Mohawke
'''
class SimpleCSVReport:
    def __init__(self):
        self.path = "playlist_report.csv"
        self.lines = []
        self.lines.append("playlist,song,path,uploaded")
        
    def append(self, line):
        if line:
            self.lines.append(line)
            
    def save(self):
        try:
            with open(self.path,'w') as stream:
                stream.write('\n'.join(self.lines))
        except:
            raise Warning("CSV Report write fail.")

def get_flacbox_path():
    flac_path = None
    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")
    if os.path.exists(flac_path):
        return flac_path 
    
def playlist_to_flacbox(user, base_flacbox_path, playlists=[], csv=False):
        
    if not os.path.exists(base_flacbox_path):
        print("Invalid path at %s" % base_flacbox_path)
        os._exit(0)
        
    if csv:
        csv = SimpleCSVReport()
        csv.path = "/home/%s/playlist_report.csv" % user
        
    process_list = [i.strip().lower() for i in playlists]

    # 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
    
    conn = sqlite3.connect(lollipop_db)
    
    cursor = conn.execute("SELECT * from playlists p JOIN tracks t ON p.id = t.playlist_id")
    count = 0
    errors = 0
    for row in cursor:
        playlist_name = row[1].strip().lower()
        
        if "popular tracks" in playlist_name: continue # This is a default Lollipop playlist and cannot be removed.
        if process_list and playlist_name not in process_list: continue # Only process specified playlist.

        song_path = urllib.parse.unquote(row[8].strip()).replace('file://','').replace('\n','')
        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 = '"{0}","{1}","{2}",Copied successfully.'.format(playlist_name,song_name,song_only_path)
                if csv: csv.append(msg)
                #time.sleep(1) # take a breath...
                count += 1
            except Exception as err:
                msg = '"{0}","{1}","{2}","{3}"'.format(playlist_name,song_name,song_only_path,err)
                if csv: csv.append(msg)
                print("COPY FAILED -> %s" % msg)
                errors += 1
        else:
            msg = '"{0}","{1}","{2}",Path not found.'.format(playlist_name,song_name,song_only_path)
            if csv: csv.append(msg)
            print(msg)
            errors += 1

    conn.close()
    if csv: csv.save()
    return "Total files copied: {0}\nTotal copy errors: {1}".format(count, errors)

if __name__ == "__main__":
    
    # *** You can hard code your user name here if you have problems.
    my_name = getpass.getuser()

    # *** Add the playlists here to be selective. Empty it will copy all found.
    my_playlists = ['punk','Rock','DOOM']

    # *** Path to FlacBox on iPhone. This could be other players with file system access including those on Android.
    my_flacbox_path = get_flacbox_path()
    if not my_flacbox_path: 
        print("iPhone not found.")
        os._exit(0)
    
    # ! do the work...
    totals = playlist_to_flacbox(my_name, my_flacbox_path, my_playlists, csv=True)
    if not totals: 
        print("Lollipop database not found.")
        os._exit(0)
        
    print(totals)
iPhone plugged in with Documents displayed and path in terminal.

For some other iPhone and iPad fun on Linux:

KDEConnect. You can use this on Gnome just fine. Basically gives you air drop.

For Photos GTKam, Shotwell, or digiKam all work great.

For notes I use Simplenote.

For passwords I use Enpass.

For eBooks I am using Marvin 3.

Of course Dropbox works great too…