pyMSync: M3U Sync

I depend on the music on my phone to get throughout my day, and I still like syncing my audio files from my computer to my Android phone. In the past, I’d been doing these syncs with DoubleTwist because it also allowed me to sync my playlists (making it easier to select the music I want to transfer); but the newest update to Catalina broke compatibility for me, so I had to look for alternatives. On top of this, I’ve recently shifted most of my day-to-day work to an Ubuntu system, so I wanted to keep my syncs compatible to both: iTunes and Amarok (or any other player that can export M3U playlists).

Given these constraints, and that I am a bit tired of depending on increasingly scarce apps that do what I need, I started coding a Python script that allowed me to take an M3U playlist, copy all the referenced files from my library to another folder, and create a second M3U file so that it pointed towards the copied files. This would allow me to copy music from my computer to my phone seamlessly.

Development

In essence, an M3U file is a text file that contains the song references in the order they are to be reproduced by the music player. There are two types of M3U files, the extended ones:

#EXTM3U
#EXTINF:237,Like A Friend - Pulp
/media/hdd/Music/Pulp/Great Expectations/14 Like A Friend.mp3
#EXTINF:254,Umpqua Rushing - Blind Pilot
/media/hdd/Music/Blind Pilot/And Then Like Lions/01 Umpqua Rushing.mp3
#EXTINF:163,Wake Me - Bleachers
/media/hdd/Music/Bleachers/Strange Desire/05 Wake Me.mp3

which hold the information about the duration, name, and artist of the song; and non-extended ones:

/media/hdd/Music/Pulp/Great Expectations/14 Like A Friend.mp3
/media/hdd/Music/Blind Pilot/And Then Like Lions/01 Umpqua Rushing.mp3
/media/hdd/Music/Bleachers/Strange Desire/05 Wake Me.mp3

which contain only the paths to the songs in the order they are to be played. Almost any media player can export absolute-paths M3U files, so we’ll be using these standards as the basis for the music transfer.

The script works by following the next steps:

  1. Get the required elements from the user.
    • M3U playlist path.
    • Path to which we want to copy the files into.
    • [optional] If the M3U is not located at the root of the music library, path pointing towards the root of the library. This is necessary to convert the absolute path reference to a relative one in the output playlist.
    • [optional] Flag to decide if we want to overwrite existing files (defaults to False).
    • [optional] Flag to decide if we want to print the process to the terminal (verbose).
    • [optional] Flag to decide if we want a txt log to be written in the output folder.
  2. Read the contents of the playlist.
    • Decide if the playlist is extended or not by reading the first line (should match: #EXTM3U)
      • If it is extended, create two lists of the same length: one containing the songs information, and another one with the paths.
      • If it is not extended, the first list contains None, and the second one the full paths to the songs.
  3. Create a new M3U file in the output folder, and start iterating through the lists.
    • For each element, we verify if the path and the file exist.
      • If they do exist, we create the same folder structure (starting from the library root) into the output folder, and we copy the audio file. Additionally, we add a relative reference to it in the output M3U.
      • If they don’t exist, we skip the song and report it to the log file.

That is pretty much it!

In the cycle defined in step 2 there are steps added to remove the absolute reference to the file (coming from the original playlist), additional tags such as the file:// added by some players, to overwrite files if desired, and to manage the log/verbose operations.

The log file/terminal output can contain three kinds of tags:

To use the script, simply run the following command on the terminal:

python pyMSync.py PATH_TO_PLAYLIST OUTPUT_FOLDER -lRt PATH_TO_MUSIC_LIBRARY_ROOT -o -l -v

Again, the -lRT is optional in case the playlist is not stored at the root of the music library. For example, if my playlist is stored at /chipdelmal/Mixtape127_TonightTonight.m3u, but my library lives in /media/hdd/Music/, I would need to add the latter as the -lRT argument; if the playlist was stored in the root (/media/hdd/Music/Mixtape127_TonightTonight.m3u), I can leave the -lRt blank.

An example of use follows. We are gonna take the Mixtape127_TonightTonight.m3u stored in my library’s root, and copy its contents to the Mixtapes folder to be synced to my second desktop. The playlist’s contents look like this:

#EXTM3U
#EXTINF:165,The Weepies - World Spins Madly On
file:///media/hdd/Music/The%20Weepies/Say%20I%20Am%20You/03%20World%20Spins%20Madly%20On.mp3
#EXTINF:217,New Radicals - Someday We'll Know
file:///media/hdd/Music/New%20Radicals/Maybe%20You've%20Been%20Brainwashed%20Too/06%20Someday%20We'll%20Know.mp3
#EXTINF:249,Damien Jurado - Cloudy Shoes
file:///media/hdd/Music/Damien%20Jurado/Saint%20Bartlett/01%20Cloudy%20Shoes.mp3
#EXTINF:257,Velveteen - The Getaway
file:///media/hdd/Music/Velveteen/Home%20Waters/04%20The%20Getaway.mp3
#EXTINF:309,The Album Leaf - Always for You
file:///media/hdd/Music/The%20Album%20Leaf/Into%20the%20Blue%20Again/02%20Always%20for%20You.mp3
#EXTINF:180,Dirty Pretty Things - Fault Lines
file:///media/hdd/Music/Dirty%20Pretty%20Things/Romance%20at%20Short%20Notice/06%20Fault%20Lines.mp3
#EXTINF:218,EELS - Climbing to the Moon
file:///media/hdd/Music/EELS/Electro-Shock%20Blues/2-04%20Climbing%20to%20the%20Moon.mp3
#EXTINF:238,The Waltons - Beats the Hell Out of Me
file:///media/hdd/Music/The%20Waltons/Empire%20Hotel/02%20Beats%20the%20Hell%20Out%20of%20Me.m4a
#EXTINF:253,Stars - Changes
file:///media/hdd/Music/Stars/The%20Five%20Ghosts/1-07%20Changes.mp3

And we want to move the contents from the folder on the top left, to the one on the top right:

so we run the following command:

python pyMSync.py '/media/hdd/Music/Mixtape127_TonightTonight.m3u' '/home/chipdelmal/Sync/Mixtapes' -o -l -v

and all the files get moved across:

With the contents of the new M3U playlist looking like this:

#EXTM3U
#EXTINF:165,The Weepies - World Spins Madly On
./The Weepies/Say I Am You/03 World Spins Madly On.mp3
#EXTINF:217,New Radicals - Someday We'll Know
./New Radicals/Maybe You've Been Brainwashed Too/06 Someday We'll Know.mp3
#EXTINF:249,Damien Jurado - Cloudy Shoes
./Damien Jurado/Saint Bartlett/01 Cloudy Shoes.mp3
#EXTINF:257,Velveteen - The Getaway
./Velveteen/Home Waters/04 The Getaway.mp3
#EXTINF:309,The Album Leaf - Always for You
./The Album Leaf/Into the Blue Again/02 Always for You.mp3
#EXTINF:180,Dirty Pretty Things - Fault Lines
./Dirty Pretty Things/Romance at Short Notice/06 Fault Lines.mp3
#EXTINF:218,EELS - Climbing to the Moon
./EELS/Electro-Shock Blues/2-04 Climbing to the Moon.mp3
#EXTINF:238,The Waltons - Beats the Hell Out of Me
./The Waltons/Empire Hotel/02 Beats the Hell Out of Me.m4a
#EXTINF:253,Stars - Changes
./Stars/The Five Ghosts/1-07 Changes.mp3

Which leaves us with a folder that we can sync over to whichever device we need to!

Further thoughts

One thing that is still causing problems in the script, is that some of the paths coming from my MacOS library are case-insensitive, so it causes problems when parsing them in my current Ubuntu-based system (which is case-sensitive). I need to add support for this special case, along with ratings and playcount conversions.

Documentation and Code