-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Changes from 9 commits
3dc2bed
04d7c88
27aef76
de5db70
cb13d21
bba8647
abd0205
afeedd2
16531b6
02bec1b
94edc7a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
# 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 | ||
import plistlib | ||
from time import mktime | ||
|
||
from beets import util | ||
from beets.dbcore import types | ||
from beets.library import DateType | ||
from beets.util.confit import ConfigValueError | ||
from beetsplug.metasync import MetaSource | ||
|
||
|
||
@contextmanager | ||
def create_temporary_copy(path): | ||
temp_dir = tempfile.mkdtemp() | ||
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(MetaSource): | ||
|
||
item_types = { | ||
'itunes_rating': types.INTEGER, # 0..100 scale | ||
'itunes_playcount': types.INTEGER, | ||
'itunes_skipcount': types.INTEGER, | ||
'itunes_lastplayed': DateType(), | ||
'itunes_lastskipped': DateType(), | ||
} | ||
|
||
def __init__(self, config, log): | ||
super(Itunes, self).__init__(config, log) | ||
|
||
# Load the iTunes library, which has to be the .xml one (not the .itl) | ||
library_path = util.normpath(config['itunes']['library'].get(str)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Has the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops! Sorry; I was freehanding. I meant |
||
|
||
try: | ||
self._log.debug( | ||
u'loading iTunes library from {0}'.format(library_path)) | ||
with create_temporary_copy(library_path) as library_copy: | ||
raw_library = plistlib.readPlist(library_copy) | ||
except IOError as e: | ||
raise ConfigValueError(u'invalid iTunes library: ' + e.strerror) | ||
except Exception: | ||
# It's likely the user configured their '.itl' library (<> xml) | ||
if os.path.splitext(library_path)[1].lower() != '.xml': | ||
hint = u': please ensure that the configured path' \ | ||
u' points to the .XML library' | ||
else: | ||
hint = '' | ||
raise ConfigValueError(u'invalid iTunes library' + hint) | ||
|
||
# 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()} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: And in beets: 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 I may be able to find my Windows VM somewhere to do some testing. |
||
|
||
def sync_from_source(self, item): | ||
key = (item.title, item.album, item.albumartist) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
||
if not all(key) or not result: | ||
self._log.warning(u'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()) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 comma-separated 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.:: | ||
|
||
metasync: | ||
source: itunes | ||
itunes: | ||
library: ~/Music/iTunes Library.xml | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
----- | ||
|
@@ -44,5 +59,5 @@ The command has a few command-line options: | |
|
||
* To preview the changes that would be made without applying them, use the | ||
``-p`` (``--pretend``) flag. | ||
* To specify a temporary source to fetch metadata from, use the ``-s`` | ||
(``--source``) flag. | ||
* To specify temporary sources to fetch metadata from, use the ``-s`` | ||
(``--source``) flag with a comma-separated list of a sources. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is some very clever work to get auto-discovery in order! It could be, though, that it would be easier just to hard-code the list of modules to import and pull classes out of—this sort of
pkgutil
andinspect
magic could make this harder to maintain in the future. (We already run into some amount of trouble with testing, for example, when it interacts with similar magic in the beets plugin loading system.)So I'll advocate gently for the "dumb" solution here: just redundantly writing down the list of available backends rather than automatically discovering them. But if anyone thinks this is too nifty to throw away, I could be convinced. 😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I should mention I realize that you didn't invent the magic here; it's just clearer this time, so it's the first time I thought about it carefully.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose you are right, considering there won't be that many sources.
Think I was just having some fun writing the auto-discovery, and sort of forgot the whole "Simple is better than complex" 😃