update Beets

This commit is contained in:
AdeHub
2024-08-24 16:44:41 +12:00
parent a63098a919
commit 046d4d82b4
116 changed files with 17353 additions and 9964 deletions
+122 -119
View File
@@ -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()