iTunes Wiedergabeliste synchronisieren

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
nemomuk
User
Beiträge: 862
Registriert: Dienstag 6. November 2007, 21:49

Hi,

habe hier ein Script geschrieben mit dem man eine iTunes Wiedergabeliste mit einem beliebigen Ordner synchronisieren kann.
Hintergrund ist, dass es für Mac (auch nach längerer Recherche) kein Skript gibt, das meine Bedürfnisse erfüllt:
* schnelle Synchronisation
* gute Ordnerstruktur (Interpret/Album/Song)
* Löschen von Songs, die nicht mehr in der Playlist sind und auch Löschen von leeren Ordnern
* Hauptgrund: iTunes erkennt mein Nexus One nicht...

Sollte unter Windows genauso funktionieren.

Wer Vorschläge hat, wie ich das Unicode-Problem eleganter lösen kann, darf sich gerne melden ;-)

Benutzung:

Code: Alles auswählen

python itunes_playlist_sync.py -p "Nexus One" -d "/Volumes/NEXUS ONE/Music/" -l "/Users/MyUser/Music/iTunes/iTunes Music Library.xml"
Skript-Verlauf: http://paste.pocoo.org/tree/305764/
Zuletzt geändert von nemomuk am Samstag 18. Dezember 2010, 14:49, insgesamt 11-mal geändert.
OverNord
User
Beiträge: 72
Registriert: Donnerstag 24. Januar 2008, 11:59
Kontaktdaten:

Hi,

das erste was mir auffällt ist Zeile 38: Warum nicht ``os.path.basename`` verwenden, für solche Aufgaben ist es doch da. Außerdem würde es dann auch unter Windows funktionieren.

Was mir sonst noch auffällt, iTunesSync.parse_tracks: die ganzen if-Abfragen könnte man wunderbar mit einem dict ersetzen.

Und zum Schluss, warum sehe ich dort so viele for-Schleifen in anderen for-Schleifen? Sowas muss doch nun wirklich nicht sein.

Das ist nur das, was mir beim Überfliegen des Codes aufgefallen ist.
nemomuk
User
Beiträge: 862
Registriert: Dienstag 6. November 2007, 21:49

das erste was mir auffällt ist Zeile 38: Warum nicht ``os.path.basename`` verwenden, für solche Aufgaben ist es doch da. Außerdem würde es dann auch unter Windows funktionieren.
Danke.
Was mir sonst noch auffällt, iTunesSync.parse_tracks: die ganzen if-Abfragen könnte man wunderbar mit einem dict ersetzen.
Bei ganzen 4 Fallunterscheidungen wird es nicht kürzer und dazu noch schwerer nachzuvollziehen.
Und zum Schluss, warum sehe ich dort so viele for-Schleifen in anderen for-Schleifen? Sowas muss doch nun wirklich nicht sein.
Diese Aufgabe lässt sich nur ohne eine verschachtelte Iteration nicht lösen, da ich die Struktur der XML iTunes Library nicht ändern kann und den Ordneraufbau genbau so haben will.
BlackJack

@ahojnnes: Das man bei einer komplexen Datenstruktur auch die entsprechende Komplexität irgendwie in Quelltext abbilden muss, ist ja klar, aber das kann man ja aufteilen.

`sync_dir()` ist für meinen Geschmack zum Beispiel zu lang und zu tief verschachtelt. Da ist auch ein Muster in den drei Verschachtelungsebenen zu erkennen, das man vielleicht herausziehen kann.

Ich kenne das XML-Format nicht, aber die Tags scheinen allgemeine Datenstrukturen zu sein. Kann man eventuell etwas gewinnen wenn man das XML am Anfang einmal in eine verschachtelte Datenstruktur aus Dictionaries und Listen umwandelt und da dann die Informationen heraus zieht?

In `parse_playlist()` scheint `playlist` unbenutzt zu sein.

Man kann den Optionen beim `OptionParser` auch einen Defaultwert mitgeben. Dann braucht man später nicht testen ob es dort einen Wert gab und den eventuell ersetzen. Also konkret kann man `options.quiet` schon beim Hinzufügen der Option auf `False` setzen.

Das Sync-Objekt vermischt mit seiner Interaktion mit dem Benutzer Programmlogik und UI. Ausserdem ist das `sync_aborted` bei der Struktur überflüssig wenn man bei einem Nein vom Benutzer einfach ``return``\ed.
nemomuk
User
Beiträge: 862
Registriert: Dienstag 6. November 2007, 21:49

@ahojnnes: Das man bei einer komplexen Datenstruktur auch die entsprechende Komplexität irgendwie in Quelltext abbilden muss, ist ja klar, aber das kann man ja aufteilen.

Ich kenne das XML-Format nicht, aber die Tags scheinen allgemeine Datenstrukturen zu sein. Kann man eventuell etwas gewinnen wenn man das XML am Anfang einmal in eine verschachtelte Datenstruktur aus Dictionaries und Listen umwandelt und da dann die Informationen heraus zieht
1. Kann nicht erkennen, wo der Vorteil sein soll, wenn ich die XML-Datei zuvor in eine dict/list-Form bringe und danach nochmal meine die Infos aus dieser Datenstruktur ziehen muss. Wiederverwendbarkeit ist da auch nicht gegeben, da parse_tracks und parse_playlist verschiedene Infos auslesen. Vllt. kannst du mir andere Vorteile nennen?
2. Sehe ich bei einer Verschachtelungstiefe von 3 Ebenen (nur for-Schleifen) keinen Grund, das in extra Funktionen/Methoden auszulagern - wahrscheinlich Geschmackssache.
`sync_dir()` ist für meinen Geschmack zum Beispiel zu lang und zu tief verschachtelt. Da ist auch ein Muster in den drei Verschachtelungsebenen zu erkennen, das man vielleicht herausziehen kann.
Habe mir durchaus überlegt da ein Muster rauszuziehen, was allerdings aufgrund der if-Abfragen, shutil.rmtree/os.remove und der verschiedenen Ausgabe komplizierter wird als die jetzige Version - lasse mich da aber gerne vom Gegenteil überzeugen.
In `parse_playlist()` scheint `playlist` unbenutzt zu sein.

Man kann den Optionen beim `OptionParser` auch einen Defaultwert mitgeben. Dann braucht man später nicht testen ob es dort einen Wert gab und den eventuell ersetzen. Also konkret kann man `options.quiet` schon beim Hinzufügen der Option auf `False` setzen.

Das Sync-Objekt vermischt mit seiner Interaktion mit dem Benutzer Programmlogik und UI. Ausserdem ist das `sync_aborted` bei der Struktur überflüssig wenn man bei einem Nein vom Benutzer einfach ``return``\ed.
Alles richtig, die run-Methode ziehe ich wohl besser aus der Klasse raus.

Oben die aktualisierte Version.
OverNord
User
Beiträge: 72
Registriert: Donnerstag 24. Januar 2008, 11:59
Kontaktdaten:

Soweit ich weiß nutzt iTunes Plist und Python liefert da auch was mit.
nemomuk
User
Beiträge: 862
Registriert: Dienstag 6. November 2007, 21:49

OverNord hat geschrieben:Soweit ich weiß nutzt iTunes Plist und Python liefert da auch was mit.
jop, das erleichtert das parsen natürlich ungemein, danke!
BlackJack

@ahojnnes: Der Vorteil wäre für mich, dass es diese hässliche Parserei von dem XML nur einmal gäbe und man sich danach komplett in "Python-Land" befindet. Dieses Umwandeln in Listen und mittels Index auf das nächste Element nach dem was eine bestimmte Bedingung erfüllt, finde ich jedenfalls ziemlich gruselig. Das sind ja, wenn ich das richtig interpretiere Dictionaries mit `key`-Tags und `value`-Tags (die Du nicht als solche sondern eben über den hässlichen Index-Offset ansprichst). Widerverwendbarkeit ist IMHO schon gegeben, denn das Parsen der verschiedenen Teilbäume kann ja die gleiche Funktion erledigen. Wenn es so ein `parse_node()` gäbe was einen übergebenen XML-Knoten in eine Python-Datenstruktur übrtführt, dann könnte die `parse_playlist()` zum Beispiel so aussehen (ungetestet):

Code: Alles auswählen

    def parse_playlist(self):
        for playlists in imap(parse_node, self.etree.findall('dict/array/dict')):
            for track in playlists.get(self.playlist, []):
                if 'Track ID' in track:
                    self.playlist_tracks.append(track['Track ID'])
IMHO sieht die so viel aufgeräumter und einfacher lesbar aus. Und wenn man `plistlib` benutzt, braucht man ja noch nicht mal ein `parse_node()` selber schreiben.

Drei ``for``-Schleifen gehen ja noch, aber in der `sync_dir()` muss man in der tiefsten Ebene ganze sieben Entscheidungen im Kopf haben wenn man wissen will, unter welchen Umständen man zu dem jeweiligen Punkt im Programmablauf kommt. Ist für mein armes Hirn jedenfalls etwas viel.

Die `sync_dir()` würde ich zuerst einmal in zwei Methoden aufteilen. Und die Verschachtelung im zweiten Teil könnte man durch Rekursion wegbekommen, denke ich. Ungetestet:

Code: Alles auswählen

     def _create_and_copy(self):
        artist_paths = set()
        album_paths = set()
        track_paths = set()
        for track in self.playlist_tracks:
            #: save and normalize (unicode comparison) paths to delete
            #: needless files afterwards
            artist_path = os.path.join(self.directory, track.artist)
            artist_path = ud.normalize('NFC', artist_path)
            artist_paths.add(artist_path)
            album_path = os.path.join(artist_path, track.album)
            album_path = ud.normalize('NFC', album_path)
            album_paths.add(album_path)
            track_path = os.path.join(album_path, track.filename)
            track_path = ud.normalize('NFC', track_path)
            track_paths.add(track_path)
            for path in [artist_path, album_path]:
                try:
                    os.mkdir(path)
                except OSError:
                    pass    # Intentionally ignored.
            if (
                os.path.exists(album_path)
                and os.path.exists(track.path)
                and not os.path.exists(track_path)
            ):
                self.verbose_print('--> Copying %s/%s/%s' % (track.artist,
                                                             track.album,
                                                             track.filename))
                shutil.copy(track.path, track_path)
        return artist_paths, album_paths, track_paths
    
    def _delete(self, base_path, paths_seq, sub_paths=()):
        if not paths_seq:
            return
        paths = paths_seq[0]
        for path in os.listdir(base_path):
            full_path = os.path.join(basepath, path)
            if ud.normalize('NFC', full_path) not in paths:
                try:
                    if os.path.isdir(full_path):
                        shutil.rmtree(full_path)
                    else:
                        os.remove(full_path)
                except OSError:
                    pass    # Intentionally ignored.
                else:
                    self.verbose_print(
                        '--> Deleted %s' % '/'.join(sub_paths + (path,))
                    )
            else:
                self._delete(full_path, paths_seq[1:], sub_paths + (path,))
    
    def sync_dir(self):
        self._delete(unicode(self.directory, 'UTF-8'), self._create_and_copy())
Ich habe da nicht tiefer drüber nachgedacht, aber sind wirklich drei verschiedene `set`\s für die Pfade nötig? Überschneidungen der Werte kann es ja eigentlich nicht geben, denn jede Ebene hat garantiert andere Pfade als die jeweils anderen Ebenen, oder!?
nemomuk
User
Beiträge: 862
Registriert: Dienstag 6. November 2007, 21:49

So, anscheinend hab ich da gestern auf die Schnelle ziemlich schlechten Code produziert - ich schiebs jetzt mal auf die Uhrzeit :roll:

Hab alles nochmal überarbeitet und oben das aktualisierte Ergebnis eingefügt - os.walk tuts nach genauerem Hinschauen auch, da es im Endeffekt hierarchisch von oben nach unten geht. Das Ganze ist zwar jetzt um einiges langsamer, was wohl auf plistlib zurückzuführen ist, aber noch in ertragbarem Maß.

Ich bedanke mich für das Feedback!
nemomuk
User
Beiträge: 862
Registriert: Dienstag 6. November 2007, 21:49

Jetzt auch Windows-kompatibel.
lunar

@ahojnnes: Nicht direkt zum Thema:

Ich weiß nicht, wie es den anderen geht, doch ich für meinen Teil möchte Dich darum bitten, solche Skripte in Zukunft in einem Pastebin wie http://paste.pocoo.org zu veröffentlichen, und hier nur das entsprechende Paste zu verlinken. Ich persönlich finde Beiträge, in denen so lange Quelltextstücke auftauchen, sind schwerer zu lesen. Zudem kann man das Skript im Pastebin direkt herunterladen, während man den Quelltext hier im Forum umständlich kopieren und manuell in eine Datei einfügen muss.

Auch fände ich es nett, wenn Du im Laufe der Diskussion um den Quelltext nicht den ursprünglichen Quelltext verändern würdest, sondern stattdessen lediglich einen Link zur aktuellen Variante zu Deinem Beitrag hinzufügen würdest (falls Du das oben genannte Pastebin verwendest, kannst Du auf Dein eigenes, vorheriges Paste antworten, dann kann man sich sogar hübsche Diffs anzeigen lassen). In dieser Diskussion ist nämlich der Kontext der ersten Antworten nun einfach weg, man kann nicht mehr nachvollziehen, was die ersten paar Antworten kritisiert haben, und wie Du daraufhin den Quelltext geändert hast.
nemomuk
User
Beiträge: 862
Registriert: Dienstag 6. November 2007, 21:49

Hast wohl in beiden Punkten Recht, letzteres lässt sich leider nicht mehr ändern.
nemomuk
User
Beiträge: 862
Registriert: Dienstag 6. November 2007, 21:49

Skript upgedated (siehe erster Post): Es wird nun zuerst gelöscht und dann kopiert, damit bei großen Änderungen der Speicher nicht ausgeht.
Antworten