mirror of
https://github.com/rembo10/headphones.git
synced 2026-07-21 08:24:00 +01:00
update Beets
This commit is contained in:
+122
-119
@@ -13,16 +13,14 @@
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
||||
import mpd
|
||||
import time
|
||||
import os
|
||||
import time
|
||||
|
||||
from beets import ui
|
||||
from beets import config
|
||||
from beets import plugins
|
||||
from beets import library
|
||||
from beets.util import displayable_path
|
||||
import mpd
|
||||
|
||||
from beets import config, library, plugins, ui
|
||||
from beets.dbcore import types
|
||||
from beets.util import displayable_path
|
||||
|
||||
# If we lose the connection, how many times do we want to retry and how
|
||||
# much time should we wait between retries?
|
||||
@@ -30,60 +28,55 @@ RETRIES = 10
|
||||
RETRY_INTERVAL = 5
|
||||
|
||||
|
||||
mpd_config = config['mpd']
|
||||
mpd_config = config["mpd"]
|
||||
|
||||
|
||||
def is_url(path):
|
||||
"""Try to determine if the path is an URL.
|
||||
"""
|
||||
"""Try to determine if the path is an URL."""
|
||||
if isinstance(path, bytes): # if it's bytes, then it's a path
|
||||
return False
|
||||
return path.split('://', 1)[0] in ['http', 'https']
|
||||
return path.split("://", 1)[0] in ["http", "https"]
|
||||
|
||||
|
||||
class MPDClientWrapper:
|
||||
def __init__(self, log):
|
||||
self._log = log
|
||||
|
||||
self.music_directory = mpd_config['music_directory'].as_str()
|
||||
self.strip_path = mpd_config['strip_path'].as_str()
|
||||
self.music_directory = mpd_config["music_directory"].as_str()
|
||||
self.strip_path = mpd_config["strip_path"].as_str()
|
||||
|
||||
# Ensure strip_path end with '/'
|
||||
if not self.strip_path.endswith('/'):
|
||||
self.strip_path += '/'
|
||||
if not self.strip_path.endswith("/"):
|
||||
self.strip_path += "/"
|
||||
|
||||
self._log.debug('music_directory: {0}', self.music_directory)
|
||||
self._log.debug('strip_path: {0}', self.strip_path)
|
||||
self._log.debug("music_directory: {0}", self.music_directory)
|
||||
self._log.debug("strip_path: {0}", self.strip_path)
|
||||
|
||||
self.client = mpd.MPDClient()
|
||||
|
||||
def connect(self):
|
||||
"""Connect to the MPD.
|
||||
"""
|
||||
host = mpd_config['host'].as_str()
|
||||
port = mpd_config['port'].get(int)
|
||||
"""Connect to the MPD."""
|
||||
host = mpd_config["host"].as_str()
|
||||
port = mpd_config["port"].get(int)
|
||||
|
||||
if host[0] in ['/', '~']:
|
||||
if host[0] in ["/", "~"]:
|
||||
host = os.path.expanduser(host)
|
||||
|
||||
self._log.info('connecting to {0}:{1}', host, port)
|
||||
self._log.info("connecting to {0}:{1}", host, port)
|
||||
try:
|
||||
self.client.connect(host, port)
|
||||
except OSError as e:
|
||||
raise ui.UserError(f'could not connect to MPD: {e}')
|
||||
raise ui.UserError(f"could not connect to MPD: {e}")
|
||||
|
||||
password = mpd_config['password'].as_str()
|
||||
password = mpd_config["password"].as_str()
|
||||
if password:
|
||||
try:
|
||||
self.client.password(password)
|
||||
except mpd.CommandError as e:
|
||||
raise ui.UserError(
|
||||
f'could not authenticate to MPD: {e}'
|
||||
)
|
||||
raise ui.UserError(f"could not authenticate to MPD: {e}")
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from the MPD.
|
||||
"""
|
||||
"""Disconnect from the MPD."""
|
||||
self.client.close()
|
||||
self.client.disconnect()
|
||||
|
||||
@@ -94,11 +87,11 @@ class MPDClientWrapper:
|
||||
try:
|
||||
return getattr(self.client, command)()
|
||||
except (OSError, mpd.ConnectionError) as err:
|
||||
self._log.error('{0}', err)
|
||||
self._log.error("{0}", err)
|
||||
|
||||
if retries <= 0:
|
||||
# if we exited without breaking, we couldn't reconnect in time :(
|
||||
raise ui.UserError('communication with MPD server failed')
|
||||
raise ui.UserError("communication with MPD server failed")
|
||||
|
||||
time.sleep(RETRY_INTERVAL)
|
||||
|
||||
@@ -119,28 +112,27 @@ class MPDClientWrapper:
|
||||
`strip_path` defaults to ''.
|
||||
"""
|
||||
result = None
|
||||
entry = self.get('currentsong')
|
||||
if 'file' in entry:
|
||||
if not is_url(entry['file']):
|
||||
file = entry['file']
|
||||
entry = self.get("currentsong")
|
||||
if "file" in entry:
|
||||
if not is_url(entry["file"]):
|
||||
file = entry["file"]
|
||||
if file.startswith(self.strip_path):
|
||||
file = file[len(self.strip_path):]
|
||||
file = file[len(self.strip_path) :]
|
||||
result = os.path.join(self.music_directory, file)
|
||||
else:
|
||||
result = entry['file']
|
||||
self._log.debug('returning: {0}', result)
|
||||
return result, entry.get('id')
|
||||
result = entry["file"]
|
||||
self._log.debug("returning: {0}", result)
|
||||
return result, entry.get("id")
|
||||
|
||||
def status(self):
|
||||
"""Return the current status of the MPD.
|
||||
"""
|
||||
return self.get('status')
|
||||
"""Return the current status of the MPD."""
|
||||
return self.get("status")
|
||||
|
||||
def events(self):
|
||||
"""Return list of events. This may block a long time while waiting for
|
||||
an answer from MPD.
|
||||
"""
|
||||
return self.get('idle')
|
||||
return self.get("idle")
|
||||
|
||||
|
||||
class MPDStats:
|
||||
@@ -148,8 +140,8 @@ class MPDStats:
|
||||
self.lib = lib
|
||||
self._log = log
|
||||
|
||||
self.do_rating = mpd_config['rating'].get(bool)
|
||||
self.rating_mix = mpd_config['rating_mix'].get(float)
|
||||
self.do_rating = mpd_config["rating"].get(bool)
|
||||
self.rating_mix = mpd_config["rating_mix"].get(float)
|
||||
self.time_threshold = 10.0 # TODO: maybe add config option?
|
||||
|
||||
self.now_playing = None
|
||||
@@ -160,22 +152,20 @@ class MPDStats:
|
||||
old rating and the fact if it was skipped or not.
|
||||
"""
|
||||
if skipped:
|
||||
rolling = (rating - rating / 2.0)
|
||||
rolling = rating - rating / 2.0
|
||||
else:
|
||||
rolling = (rating + (1.0 - rating) / 2.0)
|
||||
rolling = rating + (1.0 - rating) / 2.0
|
||||
stable = (play_count + 1.0) / (play_count + skip_count + 2.0)
|
||||
return (self.rating_mix * stable +
|
||||
(1.0 - self.rating_mix) * rolling)
|
||||
return self.rating_mix * stable + (1.0 - self.rating_mix) * rolling
|
||||
|
||||
def get_item(self, path):
|
||||
"""Return the beets item related to path.
|
||||
"""
|
||||
query = library.PathQuery('path', path)
|
||||
"""Return the beets item related to path."""
|
||||
query = library.PathQuery("path", path)
|
||||
item = self.lib.items(query).get()
|
||||
if item:
|
||||
return item
|
||||
else:
|
||||
self._log.info('item not found: {0}', displayable_path(path))
|
||||
self._log.info("item not found: {0}", displayable_path(path))
|
||||
|
||||
def update_item(self, item, attribute, value=None, increment=None):
|
||||
"""Update the beets item. Set attribute to value or increment the value
|
||||
@@ -193,10 +183,12 @@ class MPDStats:
|
||||
item[attribute] = value
|
||||
item.store()
|
||||
|
||||
self._log.debug('updated: {0} = {1} [{2}]',
|
||||
attribute,
|
||||
item[attribute],
|
||||
displayable_path(item.path))
|
||||
self._log.debug(
|
||||
"updated: {0} = {1} [{2}]",
|
||||
attribute,
|
||||
item[attribute],
|
||||
displayable_path(item.path),
|
||||
)
|
||||
|
||||
def update_rating(self, item, skipped):
|
||||
"""Update the rating for a beets item. The `item` can either be a
|
||||
@@ -207,12 +199,13 @@ class MPDStats:
|
||||
|
||||
item.load()
|
||||
rating = self.rating(
|
||||
int(item.get('play_count', 0)),
|
||||
int(item.get('skip_count', 0)),
|
||||
float(item.get('rating', 0.5)),
|
||||
skipped)
|
||||
int(item.get("play_count", 0)),
|
||||
int(item.get("skip_count", 0)),
|
||||
float(item.get("rating", 0.5)),
|
||||
skipped,
|
||||
)
|
||||
|
||||
self.update_item(item, 'rating', rating)
|
||||
self.update_item(item, "rating", rating)
|
||||
|
||||
def handle_song_change(self, song):
|
||||
"""Determine if a song was skipped or not and update its attributes.
|
||||
@@ -222,7 +215,7 @@ class MPDStats:
|
||||
|
||||
Returns whether the change was manual (skipped previous song or not)
|
||||
"""
|
||||
diff = abs(song['remaining'] - (time.time() - song['started']))
|
||||
diff = abs(song["remaining"] - (time.time() - song["started"]))
|
||||
|
||||
skipped = diff >= self.time_threshold
|
||||
|
||||
@@ -232,89 +225,89 @@ class MPDStats:
|
||||
self.handle_played(song)
|
||||
|
||||
if self.do_rating:
|
||||
self.update_rating(song['beets_item'], skipped)
|
||||
self.update_rating(song["beets_item"], skipped)
|
||||
|
||||
return skipped
|
||||
|
||||
def handle_played(self, song):
|
||||
"""Updates the play count of a song.
|
||||
"""
|
||||
self.update_item(song['beets_item'], 'play_count', increment=1)
|
||||
self._log.info('played {0}', displayable_path(song['path']))
|
||||
"""Updates the play count of a song."""
|
||||
self.update_item(song["beets_item"], "play_count", increment=1)
|
||||
self._log.info("played {0}", displayable_path(song["path"]))
|
||||
|
||||
def handle_skipped(self, song):
|
||||
"""Updates the skip count of a song.
|
||||
"""
|
||||
self.update_item(song['beets_item'], 'skip_count', increment=1)
|
||||
self._log.info('skipped {0}', displayable_path(song['path']))
|
||||
"""Updates the skip count of a song."""
|
||||
self.update_item(song["beets_item"], "skip_count", increment=1)
|
||||
self._log.info("skipped {0}", displayable_path(song["path"]))
|
||||
|
||||
def on_stop(self, status):
|
||||
self._log.info('stop')
|
||||
self._log.info("stop")
|
||||
|
||||
# if the current song stays the same it means that we stopped on the
|
||||
# current track and should not record a skip.
|
||||
if self.now_playing and self.now_playing['id'] != status.get('songid'):
|
||||
if self.now_playing and self.now_playing["id"] != status.get("songid"):
|
||||
self.handle_song_change(self.now_playing)
|
||||
|
||||
self.now_playing = None
|
||||
|
||||
def on_pause(self, status):
|
||||
self._log.info('pause')
|
||||
self._log.info("pause")
|
||||
self.now_playing = None
|
||||
|
||||
def on_play(self, status):
|
||||
|
||||
path, songid = self.mpd.currentsong()
|
||||
|
||||
if not path:
|
||||
return
|
||||
|
||||
played, duration = map(int, status['time'].split(':', 1))
|
||||
played, duration = map(int, status["time"].split(":", 1))
|
||||
remaining = duration - played
|
||||
|
||||
if self.now_playing:
|
||||
if self.now_playing['path'] != path:
|
||||
if self.now_playing["path"] != path:
|
||||
self.handle_song_change(self.now_playing)
|
||||
else:
|
||||
# In case we got mpd play event with same song playing
|
||||
# multiple times,
|
||||
# assume low diff means redundant second play event
|
||||
# after natural song start.
|
||||
diff = abs(time.time() - self.now_playing['started'])
|
||||
diff = abs(time.time() - self.now_playing["started"])
|
||||
|
||||
if diff <= self.time_threshold:
|
||||
return
|
||||
|
||||
if self.now_playing['path'] == path and played == 0:
|
||||
if self.now_playing["path"] == path and played == 0:
|
||||
self.handle_song_change(self.now_playing)
|
||||
|
||||
if is_url(path):
|
||||
self._log.info('playing stream {0}', displayable_path(path))
|
||||
self._log.info("playing stream {0}", displayable_path(path))
|
||||
self.now_playing = None
|
||||
return
|
||||
|
||||
self._log.info('playing {0}', displayable_path(path))
|
||||
self._log.info("playing {0}", displayable_path(path))
|
||||
|
||||
self.now_playing = {
|
||||
'started': time.time(),
|
||||
'remaining': remaining,
|
||||
'path': path,
|
||||
'id': songid,
|
||||
'beets_item': self.get_item(path),
|
||||
"started": time.time(),
|
||||
"remaining": remaining,
|
||||
"path": path,
|
||||
"id": songid,
|
||||
"beets_item": self.get_item(path),
|
||||
}
|
||||
|
||||
self.update_item(self.now_playing['beets_item'],
|
||||
'last_played', value=int(time.time()))
|
||||
self.update_item(
|
||||
self.now_playing["beets_item"],
|
||||
"last_played",
|
||||
value=int(time.time()),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.mpd.connect()
|
||||
events = ['player']
|
||||
events = ["player"]
|
||||
|
||||
while True:
|
||||
if 'player' in events:
|
||||
if "player" in events:
|
||||
status = self.mpd.status()
|
||||
|
||||
handler = getattr(self, 'on_' + status['state'], None)
|
||||
handler = getattr(self, "on_" + status["state"], None)
|
||||
|
||||
if handler:
|
||||
handler(status)
|
||||
@@ -325,51 +318,61 @@ class MPDStats:
|
||||
|
||||
|
||||
class MPDStatsPlugin(plugins.BeetsPlugin):
|
||||
|
||||
item_types = {
|
||||
'play_count': types.INTEGER,
|
||||
'skip_count': types.INTEGER,
|
||||
'last_played': library.DateType(),
|
||||
'rating': types.FLOAT,
|
||||
"play_count": types.INTEGER,
|
||||
"skip_count": types.INTEGER,
|
||||
"last_played": library.DateType(),
|
||||
"rating": types.FLOAT,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
mpd_config.add({
|
||||
'music_directory': config['directory'].as_filename(),
|
||||
'strip_path': '',
|
||||
'rating': True,
|
||||
'rating_mix': 0.75,
|
||||
'host': os.environ.get('MPD_HOST', 'localhost'),
|
||||
'port': int(os.environ.get('MPD_PORT', 6600)),
|
||||
'password': '',
|
||||
})
|
||||
mpd_config['password'].redact = True
|
||||
mpd_config.add(
|
||||
{
|
||||
"music_directory": config["directory"].as_filename(),
|
||||
"strip_path": "",
|
||||
"rating": True,
|
||||
"rating_mix": 0.75,
|
||||
"host": os.environ.get("MPD_HOST", "localhost"),
|
||||
"port": int(os.environ.get("MPD_PORT", 6600)),
|
||||
"password": "",
|
||||
}
|
||||
)
|
||||
mpd_config["password"].redact = True
|
||||
|
||||
def commands(self):
|
||||
cmd = ui.Subcommand(
|
||||
'mpdstats',
|
||||
help='run a MPD client to gather play statistics')
|
||||
"mpdstats", help="run a MPD client to gather play statistics"
|
||||
)
|
||||
cmd.parser.add_option(
|
||||
'--host', dest='host', type='string',
|
||||
help='set the hostname of the server to connect to')
|
||||
"--host",
|
||||
dest="host",
|
||||
type="string",
|
||||
help="set the hostname of the server to connect to",
|
||||
)
|
||||
cmd.parser.add_option(
|
||||
'--port', dest='port', type='int',
|
||||
help='set the port of the MPD server to connect to')
|
||||
"--port",
|
||||
dest="port",
|
||||
type="int",
|
||||
help="set the port of the MPD server to connect to",
|
||||
)
|
||||
cmd.parser.add_option(
|
||||
'--password', dest='password', type='string',
|
||||
help='set the password of the MPD server to connect to')
|
||||
"--password",
|
||||
dest="password",
|
||||
type="string",
|
||||
help="set the password of the MPD server to connect to",
|
||||
)
|
||||
|
||||
def func(lib, opts, args):
|
||||
mpd_config.set_args(opts)
|
||||
|
||||
# Overrides for MPD settings.
|
||||
if opts.host:
|
||||
mpd_config['host'] = opts.host.decode('utf-8')
|
||||
mpd_config["host"] = opts.host.decode("utf-8")
|
||||
if opts.port:
|
||||
mpd_config['host'] = int(opts.port)
|
||||
mpd_config["host"] = int(opts.port)
|
||||
if opts.password:
|
||||
mpd_config['password'] = opts.password.decode('utf-8')
|
||||
mpd_config["password"] = opts.password.decode("utf-8")
|
||||
|
||||
try:
|
||||
MPDStats(lib, self._log).run()
|
||||
|
||||
Reference in New Issue
Block a user