mirror of
https://github.com/beetbox/beets.git
synced 2025-12-31 21:12:43 +01:00
Merge pull request #1450 from tomjaspers/metasync-itunes
MetaSync: more OO structure + iTunes support
This commit is contained in:
commit
71d7c0b004
8 changed files with 713 additions and 45 deletions
|
|
@ -14,28 +14,64 @@
|
|||
|
||||
"""Synchronize information from music player libraries
|
||||
"""
|
||||
from abc import abstractmethod, ABCMeta
|
||||
from importlib import import_module
|
||||
|
||||
from beets import ui, logging
|
||||
from beets.util.confit import ConfigValueError
|
||||
from beets import ui
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets.dbcore import types
|
||||
from beets.library import DateType
|
||||
from sys import modules
|
||||
import inspect
|
||||
|
||||
# Loggers.
|
||||
log = logging.getLogger('beets.metasync')
|
||||
|
||||
METASYNC_MODULE = 'beetsplug.metasync'
|
||||
|
||||
# Dictionary to map the MODULE and the CLASS NAME of meta sources
|
||||
SOURCES = {
|
||||
'amarok': 'Amarok',
|
||||
'itunes': 'Itunes',
|
||||
}
|
||||
|
||||
|
||||
class MetaSource(object):
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
def __init__(self, config, log):
|
||||
self.item_types = {}
|
||||
self.config = config
|
||||
self._log = log
|
||||
|
||||
@abstractmethod
|
||||
def sync_from_source(self, item):
|
||||
pass
|
||||
|
||||
|
||||
def load_meta_sources():
|
||||
""" Returns a dictionary of all the MetaSources
|
||||
E.g., {'itunes': Itunes} with isinstance(Itunes, MetaSource) true
|
||||
"""
|
||||
meta_sources = {}
|
||||
|
||||
for module_path, class_name in SOURCES.items():
|
||||
module = import_module(METASYNC_MODULE + '.' + module_path)
|
||||
meta_sources[class_name.lower()] = getattr(module, class_name)
|
||||
|
||||
return meta_sources
|
||||
|
||||
|
||||
META_SOURCES = load_meta_sources()
|
||||
|
||||
|
||||
def load_item_types():
|
||||
""" Returns a dictionary containing the item_types of all the MetaSources
|
||||
"""
|
||||
item_types = {}
|
||||
for meta_source in META_SOURCES.values():
|
||||
item_types.update(meta_source.item_types)
|
||||
return item_types
|
||||
|
||||
|
||||
class MetaSyncPlugin(BeetsPlugin):
|
||||
|
||||
item_types = {
|
||||
'amarok_rating': types.INTEGER,
|
||||
'amarok_score': types.FLOAT,
|
||||
'amarok_uid': types.STRING,
|
||||
'amarok_playcount': types.INTEGER,
|
||||
'amarok_firstplayed': DateType(),
|
||||
'amarok_lastplayed': DateType()
|
||||
}
|
||||
item_types = load_item_types()
|
||||
|
||||
def __init__(self):
|
||||
super(MetaSyncPlugin, self).__init__()
|
||||
|
|
@ -45,9 +81,9 @@ class MetaSyncPlugin(BeetsPlugin):
|
|||
help='update metadata from music player libraries')
|
||||
cmd.parser.add_option('-p', '--pretend', action='store_true',
|
||||
help='show all changes but do nothing')
|
||||
cmd.parser.add_option('-s', '--source', action='store_false',
|
||||
default=self.config['source'].as_str_seq(),
|
||||
help="select specific sources to import from")
|
||||
cmd.parser.add_option('-s', '--source', default=[],
|
||||
action='append', dest='sources',
|
||||
help='comma-separated list of sources to sync')
|
||||
cmd.parser.add_format_option()
|
||||
cmd.func = self.func
|
||||
return [cmd]
|
||||
|
|
@ -56,31 +92,43 @@ class MetaSyncPlugin(BeetsPlugin):
|
|||
"""Command handler for the metasync function.
|
||||
"""
|
||||
pretend = opts.pretend
|
||||
source = opts.source
|
||||
query = ui.decargs(args)
|
||||
|
||||
sources = {}
|
||||
sources = []
|
||||
for source in opts.sources:
|
||||
sources.extend(source.split(','))
|
||||
|
||||
for player in source:
|
||||
__import__('beetsplug.metasync', fromlist=[str(player)])
|
||||
sources = sources or self.config['source'].as_str_seq()
|
||||
|
||||
module = 'beetsplug.metasync.' + player
|
||||
meta_source_instances = {}
|
||||
items = lib.items(query)
|
||||
|
||||
if module not in modules.keys():
|
||||
log.error(u'Unknown metadata source \'' + player + '\'')
|
||||
continue
|
||||
# Avoid needlessly instantiating meta sources (can be expensive)
|
||||
if not items:
|
||||
self._log.info(u'No items found matching query')
|
||||
return
|
||||
|
||||
classes = inspect.getmembers(modules[module], inspect.isclass)
|
||||
# Instantiate the meta sources
|
||||
for player in sources:
|
||||
try:
|
||||
meta_source_instances[player] = \
|
||||
META_SOURCES[player](self.config, self._log)
|
||||
except KeyError:
|
||||
self._log.error(u'Unknown metadata source \'{0}\''.format(
|
||||
player))
|
||||
except (ImportError, ConfigValueError) as e:
|
||||
self._log.error(u'Failed to instantiate metadata source '
|
||||
u'\'{0}\': {1}'.format(player, e))
|
||||
|
||||
for entry in classes:
|
||||
if entry[0].lower() == player:
|
||||
sources[player] = entry[1]()
|
||||
else:
|
||||
continue
|
||||
# Avoid needlessly iterating over items
|
||||
if not meta_source_instances:
|
||||
self._log.error(u'No valid metadata sources found')
|
||||
return
|
||||
|
||||
for item in lib.items(query):
|
||||
for player in sources.values():
|
||||
player.get_data(item)
|
||||
# Sync the items with all of the meta sources
|
||||
for item in items:
|
||||
for meta_source in meta_source_instances.values():
|
||||
meta_source.sync_from_source(item)
|
||||
|
||||
changed = ui.show_model_changes(item)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,12 +18,33 @@
|
|||
from os.path import basename
|
||||
from datetime import datetime
|
||||
from time import mktime
|
||||
from beets.util import displayable_path
|
||||
from xml.sax.saxutils import escape
|
||||
import dbus
|
||||
|
||||
from beets.util import displayable_path
|
||||
from beets.dbcore import types
|
||||
from beets.library import DateType
|
||||
from beetsplug.metasync import MetaSource
|
||||
|
||||
|
||||
class Amarok(object):
|
||||
def import_dbus():
|
||||
try:
|
||||
return __import__('dbus')
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
dbus = import_dbus()
|
||||
|
||||
|
||||
class Amarok(MetaSource):
|
||||
|
||||
item_types = {
|
||||
'amarok_rating': types.INTEGER,
|
||||
'amarok_score': types.FLOAT,
|
||||
'amarok_uid': types.STRING,
|
||||
'amarok_playcount': types.INTEGER,
|
||||
'amarok_firstplayed': DateType(),
|
||||
'amarok_lastplayed': DateType(),
|
||||
}
|
||||
|
||||
queryXML = u'<query version="1.0"> \
|
||||
<filters> \
|
||||
|
|
@ -31,11 +52,16 @@ class Amarok(object):
|
|||
</filters> \
|
||||
</query>'
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, config, log):
|
||||
super(Amarok, self).__init__(config, log)
|
||||
|
||||
if not dbus:
|
||||
raise ImportError('failed to import dbus')
|
||||
|
||||
self.collection = \
|
||||
dbus.SessionBus().get_object('org.kde.amarok', '/Collection')
|
||||
|
||||
def get_data(self, item):
|
||||
def sync_from_source(self, item):
|
||||
path = displayable_path(item.path)
|
||||
|
||||
# amarok unfortunately doesn't allow searching for the full path, only
|
||||
|
|
|
|||
116
beetsplug/metasync/itunes.py
Normal file
116
beetsplug/metasync/itunes.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# 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
|
||||
import urllib
|
||||
from urlparse import urlparse
|
||||
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)
|
||||
|
||||
|
||||
def _norm_itunes_path(path):
|
||||
# Itunes prepends the location with 'file://' on posix systems,
|
||||
# and with 'file://localhost/' on Windows systems.
|
||||
# The actual path to the file is always saved as posix form
|
||||
# E.g., 'file://Users/Music/bar' or 'file://localhost/G:/Music/bar'
|
||||
|
||||
# The entire path will also be capitalized (e.g., '/Music/Alt-J')
|
||||
# Note that this means the path will always have a leading separator,
|
||||
# which is unwanted in the case of Windows systems.
|
||||
# E.g., '\\G:\\Music\\bar' needs to be stripped to 'G:\\Music\\bar'
|
||||
|
||||
return util.bytestring_path(os.path.normpath(
|
||||
urllib.unquote(urlparse(path).path)).lstrip('\\')).lower()
|
||||
|
||||
|
||||
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)
|
||||
|
||||
config.add({'itunes': {
|
||||
'library': '~/Music/iTunes/iTunes Library.xml'
|
||||
}})
|
||||
|
||||
# Load the iTunes library, which has to be the .xml one (not the .itl)
|
||||
library_path = config['itunes']['library'].as_filename()
|
||||
|
||||
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)
|
||||
|
||||
# Make the iTunes library queryable using the path
|
||||
self.collection = {_norm_itunes_path(track['Location']): track
|
||||
for track in raw_library['Tracks'].values()}
|
||||
|
||||
def sync_from_source(self, item):
|
||||
result = self.collection.get(util.bytestring_path(item.path).lower())
|
||||
|
||||
if 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())
|
||||
|
|
@ -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. :bug:`1450`
|
||||
|
||||
|
||||
Fixes:
|
||||
|
||||
* :doc:`/plugins/mpdstats`: Avoid a crash when the music played is not in the
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
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.
|
||||
|
|
|
|||
167
test/rsrc/itunes_library_unix.xml
Normal file
167
test/rsrc/itunes_library_unix.xml
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Major Version</key><integer>1</integer>
|
||||
<key>Minor Version</key><integer>1</integer>
|
||||
<key>Date</key><date>2015-05-08T14:36:28Z</date>
|
||||
<key>Application Version</key><string>12.1.2.27</string>
|
||||
<key>Features</key><integer>5</integer>
|
||||
<key>Show Content Ratings</key><true/>
|
||||
<key>Music Folder</key><string>file:////Music/</string>
|
||||
<key>Library Persistent ID</key><string>1ABA8417E4946A32</string>
|
||||
<key>Tracks</key>
|
||||
<dict>
|
||||
<key>634</key>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>634</integer>
|
||||
<key>Name</key><string>Tessellate</string>
|
||||
<key>Artist</key><string>alt-J</string>
|
||||
<key>Album Artist</key><string>alt-J</string>
|
||||
<key>Album</key><string>An Awesome Wave</string>
|
||||
<key>Genre</key><string>Alternative</string>
|
||||
<key>Kind</key><string>MPEG audio file</string>
|
||||
<key>Size</key><integer>5525212</integer>
|
||||
<key>Total Time</key><integer>182674</integer>
|
||||
<key>Disc Number</key><integer>1</integer>
|
||||
<key>Disc Count</key><integer>1</integer>
|
||||
<key>Track Number</key><integer>3</integer>
|
||||
<key>Track Count</key><integer>13</integer>
|
||||
<key>Year</key><integer>2012</integer>
|
||||
<key>Date Modified</key><date>2015-02-02T15:23:08Z</date>
|
||||
<key>Date Added</key><date>2014-04-24T09:28:38Z</date>
|
||||
<key>Bit Rate</key><integer>238</integer>
|
||||
<key>Sample Rate</key><integer>44100</integer>
|
||||
<key>Play Count</key><integer>0</integer>
|
||||
<key>Play Date</key><integer>3513593824</integer>
|
||||
<key>Skip Count</key><integer>3</integer>
|
||||
<key>Skip Date</key><date>2015-02-05T15:41:04Z</date>
|
||||
<key>Rating</key><integer>80</integer>
|
||||
<key>Album Rating</key><integer>80</integer>
|
||||
<key>Album Rating Computed</key><true/>
|
||||
<key>Artwork Count</key><integer>1</integer>
|
||||
<key>Sort Album</key><string>Awesome Wave</string>
|
||||
<key>Sort Artist</key><string>alt-J</string>
|
||||
<key>Persistent ID</key><string>20E89D1580C31363</string>
|
||||
<key>Track Type</key><string>File</string>
|
||||
<key>Location</key><string>file:///Music/Alt-J/An%20Awesome%20Wave/03%20Tessellate.mp3</string>
|
||||
<key>File Folder Count</key><integer>4</integer>
|
||||
<key>Library Folder Count</key><integer>2</integer>
|
||||
</dict>
|
||||
<key>636</key>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>636</integer>
|
||||
<key>Name</key><string>Breezeblocks</string>
|
||||
<key>Artist</key><string>alt-J</string>
|
||||
<key>Album Artist</key><string>alt-J</string>
|
||||
<key>Album</key><string>An Awesome Wave</string>
|
||||
<key>Genre</key><string>Alternative</string>
|
||||
<key>Kind</key><string>MPEG audio file</string>
|
||||
<key>Size</key><integer>6827195</integer>
|
||||
<key>Total Time</key><integer>227082</integer>
|
||||
<key>Disc Number</key><integer>1</integer>
|
||||
<key>Disc Count</key><integer>1</integer>
|
||||
<key>Track Number</key><integer>4</integer>
|
||||
<key>Track Count</key><integer>13</integer>
|
||||
<key>Year</key><integer>2012</integer>
|
||||
<key>Date Modified</key><date>2015-02-02T15:23:08Z</date>
|
||||
<key>Date Added</key><date>2014-04-24T09:28:38Z</date>
|
||||
<key>Bit Rate</key><integer>237</integer>
|
||||
<key>Sample Rate</key><integer>44100</integer>
|
||||
<key>Play Count</key><integer>31</integer>
|
||||
<key>Play Date</key><integer>3513594051</integer>
|
||||
<key>Play Date UTC</key><date>2015-05-04T12:20:51Z</date>
|
||||
<key>Skip Count</key><integer>0</integer>
|
||||
<key>Rating</key><integer>100</integer>
|
||||
<key>Album Rating</key><integer>80</integer>
|
||||
<key>Album Rating Computed</key><true/>
|
||||
<key>Artwork Count</key><integer>1</integer>
|
||||
<key>Sort Album</key><string>Awesome Wave</string>
|
||||
<key>Sort Artist</key><string>alt-J</string>
|
||||
<key>Persistent ID</key><string>D7017B127B983D38</string>
|
||||
<key>Track Type</key><string>File</string>
|
||||
<key>Location</key><string>file://localhost/Music/Alt-J/An%20Awesome%20Wave/04%20Breezeblocks.mp3</string>
|
||||
<key>File Folder Count</key><integer>4</integer>
|
||||
<key>Library Folder Count</key><integer>2</integer>
|
||||
</dict>
|
||||
<key>638</key>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>638</integer>
|
||||
<key>Name</key><string>❦ (Ripe & Ruin)</string>
|
||||
<key>Artist</key><string>alt-J</string>
|
||||
<key>Album Artist</key><string>alt-J</string>
|
||||
<key>Album</key><string>An Awesome Wave</string>
|
||||
<key>Kind</key><string>MPEG audio file</string>
|
||||
<key>Size</key><integer>2173293</integer>
|
||||
<key>Total Time</key><integer>72097</integer>
|
||||
<key>Disc Number</key><integer>1</integer>
|
||||
<key>Disc Count</key><integer>1</integer>
|
||||
<key>Track Number</key><integer>2</integer>
|
||||
<key>Track Count</key><integer>13</integer>
|
||||
<key>Year</key><integer>2012</integer>
|
||||
<key>Date Modified</key><date>2015-05-09T17:04:53Z</date>
|
||||
<key>Date Added</key><date>2015-02-02T15:28:39Z</date>
|
||||
<key>Bit Rate</key><integer>233</integer>
|
||||
<key>Sample Rate</key><integer>44100</integer>
|
||||
<key>Play Count</key><integer>8</integer>
|
||||
<key>Play Date</key><integer>3514109973</integer>
|
||||
<key>Play Date UTC</key><date>2015-05-10T11:39:33Z</date>
|
||||
<key>Skip Count</key><integer>1</integer>
|
||||
<key>Skip Date</key><date>2015-02-02T15:29:10Z</date>
|
||||
<key>Album Rating</key><integer>80</integer>
|
||||
<key>Album Rating Computed</key><true/>
|
||||
<key>Artwork Count</key><integer>1</integer>
|
||||
<key>Sort Album</key><string>Awesome Wave</string>
|
||||
<key>Sort Artist</key><string>alt-J</string>
|
||||
<key>Persistent ID</key><string>183699FA0554D0E6</string>
|
||||
<key>Track Type</key><string>File</string>
|
||||
<key>Location</key><string>file:///Music/Alt-J/An%20Awesome%20Wave/02%20%E2%9D%A6%20(Ripe%20&%20Ruin).mp3</string>
|
||||
<key>File Folder Count</key><integer>4</integer>
|
||||
<key>Library Folder Count</key><integer>2</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>Playlists</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Name</key><string>Library</string>
|
||||
<key>Master</key><true/>
|
||||
<key>Playlist ID</key><integer>11480</integer>
|
||||
<key>Playlist Persistent ID</key><string>CD6FF684E7A6A166</string>
|
||||
<key>Visible</key><false/>
|
||||
<key>All Items</key><true/>
|
||||
<key>Playlist Items</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>634</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>636</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>638</integer>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Name</key><string>Music</string>
|
||||
<key>Playlist ID</key><integer>16906</integer>
|
||||
<key>Playlist Persistent ID</key><string>4FB2E64E0971DD45</string>
|
||||
<key>Distinguished Kind</key><integer>4</integer>
|
||||
<key>Music</key><true/>
|
||||
<key>All Items</key><true/>
|
||||
<key>Playlist Items</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>634</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>636</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>638</integer>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
167
test/rsrc/itunes_library_windows.xml
Normal file
167
test/rsrc/itunes_library_windows.xml
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Major Version</key><integer>1</integer>
|
||||
<key>Minor Version</key><integer>1</integer>
|
||||
<key>Date</key><date>2015-05-11T15:27:14Z</date>
|
||||
<key>Application Version</key><string>12.1.2.27</string>
|
||||
<key>Features</key><integer>5</integer>
|
||||
<key>Show Content Ratings</key><true/>
|
||||
<key>Music Folder</key><string>file://localhost/C:/Documents%20and%20Settings/Owner/My%20Documents/My%20Music/iTunes/iTunes%20Media/</string>
|
||||
<key>Library Persistent ID</key><string>B4C9F3EE26EFAF78</string>
|
||||
<key>Tracks</key>
|
||||
<dict>
|
||||
<key>180</key>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>180</integer>
|
||||
<key>Name</key><string>Tessellate</string>
|
||||
<key>Artist</key><string>alt-J</string>
|
||||
<key>Album Artist</key><string>alt-J</string>
|
||||
<key>Album</key><string>An Awesome Wave</string>
|
||||
<key>Genre</key><string>Alternative</string>
|
||||
<key>Kind</key><string>MPEG audio file</string>
|
||||
<key>Size</key><integer>5525212</integer>
|
||||
<key>Total Time</key><integer>182674</integer>
|
||||
<key>Disc Number</key><integer>1</integer>
|
||||
<key>Disc Count</key><integer>1</integer>
|
||||
<key>Track Number</key><integer>3</integer>
|
||||
<key>Track Count</key><integer>13</integer>
|
||||
<key>Year</key><integer>2012</integer>
|
||||
<key>Date Modified</key><date>2015-02-02T15:23:08Z</date>
|
||||
<key>Date Added</key><date>2014-04-24T09:28:38Z</date>
|
||||
<key>Bit Rate</key><integer>238</integer>
|
||||
<key>Sample Rate</key><integer>44100</integer>
|
||||
<key>Play Count</key><integer>0</integer>
|
||||
<key>Play Date</key><integer>3513593824</integer>
|
||||
<key>Skip Count</key><integer>3</integer>
|
||||
<key>Skip Date</key><date>2015-02-05T15:41:04Z</date>
|
||||
<key>Rating</key><integer>80</integer>
|
||||
<key>Album Rating</key><integer>80</integer>
|
||||
<key>Album Rating Computed</key><true/>
|
||||
<key>Artwork Count</key><integer>1</integer>
|
||||
<key>Sort Album</key><string>Awesome Wave</string>
|
||||
<key>Sort Artist</key><string>alt-J</string>
|
||||
<key>Persistent ID</key><string>20E89D1580C31363</string>
|
||||
<key>Track Type</key><string>File</string>
|
||||
<key>Location</key><string>file://localhost/G:/Music/Alt-J/An%20Awesome%20Wave/03%20Tessellate.mp3</string>
|
||||
<key>File Folder Count</key><integer>-1</integer>
|
||||
<key>Library Folder Count</key><integer>-1</integer>
|
||||
</dict>
|
||||
<key>183</key>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>183</integer>
|
||||
<key>Name</key><string>Breezeblocks</string>
|
||||
<key>Artist</key><string>alt-J</string>
|
||||
<key>Album Artist</key><string>alt-J</string>
|
||||
<key>Album</key><string>An Awesome Wave</string>
|
||||
<key>Genre</key><string>Alternative</string>
|
||||
<key>Kind</key><string>MPEG audio file</string>
|
||||
<key>Size</key><integer>6827195</integer>
|
||||
<key>Total Time</key><integer>227082</integer>
|
||||
<key>Disc Number</key><integer>1</integer>
|
||||
<key>Disc Count</key><integer>1</integer>
|
||||
<key>Track Number</key><integer>4</integer>
|
||||
<key>Track Count</key><integer>13</integer>
|
||||
<key>Year</key><integer>2012</integer>
|
||||
<key>Date Modified</key><date>2015-02-02T15:23:08Z</date>
|
||||
<key>Date Added</key><date>2014-04-24T09:28:38Z</date>
|
||||
<key>Bit Rate</key><integer>237</integer>
|
||||
<key>Sample Rate</key><integer>44100</integer>
|
||||
<key>Play Count</key><integer>31</integer>
|
||||
<key>Play Date</key><integer>3513594051</integer>
|
||||
<key>Play Date UTC</key><date>2015-05-04T12:20:51Z</date>
|
||||
<key>Skip Count</key><integer>0</integer>
|
||||
<key>Rating</key><integer>100</integer>
|
||||
<key>Album Rating</key><integer>80</integer>
|
||||
<key>Album Rating Computed</key><true/>
|
||||
<key>Artwork Count</key><integer>1</integer>
|
||||
<key>Sort Album</key><string>Awesome Wave</string>
|
||||
<key>Sort Artist</key><string>alt-J</string>
|
||||
<key>Persistent ID</key><string>D7017B127B983D38</string>
|
||||
<key>Track Type</key><string>File</string>
|
||||
<key>Location</key><string>file://localhost/G:/Music/Alt-J/An%20Awesome%20Wave/04%20Breezeblocks.mp3</string>
|
||||
<key>File Folder Count</key><integer>-1</integer>
|
||||
<key>Library Folder Count</key><integer>-1</integer>
|
||||
</dict>
|
||||
<key>638</key>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>638</integer>
|
||||
<key>Name</key><string>❦ (Ripe & Ruin)</string>
|
||||
<key>Artist</key><string>alt-J</string>
|
||||
<key>Album Artist</key><string>alt-J</string>
|
||||
<key>Album</key><string>An Awesome Wave</string>
|
||||
<key>Kind</key><string>MPEG audio file</string>
|
||||
<key>Size</key><integer>2173293</integer>
|
||||
<key>Total Time</key><integer>72097</integer>
|
||||
<key>Disc Number</key><integer>1</integer>
|
||||
<key>Disc Count</key><integer>1</integer>
|
||||
<key>Track Number</key><integer>2</integer>
|
||||
<key>Track Count</key><integer>13</integer>
|
||||
<key>Year</key><integer>2012</integer>
|
||||
<key>Date Modified</key><date>2015-05-09T17:04:53Z</date>
|
||||
<key>Date Added</key><date>2015-02-02T15:28:39Z</date>
|
||||
<key>Bit Rate</key><integer>233</integer>
|
||||
<key>Sample Rate</key><integer>44100</integer>
|
||||
<key>Play Count</key><integer>8</integer>
|
||||
<key>Play Date</key><integer>3514109973</integer>
|
||||
<key>Play Date UTC</key><date>2015-05-10T11:39:33Z</date>
|
||||
<key>Skip Count</key><integer>1</integer>
|
||||
<key>Skip Date</key><date>2015-02-02T15:29:10Z</date>
|
||||
<key>Album Rating</key><integer>80</integer>
|
||||
<key>Album Rating Computed</key><true/>
|
||||
<key>Artwork Count</key><integer>1</integer>
|
||||
<key>Sort Album</key><string>Awesome Wave</string>
|
||||
<key>Sort Artist</key><string>alt-J</string>
|
||||
<key>Persistent ID</key><string>183699FA0554D0E6</string>
|
||||
<key>Track Type</key><string>File</string>
|
||||
<key>Location</key><string>file://localhost/G:/Experiments/Alt-J/An%20Awesome%20Wave/02%20%E2%9D%A6%20(Ripe%20&%20Ruin).mp3</string>
|
||||
<key>File Folder Count</key><integer>4</integer>
|
||||
<key>Library Folder Count</key><integer>2</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>Playlists</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Name</key><string>Bibliotheek</string>
|
||||
<key>Master</key><true/>
|
||||
<key>Playlist ID</key><integer>72</integer>
|
||||
<key>Playlist Persistent ID</key><string>728AA5B1D00ED23B</string>
|
||||
<key>Visible</key><false/>
|
||||
<key>All Items</key><true/>
|
||||
<key>Playlist Items</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>180</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>183</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>638</integer>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Name</key><string>Muziek</string>
|
||||
<key>Playlist ID</key><integer>103</integer>
|
||||
<key>Playlist Persistent ID</key><string>8120A002B0486AD7</string>
|
||||
<key>Distinguished Kind</key><integer>4</integer>
|
||||
<key>Music</key><true/>
|
||||
<key>All Items</key><true/>
|
||||
<key>Playlist Items</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>180</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>183</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Track ID</key><integer>638</integer>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
123
test/test_metasync.py
Normal file
123
test/test_metasync.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# 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.
|
||||
import os
|
||||
import platform
|
||||
import time
|
||||
from datetime import datetime
|
||||
from beets.library import Item
|
||||
|
||||
from test import _common
|
||||
from test._common import unittest
|
||||
from test.helper import TestHelper
|
||||
|
||||
|
||||
def _parsetime(s):
|
||||
return time.mktime(datetime.strptime(s, '%Y-%m-%d %H:%M:%S').timetuple())
|
||||
|
||||
|
||||
def _is_windows():
|
||||
return platform.system() == "Windows"
|
||||
|
||||
|
||||
class MetaSyncTest(_common.TestCase, TestHelper):
|
||||
itunes_library_unix = os.path.join(_common.RSRC,
|
||||
'itunes_library_unix.xml')
|
||||
itunes_library_windows = os.path.join(_common.RSRC,
|
||||
'itunes_library_windows.xml')
|
||||
|
||||
def setUp(self):
|
||||
self.setup_beets()
|
||||
self.load_plugins('metasync')
|
||||
|
||||
self.config['metasync']['source'] = 'itunes'
|
||||
|
||||
if _is_windows():
|
||||
self.config['metasync']['itunes']['library'] = \
|
||||
self.itunes_library_windows
|
||||
else:
|
||||
self.config['metasync']['itunes']['library'] = \
|
||||
self.itunes_library_unix
|
||||
|
||||
self._set_up_data()
|
||||
|
||||
def _set_up_data(self):
|
||||
items = [_common.item() for _ in range(2)]
|
||||
|
||||
items[0].title = 'Tessellate'
|
||||
items[0].artist = 'alt-J'
|
||||
items[0].albumartist = 'alt-J'
|
||||
items[0].album = 'An Awesome Wave'
|
||||
items[0].itunes_rating = 60
|
||||
|
||||
items[1].title = 'Breezeblocks'
|
||||
items[1].artist = 'alt-J'
|
||||
items[1].albumartist = 'alt-J'
|
||||
items[1].album = 'An Awesome Wave'
|
||||
|
||||
if _is_windows():
|
||||
items[0].path = \
|
||||
u'G:\\Music\\Alt-J\\An Awesome Wave\\03 Tessellate.mp3'
|
||||
items[1].path = \
|
||||
u'G:\\Music\\Alt-J\\An Awesome Wave\\04 Breezeblocks.mp3'
|
||||
else:
|
||||
items[0].path = u'/Music/Alt-J/An Awesome Wave/03 Tessellate.mp3'
|
||||
items[1].path = u'/Music/Alt-J/An Awesome Wave/04 Breezeblocks.mp3'
|
||||
|
||||
for item in items:
|
||||
self.lib.add(item)
|
||||
|
||||
def tearDown(self):
|
||||
self.unload_plugins()
|
||||
self.teardown_beets()
|
||||
|
||||
def test_load_item_types(self):
|
||||
# This test also verifies that the MetaSources have loaded correctly
|
||||
self.assertIn('amarok_score', Item._types)
|
||||
self.assertIn('itunes_rating', Item._types)
|
||||
|
||||
def test_pretend_sync_from_itunes(self):
|
||||
out = self.run_with_output('metasync', '-p')
|
||||
|
||||
self.assertIn('itunes_rating: 60 -> 80', out)
|
||||
self.assertIn('itunes_rating: 100', out)
|
||||
self.assertIn('itunes_playcount: 31', out)
|
||||
self.assertIn('itunes_skipcount: 3', out)
|
||||
self.assertIn('itunes_lastplayed: 2015-05-04 12:20:51', out)
|
||||
self.assertIn('itunes_lastskipped: 2015-02-05 15:41:04', out)
|
||||
self.assertEqual(self.lib.items()[0].itunes_rating, 60)
|
||||
|
||||
def test_sync_from_itunes(self):
|
||||
self.run_command('metasync')
|
||||
|
||||
self.assertEqual(self.lib.items()[0].itunes_rating, 80)
|
||||
self.assertEqual(self.lib.items()[0].itunes_playcount, 0)
|
||||
self.assertEqual(self.lib.items()[0].itunes_skipcount, 3)
|
||||
self.assertFalse(hasattr(self.lib.items()[0], 'itunes_lastplayed'))
|
||||
self.assertEqual(self.lib.items()[0].itunes_lastskipped,
|
||||
_parsetime('2015-02-05 15:41:04'))
|
||||
|
||||
self.assertEqual(self.lib.items()[1].itunes_rating, 100)
|
||||
self.assertEqual(self.lib.items()[1].itunes_playcount, 31)
|
||||
self.assertEqual(self.lib.items()[1].itunes_skipcount, 0)
|
||||
self.assertEqual(self.lib.items()[1].itunes_lastplayed,
|
||||
_parsetime('2015-05-04 12:20:51'))
|
||||
self.assertFalse(hasattr(self.lib.items()[1], 'itunes_lastskipped'))
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
||||
|
||||
if __name__ == b'__main__':
|
||||
unittest.main(defaultTest='suite')
|
||||
Loading…
Reference in a new issue