/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Copyright (C) 2003 Jeffrey Yasskin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * The Rhythmbox authors hereby grant permission for non-GPL compatible * GStreamer plugins to be used and distributed together with GStreamer * and Rhythmbox. This permission is above and beyond the permissions granted * by the GPL license by which Rhythmbox is covered. If you modify this code * you may extend this exception to your version of the code, but you are not * obligated to do so. If you do not wish to do so, delete this exception * statement from your version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * */ /** * SECTION:rb-play-order-random * @short_description: base class for weighted random play orders * * Subclasses only need to override get_entry_weight() to return the * right weight for a given entry. * * This class also delays committing any changes until the user moves to the * next or previous song. So if the user changes the entry-view to contain * different songs, but changes it back before the current song finishes, they * will not see any changes to their history of played songs. */ #include "config.h" #include #include "rb-play-order-random-by-age.h" #include "rb-debug.h" #include "rhythmdb.h" #include "rb-history.h" static void rb_random_play_order_class_init (RBRandomPlayOrderClass *klass); static void rb_random_play_order_init (RBRandomPlayOrder *rorder); static void rb_random_play_order_finalize (GObject *object); static RhythmDBEntry* rb_random_play_order_get_next (RBPlayOrder* porder); static void rb_random_play_order_go_next (RBPlayOrder* porder); static RhythmDBEntry* rb_random_play_order_get_previous (RBPlayOrder* porder); static void rb_random_play_order_go_previous (RBPlayOrder* porder); static void rb_random_db_changed (RBPlayOrder *porder, RhythmDB *db); static void rb_random_playing_entry_changed (RBPlayOrder *porder, RhythmDBEntry *old_entry, RhythmDBEntry *new_entry); static void rb_random_query_model_changed (RBPlayOrder *porder); static void rb_random_db_entry_deleted (RBPlayOrder *porder, RhythmDBEntry *entry); static void rb_random_handle_query_model_changed (RBRandomPlayOrder *rorder); static void rb_random_filter_history (RBRandomPlayOrder *rorder, RhythmDBQueryModel *model); struct RBRandomPlayOrderPrivate { RBHistory *history; gboolean query_model_changed; }; G_DEFINE_TYPE (RBRandomPlayOrder, rb_random_play_order, RB_TYPE_PLAY_ORDER) #define RB_RANDOM_PLAY_ORDER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_RANDOM_PLAY_ORDER, RBRandomPlayOrderPrivate)) static void rb_random_play_order_class_init (RBRandomPlayOrderClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); RBPlayOrderClass *porder; object_class->finalize = rb_random_play_order_finalize; porder = RB_PLAY_ORDER_CLASS (klass); porder->db_changed = rb_random_db_changed; porder->playing_entry_changed = rb_random_playing_entry_changed; porder->entry_added = (void (*)(RBPlayOrder*,RhythmDBEntry*))rb_random_query_model_changed; porder->entry_removed = (void (*)(RBPlayOrder*,RhythmDBEntry*))rb_random_query_model_changed; porder->query_model_changed = rb_random_query_model_changed; porder->db_entry_deleted = rb_random_db_entry_deleted; porder->has_next = rb_play_order_model_not_empty; porder->get_next = rb_random_play_order_get_next; porder->go_next = rb_random_play_order_go_next; porder->get_previous = rb_random_play_order_get_previous; porder->go_previous = rb_random_play_order_go_previous; g_type_class_add_private (klass, sizeof (RBRandomPlayOrderPrivate)); } static void rb_random_play_order_init (RBRandomPlayOrder *rorder) { rorder->priv = RB_RANDOM_PLAY_ORDER_GET_PRIVATE (rorder); rorder->priv->history = rb_history_new (TRUE, (GFunc) rhythmdb_entry_unref, NULL); rb_history_set_maximum_size (rorder->priv->history, 50); rorder->priv->query_model_changed = TRUE; } static void rb_random_play_order_finalize (GObject *object) { RBRandomPlayOrder *rorder; g_return_if_fail (object != NULL); g_return_if_fail (RB_IS_RANDOM_PLAY_ORDER (object)); rorder = RB_RANDOM_PLAY_ORDER (object); g_object_unref (G_OBJECT (rorder->priv->history)); G_OBJECT_CLASS (rb_random_play_order_parent_class)->finalize (object); } static double rb_random_play_order_get_entry_weight (RBRandomPlayOrder *rorder, RhythmDB *db, RhythmDBEntry *entry) { g_return_val_if_fail (rorder != NULL, 1.0); g_return_val_if_fail (RB_RANDOM_PLAY_ORDER_GET_CLASS (rorder)->get_entry_weight != NULL, 1.0); return RB_RANDOM_PLAY_ORDER_GET_CLASS (rorder)->get_entry_weight (rorder, db, entry); } static inline RBHistory * get_history (RBRandomPlayOrder *rorder) { return rorder->priv->history; } typedef struct { RhythmDBEntry *entry; double weight; double cumulative_weight; } EntryWeight; static GArray * get_query_model_contents (RBRandomPlayOrder *rorder, RhythmDBQueryModel *model) { guint num_entries; guint i; RhythmDB *db; double weight = 0.0; double cumulative_weight = 0.0; GtkTreeIter iter; GArray *result; if (model == NULL) { return NULL; } num_entries = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (model), NULL); if (num_entries == 0) { return NULL; } if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (model), &iter)) return NULL; i = 0; result = g_array_new (FALSE, FALSE, sizeof (EntryWeight)); g_array_set_size (result, num_entries); db = rb_play_order_get_db (RB_PLAY_ORDER (rorder)); do { RhythmDBEntry *entry = rhythmdb_query_model_iter_to_entry (model, &iter); if (entry == NULL) continue; weight = rb_random_play_order_get_entry_weight (rorder, db, entry); g_array_index (result, EntryWeight, i).entry = entry; g_array_index (result, EntryWeight, i).weight = weight; g_array_index (result, EntryWeight, i).cumulative_weight = cumulative_weight; cumulative_weight += weight; i++; rhythmdb_entry_unref (entry); } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (model), &iter)); return result; } static void rb_random_handle_query_model_changed (RBRandomPlayOrder *rorder) { RhythmDBQueryModel *model; if (!rorder->priv->query_model_changed) return; model = rb_play_order_get_query_model (RB_PLAY_ORDER (rorder)); rb_random_filter_history (rorder, model); rorder->priv->query_model_changed = FALSE; } static void rb_random_filter_history (RBRandomPlayOrder *rorder, RhythmDBQueryModel *model) { GPtrArray *history_contents; int i; history_contents = rb_history_dump (rorder->priv->history); for (i = 0; i < history_contents->len; ++i) { gboolean remove = TRUE; if (model) { GtkTreeIter iter; if (rhythmdb_query_model_entry_to_iter (model, g_ptr_array_index (history_contents, i), &iter)) remove = FALSE; } if (remove) { rb_history_remove_entry (rorder->priv->history, g_ptr_array_index (history_contents, i)); } } g_ptr_array_free (history_contents, TRUE); } static inline double rb_random_get_total_weight (GArray *weights) { EntryWeight *last; g_return_val_if_fail (weights, 0.0); if (weights->len == 0) return 0.0; last = &g_array_index (weights, EntryWeight, weights->len - 1); return last->cumulative_weight + last->weight; } static RhythmDBEntry* rb_random_play_order_pick_entry (RBRandomPlayOrder *rorder) { /* This is O(N) because we traverse the query model to get the entries * and weights. * * The general idea of this algorithm is that there is a line segment * whose length is the sum of all the entries' weights. Each entry gets * a sub-segment whose length is equal to that entry's weight. A random * point is picked in the line segment, and the entry that point * belongs to is returned. * * The algorithm was contributed by treed. */ double total_weight, rnd; int high, low; GArray *entry_weights; RhythmDBEntry *entry; RhythmDBQueryModel *model; model = rb_play_order_get_query_model (RB_PLAY_ORDER (rorder)); entry_weights = get_query_model_contents (rorder, model); if (entry_weights == NULL) { rb_debug ("nothing to choose from"); return NULL; } total_weight = rb_random_get_total_weight (entry_weights); if (total_weight == 0.0) { low = g_random_int_range (0, entry_weights->len); rb_debug ("total weight is 0; picked entry %d of %d randomly", low, entry_weights->len); entry = g_array_index (entry_weights, EntryWeight, low).entry; g_array_free (entry_weights, TRUE); return entry; } rnd = g_random_double_range (0, total_weight); /* Binary search for the entry with cumulative weight closest to but * less than rnd */ low = -1; high = entry_weights->len; while (high - low > 1) { int mid = (high + low) / 2; if (g_array_index (entry_weights, EntryWeight, mid).cumulative_weight > rnd) high = mid; else low = mid; } entry = g_array_index (entry_weights, EntryWeight, low).entry; rb_debug ("picked entry %d of %d (total weight %f) for random value %f", low, entry_weights->len, total_weight, rnd); g_array_free (entry_weights, TRUE); return entry; } static RhythmDBEntry* rb_random_play_order_get_next (RBPlayOrder* porder) { RBRandomPlayOrder *rorder; RhythmDBEntry *entry, *current; RBHistory *history; g_return_val_if_fail (porder != NULL, NULL); g_return_val_if_fail (RB_IS_RANDOM_PLAY_ORDER (porder), NULL); rorder = RB_RANDOM_PLAY_ORDER (porder); rb_random_handle_query_model_changed (rorder); history = get_history (rorder); current = rb_play_order_get_playing_entry (porder); entry = NULL; if (rb_history_length (history) == 0 || (current == rb_history_current (history) && rb_history_current (history) == rb_history_last (history))) { rb_debug ("choosing random entry"); entry = rb_random_play_order_pick_entry (rorder); if (entry) { rhythmdb_entry_ref (entry); rb_history_append (history, rhythmdb_entry_ref (entry)); } } else { rb_debug ("choosing enqueued entry"); if (current == rb_history_current (history)) entry = rb_history_next (history); else entry = rb_history_current (history); if (entry) rhythmdb_entry_ref (entry); } if (current) rhythmdb_entry_unref (current); return entry; } static void rb_random_play_order_go_next (RBPlayOrder* porder) { RBRandomPlayOrder *rorder; RhythmDBEntry *entry; RBHistory *history; g_return_if_fail (porder != NULL); g_return_if_fail (RB_IS_RANDOM_PLAY_ORDER (porder)); rorder = RB_RANDOM_PLAY_ORDER (porder); history = get_history (rorder); /* I think this forces the next track to be added to the history */ entry = rb_random_play_order_get_next (porder); if (entry) rhythmdb_entry_unref (entry); if (rb_history_current (history) == NULL) rb_history_go_first (history); else rb_history_go_next (history); rb_play_order_set_playing_entry (porder, rb_history_current (history)); } static RhythmDBEntry* rb_random_play_order_get_previous (RBPlayOrder* porder) { RBRandomPlayOrder *rorder; RhythmDBEntry *entry; g_return_val_if_fail (porder != NULL, NULL); g_return_val_if_fail (RB_IS_RANDOM_PLAY_ORDER (porder), NULL); rorder = RB_RANDOM_PLAY_ORDER (porder); rb_random_handle_query_model_changed (rorder); rb_debug ("choosing history entry"); entry = rb_history_previous (get_history (rorder)); if (entry) rhythmdb_entry_ref (entry); return entry; } static void rb_random_play_order_go_previous (RBPlayOrder* porder) { RBRandomPlayOrder *rorder; RBHistory *history; g_return_if_fail (porder != NULL); g_return_if_fail (RB_IS_RANDOM_PLAY_ORDER (porder)); /* It doesn't make sense to call go_previous when the player is stopped */ g_return_if_fail (rb_play_order_player_is_playing (porder)); rorder = RB_RANDOM_PLAY_ORDER (porder); history = get_history (rorder); rb_history_go_previous (history); rb_play_order_set_playing_entry (porder, rb_history_current (history)); } static void rb_random_db_changed (RBPlayOrder *porder, RhythmDB *db) { g_return_if_fail (RB_IS_RANDOM_PLAY_ORDER (porder)); rb_history_clear (RB_RANDOM_PLAY_ORDER (porder)->priv->history); } static void rb_random_playing_entry_changed (RBPlayOrder *porder, RhythmDBEntry *old_entry, RhythmDBEntry *new_entry) { RBRandomPlayOrder *rorder; g_return_if_fail (RB_IS_RANDOM_PLAY_ORDER (porder)); rorder = RB_RANDOM_PLAY_ORDER (porder); if (new_entry) { if (new_entry == rb_history_current (get_history (rorder))) { /* Do nothing */ } else { rhythmdb_entry_ref (new_entry); rb_history_set_playing (get_history (rorder), new_entry); } } } static void rb_random_query_model_changed (RBPlayOrder *porder) { g_return_if_fail (RB_IS_RANDOM_PLAY_ORDER (porder)); RB_RANDOM_PLAY_ORDER (porder)->priv->query_model_changed = TRUE; } static void rb_random_db_entry_deleted (RBPlayOrder *porder, RhythmDBEntry *entry) { /* When an entry is deleted from the database, it needs to vanish from * all histories immediately. */ RBRandomPlayOrder *rorder; g_return_if_fail (RB_IS_RANDOM_PLAY_ORDER (porder)); rorder = RB_RANDOM_PLAY_ORDER (porder); rb_history_remove_entry (rorder->priv->history, entry); }