Python script to randomise an m3u playlist

While I’m blogging scripts for playlist manipulation here is one I use in a nightly cron job to shuffle our playlists so that various devices playing from them have some daily variety. All disclaimers apply, it’s rough and ready but WorksForMe (TM).

I have an entry in my crontab like this

0 4 * * * /home/colin/bin/playlist-shuffle.py -q -i /var/media/mp3/A_Colin.m3u -o /var/media/mp3/A_Colin_Shuffle.m3u

which takes a static playlist and produces a nightly shuffled version.

#!/usr/bin/env python
#
# Simple script to randomise an m3u playlist
# Colin Turner <ct@piglets.com>
# 2013
# GPL v2
#

import random
import re

# We want to be able to process some command line options.
from optparse import OptionParser

def process_lines(options, all_lines):
  'process the list of all playlist lines into three chunks'
  # Eventually we want to support several formats
  m3u = True
  extm3u = False
  if options.verbose:
    print "Read %u lines..." % len(all_lines)
  header = list()
  middle = list()
  footer = list()
  
  # Check first line for #EXTM3U
  if re.match("^#EXTM3U", all_lines[0]):     if options.verbose:       print "EXTM3U format file..."     extm3u = True     header.append(all_lines[0])     del all_lines[0]      loop = 0   while loop < len(all_lines):     # Each 'item' may be multiline     item = list()     if re.match("^#EXTINF.*", all_lines[loop]):
      item.append(all_lines[loop])
      loop = loop + 1
    # A proper regexp for filenames would be good
    if loop < len(all_lines):
      item.append(all_lines[loop])
      loop = loop + 1
    if options.verbose: print item
    middle.append(item)
            
  return (header, middle, footer)

def load_playlist(options):
  'loads the playlist into an array of arrays'
  if options.verbose:
    print "Reading playlist %s ..." % options.in_filename
  with open(options.in_filename, 'r') as file:
    all_lines = file.readlines()
  (header, middle, footer) = process_lines(options, all_lines)
  return (header, middle, footer)

def write_playlist(options, header, middle, footer):
  'writes the shuffled playlist'
  if options.verbose:
    print "Writing playlist %s ..." % options.out_filename
  with open(options.out_filename, 'w') as file:
    for line in header:
      file.write(line)
    for item in middle:
      for line in item:
        file.write(line)
    for line in footer:
      file.write(line)


def shuffle(options):
  'perform the shuffle on the playlist'
  # read the existing data into three arrays in a tuple
  (header, middle, footer) = load_playlist(options)
  # and shuffle the lines array
  if options.verbose:
    print "Shuffling..."
  random.shuffle(middle)
  # now spit them back out
  write_playlist(options, header, middle, footer)

def print_banner():
  print "playlist-shuffle"

def main():
  'the main function that kicks everything else off'
  
  usage = "usage: %prog [options] arg"
  parser = OptionParser(usage)
  parser.add_option("-i", "--input-file", dest="in_filename",
                    help="read playlist from FILENAME")
  parser.add_option("-o", "--output-file", dest="out_filename",
                    help="write new playlist to FILENAME")
  parser.add_option("-v", "--verbose",
                    action="store_true", dest="verbose")
  parser.add_option("-q", "--quiet", default=False,
                    action="store_true", dest="quiet")
                    
  (options, args) = parser.parse_args()
#  if len(args) == 0:
#      parser.error("use -h for more help")
  
  if not options.quiet:
    print_banner()
  
  shuffle(options)
  
  if not options.quiet:
      print "Playlist shuffle complete..."
  
 
if  __name__ == '__main__':
  main()

Python script to add a file to a playlist

I have a number of playlists on Gondolin, which is a headless machine. I wanted to be able to easily add a given mp3 file to the playlists which are in m3u format. That means that each entry has both the filename and an extended line with some basic metadata, in particular the track length in seconds, the track artist and name. I wanted a script that could extract this information from the mp3 file and make adding the entry easy. So I wrote this in Python. It’s rough and ready and it is probably not very Pythonic but it’s working for me. The script should create a playlist if it doesn’t currently exist, and check for a newline at the end of the file so that the appended lines are really on a new line. ItWorksForMe (TM).

This uses the eyeD3 Python library, which on Debian is provided in python-eyed3.

My basic usage is

playlist-append -m the_mp3_file.mp3 -p the_playlist.m3u -r /var/media/mp3

the last parameter is the path relative to which the mp3 filename should be written to. This is useful for me because I rsync the whole tree between machines, as you will see there are options for writing an absolute pathname if you prefer. I should probably rewrite the script to do it relative to the playlist, but that’s another day.

#!/usr/bin/env python

#
# Trivial script to extract meta data from an mp3 file and add
# the mp3 file and data to an existing m3u file
#
# Colin Turner <ct@piglets.com>
# 2014
# GPL v2
#
# v 20140801.0 Initial Version
#
# v 20140802.0
# The mp3 filename is now, by default, written relative to the path of
# the playlist if possible.
#

import eyeD3
import re
import os

# We want to be able to process some command line options.
from optparse import OptionParser

def append(options, artist, title, seconds):
  'append the fetched data to the playlist'
  mp3_filename = resolve_mp3_filename(options)
  # Check if the playlist file exists
  there_is_no_spoon = not os.path.isfile(options.out_filename)

  with open(options.out_filename, 'a+') as playlist:
    # was the file frshly created?
    if there_is_no_spoon:
      # So write the header
      print >> playlist, "#EXTM3U"
    else:
      # There was a file, so check the last character, in case there was no \n
      playlist.seek(-1, os.SEEK_END)
      last_char = playlist.read(1)
      if(last_char != '\n'):
        print >> playlist

    # OK, now able to write
    print >> playlist, "#EXTINF:%u,%s - %s" % (seconds, artist, title)
    print >> playlist, "%s" % mp3_filename

def resolve_mp3_filename(options):
  'resolve the mp3 filename appropriately, if we can, and if we are asked to'
  'there are three modes, depending on command line parameters:'
  '-l we write the filename precisely as on the command list'
  '-r specifies a base relative which to write the filename'
  'otherwise we try to resolve relative to the directory of the playlist'
  'the absolute filename will be the fall back position is resolution is impossible'

  if options.leave_filename:
    # we have been specifcally told not to resolve the filename
    mp3_filename = options.in_filename
    if options.verbose:
      print "Filename resolution disabled."

  if not options.leave_filename and not len(options.relative_to):
    # Neither argument used, automatcally resolve relative to the playlist
    (playlist_path, playlist_name) = os.path.split(os.path.abspath(options.out_filename))
    options.relative_to = playlist_path + os.path.sep
    if options.verbose:
      print "Automatic filename resolution relative to playlist base %s" % options.relative_to

  if len(options.relative_to):
    # We have been told to map the path relative to another path
    mp3_filename = os.path.abspath(options.in_filename)
    # Check that the root is actually present
    if mp3_filename.find(options.relative_to) == 0:
      # It is present and at the start of the line
      mp3_filename = mp3_filename.replace(options.relative_to, '', 1)

  if options.verbose:
    print "mp3 filename will be written as %s..." % mp3_filename
  return mp3_filename

def get_meta_data(options):
  'perform the append on the playlist'
  # read the existing data into three arrays in a tuple
  if options.verbose:
    print "Opening MP3 file %s ..." % options.in_filename
  if eyeD3.isMp3File(options.in_filename):
    # Ok, so it's an mp3
    audioFile = eyeD3.Mp3AudioFile(options.in_filename)
    tag = audioFile.getTag()
    artist = tag.getArtist()
    title = tag.getTitle()
    seconds = audioFile.getPlayTime()
    if not options.quiet:
      print "%s - %s (%s s)" % (artist, title, seconds)
    # OK, we have the required information, now time to write to the playlist
    return artist, title, seconds
  else:
    print "Not a valid mp3 file."
    exit(1)

def print_banner():
  print "playlist-append"

def main():
  'the main function that kicks everything else off'

  usage = "usage: %prog [options] arg"
  parser = OptionParser(usage)
  parser.add_option("-m", "--mp3-file", dest="in_filename",
                    help="the FILENAME of the mp3 file to add")
  parser.add_option("-p", "--playlist-file", dest="out_filename",
                    help="the FILENAME of the playlist to append to")
  parser.add_option("-l", "--leave-filename", dest="leave_filename", action="store_false",
                    help="leaves the mp3 path as specified on the command line, rather than resolving it")
  parser.add_option("-r", "--relative-to", dest="relative_to", default="",
                    help="resolves mp3 filename relative to this path")
  parser.add_option("-v", "--verbose",
                    action="store_true", dest="verbose")
  parser.add_option("-q", "--quiet", default=False,
                    action="store_true", dest="quiet")

  (options, args) = parser.parse_args()
#  if len(args) == 0:
#      parser.error("use -h for more help")

  if not options.quiet:
    print_banner()

  (artist, title, seconds) = get_meta_data(options)
  append(options, artist, title, seconds)

  if not options.quiet:
      print "Appended to playlist..."


if  __name__ == '__main__':
  main()



Extending a wired doorbell with a wireless one

We moved into our new house in January 2013. There was, and still is, plenty of remedial work to do in the house, but by the end of 2013 we had renovated the main reception room that we “live” in. This is the middle floor (it’s a three floor property) and immediately above the wired, AC doorbell in the hall downstairs. The thing is, when we get into that room and close the door to stop any noise disturbing Matilda, you can’t hear the doorbell below at all. It’s largely easy to hear from other parts of the house. I didn’t want to replace the good working doorbell, especially since in my experience outdoor switches last longest when there is AC current going through them.

So I began researching doorbell extenders but found they were quite pricey and typically required batteries. I did find what looked like a suitable system for US voltages and sockets but nothing for the UK. So I bagan to wonder if I could just rig something up from a cheap wireless doorbell to have an additional sounder. Basically to wire the voltage from the existing wired door bell to a switch of a wireless one. Because I usually search for solutions on the Oracle of Google before such undertakings, and didn’t find anything quite like I wanted, I’m adding this to the mix.

My first problem was trying to convert the 12V AC I had metered in the bell box to something appropriate in DC. I looked at the components to build my own circuit for this, but remarkably discovered I could buy something from eBay from China for £1.60 (including postage) that performed the AC to DC conversion with a pot to allow the voltage to be calibrated. I will leave you to search for your own.

The AC to DC Converter
The AC to DC Converter

I then bought a pretty cheap wireless bell from Amazon. It cost £12.40 for a bell that would plug directly into the mains (so no battery), so this Kingavon wireless door bell did the job.

Wireless Bell
The Kingavon wireless bell. Nice and cheap, reasonable feature set.

The bell push normally takes a 3 V battery. My plan was to supply that voltage directly from the AC to DC converter to avoid the need for a battery at that end too. If I could rig up the 12 V AC that was produced in the wired bell box when the push was pressed this might work. So I took the bell and push into work and finally in a free 30 minutes grabbed two colleages and headed to the lab.

We connected the AC to DC converter to 12 V AC (as it would be in the bell box) and calibrated the pot until we had a 3 V DC output.

The micro-switch in the doorbell push was soldered closed (so that in effect the button was always pressed down). Finally the DC output was soldered to the battery pins on the door push.

Soldered Push Button
Note the short circuit across the micro switch (just to its left).

In bench tests we then tried turning on the supply to see how quickly the wireless doorbell push came up and activated the bell. As I suspected if the power was “jabbed” on there wasn’t enough time for the transmitted circuitry to get its act together, but when “pressed” for say just under a second (simulated by supplying the AC) the wireess bell went off. I figured this was the downside of going to a battery-less solution, but given that I had guests trying to ring the bell repeatedly (since they knew I was in) I figured this would work.

So I used a multi-meter to work out which terminals in the bell box went to 12 V when the bell push was pressed, hooked up the leads into the AC to DC converter and bingo, the set-up works. At the time of writing I still consider it to be in testing so the components are literally wrapped around the bell box, but since the box doesn’t contain batteries, there will be room to place them in later, once i get them properly insulated – and I get a new glue stick for my glue gun to fasten them in.

(Almost) final wiring
Wired up inside the box, but still trailing during extended testing.

So this approach works, but it won’t be for everyone – the wireless bell can fail to go off if someone really jabs the outdoor bell push, but for us, it’s a workable solution for £14 and the cost of some wire.