Guest User

Untitled

a guest
Jul 18th, 2018
132
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 15.36 KB | None | 0 0
  1. # Copyright (C) 2010 Andrew Clunis <andrew@orospakr.ca>
  2. # Daniel Rubin <dan@fracturedproject.net>
  3. # 2005-2009 Quentin Sculo <squentin@free.fr>
  4. #
  5. # This file is part of Gmusicbrowser.
  6. # Gmusicbrowser is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License version 3, as
  8. # published by the Free Software Foundation
  9.  
  10. =gmbplugin EPICRATING
  11. name EpicRating
  12. title EpicRating plugin - automatically update ratings
  13. author Andrew Clunis <andrew@orospakr.ca>
  14. author Daniel Rubin <dan@fracturedproject.net>
  15. desc Automatic rating updates on configurable listening behaviour events.
  16. =cut
  17.  
  18. # dependencies: Text::CSV, libtext-csv-perl
  19.  
  20. package GMB::Plugin::EPICRATING;
  21. use strict;
  22. use warnings;
  23.  
  24. Glib->install_exception_handler (sub {
  25. warn shift;
  26. exit -1;
  27. });
  28.  
  29. use constant {
  30. OPT => 'PLUGIN_EPICRATING_', #used to identify the plugin's options
  31. };
  32.  
  33. ::SetDefaultOptions(OPT, SetDefaultRatingOnSkipped => 1, SetDefaultRatingOnFinished => 1, Rules => [ {signal => 'Finished', field => "rating", value => 5}, {signal => 'Skipped', field => 'rating', value => -5 }, { signal => "Skipped", before => 15, field => "rating", value => -1}]);
  34.  
  35. my $self=bless {},__PACKAGE__;
  36.  
  37. sub IsFieldSet {
  38. my ($self, $song_id, $field) = @_;
  39.  
  40. my $val = ::Songs::Get($song_id, $field);
  41.  
  42. my $answer = defined($val) && ($val ne "");
  43.  
  44. return $answer;
  45. }
  46.  
  47. sub AddRatingPointsToSong {
  48. my ($self, $ID, $PointsToRemove) = @_;
  49. my $ExistingRating = ::Songs::Get($ID, 'rating');
  50.  
  51. # actually, ApplyRulesByName() skips apply the rule if the target field (ie., rating, is unset)
  52. # Skipped rules, however, do not make such a check.
  53.  
  54. if(!($self->IsFieldSet($ID, 'rating'))) {
  55. warn "EpicRating cannot change the rating of a song that has no rating. Ignoring.";
  56. return;
  57. }
  58.  
  59. if(($ExistingRating + $PointsToRemove) > 100)
  60. {
  61. warn "Yikes, can't rate this song above one hundred.";
  62. ::Songs::Set($ID, rating=>100);
  63. } elsif(($ExistingRating + $PointsToRemove) < 0) {
  64. warn "Negative addend in EpicRating pushed song rating to below 0. Pinning at 0.";
  65. ::Songs::Set($ID, rating => 0);
  66. } else {
  67. warn "EpicRating changing song rating (title: " . ::Songs::Get($ID, 'title') . ", existing: " . $ExistingRating . ") by " . $PointsToRemove;
  68. ::Songs::Set($ID, rating=>($ExistingRating + $PointsToRemove));
  69. }
  70. }
  71.  
  72. sub GetRulesBySignal {
  73. my ($rule_name) = @_;
  74. my $rules = $::Options{OPT.'Rules'};
  75. my $matched_rules = [];
  76.  
  77. foreach my $rule (@{$rules}) {
  78. if(${$rule}{signal} eq $rule_name) {
  79. push @{$matched_rules}, $rule;
  80. }
  81. }
  82. return $matched_rules;
  83. }
  84.  
  85. sub gettimeofday_us {
  86. require Time::HiRes;
  87. }
  88.  
  89. sub SaveRatingScoresCSV {
  90. my ($self) = @_;
  91.  
  92. use Text::CSV;
  93. my $file_chooser = Gtk2::FileChooserDialog->new(
  94. _"Save lastplay ratingscore to file",
  95. undef, 'save', 'gtk-save', => 'ok', 'gtk-cancel' => 'cancel');
  96. my $save_response = $file_chooser->run();
  97.  
  98. if($save_response eq 'ok') {
  99. my $filename = $file_chooser->get_filename();
  100. open RSF, ">", $filename or warn("Could not save file.");
  101. warn "Processing rating scores...";
  102. for (0..100) {
  103. warn "... Processing rating #" . $_;
  104. my $filter = Filter->new("rating:e:" . $_)->filter;
  105. my $length = @{$filter};
  106. warn "... has " . $length . " songs.";
  107.  
  108. my @sorted_by_lastplay = sort {
  109. my $a_played = ::Songs::Get($a, 'lastplay');
  110. my $b_played = ::Songs::Get($b, 'lastplay');
  111. $a_played <=> $b_played;
  112. } @{$filter};
  113.  
  114. if($length != 0) {
  115. my $step = 1.0 / $length;
  116.  
  117. my $used = 0.0;
  118.  
  119. foreach my $song (@sorted_by_lastplay) {
  120. my $rating_score = $used;
  121. my $rating = ::Songs::Get($song, "rating");
  122. my $final_rating = ($rating * 1.0) + ($rating_score - 0.5);
  123. $used += $step;
  124. print RSF $rating . ", " . $final_rating . "\n";
  125. warn "Song: " . ::Songs::Get($song, "title") . " gets ratingscore " . $rating_score;
  126. }
  127. } else {
  128. warn "... not sorting empty rating.";
  129. }
  130. }
  131. close RSF;
  132. }
  133. $file_chooser->destroy();
  134. }
  135.  
  136. # apply the action this rule specifies.
  137. sub ApplyRule {
  138. my ($self, $rule, $song_id) = @_;
  139. $self->AddRatingPointsToSong($song_id, ${$rule}{value});
  140. }
  141.  
  142. # lasso all rules with a given signal and apply them all. does not evalulate conditions.
  143. sub ApplyRulesByName {
  144. my ($self, $rule_name, $song_id) = @_;
  145. my $rules = $::Options{OPT.'Rules'};
  146.  
  147. my $matched_rules = GetRulesBySignal($rule_name);
  148.  
  149. foreach my $matched_rule (@{$matched_rules}) {
  150. my $value = ::Songs::Get($song_id, ${$matched_rule}{field});
  151. # if the target field is unset, skip it.
  152. if((defined $value) && ($value ne "")) {
  153. $self->ApplyRule($matched_rule, $song_id);
  154. }
  155. }
  156. }
  157.  
  158. sub Played {
  159. my ($self, $song_id, $finished, $start_time, $seconds, $coverage_ratio, $played_segments) = @_;
  160. if(!$finished) {
  161. $self->Skipped($song_id, $played_segments->[-1]);
  162. } else {
  163. $self->Finished($song_id);
  164. }
  165. }
  166.  
  167. # Finished playing song (actually PlayedPercent or more, neat eh?)
  168. sub Finished {
  169. my ($self, $song_id) = @_;
  170. warn 'EpicRating has noticed that a song has finished!';
  171. my $rules = $::Options{OPT.'Rules'};
  172. my $DefaultRating = $::Options{"DefaultRating"};
  173.  
  174. my $song_rating = Songs::Get($song_id, 'rating');
  175. if(!($self->IsFieldSet($song_id, 'rating')) && $::Options{OPT."SetDefaultRatingOnFinished"}) {
  176. ::Songs::Set($song_id, rating=>$DefaultRating);
  177. }
  178.  
  179. $self->ApplyRulesByName('Finished', $song_id);
  180. }
  181.  
  182. sub Skipped {
  183. my ($self, $song_id, $play_time) = @_;
  184. warn 'EpicRating has noticed that a song has been skipped!';
  185. my $DefaultRating = $::Options{"DefaultRating"};
  186. my $rules = $::Options{OPT.'Rules'};
  187.  
  188. # we apply the default if the checkbox is enabled regardless
  189. # of rules.
  190. warn "Getting rating of song #" . $song_id;
  191. my $song_rating = Songs::Get($song_id, 'rating');
  192. if(!($self->IsFieldSet($song_id, 'rating')) && $::Options{OPT."SetDefaultRatingOnSkipped"}) {
  193. ::Songs::Set($song_id, rating=>$DefaultRating);
  194. }
  195.  
  196. my $all_skip_rules = GetRulesBySignal('Skipped');
  197.  
  198. foreach my $skip_rule (@{$all_skip_rules}) {
  199. my $before = ${$skip_rule}{'before'};
  200. my $after = ${$skip_rule}{'after'};
  201.  
  202. # takes a list of expressions
  203. # if an expression is true, OR an operand is nil, AND it with the others.
  204. # return true
  205. # meh, maybe not useful
  206.  
  207. my $after_exists = defined($after) && $after ne "";
  208. my $before_exists = defined($before) && $before ne "";
  209.  
  210. if(!$before_exists && !$after_exists) {
  211. # neither
  212. # warn "Evalauted skip rule... neither after or before constraints.";
  213. $self->ApplyRule($skip_rule, $song_id);
  214. return;
  215. } elsif($before_exists && $after_exists) {
  216. # both
  217. # warn "Evaluated skip rule... there's a range.";
  218. if(($play_time >= $after) && ($play_time <= $before)) {
  219. $self->ApplyRule($skip_rule, $song_id);
  220. return;
  221. }
  222. } elsif($before_exists) {
  223. # only before
  224. # warn "Evaluated skip rule... only before constraint.";
  225. if($play_time <= $before) {
  226. $self->ApplyRule($skip_rule, $song_id);
  227. return;
  228. }
  229.  
  230. } elsif($after_exists) {
  231. # only after
  232. # warn "Evaluated skip rule... only after constraint.";
  233. if($play_time => $after) {
  234. $self->ApplyRule($skip_rule, $song_id);
  235. return;
  236. }
  237. } else {
  238. warn "wow, um, I missed a case?";
  239. }
  240. }
  241. }
  242.  
  243. sub Start {
  244. ::Watch($self, Played => \&Played);
  245. }
  246.  
  247. sub Stop {
  248. ::UnWatch($self, 'Played');
  249. }
  250.  
  251. # rule editor.
  252. # - event
  253. # - value
  254. # - operator
  255.  
  256. my $editor_signals = ['Finished', 'Skipped'];
  257. my $editor_fields = ['rating'];
  258.  
  259. # perl, sigh.
  260. # sub indexOfStr {
  261. # my ($arr, $matey) = @_;
  262. # for(my $idx = 0; $idx <= $#{$arr}; $idx ++) {
  263. # return $idx if $arr eq $matey;
  264. # }
  265. # }
  266.  
  267. sub indexOfRef {
  268. my ($arr, $matey) = @_;
  269. for(my $idx = 0; $idx <= $#{$arr}; $idx ++) {
  270. return $idx if $arr == $matey;
  271. }
  272. }
  273.  
  274. # sub deleteStrFromArr {
  275. # my ($arr, $strval) = @_;
  276. # splice($arr, indexOfStr($strval), 1);
  277. # }
  278.  
  279. sub deleteRefFromArr {
  280. my ($arr, $ref) = @_;
  281. splice(@$arr, indexOfRef($arr, $ref), 1);
  282. }
  283.  
  284. sub RulesListAddRow {
  285. my $rule = $_[0]; # hash reference
  286. my $rule_editor = GMB::Plugin::EPICRATING::Editor->new($rule);
  287. $self->{rules_table}->add_with_properties($rule_editor, "expand", ::FALSE);
  288.  
  289. $rule_editor->show_all();
  290. $self->{current_row} += 1;
  291. }
  292.  
  293. sub NewRule {
  294. my $new_rule = { signal => "", field => "", value => 0};
  295. my $options_rules_array = $::Options{OPT.'Rules'};
  296.  
  297. push(@$options_rules_array, $new_rule);
  298. return $new_rule;
  299. }
  300.  
  301. sub PopulateRulesList {
  302. my $rules = $::Options{OPT.'Rules'};
  303. $self->{current_row} = 0;
  304.  
  305. foreach my $rule (@{$rules}) {
  306. RulesListAddRow($rule);
  307. }
  308. }
  309.  
  310. sub prefbox {
  311. # TODO validate good values?E!??!
  312. my $big_vbox = Gtk2::VBox->new(::FALSE, 2);
  313. my $rules_scroller = Gtk2::ScrolledWindow->new();
  314. $rules_scroller->set_policy('never', 'automatic');
  315. # $self->{rules_table} = Gtk2::Table->new(1, 4, ::FALSE);
  316. $self->{rules_table} = Gtk2::VBox->new();
  317. $rules_scroller->add_with_viewport($self->{rules_table});
  318.  
  319. PopulateRulesList();
  320. # force some debug fixtures in
  321. # $::Options{OPT.'Rules'} = [ {signal => 'Finished', field => "rating", value => 5}, {signal => 'Skipped', field => 'rating', value => -5 }, { signal => "SkippedBefore15", field => "rating", value => -1}];
  322.  
  323. my $add_rule_button = Gtk2::Button->new_from_stock('gtk-add');
  324. $add_rule_button->signal_connect('clicked', sub {
  325.  
  326. my $rule = NewRule();
  327. # manually add the new rule, no point in repopulating everything
  328. RulesListAddRow($rule);
  329. });
  330.  
  331. my $default_rating_box = Gtk2::VBox->new();
  332. my $set_default_rating_label = Gtk2::Label->new(_"Apply your default rating to files when they are first played (required for rating update on files with default rating):");
  333. my $set_default_rating_skip_check = ::NewPrefCheckButton(OPT."SetDefaultRatingOnSkipped", _"... on skipped songs");
  334. my $set_default_rating_finished_check = ::NewPrefCheckButton(OPT."SetDefaultRatingOnFinished", _"... on played songs");
  335. $default_rating_box->add($set_default_rating_label);
  336. $default_rating_box->add($set_default_rating_skip_check);
  337. $default_rating_box->add($set_default_rating_finished_check);
  338.  
  339. my $song_dump_button = Gtk2::Button->new("CSV dump of songs");
  340.  
  341. my $produce_ratingscore_button = Gtk2::Button->new("Emit CSV of heuristic rating scores");
  342.  
  343. $produce_ratingscore_button->signal_connect(clicked => sub {
  344. warn "Ready to begin calculating rating scores.";
  345. my $rating_scores = $self->SaveRatingScoresCSV();
  346.  
  347. });
  348.  
  349. use Text::CSV;
  350. $song_dump_button->signal_connect(clicked => sub {
  351. my $file_chooser = Gtk2::FileChooserDialog->new(
  352. _"Save gmusicbrowser song stats CSV dump as...",
  353. undef, 'save', 'gtk-save' => 'ok', 'gtk-cancel' => 'cancel');
  354. my $response = $file_chooser->run();
  355.  
  356. if($response eq 'ok') {
  357. my $csv_filename = $file_chooser->get_filename();
  358. open CSVF, ">", $csv_filename or warn "Couldn't open CSV output!";
  359.  
  360. use Data::Dumper;
  361. my $csv = Text::CSV->new ({binary => 1 });
  362.  
  363. my $all_songs = Filter->new("")->filter;
  364. for my $song_id (@{$all_songs}) {
  365. my $rating = ::Songs::Get($song_id, 'rating');
  366. my $title = ::Songs::Get($song_id, 'title');
  367. my $playcount = ::Songs::Get($song_id, 'playcount');
  368. my $skipcount = ::Songs::Get($song_id, 'skipcount');
  369. $csv->combine(@{[$song_id, $title, $rating, $playcount, $skipcount]});
  370. print CSVF $csv->string . "\n";
  371. }
  372. close CSVF;
  373. }
  374. $file_chooser->destroy();
  375. });
  376.  
  377. $big_vbox->add($rules_scroller);
  378. $big_vbox->add_with_properties($add_rule_button, "expand", ::FALSE);
  379. $big_vbox->add_with_properties($default_rating_box, "expand", ::FALSE);
  380. $big_vbox->add_with_properties($song_dump_button, "expand", ::FALSE);
  381. $big_vbox->add_with_properties($produce_ratingscore_button, "expand", ::FALSE);
  382.  
  383. $big_vbox->show_all();
  384. return $big_vbox;
  385. }
  386.  
  387. package GMB::Plugin::EPICRATING::Editor;
  388. use Gtk2;
  389. use base 'Gtk2::Frame';
  390.  
  391. sub ExtraFieldsEditor {
  392. my ($self) = @_;
  393. my $hbox = Gtk2::HBox->new();
  394.  
  395. if($self->{rule}{signal} eq "Skipped") {
  396. my $b_label = Gtk2::Label->new(_"Before: ");
  397. my $b_entry = Gtk2::Entry->new();
  398. $b_entry->set_width_chars(4);
  399. $b_entry->set_text($self->{rule}{before}) if defined($self->{rule}{before});
  400. $b_entry->signal_connect('changed', sub {
  401. $self->{rule}{before} = $b_entry->get_text();
  402. });
  403.  
  404. my $a_label = Gtk2::Label->new(_"After: ");
  405. my $a_entry = Gtk2::Entry->new();
  406. $a_entry->set_width_chars(4);
  407. $a_entry->set_text($self->{rule}{after}) if defined($self->{rule}{after});
  408. $a_entry->signal_connect('changed', sub {
  409. $self->{rule}{after} = $a_entry->get_text();
  410. });
  411.  
  412. $hbox->add_with_properties($b_label, "expand", ::FALSE);
  413. $hbox->add_with_properties($b_entry, "expand", ::FALSE);
  414. $hbox->add_with_properties($a_label, "expand", ::FALSE);
  415. $hbox->add_with_properties($a_entry, "expand", ::FALSE);
  416. $hbox->show_all();
  417. }
  418. return $hbox;
  419. }
  420.  
  421. sub rebuild_extra_fields {
  422. my $self = shift;
  423. $self->{editor_hbox}->remove($self->{extra_hbox}) unless(!defined($self->{extra_hbox}));
  424. $self->{extra_hbox} = $self->ExtraFieldsEditor();
  425. $self->{editor_hbox}->add_with_properties($self->{extra_hbox}, "expand", ::FALSE);
  426. }
  427.  
  428. sub new {
  429. my ($class, $rule) = @_;
  430. my $self = bless Gtk2::Frame->new;
  431. $self->{rule} = $rule;
  432.  
  433. $self->{editor_hbox} = Gtk2::HBox->new();
  434.  
  435. $self->{extra_hbox} = undef;
  436.  
  437. my $extra_fields = [];
  438.  
  439. my $signal_combo = Gtk2::ComboBox->new_text();
  440. my $signal_idx = 0;
  441. foreach my $signal (@{$editor_signals}) {
  442. $signal_combo->append_text($signal);
  443. if($signal eq ${$rule}{signal}) {
  444. $signal_combo->set_active($signal_idx);
  445. }
  446. $signal_idx++;
  447. }
  448. $signal_combo->signal_connect('changed', sub {
  449. ${$rule}{signal} = $signal_combo->get_active_text();
  450. # shit, gotta repopulate the entire special-fields area
  451. # even better just to repopulate the whole thing?
  452. $self->rebuild_extra_fields();
  453. });
  454.  
  455. my $field_combo = Gtk2::ComboBox->new_text();
  456. my $field_idx = 0;
  457. foreach my $field (@{$editor_fields}) {
  458. $field_combo->append_text($field);
  459. if($field eq ${$rule}{field}) {
  460. $field_combo->set_active($field_idx);
  461. }
  462. $field_idx++;
  463. }
  464. $field_combo->signal_connect('changed', sub {
  465. ${$rule}{field} = $field_combo->get_active_text();
  466. });
  467.  
  468. my $value_entry = Gtk2::Entry->new();
  469. $value_entry->set_width_chars(4);
  470. $value_entry->set_text(${$rule}{value});
  471. $value_entry->signal_connect('changed', sub {
  472. ${$rule}{value} = $value_entry->get_text();
  473. });
  474.  
  475. $self->{editor_hbox}->add_with_properties(Gtk2::Label->new(_"Signal: "), "expand", ::FALSE);
  476. $self->{editor_hbox}->add_with_properties($signal_combo, "expand", ::FALSE);
  477.  
  478. $self->{editor_hbox}->add_with_properties(Gtk2::Label->new(_"Field: "), "expand", ::FALSE);
  479. $self->{editor_hbox}->add_with_properties($field_combo, "expand", ::FALSE);
  480. $self->{editor_hbox}->add_with_properties(Gtk2::Label->new(_"Differential: "), "expand", ::FALSE);
  481. $self->{editor_hbox}->add_with_properties($value_entry, "expand", ::FALSE);
  482.  
  483. $self->rebuild_extra_fields();
  484.  
  485. my $remove_button = Gtk2::Button->new_from_stock('gtk-delete');
  486. $remove_button->signal_connect('clicked', sub {
  487. GMB::Plugin::EPICRATING::deleteRefFromArr($::Options{ GMB::Plugin::EPICRATING::OPT.'Rules'}, $rule);
  488. $self->destroy();
  489. });
  490.  
  491. $self->{editor_hbox}->pack_end($remove_button, ::FALSE, ::FALSE, 1);
  492. $self->{editor_hbox}->show_all();
  493. $self->add($self->{editor_hbox});
  494.  
  495. return $self;
  496. }
Add Comment
Please, Sign In to add comment