From 3dc2beda4104b220efefed37d745c78e0c58853c Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Wed, 6 May 2015 22:01:00 +0200 Subject: [PATCH] MetaSync: add iTunes synchronization Uses plistlib to read a temp copy of `iTunes Library.xml` --- beetsplug/metasync/__init__.py | 10 ++++- beetsplug/metasync/amarok.py | 2 +- beetsplug/metasync/itunes.py | 80 ++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 beetsplug/metasync/itunes.py diff --git a/beetsplug/metasync/__init__.py b/beetsplug/metasync/__init__.py index 743bc8277..b02f43c28 100644 --- a/beetsplug/metasync/__init__.py +++ b/beetsplug/metasync/__init__.py @@ -34,7 +34,13 @@ class MetaSyncPlugin(BeetsPlugin): 'amarok_uid': types.STRING, 'amarok_playcount': types.INTEGER, 'amarok_firstplayed': DateType(), - 'amarok_lastplayed': DateType() + 'amarok_lastplayed': DateType(), + + 'itunes_rating': types.INTEGER, # 0..100 scale + 'itunes_playcount': types.INTEGER, + 'itunes_skipcount': types.INTEGER, + 'itunes_lastplayed': DateType(), + 'itunes_lastskipped': DateType(), } def __init__(self): @@ -74,7 +80,7 @@ class MetaSyncPlugin(BeetsPlugin): for entry in classes: if entry[0].lower() == player: - sources[player] = entry[1]() + sources[player] = entry[1](self.config) else: continue diff --git a/beetsplug/metasync/amarok.py b/beetsplug/metasync/amarok.py index 3ffeaeac9..f3ecb5ffb 100644 --- a/beetsplug/metasync/amarok.py +++ b/beetsplug/metasync/amarok.py @@ -31,7 +31,7 @@ class Amarok(object): \ ' - def __init__(self): + def __init__(self, config=None): self.collection = \ dbus.SessionBus().get_object('org.kde.amarok', '/Collection') diff --git a/beetsplug/metasync/itunes.py b/beetsplug/metasync/itunes.py new file mode 100644 index 000000000..9c3b670f7 --- /dev/null +++ b/beetsplug/metasync/itunes.py @@ -0,0 +1,80 @@ +# This file is part of beets. +# Copyright 2015, Tom Jaspers. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Synchronize information from iTunes's library +""" +from contextlib import contextmanager +import os +import shutil +import tempfile +from time import mktime + +import plistlib +from beets import util +from beets.util.confit import ConfigValueError + + +@contextmanager +def create_temporary_copy(path): + temp_dir = tempfile.gettempdir() + temp_path = os.path.join(temp_dir, 'temp_itunes_lib') + shutil.copyfile(path, temp_path) + try: + yield temp_path + finally: + shutil.rmtree(temp_dir) + + +class ITunes(object): + + def __init__(self, config=None): + # Load the iTunes library, which has to be the .xml one + library_path = util.normpath(config['itunes']['library'].get(str)) + + try: + with create_temporary_copy(library_path) as library_copy: + raw_library = plistlib.readPlist(library_copy) + except IOError as e: + raise ConfigValueError("invalid iTunes library: " + e.strerror) + except: + # TODO: Tell user to make sure it is the .xml one? + raise ConfigValueError("invalid iTunes library") + + # Convert the library in to something we can query more easily + self.collection = { + (track['Name'], track['Album'], track['Album Artist']): track + for track in raw_library['Tracks'].values()} + + def get_data(self, item): + key = (item.title, item.album, item.albumartist) + result = self.collection.get(key) + + # TODO: Need to investigate behavior for items without title, album, or + # albumartist before allowing them to be queried + if not all(key) or not result: + # TODO: Need to log something here later + # print "No iTunes match found for {0}".format(item) + return + + item.itunes_rating = result.get('Rating') + item.itunes_playcount = result.get('Play Count') + item.itunes_skipcount = result.get('Skip Count') + + if result.get('Play Date UTC'): + item.itunes_lastplayed = mktime( + result.get('Play Date UTC').timetuple()) + + if result.get('Skip Date'): + item.itunes_lastskipped = mktime( + result.get('Skip Date').timetuple())