/
autopilot.py
210 lines (158 loc) · 7.5 KB
/
autopilot.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import recommend
import xmmsclient
import xmmsclient.collections
import argparse
import collections
import logging
import random
import time
# FIXME outdated bindings
xmmsclient.PLAYLIST_CHANGED_REPLACE = 8
class Autopilot(object):
FAST_SONG_CHANGE_FACTOR = 0.2
def __init__(self):
self.xsync = xmmsclient.XMMSSync("autopilot-sync")
self.xsync.connect()
self.xmms_config_keys = {} # xmms config key -> setter
self.register_attr_as_xmms_config(self, "FAST_SONG_CHANGE_FACTOR")
self.register_attr_as_xmms_config(recommend, "MIN_GRAPH_SIZE")
self.register_attr_as_xmms_config(recommend, "MIN_CANDIDATES")
self.register_attr_as_xmms_config(recommend, "MAX_CANDIDATE_DIST")
self.register_attr_as_xmms_config(recommend, "MAX_OUT_DEGREE")
self.register_attr_as_xmms_config(recommend, "MAX_IN_DEGREE")
self.register_attr_as_xmms_config(recommend, "FEEDBACK_WEIGHT_HIGH")
self.register_attr_as_xmms_config(recommend, "FEEDBACK_WEIGHT_LOW")
self.load_xmms_config(self.xsync.config_list_values())
self.xasync = xmmsclient.XMMS("autopilot-async")
self.xasync.connect()
self.reset_playlist_cache()
self.xasync.broadcast_playlist_loaded(cb = self.on_playlist_loaded)
self.xasync.broadcast_playlist_changed(cb = self.on_playlist_changed)
self.xasync.broadcast_playlist_current_pos(cb = self.on_current_pos)
self.xasync.broadcast_playback_current_id(cb = self.on_current_id)
self.xasync.broadcast_config_value_changed(cb = self.on_config_changed)
logging.info("autopilot setup, starting mainloop")
self.xasync.loop()
def register_attr_as_xmms_config(self, obj, attr):
default = getattr(obj, attr)
original_type = type(default) # need to convert back from str
key = "autopilot." + attr.lower()
xmms_config_key = self.xsync.config_register_value(key, str(default))
self.xmms_config_keys[xmms_config_key] = \
lambda v: setattr(obj, attr, original_type(v))
def load_xmms_config(self, xmms_config):
for key, setter in self.xmms_config_keys.items():
if key in xmms_config:
logging.info("loaded config '%s': %s", key, xmms_config[key])
setter(xmms_config[key])
def on_config_changed(self, config_val):
self.load_xmms_config(config_val.get_dict())
def on_playlist_loaded(self, pls_val):
self.reset_playlist_cache()
def on_current_pos(self, pos_val):
self.fill_playlist()
def on_current_id(self, id_val):
if len(self.last_mids) == 2:
previous, skipped = self.last_mids
infos = self.query_infos_for_mid(skipped, ("duration", "laststarted"))
playtime = time.time() - infos["laststarted"]
duration = infos["duration"] / 1000.0 # ms -> s
if playtime < duration*self.FAST_SONG_CHANGE_FACTOR:
logging.debug("fast song change, giving negative feedback")
recommend.negative(previous,
skipped,
recommend.FEEDBACK_WEIGHT_LOW)
self.last_mids.append(id_val.get_int())
self.fill_playlist(id_val.get_int())
return True
def on_playlist_changed(self, changed_val):
changed_dict = changed_val.get_dict()
type = changed_dict["type"]
if type not in (xmmsclient.PLAYLIST_CHANGED_INSERT,
xmmsclient.PLAYLIST_CHANGED_MOVE,
xmmsclient.PLAYLIST_CHANGED_REMOVE,
xmmsclient.PLAYLIST_CHANGED_REPLACE):
return True
if type == xmmsclient.PLAYLIST_CHANGED_REPLACE:
logging.debug("playlist replaced, resetting")
self.reset_playlist_cache()
return True
pos = changed_dict["position"]
if type == xmmsclient.PLAYLIST_CHANGED_REMOVE:
logging.debug("removal dict: %s", changed_dict)
if pos > 0:
recommend.negative(self.playlist_entries_cache[pos-1],
self.playlist_entries_cache[pos],
recommend.FEEDBACK_WEIGHT_HIGH)
del self.playlist_entries_cache[pos]
elif type == xmmsclient.PLAYLIST_CHANGED_INSERT:
logging.debug("insert dict: %s", changed_dict)
mid = changed_dict["id"]
if self.check_own_insertion(pos, mid):
weight = recommend.FEEDBACK_WEIGHT_LOW
else:
weight = recommend.FEEDBACK_WEIGHT_HIGH
if pos > 0:
recommend.positive(self.playlist_entries_cache[pos-1],
mid, weight)
self.playlist_entries_cache.insert(pos, mid)
elif type == xmmsclient.PLAYLIST_CHANGED_MOVE:
logging.debug("move dict: %s", changed_dict)
mid = changed_dict["id"]
newpos = changed_dict["newposition"]
del self.playlist_entries_cache[pos]
self.playlist_entries_cache.insert(newpos, mid)
if newpos > 0:
recommend.positive(self.playlist_entries_cache[newpos-1],
self.playlist_entries_cache[newpos],
recommend.FEEDBACK_WEIGHT_HIGH)
self.fill_playlist()
return True
def choose_random_media(self):
all_media_coll = xmmsclient.coll_parse("in:'All Media'")
return random.choice(self.xsync.coll_query_ids(all_media_coll))
def fill_playlist(self, id_to_draw_next = None):
try:
curr_pos = self.xsync.playlist_current_pos()["position"]
if curr_pos == -1:
return
except xmmsclient.XMMSError:
return
playlist_entries = self.xsync.playlist_list_entries()
if id_to_draw_next is None:
id_to_draw_next = playlist_entries[curr_pos]
if curr_pos == len(playlist_entries)-1:
next = recommend.next(id_to_draw_next,
default = self.choose_random_media())
logging.info("next(%s) -> %s", id_to_draw_next, next)
self.do_insertion(curr_pos+1, next)
def do_insertion(self, pos, mid):
self.insertions.append((pos, mid))
self.xsync.playlist_insert_id(pos, mid)
def check_own_insertion(self, pos, mid):
try:
self.insertions.remove((pos, mid))
except ValueError:
return False
return True
def reset_playlist_cache(self):
self.insertions = []
self.last_song_start_time = 0
self.last_mids = collections.deque(maxlen = 2)
self.playlist_entries_cache = self.xsync.playlist_list_entries()
def query_infos_for_mid(self, mid, fields):
coll = xmmsclient.collections.Equals(type = "id", value = str(mid))
return self.xsync.coll_query_infos(coll, fields)[0]
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--loglevel",
type = str,
choices = ("DEBUG", "INFO"),
default = "INFO")
args = parser.parse_args()
logging.basicConfig(level = getattr(logging, args.loglevel),
format = "%(levelname)s:%(funcName)s:%(lineno)s - "
"%(message)s")
recommend.GRAPH_DOT_FILE = "autopilot_graph.dot"
recommend.GRAPH_PERSISTENCE_FILE = "autopilot_graph.pickle"
autopilot = Autopilot()