mirror of
https://github.com/beetbox/beets.git
synced 2026-01-02 14:03:12 +01:00
Merge branch 'master' into editor
This commit is contained in:
commit
72b26235ad
31 changed files with 670 additions and 122 deletions
|
|
@ -511,7 +511,10 @@ def album_for_mbid(release_id):
|
|||
if the ID is not found.
|
||||
"""
|
||||
try:
|
||||
return mb.album_for_id(release_id)
|
||||
album = mb.album_for_id(release_id)
|
||||
if album:
|
||||
plugins.send('albuminfo_received', info=album)
|
||||
return album
|
||||
except mb.MusicBrainzAPIError as exc:
|
||||
exc.log(log)
|
||||
|
||||
|
|
@ -521,7 +524,10 @@ def track_for_mbid(recording_id):
|
|||
if the ID is not found.
|
||||
"""
|
||||
try:
|
||||
return mb.track_for_id(recording_id)
|
||||
track = mb.track_for_id(recording_id)
|
||||
if track:
|
||||
plugins.send('trackinfo_received', info=track)
|
||||
return track
|
||||
except mb.MusicBrainzAPIError as exc:
|
||||
exc.log(log)
|
||||
|
||||
|
|
@ -529,14 +535,20 @@ def track_for_mbid(recording_id):
|
|||
def albums_for_id(album_id):
|
||||
"""Get a list of albums for an ID."""
|
||||
candidates = [album_for_mbid(album_id)]
|
||||
candidates.extend(plugins.album_for_id(album_id))
|
||||
plugin_albums = plugins.album_for_id(album_id)
|
||||
for a in plugin_albums:
|
||||
plugins.send('albuminfo_received', info=a)
|
||||
candidates.extend(plugin_albums)
|
||||
return filter(None, candidates)
|
||||
|
||||
|
||||
def tracks_for_id(track_id):
|
||||
"""Get a list of tracks for an ID."""
|
||||
candidates = [track_for_mbid(track_id)]
|
||||
candidates.extend(plugins.track_for_id(track_id))
|
||||
plugin_tracks = plugins.track_for_id(track_id)
|
||||
for t in plugin_tracks:
|
||||
plugins.send('trackinfo_received', info=t)
|
||||
candidates.extend(plugin_tracks)
|
||||
return filter(None, candidates)
|
||||
|
||||
|
||||
|
|
@ -566,6 +578,10 @@ def album_candidates(items, artist, album, va_likely):
|
|||
# Candidates from plugins.
|
||||
out.extend(plugins.candidates(items, artist, album, va_likely))
|
||||
|
||||
# Notify subscribed plugins about fetched album info
|
||||
for a in out:
|
||||
plugins.send('albuminfo_received', info=a)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
|
|
@ -586,4 +602,8 @@ def item_candidates(item, artist, title):
|
|||
# Plugin candidates.
|
||||
out.extend(plugins.item_candidates(item, artist, title))
|
||||
|
||||
# Notify subscribed plugins about fetched track info
|
||||
for i in out:
|
||||
plugins.send('trackinfo_received', info=i)
|
||||
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -259,6 +259,8 @@ def album_info(release):
|
|||
data_url=album_url(release['id']),
|
||||
)
|
||||
info.va = info.artist_id == VARIOUS_ARTISTS_ID
|
||||
if info.va:
|
||||
info.artist = config['va_name'].get(unicode)
|
||||
info.asin = release.get('asin')
|
||||
info.releasegroup_id = release['release-group']['id']
|
||||
info.country = release.get('country')
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ verbose: 0
|
|||
terminal_encoding:
|
||||
original_date: no
|
||||
id3v23: no
|
||||
va_name: "Various Artists"
|
||||
|
||||
ui:
|
||||
terminal_width: 80
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ action = Enum('action',
|
|||
|
||||
QUEUE_SIZE = 128
|
||||
SINGLE_ARTIST_THRESH = 0.25
|
||||
VARIOUS_ARTISTS = u'Various Artists'
|
||||
PROGRESS_KEY = 'tagprogress'
|
||||
HISTORY_KEY = 'taghistory'
|
||||
|
||||
|
|
@ -631,7 +630,7 @@ class ImportTask(BaseImportTask):
|
|||
changes['comp'] = False
|
||||
else:
|
||||
# VA.
|
||||
changes['albumartist'] = VARIOUS_ARTISTS
|
||||
changes['albumartist'] = config['va_name'].get(unicode)
|
||||
changes['comp'] = True
|
||||
|
||||
elif self.choice_flag == action.APPLY:
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ from __future__ import (division, absolute_import, print_function,
|
|||
|
||||
import os
|
||||
import sys
|
||||
import shlex
|
||||
import unicodedata
|
||||
import time
|
||||
import re
|
||||
|
|
@ -1139,13 +1138,8 @@ def parse_query_string(s, model_cls):
|
|||
The string is split into components using shell-like syntax.
|
||||
"""
|
||||
assert isinstance(s, unicode), "Query is not unicode: {0!r}".format(s)
|
||||
|
||||
# A bug in Python < 2.7.3 prevents correct shlex splitting of
|
||||
# Unicode strings.
|
||||
# http://bugs.python.org/issue6988
|
||||
s = s.encode('utf8')
|
||||
try:
|
||||
parts = [p.decode('utf8') for p in shlex.split(s)]
|
||||
parts = util.shlex_split(s)
|
||||
except ValueError as exc:
|
||||
raise dbcore.InvalidQueryError(s, exc)
|
||||
return parse_query_parts(parts, model_cls)
|
||||
|
|
|
|||
|
|
@ -215,9 +215,9 @@ def _sc_decode(soundcheck):
|
|||
# SoundCheck tags consist of 10 numbers, each represented by 8
|
||||
# characters of ASCII hex preceded by a space.
|
||||
try:
|
||||
soundcheck = soundcheck.replace(' ', '').decode('hex')
|
||||
soundcheck = soundcheck.replace(b' ', b'').decode('hex')
|
||||
soundcheck = struct.unpack(b'!iiiiiiiiii', soundcheck)
|
||||
except (struct.error, TypeError, UnicodeEncodeError):
|
||||
except (struct.error, TypeError):
|
||||
# SoundCheck isn't in the format we expect, so return default
|
||||
# values.
|
||||
return 0.0, 0.0
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class UserError(Exception):
|
|||
"""
|
||||
|
||||
|
||||
# Utilities.
|
||||
# Encoding utilities.
|
||||
|
||||
def _out_encoding():
|
||||
"""Get the encoding to use for *outputting* strings to the console.
|
||||
|
|
@ -137,6 +137,45 @@ def print_(*strings, **kwargs):
|
|||
sys.stdout.write(txt)
|
||||
|
||||
|
||||
# Configuration wrappers.
|
||||
|
||||
def _bool_fallback(a, b):
|
||||
"""Given a boolean or None, return the original value or a fallback.
|
||||
"""
|
||||
if a is None:
|
||||
assert isinstance(b, bool)
|
||||
return b
|
||||
else:
|
||||
assert isinstance(a, bool)
|
||||
return a
|
||||
|
||||
|
||||
def should_write(write_opt=None):
|
||||
"""Decide whether a command that updates metadata should also write
|
||||
tags, using the importer configuration as the default.
|
||||
"""
|
||||
return _bool_fallback(write_opt, config['import']['write'].get(bool))
|
||||
|
||||
|
||||
def should_move(move_opt=None):
|
||||
"""Decide whether a command that updates metadata should also move
|
||||
files when they're inside the library, using the importer
|
||||
configuration as the default.
|
||||
|
||||
Specifically, commands should move files after metadata updates only
|
||||
when the importer is configured *either* to move *or* to copy files.
|
||||
They should avoid moving files when the importer is configured not
|
||||
to touch any filenames.
|
||||
"""
|
||||
return _bool_fallback(
|
||||
move_opt,
|
||||
config['import']['move'].get(bool) or
|
||||
config['import']['copy'].get(bool)
|
||||
)
|
||||
|
||||
|
||||
# Input prompts.
|
||||
|
||||
def input_(prompt=None):
|
||||
"""Like `raw_input`, but decodes the result to a Unicode string.
|
||||
Raises a UserError if stdin is not available. The prompt is sent to
|
||||
|
|
@ -327,6 +366,8 @@ def input_yn(prompt, require=False):
|
|||
return sel == 'y'
|
||||
|
||||
|
||||
# Human output formatting.
|
||||
|
||||
def human_bytes(size):
|
||||
"""Formats size, a number of bytes, in a human-readable way."""
|
||||
powers = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'H']
|
||||
|
|
@ -374,6 +415,8 @@ def human_seconds_short(interval):
|
|||
return u'%i:%02i' % (interval // 60, interval % 60)
|
||||
|
||||
|
||||
# Colorization.
|
||||
|
||||
# ANSI terminal colorization code heavily inspired by pygments:
|
||||
# http://dev.pocoo.org/hg/pygments-main/file/b2deea5b5030/pygments/console.py
|
||||
# (pygments is by Tim Hatch, Armin Ronacher, et al.)
|
||||
|
|
|
|||
|
|
@ -1055,7 +1055,8 @@ def update_items(lib, query, album, move, pretend):
|
|||
|
||||
|
||||
def update_func(lib, opts, args):
|
||||
update_items(lib, decargs(args), opts.album, opts.move, opts.pretend)
|
||||
update_items(lib, decargs(args), opts.album, ui.should_move(opts.move),
|
||||
opts.pretend)
|
||||
|
||||
|
||||
update_cmd = ui.Subcommand(
|
||||
|
|
@ -1064,7 +1065,11 @@ update_cmd = ui.Subcommand(
|
|||
update_cmd.parser.add_album_option()
|
||||
update_cmd.parser.add_format_option()
|
||||
update_cmd.parser.add_option(
|
||||
'-M', '--nomove', action='store_false', default=True, dest='move',
|
||||
'-m', '--move', action='store_true', dest='move',
|
||||
help="move files in the library directory"
|
||||
)
|
||||
update_cmd.parser.add_option(
|
||||
'-M', '--nomove', action='store_false', dest='move',
|
||||
help="don't move files in library"
|
||||
)
|
||||
update_cmd.parser.add_option(
|
||||
|
|
@ -1293,17 +1298,19 @@ def modify_func(lib, opts, args):
|
|||
query, mods, dels = modify_parse_args(decargs(args))
|
||||
if not mods and not dels:
|
||||
raise ui.UserError('no modifications specified')
|
||||
write = opts.write if opts.write is not None else \
|
||||
config['import']['write'].get(bool)
|
||||
modify_items(lib, mods, dels, query, write, opts.move, opts.album,
|
||||
not opts.yes)
|
||||
modify_items(lib, mods, dels, query, ui.should_write(opts.write),
|
||||
ui.should_move(opts.move), opts.album, not opts.yes)
|
||||
|
||||
|
||||
modify_cmd = ui.Subcommand(
|
||||
'modify', help='change metadata fields', aliases=('mod',)
|
||||
)
|
||||
modify_cmd.parser.add_option(
|
||||
'-M', '--nomove', action='store_false', default=True, dest='move',
|
||||
'-m', '--move', action='store_true', dest='move',
|
||||
help="move files in the library directory"
|
||||
)
|
||||
modify_cmd.parser.add_option(
|
||||
'-M', '--nomove', action='store_false', dest='move',
|
||||
help="don't move files in library"
|
||||
)
|
||||
modify_cmd.parser.add_option(
|
||||
|
|
@ -1466,7 +1473,7 @@ def config_edit():
|
|||
An empty config file is created if no existing config file exists.
|
||||
"""
|
||||
path = config.user_config_path()
|
||||
editor = os.environ.get('EDITOR')
|
||||
editor = util.editor_command()
|
||||
try:
|
||||
if not os.path.isfile(path):
|
||||
open(path, 'w+').close()
|
||||
|
|
|
|||
|
|
@ -736,27 +736,52 @@ def open_anything():
|
|||
return base_cmd
|
||||
|
||||
|
||||
def interactive_open(targets, command=None):
|
||||
"""Open the files in `targets` by `exec`ing a new command. (The new
|
||||
program takes over, and Python execution ends: this does not fork a
|
||||
subprocess.)
|
||||
def editor_command():
|
||||
"""Get a command for opening a text file.
|
||||
|
||||
If `command` is provided, use it. Otherwise, use an OS-specific
|
||||
command (from `open_anything`) to open the file.
|
||||
Use the `EDITOR` environment variable by default. If it is not
|
||||
present, fall back to `open_anything()`, the platform-specific tool
|
||||
for opening files in general.
|
||||
"""
|
||||
editor = os.environ.get('EDITOR')
|
||||
if editor:
|
||||
return editor
|
||||
return open_anything()
|
||||
|
||||
|
||||
def shlex_split(s):
|
||||
"""Split a Unicode or bytes string according to shell lexing rules.
|
||||
|
||||
Raise `ValueError` if the string is not a well-formed shell string.
|
||||
This is a workaround for a bug in some versions of Python.
|
||||
"""
|
||||
if isinstance(s, bytes):
|
||||
# Shlex works fine.
|
||||
return shlex.split(s)
|
||||
|
||||
elif isinstance(s, unicode):
|
||||
# Work around a Python bug.
|
||||
# http://bugs.python.org/issue6988
|
||||
bs = s.encode('utf8')
|
||||
return [c.decode('utf8') for c in shlex.split(bs)]
|
||||
|
||||
else:
|
||||
raise TypeError('shlex_split called with non-string')
|
||||
|
||||
|
||||
def interactive_open(targets, command):
|
||||
"""Open the files in `targets` by `exec`ing a new `command`, given
|
||||
as a Unicode string. (The new program takes over, and Python
|
||||
execution ends: this does not fork a subprocess.)
|
||||
|
||||
Can raise `OSError`.
|
||||
"""
|
||||
if command:
|
||||
command = command.encode('utf8')
|
||||
try:
|
||||
command = [c.decode('utf8')
|
||||
for c in shlex.split(command)]
|
||||
except ValueError: # Malformed shell tokens.
|
||||
command = [command]
|
||||
command.insert(0, command[0]) # for argv[0]
|
||||
else:
|
||||
base_cmd = open_anything()
|
||||
command = [base_cmd, base_cmd]
|
||||
# Split the command string into its arguments.
|
||||
try:
|
||||
command = shlex_split(command)
|
||||
except ValueError: # Malformed shell tokens.
|
||||
command = [command]
|
||||
command.insert(0, command[0]) # for argv[0]
|
||||
|
||||
command += targets
|
||||
|
||||
|
|
|
|||
|
|
@ -194,8 +194,7 @@ class AcoustidPlugin(plugins.BeetsPlugin):
|
|||
|
||||
def fingerprint_cmd_func(lib, opts, args):
|
||||
for item in lib.items(ui.decargs(args)):
|
||||
fingerprint_item(self._log, item,
|
||||
write=config['import']['write'].get(bool))
|
||||
fingerprint_item(self._log, item, write=ui.should_write())
|
||||
fingerprint_cmd.func = fingerprint_cmd_func
|
||||
|
||||
return [submit_cmd, fingerprint_cmd]
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from __future__ import (division, absolute_import, print_function,
|
|||
|
||||
import beets.ui
|
||||
from beets import logging
|
||||
from beets import config
|
||||
from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets.util import confit
|
||||
|
|
@ -223,6 +224,8 @@ class DiscogsPlugin(BeetsPlugin):
|
|||
albumtype = ', '.join(
|
||||
result.data['formats'][0].get('descriptions', [])) or None
|
||||
va = result.data['artists'][0]['name'].lower() == 'various'
|
||||
if va:
|
||||
artist = config['va_name'].get(unicode)
|
||||
year = result.data['year']
|
||||
label = result.data['labels'][0]['name']
|
||||
mediums = len(set(t.medium for t in tracks))
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import tempfile
|
|||
from string import Template
|
||||
import subprocess
|
||||
|
||||
from beets import util, config, plugins, ui
|
||||
from beets import util, plugins, ui
|
||||
from beets.dbcore import types
|
||||
import pyechonest
|
||||
import pyechonest.song
|
||||
|
|
@ -472,7 +472,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin):
|
|||
|
||||
def fetch_func(lib, opts, args):
|
||||
self.config.set_args(opts)
|
||||
write = config['import']['write'].get(bool)
|
||||
write = ui.should_write()
|
||||
for item in lib.items(ui.decargs(args)):
|
||||
self._log.info(u'{0}', item)
|
||||
if self.config['force'] or self.requires_update(item):
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ class EmbedCoverArtPlugin(BeetsPlugin):
|
|||
def process_album(self, album):
|
||||
"""Automatically embed art after art has been set
|
||||
"""
|
||||
if self.config['auto'] and config['import']['write']:
|
||||
if self.config['auto'] and ui.should_write():
|
||||
max_width = self.config['maxwidth'].get(int)
|
||||
art.embed_album(self._log, album, max_width, True,
|
||||
self.config['compare_threshold'].get(int),
|
||||
|
|
|
|||
133
beetsplug/embyupdate.py
Normal file
133
beetsplug/embyupdate.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"""Updates the Emby Library whenever the beets library is changed.
|
||||
|
||||
emby:
|
||||
host: localhost
|
||||
port: 8096
|
||||
username: user
|
||||
password: password
|
||||
"""
|
||||
from __future__ import (division, absolute_import, print_function,
|
||||
unicode_literals)
|
||||
|
||||
from beets import config
|
||||
from beets.plugins import BeetsPlugin
|
||||
from urllib import urlencode
|
||||
from urlparse import urljoin, parse_qs, urlsplit, urlunsplit
|
||||
import hashlib
|
||||
import requests
|
||||
|
||||
|
||||
def api_url(host, port, endpoint):
|
||||
"""Returns a joined url.
|
||||
"""
|
||||
joined = urljoin('http://{0}:{1}'.format(host, port), endpoint)
|
||||
scheme, netloc, path, query_string, fragment = urlsplit(joined)
|
||||
query_params = parse_qs(query_string)
|
||||
|
||||
query_params['format'] = ['json']
|
||||
new_query_string = urlencode(query_params, doseq=True)
|
||||
|
||||
return urlunsplit((scheme, netloc, path, new_query_string, fragment))
|
||||
|
||||
|
||||
def password_data(username, password):
|
||||
"""Returns a dict with username and its encoded password.
|
||||
"""
|
||||
return {
|
||||
'username': username,
|
||||
'password': hashlib.sha1(password).hexdigest(),
|
||||
'passwordMd5': hashlib.md5(password).hexdigest()
|
||||
}
|
||||
|
||||
|
||||
def create_headers(user_id, token=None):
|
||||
"""Return header dict that is needed to talk to the Emby API.
|
||||
"""
|
||||
headers = {
|
||||
'Authorization': 'MediaBrowser',
|
||||
'UserId': user_id,
|
||||
'Client': 'other',
|
||||
'Device': 'empy',
|
||||
'DeviceId': 'beets',
|
||||
'Version': '0.0.0'
|
||||
}
|
||||
|
||||
if token:
|
||||
headers['X-MediaBrowser-Token'] = token
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
def get_token(host, port, headers, auth_data):
|
||||
"""Return token for a user.
|
||||
"""
|
||||
url = api_url(host, port, '/Users/AuthenticateByName')
|
||||
r = requests.post(url, headers=headers, data=auth_data)
|
||||
|
||||
return r.json().get('AccessToken')
|
||||
|
||||
|
||||
def get_user(host, port, username):
|
||||
"""Return user dict from server or None if there is no user.
|
||||
"""
|
||||
url = api_url(host, port, '/Users/Public')
|
||||
r = requests.get(url)
|
||||
user = [i for i in r.json() if i['Name'] == username]
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class EmbyUpdate(BeetsPlugin):
|
||||
def __init__(self):
|
||||
super(EmbyUpdate, self).__init__()
|
||||
|
||||
# Adding defaults.
|
||||
config['emby'].add({
|
||||
u'host': u'localhost',
|
||||
u'port': 8096
|
||||
})
|
||||
|
||||
self.register_listener('database_change', self.listen_for_db_change)
|
||||
|
||||
def listen_for_db_change(self, lib, model):
|
||||
"""Listens for beets db change and register the update for the end.
|
||||
"""
|
||||
self.register_listener('cli_exit', self.update)
|
||||
|
||||
def update(self, lib):
|
||||
"""When the client exists try to send refresh request to Emby.
|
||||
"""
|
||||
self._log.info(u'Updating Emby library...')
|
||||
|
||||
host = config['emby']['host'].get()
|
||||
port = config['emby']['port'].get()
|
||||
username = config['emby']['username'].get()
|
||||
password = config['emby']['password'].get()
|
||||
|
||||
# Get user information from the Emby API.
|
||||
user = get_user(host, port, username)
|
||||
if not user:
|
||||
self._log.warning(u'User {0} could not be found.'.format(username))
|
||||
return
|
||||
|
||||
# Create Authentication data and headers.
|
||||
auth_data = password_data(username, password)
|
||||
headers = create_headers(user[0]['Id'])
|
||||
|
||||
# Get authentication token.
|
||||
token = get_token(host, port, headers, auth_data)
|
||||
if not token:
|
||||
self._log.warning(
|
||||
u'Couldnt not get token for user {0}'.format(username))
|
||||
return
|
||||
|
||||
# Recreate headers with a token.
|
||||
headers = create_headers(user[0]['Id'], token=token)
|
||||
|
||||
# Trigger the Update.
|
||||
url = api_url(host, port, '/Library/Refresh')
|
||||
r = requests.post(url, headers=headers)
|
||||
if r.status_code != 204:
|
||||
self._log.warning(u'Update could not be triggered')
|
||||
else:
|
||||
self._log.info(u'Update triggered.')
|
||||
|
|
@ -22,7 +22,6 @@ import re
|
|||
from beets import plugins
|
||||
from beets import ui
|
||||
from beets.util import displayable_path
|
||||
from beets import config
|
||||
|
||||
|
||||
def split_on_feat(artist):
|
||||
|
|
@ -102,7 +101,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
|
|||
def func(lib, opts, args):
|
||||
self.config.set_args(opts)
|
||||
drop_feat = self.config['drop'].get(bool)
|
||||
write = config['import']['write'].get(bool)
|
||||
write = ui.should_write()
|
||||
|
||||
for item in lib.items(ui.decargs(args)):
|
||||
self.ft_in_title(item, drop_feat)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import subprocess
|
|||
from beets import ui
|
||||
from beets import util
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets import config
|
||||
|
||||
|
||||
class KeyFinderPlugin(BeetsPlugin):
|
||||
|
|
@ -46,8 +45,7 @@ class KeyFinderPlugin(BeetsPlugin):
|
|||
return [cmd]
|
||||
|
||||
def command(self, lib, opts, args):
|
||||
self.find_key(lib.items(ui.decargs(args)),
|
||||
write=config['import']['write'].get(bool))
|
||||
self.find_key(lib.items(ui.decargs(args)), write=ui.should_write())
|
||||
|
||||
def imported(self, session, task):
|
||||
self.find_key(task.items)
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ import traceback
|
|||
|
||||
from beets import plugins
|
||||
from beets import ui
|
||||
from beets.util import normpath, plurality
|
||||
from beets import config
|
||||
from beets.util import normpath, plurality
|
||||
from beets import library
|
||||
|
||||
|
||||
|
|
@ -292,7 +292,7 @@ class LastGenrePlugin(plugins.BeetsPlugin):
|
|||
result = None
|
||||
if isinstance(obj, library.Item):
|
||||
result = self.fetch_artist_genre(obj)
|
||||
elif obj.albumartist != 'Various Artists':
|
||||
elif obj.albumartist != config['va_name'].get(unicode):
|
||||
result = self.fetch_album_artist_genre(obj)
|
||||
else:
|
||||
# For "Various Artists", pick the most popular track genre.
|
||||
|
|
@ -336,7 +336,7 @@ class LastGenrePlugin(plugins.BeetsPlugin):
|
|||
)
|
||||
|
||||
def lastgenre_func(lib, opts, args):
|
||||
write = config['import']['write'].get(bool)
|
||||
write = ui.should_write()
|
||||
self.config.set_args(opts)
|
||||
|
||||
for album in lib.albums(ui.decargs(args)):
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import warnings
|
|||
from HTMLParser import HTMLParseError
|
||||
|
||||
from beets import plugins
|
||||
from beets import config, ui
|
||||
from beets import ui
|
||||
|
||||
|
||||
DIV_RE = re.compile(r'<(/?)div>?', re.I)
|
||||
|
|
@ -557,7 +557,7 @@ class LyricsPlugin(plugins.BeetsPlugin):
|
|||
def func(lib, opts, args):
|
||||
# The "write to files" option corresponds to the
|
||||
# import_write config value.
|
||||
write = config['import']['write'].get(bool)
|
||||
write = ui.should_write()
|
||||
for item in lib.items(ui.decargs(args)):
|
||||
self.fetch_item_lyrics(
|
||||
lib, item, write,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ from __future__ import (division, absolute_import, print_function,
|
|||
from beets.plugins import BeetsPlugin
|
||||
from beets import autotag, library, ui, util
|
||||
from beets.autotag import hooks
|
||||
from beets import config
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
|
|
@ -46,11 +45,14 @@ class MBSyncPlugin(BeetsPlugin):
|
|||
help='update metadata from musicbrainz')
|
||||
cmd.parser.add_option('-p', '--pretend', action='store_true',
|
||||
help='show all changes but do nothing')
|
||||
cmd.parser.add_option('-m', '--move', action='store_true',
|
||||
dest='move',
|
||||
help="move files in the library directory")
|
||||
cmd.parser.add_option('-M', '--nomove', action='store_false',
|
||||
default=True, dest='move',
|
||||
dest='move',
|
||||
help="don't move files in library")
|
||||
cmd.parser.add_option('-W', '--nowrite', action='store_false',
|
||||
default=config['import']['write'], dest='write',
|
||||
default=None, dest='write',
|
||||
help="don't write updated metadata to files")
|
||||
cmd.parser.add_format_option()
|
||||
cmd.func = self.func
|
||||
|
|
@ -59,9 +61,9 @@ class MBSyncPlugin(BeetsPlugin):
|
|||
def func(self, lib, opts, args):
|
||||
"""Command handler for the mbsync function.
|
||||
"""
|
||||
move = opts.move
|
||||
move = ui.should_move(opts.move)
|
||||
pretend = opts.pretend
|
||||
write = opts.write
|
||||
write = ui.should_write(opts.write)
|
||||
query = ui.decargs(args)
|
||||
|
||||
self.singletons(lib, query, move, pretend, write)
|
||||
|
|
@ -137,6 +139,7 @@ class MBSyncPlugin(BeetsPlugin):
|
|||
break
|
||||
|
||||
# Apply.
|
||||
self._log.debug('applying changes to {}', album_formatted)
|
||||
with lib.transaction():
|
||||
autotag.apply_metadata(album_info, mapping)
|
||||
changed = False
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ from beets import logging
|
|||
from beets import ui
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets.util import syspath, command_output, displayable_path
|
||||
from beets import config
|
||||
|
||||
|
||||
# Utilities.
|
||||
|
|
@ -926,7 +925,7 @@ class ReplayGainPlugin(BeetsPlugin):
|
|||
def func(lib, opts, args):
|
||||
self._log.setLevel(logging.INFO)
|
||||
|
||||
write = config['import']['write'].get(bool)
|
||||
write = ui.should_write()
|
||||
|
||||
if opts.album:
|
||||
for album in lib.albums(ui.decargs(args)):
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from beets import ui
|
|||
from beets.util import mkdirall, normpath, syspath
|
||||
from beets.library import Item, Album, parse_query_string
|
||||
from beets.dbcore import OrQuery
|
||||
from beets.dbcore.query import MultipleSort
|
||||
from beets.dbcore.query import MultipleSort, ParsingError
|
||||
import os
|
||||
|
||||
|
||||
|
|
@ -93,36 +93,46 @@ class SmartPlaylistPlugin(BeetsPlugin):
|
|||
self._matched_playlists = set()
|
||||
|
||||
for playlist in self.config['playlists'].get(list):
|
||||
playlist_data = (playlist['name'],)
|
||||
for key, Model in (('query', Item), ('album_query', Album)):
|
||||
qs = playlist.get(key)
|
||||
if qs is None:
|
||||
query_and_sort = None, None
|
||||
elif isinstance(qs, basestring):
|
||||
query_and_sort = parse_query_string(qs, Model)
|
||||
elif len(qs) == 1:
|
||||
query_and_sort = parse_query_string(qs[0], Model)
|
||||
else:
|
||||
# multiple queries and sorts
|
||||
queries, sorts = zip(*(parse_query_string(q, Model)
|
||||
for q in qs))
|
||||
query = OrQuery(queries)
|
||||
final_sorts = []
|
||||
for s in sorts:
|
||||
if s:
|
||||
if isinstance(s, MultipleSort):
|
||||
final_sorts += s.sorts
|
||||
else:
|
||||
final_sorts.append(s)
|
||||
if not final_sorts:
|
||||
sort = None
|
||||
elif len(final_sorts) == 1:
|
||||
sort, = final_sorts
|
||||
else:
|
||||
sort = MultipleSort(final_sorts)
|
||||
query_and_sort = query, sort
|
||||
if 'name' not in playlist:
|
||||
self._log.warn("playlist configuration is missing name")
|
||||
continue
|
||||
|
||||
playlist_data += (query_and_sort,)
|
||||
playlist_data = (playlist['name'],)
|
||||
try:
|
||||
for key, Model in (('query', Item), ('album_query', Album)):
|
||||
qs = playlist.get(key)
|
||||
if qs is None:
|
||||
query_and_sort = None, None
|
||||
elif isinstance(qs, basestring):
|
||||
query_and_sort = parse_query_string(qs, Model)
|
||||
elif len(qs) == 1:
|
||||
query_and_sort = parse_query_string(qs[0], Model)
|
||||
else:
|
||||
# multiple queries and sorts
|
||||
queries, sorts = zip(*(parse_query_string(q, Model)
|
||||
for q in qs))
|
||||
query = OrQuery(queries)
|
||||
final_sorts = []
|
||||
for s in sorts:
|
||||
if s:
|
||||
if isinstance(s, MultipleSort):
|
||||
final_sorts += s.sorts
|
||||
else:
|
||||
final_sorts.append(s)
|
||||
if not final_sorts:
|
||||
sort = None
|
||||
elif len(final_sorts) == 1:
|
||||
sort, = final_sorts
|
||||
else:
|
||||
sort = MultipleSort(final_sorts)
|
||||
query_and_sort = query, sort
|
||||
|
||||
playlist_data += (query_and_sort,)
|
||||
|
||||
except ParsingError as exc:
|
||||
self._log.warn("invalid query in playlist {}: {}",
|
||||
playlist['name'], exc)
|
||||
continue
|
||||
|
||||
self._unmatched_playlists.add(playlist_data)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,29 @@ Changelog
|
|||
with the items and the fields of the items you want to edit. Afterwards you can
|
||||
review your changes save them back into the items.
|
||||
|
||||
New:
|
||||
|
||||
* Three commands, ``modify``, ``update``, and ``mbsync``, would previously
|
||||
move files by default after changing their metadata. Now, these commands
|
||||
will only move files if you have the :ref:`config-import-copy` or
|
||||
:ref:`config-import-move` options enabled in your importer configuration.
|
||||
This way, if you configure the importer not to touch your filenames, other
|
||||
commands will respect that decision by default too. Each command also
|
||||
sprouted a ``--move`` command-line option to override this default (in
|
||||
addition to the ``--nomove`` flag they already had). :bug:`1697`
|
||||
* A new configuration option, ``va_name``, controls the album artist name for
|
||||
various-artists albums. The setting defaults to "Various Artists," the
|
||||
MusicBrainz standard. In order to match MusicBrainz, the
|
||||
:doc:`/plugins/discogs` also adopts the same setting.
|
||||
* :doc:`/plugins/embyupdate`: A plugin to trigger a library refresh on a
|
||||
`Emby Server`_ if database changed.
|
||||
|
||||
For developers:
|
||||
|
||||
* :doc:`/dev/plugins`: Two new hooks, ``albuminfo_received`` and
|
||||
``trackinfo_received``, let plugins intercept metadata as soon as it is
|
||||
received, before it is applied to music in the database. :bug:`872`
|
||||
|
||||
Fixes:
|
||||
|
||||
* :doc:`/plugins/plexupdate`: Fix a crash when Plex libraries use non-ASCII
|
||||
|
|
@ -35,6 +58,12 @@ Fixes:
|
|||
* :doc:`/plugins/metasync`: Fix a crash when syncing with recent versions of
|
||||
iTunes. :bug:`1700`
|
||||
* :doc:`/plugins/duplicates`: Fix a crash when merging items. :bug:`1699`
|
||||
* :doc:`/plugins/smartplaylist`: More gracefully handle malformed queries and
|
||||
missing configuration.
|
||||
* Fix a crash with some files with unreadable iTunes SoundCheck metadata.
|
||||
:bug:`1666`
|
||||
|
||||
.. _Emby Server: http://emby.media
|
||||
|
||||
|
||||
1.3.15 (October 17, 2015)
|
||||
|
|
|
|||
|
|
@ -133,39 +133,39 @@ registration process in this case::
|
|||
|
||||
The events currently available are:
|
||||
|
||||
* *pluginload*: called after all the plugins have been loaded after the ``beet``
|
||||
* `pluginload`: called after all the plugins have been loaded after the ``beet``
|
||||
command starts
|
||||
|
||||
* *import*: called after a ``beet import`` command finishes (the ``lib`` keyword
|
||||
* `import`: called after a ``beet import`` command finishes (the ``lib`` keyword
|
||||
argument is a Library object; ``paths`` is a list of paths (strings) that were
|
||||
imported)
|
||||
|
||||
* *album_imported*: called with an ``Album`` object every time the ``import``
|
||||
* `album_imported`: called with an ``Album`` object every time the ``import``
|
||||
command finishes adding an album to the library. Parameters: ``lib``,
|
||||
``album``
|
||||
|
||||
* *item_copied*: called with an ``Item`` object whenever its file is copied.
|
||||
* `item_copied`: called with an ``Item`` object whenever its file is copied.
|
||||
Parameters: ``item``, ``source`` path, ``destination`` path
|
||||
|
||||
* *item_imported*: called with an ``Item`` object every time the importer adds a
|
||||
* `item_imported`: called with an ``Item`` object every time the importer adds a
|
||||
singleton to the library (not called for full-album imports). Parameters:
|
||||
``lib``, ``item``
|
||||
|
||||
* *before_item_moved*: called with an ``Item`` object immediately before its
|
||||
* `before_item_moved`: called with an ``Item`` object immediately before its
|
||||
file is moved. Parameters: ``item``, ``source`` path, ``destination`` path
|
||||
|
||||
* *item_moved*: called with an ``Item`` object whenever its file is moved.
|
||||
* `item_moved`: called with an ``Item`` object whenever its file is moved.
|
||||
Parameters: ``item``, ``source`` path, ``destination`` path
|
||||
|
||||
* *item_linked*: called with an ``Item`` object whenever a symlink is created
|
||||
* `item_linked`: called with an ``Item`` object whenever a symlink is created
|
||||
for a file.
|
||||
Parameters: ``item``, ``source`` path, ``destination`` path
|
||||
|
||||
* *item_removed*: called with an ``Item`` object every time an item (singleton
|
||||
* `item_removed`: called with an ``Item`` object every time an item (singleton
|
||||
or album's part) is removed from the library (even when its file is not
|
||||
deleted from disk).
|
||||
|
||||
* *write*: called with an ``Item`` object, a ``path``, and a ``tags``
|
||||
* `write`: called with an ``Item`` object, a ``path``, and a ``tags``
|
||||
dictionary just before a file's metadata is written to disk (i.e.,
|
||||
just before the file on disk is opened). Event handlers may change
|
||||
the ``tags`` dictionary to customize the tags that are written to the
|
||||
|
|
@ -174,46 +174,59 @@ The events currently available are:
|
|||
operation. Beets will catch that exception, print an error message
|
||||
and continue.
|
||||
|
||||
* *after_write*: called with an ``Item`` object after a file's metadata is
|
||||
* `after_write`: called with an ``Item`` object after a file's metadata is
|
||||
written to disk (i.e., just after the file on disk is closed).
|
||||
|
||||
* *import_task_created*: called immediately after an import task is
|
||||
* `import_task_created`: called immediately after an import task is
|
||||
initialized. Plugins can use this to, for example, change imported files of a
|
||||
task before anything else happens. It's also possible to replace the task
|
||||
with another task by returning a list of tasks. This list can contain zero
|
||||
or more `ImportTask`s. Returning an empty list will stop the task.
|
||||
Parameters: ``task`` (an `ImportTask`) and ``session`` (an `ImportSession`).
|
||||
|
||||
* *import_task_start*: called when before an import task begins processing.
|
||||
* `import_task_start`: called when before an import task begins processing.
|
||||
Parameters: ``task`` and ``session``.
|
||||
|
||||
* *import_task_apply*: called after metadata changes have been applied in an
|
||||
* `import_task_apply`: called after metadata changes have been applied in an
|
||||
import task. This is called on the same thread as the UI, so use this
|
||||
sparingly and only for tasks that can be done quickly. For most plugins, an
|
||||
import pipeline stage is a better choice (see :ref:`plugin-stage`).
|
||||
Parameters: ``task`` and ``session``.
|
||||
|
||||
* *import_task_choice*: called after a decision has been made about an import
|
||||
* `import_task_choice`: called after a decision has been made about an import
|
||||
task. This event can be used to initiate further interaction with the user.
|
||||
Use ``task.choice_flag`` to determine or change the action to be
|
||||
taken. Parameters: ``task`` and ``session``.
|
||||
|
||||
* *import_task_files*: called after an import task finishes manipulating the
|
||||
* `import_task_files`: called after an import task finishes manipulating the
|
||||
filesystem (copying and moving files, writing metadata tags). Parameters:
|
||||
``task`` and ``session``.
|
||||
|
||||
* *library_opened*: called after beets starts up and initializes the main
|
||||
* `library_opened`: called after beets starts up and initializes the main
|
||||
Library object. Parameter: ``lib``.
|
||||
|
||||
* *database_change*: a modification has been made to the library database. The
|
||||
* `database_change`: a modification has been made to the library database. The
|
||||
change might not be committed yet. Parameters: ``lib`` and ``model``.
|
||||
|
||||
* *cli_exit*: called just before the ``beet`` command-line program exits.
|
||||
* `cli_exit`: called just before the ``beet`` command-line program exits.
|
||||
Parameter: ``lib``.
|
||||
|
||||
* *import_begin*: called just before a ``beet import`` session starts up.
|
||||
* `import_begin`: called just before a ``beet import`` session starts up.
|
||||
Parameter: ``session``.
|
||||
|
||||
* `trackinfo_received`: called after metadata for a track item has been
|
||||
fetched from a data source, such as MusicBrainz. You can modify the tags
|
||||
that the rest of the pipeline sees on a ``beet import`` operation or during
|
||||
later adjustments, such as ``mbsync``. Slow handlers of the event can impact
|
||||
the operation, since the event is fired for any fetched possible match
|
||||
`before` the user (or the autotagger machinery) gets to see the match.
|
||||
Parameter: ``info``.
|
||||
|
||||
* `albuminfo_received`: like `trackinfo_received`, the event indicates new
|
||||
metadata for album items. The parameter is an ``AlbumInfo`` object instead
|
||||
of a ``TrackInfo``.
|
||||
Parameter: ``info``.
|
||||
|
||||
The included ``mpdupdate`` plugin provides an example use case for event listeners.
|
||||
|
||||
Extend the Autotagger
|
||||
|
|
|
|||
|
|
@ -67,9 +67,6 @@ file. The available options mirror the command-line options:
|
|||
- **full**: List every track or album that has duplicates, not just the
|
||||
duplicates themselves.
|
||||
Default: ``no``
|
||||
- **strict**: Do not report duplicate matches if some of the
|
||||
attributes are not defined (ie. null or empty).
|
||||
Default: ``no``
|
||||
- **keys**: Define in which track or album fields duplicates are to be
|
||||
searched. By default, the plugin uses the musicbrainz track and album IDs for
|
||||
this purpose. Using the ``keys`` option (as a YAML list in the configuration
|
||||
|
|
@ -83,6 +80,9 @@ file. The available options mirror the command-line options:
|
|||
Default: none (disabled).
|
||||
- **path**: Output the path instead of metadata when listing duplicates.
|
||||
Default: ``no``.
|
||||
- **strict**: Do not report duplicate matches if some of the
|
||||
attributes are not defined (ie. null or empty).
|
||||
Default: ``no``
|
||||
- **tag**: A ``key=value`` pair. The plugin will add a new ``key`` attribute
|
||||
with ``value`` value as a flexattr to the database for duplicate items.
|
||||
Default: ``no``.
|
||||
|
|
|
|||
33
docs/plugins/embyupdate.rst
Normal file
33
docs/plugins/embyupdate.rst
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
EmbyUpdate Plugin
|
||||
=================
|
||||
|
||||
``embyupdate`` is a plugin that lets you automatically update `Emby`_'s library whenever you change your beets library.
|
||||
|
||||
To use ``embyupdate`` plugin, enable it in your configuration (see :ref:`using-plugins`). Then, you'll probably want to configure the specifics of your Emby server. You can do that using an ``emby:`` section in your ``config.yaml``, which looks like this::
|
||||
|
||||
emby:
|
||||
host: localhost
|
||||
port: 8096
|
||||
username: user
|
||||
password: password
|
||||
|
||||
To use the ``embyupdate`` plugin you need to install the `requests`_ library with::
|
||||
|
||||
pip install requests
|
||||
|
||||
With that all in place, you'll see beets send the "update" command to your Emby server every time you change your beets library.
|
||||
|
||||
.. _Emby: http://emby.media/
|
||||
.. _requests: http://docs.python-requests.org/en/latest/
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
The available options under the ``emby:`` section are:
|
||||
|
||||
- **host**: The Emby server name.
|
||||
Default: ``localhost``
|
||||
- **port**: The Emby server port.
|
||||
Default: 8096
|
||||
- **username**: A username of a Emby user that is allowed to refresh the library.
|
||||
- **password**: That user's password.
|
||||
|
|
@ -13,7 +13,7 @@ Using Plugins
|
|||
-------------
|
||||
|
||||
To use one of the plugins included with beets (see the rest of this page for a
|
||||
list), just use the `plugins` option in your :doc:`config.yaml </reference/config>`: file, like so::
|
||||
list), just use the `plugins` option in your :doc:`config.yaml </reference/config>` file, like so::
|
||||
|
||||
plugins: inline convert web
|
||||
|
||||
|
|
@ -42,6 +42,7 @@ Each plugin has its own set of options that can be defined in a section bearing
|
|||
echonest
|
||||
edit
|
||||
embedart
|
||||
embyupdate
|
||||
fetchart
|
||||
fromfilename
|
||||
ftintitle
|
||||
|
|
@ -133,6 +134,7 @@ Path Formats
|
|||
Interoperability
|
||||
----------------
|
||||
|
||||
* :doc:`embyupdate`: Automatically notifies `Emby`_ whenever the beets library changes.
|
||||
* :doc:`importfeeds`: Keep track of imported files via ``.m3u`` playlist file(s) or symlinks.
|
||||
* :doc:`ipfs`: Import libraries from friends and get albums from them via ipfs.
|
||||
* :doc:`mpdupdate`: Automatically notifies `MPD`_ whenever the beets library
|
||||
|
|
@ -145,6 +147,7 @@ Interoperability
|
|||
* :doc:`badfiles`: Check audio file integrity.
|
||||
|
||||
|
||||
.. _Emby: http://emby.media
|
||||
.. _Plex: http://plex.tv
|
||||
|
||||
Miscellaneous
|
||||
|
|
@ -217,6 +220,8 @@ Here are a few of the plugins written by the beets community:
|
|||
|
||||
* `whatlastgenre`_ fetches genres from various music sites.
|
||||
|
||||
* `beets-usertag`_ lets you use keywords to tag and organize your music.
|
||||
|
||||
.. _beets-check: https://github.com/geigerzaehler/beets-check
|
||||
.. _copyartifacts: https://github.com/sbarakat/beets-copyartifacts
|
||||
.. _dsedivec: https://github.com/dsedivec/beets-plugins
|
||||
|
|
@ -233,3 +238,4 @@ Here are a few of the plugins written by the beets community:
|
|||
.. _beets-setlister: https://github.com/tomjaspers/beets-setlister
|
||||
.. _beets-noimport: https://github.com/ttsda/beets-noimport
|
||||
.. _whatlastgenre: https://github.com/YetAnotherNerd/whatlastgenre/tree/master/plugin/beets
|
||||
.. _beets-usertag: https://github.com/igordertigor/beets-usertag
|
||||
|
|
|
|||
|
|
@ -304,6 +304,15 @@ By default, beets writes MP3 tags using the ID3v2.4 standard, the latest
|
|||
version of ID3. Enable this option to instead use the older ID3v2.3 standard,
|
||||
which is preferred by certain older software such as Windows Media Player.
|
||||
|
||||
.. _va_name:
|
||||
|
||||
va_name
|
||||
~~~~~~~
|
||||
|
||||
Sets the albumartist for various-artist compilations. Defaults to ``'Various
|
||||
Artists'`` (the MusicBrainz standard). Affects other sources, such as
|
||||
:doc:`/plugins/discogs`, too.
|
||||
|
||||
|
||||
UI Options
|
||||
----------
|
||||
|
|
@ -370,6 +379,8 @@ Either ``yes`` or ``no``, controlling whether metadata (e.g., ID3) tags are
|
|||
written to files when using ``beet import``. Defaults to ``yes``. The ``-w``
|
||||
and ``-W`` command-line options override this setting.
|
||||
|
||||
.. _config-import-copy:
|
||||
|
||||
copy
|
||||
~~~~
|
||||
|
||||
|
|
@ -380,6 +391,8 @@ overridden with the ``-c`` and ``-C`` command-line options.
|
|||
The option is ignored if ``move`` is enabled (i.e., beets can move or
|
||||
copy files but it doesn't make sense to do both).
|
||||
|
||||
.. _config-import-move:
|
||||
|
||||
move
|
||||
~~~~
|
||||
|
||||
|
|
|
|||
BIN
test/rsrc/soundcheck-nonascii.m4a
Normal file
BIN
test/rsrc/soundcheck-nonascii.m4a
Normal file
Binary file not shown.
212
test/test_embyupdate.py
Normal file
212
test/test_embyupdate.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
from __future__ import (division, absolute_import, print_function,
|
||||
unicode_literals)
|
||||
|
||||
from test._common import unittest
|
||||
from test.helper import TestHelper
|
||||
from beetsplug import embyupdate
|
||||
import responses
|
||||
|
||||
|
||||
class EmbyUpdateTest(unittest.TestCase, TestHelper):
|
||||
def setUp(self):
|
||||
self.setup_beets()
|
||||
self.load_plugins('embyupdate')
|
||||
|
||||
self.config['emby'] = {
|
||||
u'host': u'localhost',
|
||||
u'port': 8096,
|
||||
u'username': u'username',
|
||||
u'password': u'password'
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
self.teardown_beets()
|
||||
self.unload_plugins()
|
||||
|
||||
def test_api_url(self):
|
||||
self.assertEqual(
|
||||
embyupdate.api_url(self.config['emby']['host'].get(),
|
||||
self.config['emby']['port'].get(),
|
||||
'/Library/Refresh'),
|
||||
'http://localhost:8096/Library/Refresh?format=json'
|
||||
)
|
||||
|
||||
def test_password_data(self):
|
||||
self.assertEqual(
|
||||
embyupdate.password_data(self.config['emby']['username'].get(),
|
||||
self.config['emby']['password'].get()),
|
||||
{
|
||||
'username': 'username',
|
||||
'password': '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8',
|
||||
'passwordMd5': '5f4dcc3b5aa765d61d8327deb882cf99'
|
||||
}
|
||||
)
|
||||
|
||||
def test_create_header_no_token(self):
|
||||
self.assertEqual(
|
||||
embyupdate.create_headers('e8837bc1-ad67-520e-8cd2-f629e3155721'),
|
||||
{
|
||||
'Authorization': 'MediaBrowser',
|
||||
'UserId': 'e8837bc1-ad67-520e-8cd2-f629e3155721',
|
||||
'Client': 'other',
|
||||
'Device': 'empy',
|
||||
'DeviceId': 'beets',
|
||||
'Version': '0.0.0'
|
||||
}
|
||||
)
|
||||
|
||||
def test_create_header_with_token(self):
|
||||
self.assertEqual(
|
||||
embyupdate.create_headers('e8837bc1-ad67-520e-8cd2-f629e3155721',
|
||||
token='abc123'),
|
||||
{
|
||||
'Authorization': 'MediaBrowser',
|
||||
'UserId': 'e8837bc1-ad67-520e-8cd2-f629e3155721',
|
||||
'Client': 'other',
|
||||
'Device': 'empy',
|
||||
'DeviceId': 'beets',
|
||||
'Version': '0.0.0',
|
||||
'X-MediaBrowser-Token': 'abc123'
|
||||
}
|
||||
)
|
||||
|
||||
@responses.activate
|
||||
def test_get_token(self):
|
||||
body = ('{"User":{"Name":"username", '
|
||||
'"ServerId":"1efa5077976bfa92bc71652404f646ec",'
|
||||
'"Id":"2ec276a2642e54a19b612b9418a8bd3b","HasPassword":true,'
|
||||
'"HasConfiguredPassword":true,'
|
||||
'"HasConfiguredEasyPassword":false,'
|
||||
'"LastLoginDate":"2015-11-09T08:35:03.6357440Z",'
|
||||
'"LastActivityDate":"2015-11-09T08:35:03.6665060Z",'
|
||||
'"Configuration":{"AudioLanguagePreference":"",'
|
||||
'"PlayDefaultAudioTrack":true,"SubtitleLanguagePreference":"",'
|
||||
'"DisplayMissingEpisodes":false,'
|
||||
'"DisplayUnairedEpisodes":false,'
|
||||
'"GroupMoviesIntoBoxSets":false,'
|
||||
'"DisplayChannelsWithinViews":[],'
|
||||
'"ExcludeFoldersFromGrouping":[],"GroupedFolders":[],'
|
||||
'"SubtitleMode":"Default","DisplayCollectionsView":true,'
|
||||
'"DisplayFoldersView":false,"EnableLocalPassword":false,'
|
||||
'"OrderedViews":[],"IncludeTrailersInSuggestions":true,'
|
||||
'"EnableCinemaMode":true,"LatestItemsExcludes":[],'
|
||||
'"PlainFolderViews":[],"HidePlayedInLatest":true,'
|
||||
'"DisplayChannelsInline":false},'
|
||||
'"Policy":{"IsAdministrator":true,"IsHidden":false,'
|
||||
'"IsDisabled":false,"BlockedTags":[],'
|
||||
'"EnableUserPreferenceAccess":true,"AccessSchedules":[],'
|
||||
'"BlockUnratedItems":[],'
|
||||
'"EnableRemoteControlOfOtherUsers":false,'
|
||||
'"EnableSharedDeviceControl":true,'
|
||||
'"EnableLiveTvManagement":true,"EnableLiveTvAccess":true,'
|
||||
'"EnableMediaPlayback":true,'
|
||||
'"EnableAudioPlaybackTranscoding":true,'
|
||||
'"EnableVideoPlaybackTranscoding":true,'
|
||||
'"EnableContentDeletion":false,'
|
||||
'"EnableContentDownloading":true,"EnableSync":true,'
|
||||
'"EnableSyncTranscoding":true,"EnabledDevices":[],'
|
||||
'"EnableAllDevices":true,"EnabledChannels":[],'
|
||||
'"EnableAllChannels":true,"EnabledFolders":[],'
|
||||
'"EnableAllFolders":true,"InvalidLoginAttemptCount":0,'
|
||||
'"EnablePublicSharing":true}},'
|
||||
'"SessionInfo":{"SupportedCommands":[],'
|
||||
'"QueueableMediaTypes":[],"PlayableMediaTypes":[],'
|
||||
'"Id":"89f3b33f8b3a56af22088733ad1d76b3",'
|
||||
'"UserId":"2ec276a2642e54a19b612b9418a8bd3b",'
|
||||
'"UserName":"username","AdditionalUsers":[],'
|
||||
'"ApplicationVersion":"Unknown version",'
|
||||
'"Client":"Unknown app",'
|
||||
'"LastActivityDate":"2015-11-09T08:35:03.6665060Z",'
|
||||
'"DeviceName":"Unknown device","DeviceId":"Unknown device id",'
|
||||
'"SupportsRemoteControl":false,"PlayState":{"CanSeek":false,'
|
||||
'"IsPaused":false,"IsMuted":false,"RepeatMode":"RepeatNone"}},'
|
||||
'"AccessToken":"4b19180cf02748f7b95c7e8e76562fc8",'
|
||||
'"ServerId":"1efa5077976bfa92bc71652404f646ec"}')
|
||||
|
||||
responses.add(responses.POST,
|
||||
('http://localhost:8096'
|
||||
'/Users/AuthenticateByName'),
|
||||
body=body,
|
||||
status=200,
|
||||
content_type='application/json')
|
||||
|
||||
headers = {
|
||||
'Authorization': 'MediaBrowser',
|
||||
'UserId': 'e8837bc1-ad67-520e-8cd2-f629e3155721',
|
||||
'Client': 'other',
|
||||
'Device': 'empy',
|
||||
'DeviceId': 'beets',
|
||||
'Version': '0.0.0'
|
||||
}
|
||||
|
||||
auth_data = {
|
||||
'username': 'username',
|
||||
'password': '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8',
|
||||
'passwordMd5': '5f4dcc3b5aa765d61d8327deb882cf99'
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
embyupdate.get_token('localhost', 8096, headers, auth_data),
|
||||
'4b19180cf02748f7b95c7e8e76562fc8')
|
||||
|
||||
@responses.activate
|
||||
def test_get_user(self):
|
||||
body = ('[{"Name":"username",'
|
||||
'"ServerId":"1efa5077976bfa92bc71652404f646ec",'
|
||||
'"Id":"2ec276a2642e54a19b612b9418a8bd3b","HasPassword":true,'
|
||||
'"HasConfiguredPassword":true,'
|
||||
'"HasConfiguredEasyPassword":false,'
|
||||
'"LastLoginDate":"2015-11-09T08:35:03.6357440Z",'
|
||||
'"LastActivityDate":"2015-11-09T08:42:39.3693220Z",'
|
||||
'"Configuration":{"AudioLanguagePreference":"",'
|
||||
'"PlayDefaultAudioTrack":true,"SubtitleLanguagePreference":"",'
|
||||
'"DisplayMissingEpisodes":false,'
|
||||
'"DisplayUnairedEpisodes":false,'
|
||||
'"GroupMoviesIntoBoxSets":false,'
|
||||
'"DisplayChannelsWithinViews":[],'
|
||||
'"ExcludeFoldersFromGrouping":[],"GroupedFolders":[],'
|
||||
'"SubtitleMode":"Default","DisplayCollectionsView":true,'
|
||||
'"DisplayFoldersView":false,"EnableLocalPassword":false,'
|
||||
'"OrderedViews":[],"IncludeTrailersInSuggestions":true,'
|
||||
'"EnableCinemaMode":true,"LatestItemsExcludes":[],'
|
||||
'"PlainFolderViews":[],"HidePlayedInLatest":true,'
|
||||
'"DisplayChannelsInline":false},'
|
||||
'"Policy":{"IsAdministrator":true,"IsHidden":false,'
|
||||
'"IsDisabled":false,"BlockedTags":[],'
|
||||
'"EnableUserPreferenceAccess":true,"AccessSchedules":[],'
|
||||
'"BlockUnratedItems":[],'
|
||||
'"EnableRemoteControlOfOtherUsers":false,'
|
||||
'"EnableSharedDeviceControl":true,'
|
||||
'"EnableLiveTvManagement":true,"EnableLiveTvAccess":true,'
|
||||
'"EnableMediaPlayback":true,'
|
||||
'"EnableAudioPlaybackTranscoding":true,'
|
||||
'"EnableVideoPlaybackTranscoding":true,'
|
||||
'"EnableContentDeletion":false,'
|
||||
'"EnableContentDownloading":true,'
|
||||
'"EnableSync":true,"EnableSyncTranscoding":true,'
|
||||
'"EnabledDevices":[],"EnableAllDevices":true,'
|
||||
'"EnabledChannels":[],"EnableAllChannels":true,'
|
||||
'"EnabledFolders":[],"EnableAllFolders":true,'
|
||||
'"InvalidLoginAttemptCount":0,"EnablePublicSharing":true}}]')
|
||||
|
||||
responses.add(responses.GET,
|
||||
'http://localhost:8096/Users/Public',
|
||||
body=body,
|
||||
status=200,
|
||||
content_type='application/json')
|
||||
|
||||
response = embyupdate.get_user('localhost', 8096, 'username')
|
||||
|
||||
self.assertEqual(response[0]['Id'],
|
||||
'2ec276a2642e54a19b612b9418a8bd3b')
|
||||
|
||||
self.assertEqual(response[0]['Name'],
|
||||
'username')
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
||||
|
||||
if __name__ == b'__main__':
|
||||
unittest.main(defaultTest='suite')
|
||||
|
|
@ -89,6 +89,13 @@ class EdgeTest(unittest.TestCase):
|
|||
beets.mediafile._image_mime_type(jpg_data),
|
||||
'image/jpeg')
|
||||
|
||||
def test_soundcheck_non_ascii(self):
|
||||
# Make sure we don't crash when the iTunes SoundCheck field contains
|
||||
# non-ASCII binary data.
|
||||
f = beets.mediafile.MediaFile(os.path.join(_common.RSRC,
|
||||
'soundcheck-nonascii.m4a'))
|
||||
self.assertEqual(f.rg_track_gain, 0.0)
|
||||
|
||||
|
||||
class InvalidValueToleranceTest(unittest.TestCase):
|
||||
|
||||
|
|
@ -269,19 +276,19 @@ class SoundCheckTest(unittest.TestCase):
|
|||
self.assertEqual(peak, 1.0)
|
||||
|
||||
def test_decode_zero(self):
|
||||
data = u' 80000000 80000000 00000000 00000000 00000000 00000000 ' \
|
||||
u'00000000 00000000 00000000 00000000'
|
||||
data = b' 80000000 80000000 00000000 00000000 00000000 00000000 ' \
|
||||
b'00000000 00000000 00000000 00000000'
|
||||
gain, peak = beets.mediafile._sc_decode(data)
|
||||
self.assertEqual(gain, 0.0)
|
||||
self.assertEqual(peak, 0.0)
|
||||
|
||||
def test_malformatted(self):
|
||||
gain, peak = beets.mediafile._sc_decode(u'foo')
|
||||
gain, peak = beets.mediafile._sc_decode(b'foo')
|
||||
self.assertEqual(gain, 0.0)
|
||||
self.assertEqual(peak, 0.0)
|
||||
|
||||
def test_special_characters(self):
|
||||
gain, peak = beets.mediafile._sc_decode(u'caf\xe9')
|
||||
gain, peak = beets.mediafile._sc_decode(u'caf\xe9'.encode('utf8'))
|
||||
self.assertEqual(gain, 0.0)
|
||||
self.assertEqual(peak, 0.0)
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class UtilTest(unittest.TestCase):
|
|||
@patch('beets.util.open_anything')
|
||||
def test_interactive_open(self, mock_open, mock_execlp):
|
||||
mock_open.return_value = 'tagada'
|
||||
util.interactive_open(['foo'])
|
||||
util.interactive_open(['foo'], util.open_anything())
|
||||
mock_execlp.assert_called_once_with('tagada', 'tagada', 'foo')
|
||||
mock_execlp.reset_mock()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue