Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MetaSync: more OO structure + iTunes support #1450

Merged
merged 11 commits into from
May 13, 2015
10 changes: 8 additions & 2 deletions beetsplug/metasync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -74,7 +80,7 @@ def func(self, lib, opts, args):

for entry in classes:
if entry[0].lower() == player:
sources[player] = entry[1]()
sources[player] = entry[1](self.config)
else:
continue

Expand Down
2 changes: 1 addition & 1 deletion beetsplug/metasync/amarok.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class Amarok(object):
</filters> \
</query>'

def __init__(self):
def __init__(self, config=None):
self.collection = \
dbus.SessionBus().get_object('org.kde.amarok', '/Collection')

Expand Down
80 changes: 80 additions & 0 deletions beetsplug/metasync/itunes.py
Original file line number Diff line number Diff line change
@@ -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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think .as_path() instead of .get(str) will obviate the need for the normpath.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has the as_path() method maybe been renamed or removed? I can't seem to find it anywhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops! Sorry; I was freehanding. I meant as_filename: https://github.com/sampsyo/beets/blob/master/beets/util/confit.py#L378


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()}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this is looking up tracks by their metadata. Would it be possible/simpler to use their paths instead? Then we wouldn't run into any weird aliasing problems.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my original intention (and how I would prefer) it, but iTunes saves path in Unix style on all systems. This is conflicting with the way paths are saved in beets for Windows.

In Itunes Library.xml:
file://localhost/G:/Experiments/Alt-J/An%20Awesome%20Wave/01%20Intro.mp3

And in beets:
G:\Experiments\Alt-J\An Awesome Wave\01 Intro.mp3

I'm gonna try to resort to converting the iTunes path to match beets' style. Will take a while to test it properly on Windows system though, but using a path is probably better in the long run.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha; that's really tricky. Yes, maybe converting iTunes's path on load is the way to go (perhaps just a normpath would do it?).

I may be able to find my Windows VM somewhere to do some testing.


def get_data(self, item):
key = (item.title, item.album, item.albumartist)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny style thing: no parentheses are required on this tuple.

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())
6 changes: 6 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Changelog
1.3.14 (in development)
-----------------------

New features:

* The :doc:`/plugins/metasync` plugin now lets you get metadata from iTunes.
This plugin is still in an experimental phase.


Fixes:

* :doc:`/plugins/mpdstats`: Avoid a crash when the music played is not in the
Expand Down
21 changes: 18 additions & 3 deletions docs/plugins/metasync.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ MetaSync Plugin
This plugin provides the ``metasync`` command, which lets you fetch certain
metadata from other sources: for example, your favorite audio player.

Currently, the plugin supports synchronizing with the `Amarok`_ music player.
Currently, the plugin supports synchronizing with the `Amarok`_ music player,
and with `iTunes`_.
It can fetch the rating, score, first-played date, last-played date, play
count, and track uid from Amarok.

.. _Amarok: https://amarok.kde.org/
.. _iTunes: https://www.apple.com/itunes/


Installation
Expand All @@ -29,10 +31,23 @@ Configuration
To configure the plugin, make a ``metasync:`` section in your configuration
file. The available options are:

- **source**: A list of sources to fetch metadata from. Set this to "amarok"
to enable synchronization with that player.
- **source**: A list of sources to fetch metadata from. Set this to "amarok" or
"itunes" to enable synchronization with that player.
Default: empty

The follow subsections describe additional configure required for some players.

itunes
''''''

The path to your iTunes library **xml** file has to be configured, e.g.::

metaysnc:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metaysnc => metasync

source: itunes
itunes:
library: ~/Music/iTunes Library.xml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we provide a default location for the library XML? It seems like most users will probably have theirs here (at least on OS X).


Please note the indentation.

Usage
-----
Expand Down