Skip to content

Commit

Permalink
Implement Play/Skip ratio
Browse files Browse the repository at this point in the history
Up to now, a track was considered played as soon as it was loaded. This
was incorrect in many situations. For example, clicking on 'Stop'
increments the play count.

This commit introduces the following behavior:
- a track is considered played if playback >= 50 % of the track duration
- a track is considered skipped if 'Next' or 'Previous' is clicked and
  playback is < 50 % of the track duration

The Play/Skip ratio of a track can only be updated once every 5 minutes
to account for users coming back and forth on a track.

Closes DocMarty84/koozic#41
  • Loading branch information
DocMarty84 committed Mar 1, 2020
1 parent 6b37782 commit 1403edc
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 4 deletions.
22 changes: 20 additions & 2 deletions models/oomusic_playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,13 +372,31 @@ def oomusic_set_current(self):
self.search([]).playing = False
self.last_play = now if self.playlist_id.dynamic else False
self.playing = True
self.track_id.last_play = now
self.track_id.play_count += 1

# Specific case of a dynamic playlist
self.playlist_id._update_dynamic()
return json.dumps(res)

def oomusic_play_skip(self, play=False):
now = fields.Datetime.now()
# Do not update the Play/Skip ratio more than once every 5 minutes for a given track
if (
self.track_id.last_play_skip_ratio
and (now - self.track_id.last_play_skip_ratio).total_seconds() < 300
):
return True
if play:
self.track_id.last_play = now
self.track_id.play_count += 1
else:
self.track_id.last_skip = now
self.track_id.skip_count += 1
play_count = self.track_id.play_count or 1.0
skip_count = self.track_id.skip_count or 1.0
self.track_id.play_skip_ratio = play_count / skip_count
self.track_id.last_play_skip_ratio = now
return True

def oomusic_play(self, seek=0):
res = {}
if not self:
Expand Down
65 changes: 64 additions & 1 deletion models/oomusic_preference.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ class MusicPreference(models.Model):
default=lambda self: self.env.user,
)
play_count = fields.Integer("Play Count", default=0, readonly=True)
skip_count = fields.Integer("Skip Count", default=0, readonly=True)
play_skip_ratio = fields.Float("Play/Skip Ratio", default=1.0, readonly=True)
last_play = fields.Datetime("Last Played", index=True, readonly=True)
last_skip = fields.Datetime("Last Skipped", index=True, readonly=True)
last_play_skip_ratio = fields.Datetime("Last Play/Skip Update", readonly=True)
star = fields.Selection([("0", "Normal"), ("1", "I Like It!")], "Favorite", default="0")
rating = fields.Selection(
[("0", "0"), ("1", "1"), ("2", "2"), ("3", "3"), ("4", "4"), ("5", "5")],
Expand Down Expand Up @@ -73,6 +77,30 @@ def _inverse_play_count(self):
def _search_play_count(self, operator, value):
return self._search_pref("play_count", operator, value)

@api.depends("pref_ids")
def _compute_skip_count(self):
for obj in self:
obj.skip_count = obj._get_pref("skip_count")

def _inverse_skip_count(self):
for obj in self:
obj._set_pref({"skip_count": obj.skip_count})

def _search_skip_count(self, operator, value):
return self._search_pref("skip_count", operator, value)

@api.depends("pref_ids")
def _compute_play_skip_ratio(self):
for obj in self:
obj.play_skip_ratio = obj._get_pref("play_skip_ratio")

def _inverse_play_skip_ratio(self):
for obj in self:
obj._set_pref({"play_skip_ratio": obj.play_skip_ratio})

def _search_play_skip_ratio(self, operator, value):
return self._search_pref("play_skip_ratio", operator, value)

@api.depends("pref_ids")
def _compute_last_play(self):
for obj in self:
Expand All @@ -85,6 +113,30 @@ def _inverse_last_play(self):
def _search_last_play(self, operator, value):
return self._search_pref("last_play", operator, value)

@api.depends("pref_ids")
def _compute_last_skip(self):
for obj in self:
obj.last_skip = obj._get_pref("last_skip")

def _inverse_last_skip(self):
for obj in self:
obj._set_pref({"last_skip": obj.last_skip})

def _search_last_skip(self, operator, value):
return self._search_pref("last_skip", operator, value)

@api.depends("pref_ids")
def _compute_last_play_skip_ratio(self):
for obj in self:
obj.last_play_skip_ratio = obj._get_pref("last_play_skip_ratio")

def _inverse_last_play_skip_ratio(self):
for obj in self:
obj._set_pref({"last_play_skip_ratio": obj.last_play_skip_ratio})

def _search_last_play_skip_ratio(self, operator, value):
return self._search_pref("last_play_skip_ratio", operator, value)

@api.depends("pref_ids")
def _compute_star(self):
for obj in self:
Expand Down Expand Up @@ -173,7 +225,18 @@ def write(self, vals):
# `oomusic.preference`.
# When the library is shared, this triggers an AccessError if the user is not the owner
# of the object.
fields = {"play_count", "last_play", "star", "rating", "bit_follow", "tag_ids"}
fields = {
"play_count",
"skip_count",
"play_skip_ratio",
"last_play",
"last_skip",
"last_play_skip_ratio",
"star",
"rating",
"bit_follow",
"tag_ids",
}
new_self = self
if any([k in fields for k in vals.keys()]):
self.check_access_rule("read")
Expand Down
28 changes: 28 additions & 0 deletions models/oomusic_track.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,41 @@ class MusicTrack(models.Model):
inverse="_inverse_play_count",
search="_search_play_count",
)
skip_count = fields.Integer(
"Skip Count",
readonly=True,
compute="_compute_skip_count",
inverse="_inverse_skip_count",
search="_search_skip_count",
)
play_skip_ratio = fields.Float(
"Play/Skip Ratio",
readonly=True,
compute="_compute_play_skip_ratio",
inverse="_inverse_play_skip_ratio",
search="_search_play_skip_ratio",
)
last_play = fields.Datetime(
"Last Played",
readonly=True,
compute="_compute_last_play",
inverse="_inverse_last_play",
search="_search_last_play",
)
last_skip = fields.Datetime(
"Last Skipped",
readonly=True,
compute="_compute_last_skip",
inverse="_inverse_last_skip",
search="_search_last_skip",
)
last_play_skip_ratio = fields.Datetime(
"Last Play/Skip Update",
readonly=True,
compute="_compute_last_play_skip_ratio",
inverse="_inverse_last_play_skip_ratio",
search="_search_last_play_skip_ratio",
)
last_modification = fields.Integer("Last Modification", readonly=True)
root_folder_id = fields.Many2one(
"oomusic.folder", string="Root Folder", index=True, required=True
Expand Down
32 changes: 31 additions & 1 deletion static/src/js/panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ var Panel = Widget.extend({
this.current_playlist_line_id = undefined;
this.current_track_id = undefined;
this.current_model = 'oomusic.playlist.line';
this.current_progress = 0;
this.repeat = false;
this.shuffle = false;
this.duration = 1;
this.user_seek = 0;
this.sound_seek = 0;
this.sound_seek_last_played = 0;
this.play_skip = false;

// ========================================================================================
// Bus events
Expand All @@ -68,6 +70,7 @@ var Panel = Widget.extend({
setInterval(this._infUpdateProgress.bind(this), 1000);
setInterval(this._infCheckStuck.bind(this), 500);
setInterval(this._infLoadNext.bind(this), 5000);
setInterval(this._infSetPlay.bind(this), 5000);

this.appendTo(web_client.$el);

Expand Down Expand Up @@ -157,6 +160,7 @@ var Panel = Widget.extend({
return;
}
var self = this;
this._updatePlaySkip(playlist_line_id);
return this._rpc({
model: 'oomusic.playlist.line',
method: 'oomusic_previous',
Expand All @@ -173,6 +177,7 @@ var Panel = Widget.extend({
return;
}
var self = this;
this._updatePlaySkip(playlist_line_id);
var params = _.extend(params || {}, {play_now: true});
if (this.next_playlist_line_id) {
self.user_seek = 0;
Expand Down Expand Up @@ -274,8 +279,10 @@ var Panel = Widget.extend({
this.$el.find('.oom_play').show();
}

// Reset next sound
// Reset next line, progress and play/skip ratio
this.next_playlist_line_id = undefined;
this.current_progress = 0;
this.play_skip = false;

// Update time, title and album picture
this.duration = data_json.duration;
Expand All @@ -299,6 +306,22 @@ var Panel = Widget.extend({

},

_updatePlaySkip: function (playlist_line_id) {
if (!_.isNumber(playlist_line_id)) {
return;
}
if (this.play_skip === false) {
this.play_skip = true;
this._rpc({
model: 'oomusic.playlist.line',
method: 'oomusic_play_skip',
args: [[playlist_line_id], this.current_progress >= 50],
}, {
shadow: true,
})
}
},

_updateGlobalData: function (data, params) {
var self = this;
var data_json = JSON.parse(data);
Expand Down Expand Up @@ -440,6 +463,13 @@ var Panel = Widget.extend({
}
},

_infSetPlay: function () {
if (!this.sound || !this.sound.playing() || this.play_skip === true || this.current_progress < 50) {
return;
}
this._updatePlaySkip(this.current_playlist_line_id);
},

//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
Expand Down
18 changes: 18 additions & 0 deletions tests/test_playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import time
from datetime import datetime as dt

from . import test_common

Expand Down Expand Up @@ -91,7 +92,19 @@ def test_20_player_interaction(self):
self.assertEqual(playlist2.current, True)
self.assertEqual(playlist1.playlist_line_ids[0].playing, False)
self.assertEqual(playlist2.playlist_line_ids[0].playing, True)

# oomusic_play_skip
playlist2.playlist_line_ids[0].oomusic_play_skip(play=True)
self.assertEqual(playlist2.playlist_line_ids[0].track_id.play_count, 1)
self.assertEqual(playlist2.playlist_line_ids[0].track_id.play_skip_ratio, 1)
playlist2.playlist_line_ids[0].track_id.last_play_skip_ratio = dt.now().replace(year=2016)
playlist2.playlist_line_ids[0].oomusic_play_skip(play=True)
self.assertEqual(playlist2.playlist_line_ids[0].track_id.play_count, 2)
self.assertEqual(playlist2.playlist_line_ids[0].track_id.play_skip_ratio, 2)
playlist2.playlist_line_ids[0].track_id.last_play_skip_ratio = dt.now().replace(year=2016)
playlist2.playlist_line_ids[0].oomusic_play_skip(play=False)
self.assertEqual(playlist2.playlist_line_ids[0].track_id.skip_count, 1)
self.assertEqual(playlist2.playlist_line_ids[0].track_id.play_skip_ratio, 2)

# oomusic_play
res = json.loads(playlist1.playlist_line_ids[0].with_context(test_mode=True).oomusic_play())
Expand Down Expand Up @@ -178,9 +191,14 @@ def test_30_smart_playlist(self):
playlist.album_id = album1
playlist._onchange_album_id()
playlist.playlist_line_ids[0].with_context(test_mode=True).oomusic_set_current()
playlist.playlist_line_ids[0].oomusic_play_skip(play=True)
playlist.playlist_line_ids[0].track_id.last_play_skip_ratio = dt.now().replace(year=2016)
playlist.playlist_line_ids[0].with_context(test_mode=True).oomusic_set_current()
playlist.playlist_line_ids[0].oomusic_play_skip(play=True)
playlist.playlist_line_ids[0].track_id.last_play_skip_ratio = dt.now().replace(year=2016)
time.sleep(2)
playlist.playlist_line_ids[1].with_context(test_mode=True).oomusic_set_current()
playlist.playlist_line_ids[1].oomusic_play_skip(play=True)
playlist.invalidate_cache()
playlist.action_purge()
playlist.invalidate_cache()
Expand Down
3 changes: 3 additions & 0 deletions views/oomusic_track_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@
<field name="rating" widget="priority"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="play_count"/>
<field name="skip_count"/>
<field name="play_skip_ratio" groups="base.group_no_one"/>
<field name="last_play"/>
<field name="last_skip"/>
<field name="last_modification" groups="base.group_no_one"/>
</group>
<group>
Expand Down

0 comments on commit 1403edc

Please sign in to comment.