ITunes Playlists Importieren

Aus Synology Wiki
Wechseln zu: Navigation, Suche

Oft will man iTunes Musik und Playlists auf die Synology kopieren und von der Synology auch abspielen. Zahlreiche Foreneinträge fragen nach dieser Möglichkeit. Da im Englischen Forum nicht so leicht zu editieren, trage ichs mal hier ein.

Ziel

Voraussetzung ist, dass die Musiksammlung primär in iTunes organisiert wird und regelmäßig (etwa per rsync) auf die Synology kopiert wird um sie von dort weiter per upnp oder ähnlichem zu verbreiten. Das Problem das dabei auftritt, ist, dass es nicht so einfach ist, die wohlgewarteten Playlists auf upnp zu übernehmen.

Achtung:

  1. nur für Mutige
  2. die bereits ipkg installiert haben
  3. ein wenig shell-scripten können
  4. und ihre eigenen Playlisten aus Audio Station vorher gebackuped haben

Ziel des Setup ist es, den Standardpaketen - insbesondere "Audio Station" - die Playlisten als m3u unterzujubeln. WARNUNG: die bestehenden "shared" playlists werden automatisch gelöscht

Es funktioniert durch ein Python Skript, das aus der iTunes XML Datei eine Vielzahl M3Us macht. Die playlists werden nur geändert, wenn sich das Datum der iTunes XML Datei ändert.

Voraussetzungen

  1. ipkg - siehe [IPKG]
  2. python - dank ipkg per 'ipkg install python' leicht machbar
  3. mein selbstgestricktes shell script 'updateplaylists.sh' (siehe unten)
  4. ein nützliches Python script um das iTunes XML in M3Us umzuwandeln: 'itunes2m3u.py' (siehe unten)

Die iTunes Musiklibrary muss auf der DiskStation gespeichert sein. Genauso muss die 'iTunes Music Library.xml' vorhanden sein. Die ist in Windows übrigens in "Eigene Musik\iTunes" zu finden.

Installation

  1. updateplaylists.sh in einen folder kopieren. Ich schlage /opt/sbin vor, aber das kann man beliebig wählen
  2. itunes2m3u.py in einen folder kopieren. Wieder ist /opt/sbin ein guter ort
  3. in updateplaylists.sh alle Konfigurationsvariablen ändern. Absolute Pfade in der Konfiguration ftw.

Kurzes Gebet, danach testen:

myserver>./updateplaylists.sh
cat: updateplaylists_oldlibrarydate: No such file or directory
rm: cannot remove m3u, not found
Parsing /volume1/music/iTunes/iTunesMusic/iTunes Music Library.xml... done

Die zwei Meldungen sind mit dem cat und rm sind OK. War ich zu faul, es schöner zu machen. Nach dem Durchlauf sollten die Playlists generiert worden sein. Wenn sie nicht funktionieren - einfach in die Listen hineinschauen, man vertippt sich leicht mit den Pfadangaben.

Jetzt noch in /etc/crontab eine Zeile hinzufügen. Das Skript macht nur was, wenn sich das Datum der iTunes XML Datei ändert, kann also ruhig alle 2 Stunden laufen.

Skripte

Da dieses wiki keine sh/py Anhänge erlaubt, die Skripte der Einfachheit halber hier:

updateplaylists.sh

#!/bin/ash

# CONFIGURE THIS:
# location of the xml file
LIBRARYFILE="/volume1/music/iTunes/iTunesMusic/iTunes Music Library.xml"
# location of the MP3s. This folder must have the many many subfolders iTunes manages
LIBRARYPATH="/volume1/music/iTunes/iTunesMusic/"
# location of itunes2m3u.py
ITUNES2M3U="/volume1/homes/leobard/bin/itunes2m3u.py"
# destination folder of the m3u. 
# WARNING: ALL PLAYLISTS IN THIS FOLDER WILL BE DELETED!!!
PLAYLISTFOLDER="/volume1/music/playlists"



cd $PLAYLISTFOLDER

# check changedate, I keep a copy of the last stamp somewhere
OLDDATE=`cat updateplaylists_oldlibrarydate`
CURDATE=`stat -c %Y "$LIBRARYFILE"`

# is the old filedate different from the new one?
if [ "$OLDDATE" != "$CURDATE" ]; then
 # remove the playlists, just to be sure
 rm *.m3u
 # recreate them from itunes
 python $ITUNES2M3U -d "$LIBRARYPATH" "$LIBRARYFILE"

 # CONFIGURE THIS
 # remove the big ones I don't need anyway to speed up updating them in the database in mediatomb database
 rm Mediathek.m3u
 rm Musik.m3u
 rm radio-base.m3u

 # keep the new changedate, also marking that this run was successful
 stat -c %Y "$LIBRARYFILE" > updateplaylists_oldlibrarydate
 echo "updated all playlists." 
else
 #  no changes
 exit 0
fi

itunes2m3u.py


#!/usr/bin/python
#
# Converts an Apple iTunes Music Library.xml file into a set of .m3u
# playlists.
#
# Copyright (C) 2006 Mark Huang <mark.l.huang@gmail.com>
# License is GPL
#
# $Id: itunes2m3u.py,v 1.1 2006/07/05 05:24:42 mlhuang Exp $
#

import os
import sys
import getopt
import xml.sax.handler
import pprint
import base64
import datetime
import urlparse
import codecs
import array
import re

# Defaults

# Encoding of track and file names in .m3u files. Escaped 8-bit
# character codes in URIs are NOT encoded, they are written in raw
# binary mode.
encoding = "utf-8"

class PropertyList(xml.sax.handler.ContentHandler):
    """
    Parses an Apple iTunes Music Library.xml file into a
    dictionary.
    """
    
    def __init__(self, file = None):
        # Root object
        self.plist = None
        # Names of parent elements
        self.parents = [None]
        # Dicts can be nested
        self.dicts = []
        # So can arrays
        self.arrays = []
        # Since dicts can be nested, we have to keep a queue of the
        # current outstanding keys whose values we have yet to set.
        self.keys = []
        # Accumulated CDATA
        self.cdata = ""

        # Open file
        if type(file) == str:
            file = open(file, 'r')

        # Parse it
        parser = xml.sax.make_parser()
        parser.setContentHandler(self)
        parser.parse(file)

    def __str__(self):
        return pprint.pformat(self.plist)

    def __getitem__(self, name):
        if type(self.plist) == dict:
            return self.plist[name]
        else:
            return self.plist

    def startElement(self, name, attrs):
        if name == "dict":
            self.dicts.append({})
        elif name == "array":
            self.arrays.append([])
        else:
            self.cdata = ""

        self.parents.append(name)

    def endElement(self, name):
        last = self.parents.pop()
        assert last == name

        value = None

        if name == "dict":
            value = self.dicts.pop()
        elif name == "key":
            if self.keys and self.keys[-1] == "Tracks":
                # Convert track keys to integer
                self.keys.append(int(self.cdata.strip()))
            else:
                self.keys.append(self.cdata.strip())
        elif name == "array":
            value = self.arrays.pop()
        elif name == "data":
            # Contents interpreted as Base-64 encoded
            value = base64.b64decode(self.cdata.strip())
        elif name == "date":
            # Contents should conform to a subset of ISO 8601 (in
            # particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'.
            # Smaller units may be omitted with a loss of precision)
            year = month = day = hour = minutes = seconds = 0
            try:
                (date, time) = self.cdata.strip().split('T')
                parts = date.split('-')
                if len(parts) >= 1:
                    year = int(parts[0])
                    if len(parts) >= 2:
                        month = int(parts[1])
                        if len(parts) >= 3:
                            day = int(parts[2])
                time = time.replace('Z', '')
                parts = time.split(':')
                if len(parts) >= 1:
                    hour = int(parts[0])
                    if len(parts) >= 2:
                        minutes = int(parts[1])
                        if len(parts) >= 3:
                            seconds = int(parts[2])
            except:
                pass
            value = datetime.datetime(year, month, day, hour, minutes, seconds)
        elif name == "real":
            # Contents should represent a floating point number
            # matching ("+" | "-")? d+ ("."d*)? ("E" ("+" | "-") d+)?
            # where d is a digit 0-9.
            value = float(self.cdata.strip())
        elif name == "integer":
            # Contents should represent a (possibly signed) integer
            # number in base 10
            value = int(self.cdata.strip())
        elif name == "string":
            value = self.cdata.strip()
        elif name == "true":
            # Boolean constant true
            value = True
        elif name == "false":
            # Boolean constant false
            value = False

        if self.parents[-1] == "plist":
            self.plist = value
        elif self.parents[-1] == "dict" and name != "key":
            if self.dicts and self.keys:
                key = self.keys.pop()
                self.dicts[-1][key] = value
        elif self.parents[-1] == "array":
            if self.arrays:
                self.arrays[-1].append(value)

    def characters(self, content):
        self.cdata += content

def writeurl(s, fileobj, encoding = "utf-8"):
    """
    Write a URI to the specified file object using the specified
    encoding. Escaped 8-bit character codes in URIs are NOT encoded,
    they are written in raw binary mode.
    """

    skip = 0
    for i, c in enumerate(s):
        if skip:
            skip -= 1
            continue
        if c == '%':
            # Write 8-bit ASCII character codes in raw binary mode
            try:
                a = array.array('B', [int(s[i+1:i+3], 16)])
                a.tofile(fileobj)
                skip = 2
                continue
            except IndexError:
                pass
            except ValueError:
                pass
        # Write everything else in the specified encoding
        fileobj.write(c.encode(encoding))

def usage():
    print """
Usage: %s [OPTION]... [FILE]

Options:

-e, --encoding=ENCODING  Use specified encoding for track and file names (default: %s)
-d, --directory=DIR      Replace path to Music Library with specified path
-h, --help               This message
""".lstrip() % (sys.argv[0], encoding)
    sys.exit(1)

def main():
    global encoding
    directory = None

    if len(sys.argv) < 1:
        usage()

    try:
        (opts, argv) = getopt.getopt(sys.argv[1:], "e:d:h", ["encoding=", "directory=", "help"])
    except getopt.GetoptError, e:
        print "Error: " + e.msg
        usage()

    for (opt, optval) in opts:
        if opt == "-e" or opt == "--encoding":
            encoding = optval
        if opt == "-d" or opt == "--directory":
            directory = optval
        else:
            usage()

    print "Parsing " + argv[0] + "...",
    sys.stdout.flush()
    plist = PropertyList(argv[0])
    print "done"

    (scheme, netloc, music_folder_path, params, query, fragment) = \
             urlparse.urlparse(plist['Music Folder'])

    for playlist in plist['Playlists']:
        if not playlist.has_key('Playlist Items'):
            continue

        try:
            filename = playlist['Name'] + ".m3u"
            m3u = open(filename, mode = "wb")
            m3u.write("#EXTM3U" + os.linesep)
        except:
            # Try to continue
            continue

        tracks = 0
        for item in playlist['Playlist Items']:
            try:
                track = plist['Tracks'][item['Track ID']]
                seconds = track['Total Time'] / 1000
                m3u.write("#EXTINF:" + "%d" % seconds + ",")
                m3u.write(track['Name'].encode(encoding))
                m3u.write(os.linesep)
                (scheme, netloc, path, params, query, fragment) = \
                         urlparse.urlparse(track['Location'])
                if directory is not None:
                    path = path.replace(music_folder_path, directory)
                writeurl(path, m3u, encoding)
                m3u.write(os.linesep)

                tracks += 1
                print filename + ": %d tracks\r" % tracks,
            except:
                # Try to continue
                continue

        print

        m3u.close()

if __name__ == "__main__":
    main()