NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Movie Dip - find movies to double dip (watch back-to-back) // @namespace driver8.net // @description On the Google Movie Showtimes, select the movies you are interested in seeing to find out which ones you can see back-to-back at the same theater. // @version 0.5 // @grant GM_addStyle // @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/sprintf/0.0.7/sprintf.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.17.5/js/jquery.tablesorter.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.7.0/moment.min.js // @match *://*.google.com/movies* // ==/UserScript== // im not vry good @ javascript sry var AND_BUTTS = false; var NUM_DIPS = 2; var FRONT_TOLERANCE = 5; var END_TOLERANCE = 40; var FADE_SPEED = 200; var PRETTY = true; var LOGGING = false; function log(msg) { LOGGING && console.log(msg); } function Film(title, id, length, times) { this.title = title; this.id = id; this.len = length; this.selected = false; this.checkBox = null; this.div = null; this.times = times; } Film.prototype.constructor = Film; function Theater(name, id, num, films) { this.name = name; this.id = id; this.films = films; this.num = num; this.activeFilms = []; this.tableDiv = null; } Theater.prototype.constructor = Theater; Theater.prototype.addFilm = function(newFilm) { this.films.push(newFilm); }; Theater.prototype.toggleFilm = function(film) { if (this.activeFilms.contains(film)) { this.activeFilms.remove(film); } else this.activeFilms.push(film); }; $.tablesorter.addParser({ // set a unique id id: 'showtime', is: function(s) { // return false so this parser is not auto detected return false; }, format: function(s, table, cell) { // format your data for normalization var sortNum = $(cell).data('sortNum'); return sortNum; }, // set type, either numeric or text type: 'numeric' }); // Set things up var theaters = []; var theaterMap = {}; var tableMap = {}; // Gather list of theaters var $allTheaters = $("div.theater"); var $settingsDiv = $('<div id="mdSettingsDiv"><b>Min. wait: </b><input type="text" class="mdSettings" id="mdFrontTolerance" name="FT" value="'+ FRONT_TOLERANCE +'" /> ' + '<b>Max. wait: </b><input type="text" class="mdSettings" id="mdEndTolerance" name="ET" value="'+ END_TOLERANCE +'" />' + '<b>Num. dips: </b><select class="mdSettings" id="mdNumDips" name="ND"><option value="2" selected>2</option><option value="3">3</option><option value="4">4</option>' + '<option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option></select>'); $("#results table:first").remove(); // just took up space at the top $("#movie_results").before($settingsDiv); $("input.mdSettings").blur(selectionsChanged); $("#mdNumDips").change(selectionsChanged); $allTheaters.each(function(idx) { var $theaterDiv = $(this); var theaterName = $theaterDiv.find("h2.name a").text(); var theaterId = $theaterDiv.children("div.desc:first").attr("id"); // For each theater, gather list of movies var $movieDivs = $theaterDiv.find("div.movie"); var newFilms = []; $movieDivs.each(function(idx2) { var $movieDiv = $(this); var movieId = theaterId + "_" + idx2; // For each movie, gather names, lengths and showtimes var $movieNameA = $movieDiv.find("div.name a"); var movieName = $movieNameA.text(); AND_BUTTS && $movieNameA.text(movieName + " and Butts"); var $newCheckbox = $('<input type="checkbox" />'); $newCheckbox.data("theater", theaterId); $newCheckbox.data("movie", idx2); $newCheckbox.attr("id", theaterId + "-" + idx2); $newCheckbox.change(movieCheckboxChange); $movieNameA.before($newCheckbox); // Cute stuff $movieDiv.click(function(event) { event.target.tagName !== "INPUT" && $newCheckbox.click(); return true; }); // Find movie length var movieLength = 90; var lengthMatches = $movieDiv.find("span.info").text().match(/\D*(?:(\d+)h)?\D*(\d+)m/); if (lengthMatches) { var hours = parseInt(lengthMatches[1]); var mins = parseInt(lengthMatches[2]); mins = mins >= 0 ? mins : 90; mins += hours > 0 ? hours * 60 : 0; movieLength = mins; } // Deal with showtimes var newTimes = []; var $movieTimes = $movieDiv.find("div.times > span"); var am1 = -1, am2 = -1, pm1 = -1; $movieTimes.each(function(idx) { var timeText = $(this).text(); var matches = timeText.match(/(\d+):(\d+)\s*(am|pm)?/); if (matches[3] === 'am') { if (am1 == -1) { am1 = idx; } else { am2 = idx; } } else if (matches[3] === 'pm') { pm1 = idx; } }); $movieTimes.each(function(idx) { var matches = $(this).text().match(/((\d+):(\d+))\s*(am|pm)?/); var timeStr = matches[1]; var momTime = moment(); if (pm1 == -1) { momTime = moment(timeStr + 'am', 'hh:mma'); } else if (am1 < pm1) { if (idx <= am1) { momTime = moment(timeStr + 'am', 'hh:mma'); } else if (idx > pm1) { momTime = moment(timeStr + 'am', 'hh:mma'); momTime.add(1, 'days'); } else { momTime = moment(timeStr + 'pm', 'hh:mma'); } } else { if (idx <= pm1) { momTime = moment(timeStr + 'pm', 'hh:mma'); } else { momTime = moment(timeStr + 'am', 'hh:mma'); momTime.add(1, 'days'); } } newTimes.push(momTime); }); var newFilm = new Film(movieName, movieId, movieLength, newTimes); newFilm.checkBox = $newCheckbox; newFilm.div = $movieDiv; newFilms[idx2] = newFilm; }); var newTheater = new Theater(theaterName, theaterId, idx, newFilms); theaters.push(newTheater); theaterMap[theaterId] = newTheater; }); function movieDivEnter(event) { var id = event.data.id; $('table.mdTable td.title_cell, table.mdTable td.time_cell') .filter(function () { return $(this).data("movieId") === id; }) .addClass('times_table_highlight'); } function movieDivLeave() { $('table.mdTable td.times_table_highlight').removeClass('times_table_highlight'); } // Runs whenever a checkbox is checked or unchecked function movieCheckboxChange(event) { var $checkBox = $(event.target); var theaterId = $checkBox.data("theater"); var filmNum = $checkBox.data("movie"); var theater = theaterMap[theaterId]; var film = theater.films[filmNum]; var $movieDiv = film.div; film.selected = $checkBox.prop("checked"); if (film.selected) { theater.activeFilms.push(film); $movieDiv.addClass("mdSelected"); $movieDiv.mouseenter({'id': film.id}, movieDivEnter); $movieDiv.mouseleave(movieDivLeave); } else { theater.activeFilms = theater.activeFilms.filter(function(item) { return item.id !== film.id; }); $movieDiv.removeClass("mdSelected"); $movieDiv.off("mouseenter"); $movieDiv.off("mouseleave"); } selectionsChanged(event, theater); $movieDiv.mouseenter(); return false; } // Runs whenever there is a change in selected movies (with theater ID) or in the settings (w/o ID) function selectionsChanged(event, theater) { if (theater) { checkTheater(theater) } else { $.each(theaters, function () { checkTheater(this); }); } return false; } // Check theater for selections. If there are enough selections, makeTable. function checkTheater(theater) { if (theater.activeFilms.length >= NUM_DIPS) { var $newTable = makeTable(theater.activeFilms, theater.id); if ($newTable) { $newTable.hide(); $newTable.children("table").tablesorter({ theme: 'blue', headers: { 1: { sorter:'showtime' }, 3: { sorter:'showtime' }, 5: { sorter:'showtime' }, 7: { sorter:'showtime' }, 9: { sorter:'showtime' }, 11: { sorter:'showtime' } } }); $('#' + theater.id).next().after($newTable); if (theater.tableDiv !== null) { theater.tableDiv.remove(); theater.tableDiv = $newTable; theater.tableDiv.show(); } else { theater.tableDiv = $newTable; theater.tableDiv.show(FADE_SPEED); } } else if (theater.tableDiv !== null) { theater.tableDiv.hide(FADE_SPEED, function() { theater.tableDiv.remove(); theater.tableDiv = null; }); } } else { if (theater.tableDiv !== null) { theater.tableDiv.hide(FADE_SPEED, function() { theater.tableDiv.remove(); theater.tableDiv = null; }); } } } // Figures out which selected films can be multi-dipped function makeTable(films, tId) { log("making table"); var selected_films = films; var matches = []; var front_tolerance = parseInt($("#mdFrontTolerance").prop("value")); var end_tolerance = parseInt($("#mdEndTolerance").prop("value")); var num_dips = parseInt($("#mdNumDips").prop("value")); for (var i = 0, len = selected_films.length; i < len; i++) { var film1 = selected_films[i]; var times1 = film1.times; var len1 = film1.len; for (var j = i + 1; j < len; j++) { var film2 = selected_films[j]; if ( !(film1.title.indexOf(film2.title) == 0 && film1.title.lastIndexOf("3D") > 0) && !(film2.title.indexOf(film1.title) == 0 && film2.title.lastIndexOf("3D") > 0)) { var times2 = film2.times; var len2 = film2.len; times1.forEach(function (val1) { times2.forEach(function (val2) { var match; var diff2 = val1.diff(val2, 'minutes'); var diff1 = val2.diff(val1, 'minutes'); if (diff1 - len1 >= front_tolerance && diff1 - len1 <= end_tolerance) { match = { m: [film1, film2], t: [val1, val2], wait: [diff1 - len1] }; matches.push(match); } else if (diff2 - len2 >= front_tolerance && diff2 - len2 <= end_tolerance) { match = { m: [film2, film1], t: [val2, val1], wait: [diff2 - len2] }; matches.push(match); } }); }); } } } log('num dips: ' + num_dips); var new_matches; var dipnum = 2; while (dipnum < num_dips && matches.length > 0) { new_matches = []; $.each(matches, function() { var this_match = this; $.each(selected_films, function() { var film2 = this; var nogood = false; $.each(this_match.m, function() { if ( this.title === film2.title || (this.title.indexOf(film2.title) == 0 && this.title.lastIndexOf("3D") > 0) || (film2.title.indexOf(this.title) == 0 && film2.title.lastIndexOf("3D") > 0) ) { nogood = true; return false; // break } }); if (nogood) return true; // continue var film1 = this_match.m[dipnum - 1]; var val1 = this_match.t[dipnum - 1]; var len1 = film1.len; var times2 = film2.times; times2.forEach(function (val2) { var diff1 = val2.diff(val1, 'minutes'); if (diff1 - len1 >= front_tolerance && diff1 - len1 <= end_tolerance) { var new_match = {}; new_match.m = this_match.m.concat(film2); new_match.t = this_match.t.concat(val2); new_match.wait = this_match.wait.concat(diff1 - len1); new_matches.push(new_match); } }); }); }); dipnum++; console.log("new matches: " + new_matches.length); matches = new_matches; } if (matches.length > 0) { console.log(matches.length + " matches"); var html = ""; // Sort the matched pairs of movies by the first movie's starting time. matches.sort(function(a, b) { var diff = a.t[0].diff(b.t[0], 'minutes'); return diff; }); // Create the html table html = '<div class="mdTableDiv" id="' + tId + 'mdTableDiv"> <table id="' + tId + 'mdTable" class="tablesorter mdTable"> <thead> <tr> '; for (var mnum = 1; mnum <= num_dips; mnum++) { html += '<th>Movie ' + mnum + '</th> <th>Time ' + mnum + '</th>'; if (mnum > 1) { html += '<th>Wait</th>'; } } html += ' </tr> </thead> <tbody>'; var fmt = 'hh:mm A'; matches.forEach(function(val) { var s = '<tr> '; for (mnum = 0; mnum < num_dips; mnum++) { s += '<td class="title_cell" data-movie-id="' + val.m[mnum].id + '">' + val.m[mnum].title + '</td> <td class="time_cell" data-movie-id="' + val.m[mnum].id + '" data-sort-num="' + val.t[mnum].valueOf() + '"><b>' + val.t[mnum].format(fmt) + '</b></td> '; if (mnum > 0) { s+= '<td class="wait_cell"><b>' + val.wait[mnum - 1] + '</b> min wait</td> '; } } s += '</tr>\n'; html += s; }); html += "</tbody> </table> </div>"; return $(html); } else return null; } // Add custom CSS var userStyles0 = "div.mdDiv { display: inline-block; !important}" + "div.movie:hover { background-color: #FFF8D4;}" + "a.mdCheck { display: inline-block; border: 2px solid #588220; border-radius: 8px; background-color: #588220; color: #D8FFA8; " + "padding: 2px 5px; margin-right: 5px; margin-bottom: 5px; line-height: 24px; !important}" + "a.mdCheck:link { text-decoration: none; }" + "div.mdSelected { background-color: #CCFFBB; !important}" + "div.mdSelected:hover { background-color: #99FF88 }" + ".theater .showtimes { margin-bottom: 0px; !important}" + "div.theater { margin-bottom: 40px; }" + ".mdTable td.wait_cell { color: #888888; !important }" + ".mdTable td.times_table_highlight { background-color: #CCFFBB; !important }" + "#mdSettingsDiv b { font-size: 1.1em; }" + "#mdSettingsDiv input { margin-right: 5px; font-size: 1em; }" + "#mdSettingsDiv select { margin-right: 5px; font-size: 1em; width: 4em; }" + ""; var userStyles3 = "" + ".tablesorter-blue {" + " width: auto;" + " background-color: #fff;" + " margin: 10px 0 15px;" + " text-align: left;" + " border-spacing: 0;" + " border: #cdcdcd 1px solid;" + " border-width: 1px 0 0 1px;" + "}" + ".tablesorter-blue th," + ".tablesorter-blue td {" + " border: #cdcdcd 1px solid;" + " border-width: 0 1px 1px 0;" + "}" + "" + "/* header */" + ".tablesorter-blue th," + ".tablesorter-blue thead td {" + " font: bold 12px/18px Arial, Sans-serif;" + " color: #000;" + " background-color: #99bfe6;" + " border-collapse: collapse;" + " padding: 6px;" + " text-shadow: 0 1px 0 rgba(204, 204, 204, 0.7);" + "}" + ".tablesorter-blue tbody td," + ".tablesorter-blue tfoot th," + ".tablesorter-blue tfoot td {" + " padding: 6px;" + " vertical-align: top;" + "}" + ".tablesorter-blue .header," + ".tablesorter-blue .tablesorter-header {" + " /* black (unsorted) double arrow */" + " background-image: url(data:image/gif;base64,R0lGODlhFQAJAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAkAAAIXjI+AywnaYnhUMoqt3gZXPmVg94yJVQAAOw==);" + " /* white (unsorted) double arrow */" + " /* background-image: url(data:image/gif;base64,R0lGODlhFQAJAIAAAP///////yH5BAEAAAEALAAAAAAVAAkAAAIXjI+AywnaYnhUMoqt3gZXPmVg94yJVQAAOw==); */" + " /* image */" + " /* background-image: url(images/black-unsorted.gif); */" + " background-repeat: no-repeat;" + " background-position: center right;" + " padding: 6px 18px 6px 6px;" + " white-space: normal;" + " cursor: pointer;" + "}" + ".tablesorter-blue .headerSortUp," + ".tablesorter-blue .tablesorter-headerSortUp," + ".tablesorter-blue .tablesorter-headerAsc {" + " background-color: #9fbfdf;" + " /* black asc arrow */" + " background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7);" + " /* white asc arrow */" + " /* background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7); */" + " /* image */" + " /* background-image: url(images/black-asc.gif); */" + "}" + ".tablesorter-blue .headerSortDown," + ".tablesorter-blue .tablesorter-headerSortDown," + ".tablesorter-blue .tablesorter-headerDesc {" + " background-color: #8cb3d9;" + " /* black desc arrow */" + " background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7);" + " /* white desc arrow */" + " /* background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7); */" + " /* image */" + " /* background-image: url(images/black-desc.gif); */" + "}" + ".tablesorter-blue thead .sorter-false {" + " background-image: none;" + " cursor: default;" + " padding: 6px;" + "}" + "" + "/* tfoot */" + ".tablesorter-blue tfoot .tablesorter-headerSortUp," + ".tablesorter-blue tfoot .tablesorter-headerSortDown," + ".tablesorter-blue tfoot .tablesorter-headerAsc," + ".tablesorter-blue tfoot .tablesorter-headerDesc {" + " /* remove sort arrows from footer */" + " background-image: none;" + "}" + "" + "/* tbody */" + ".tablesorter-blue td {" + " color: #3d3d3d;" + " background-color: #fff;" + " padding: 6px;" + " vertical-align: top;" + "}" + "" + "/* hovered row colors" + " you'll need to add additional lines for" + " rows with more than 2 child rows" + " */" + ".tablesorter-blue tbody > tr:hover > td," + ".tablesorter-blue tbody > tr:hover + tr.tablesorter-childRow > td," + ".tablesorter-blue tbody > tr:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td," + ".tablesorter-blue tbody > tr.even:hover > td," + ".tablesorter-blue tbody > tr.even:hover + tr.tablesorter-childRow > td," + ".tablesorter-blue tbody > tr.even:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td {" + " background: #d9d9d9;" + "}" + ".tablesorter-blue tbody > tr.odd:hover > td," + ".tablesorter-blue tbody > tr.odd:hover + tr.tablesorter-childRow > td," + ".tablesorter-blue tbody > tr.odd:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td {" + " background: #bfbfbf;" + "}" + "" + "/* table processing indicator */" + ".tablesorter-blue .tablesorter-processing {" + " background-position: center center !important;" + " background-repeat: no-repeat !important;" + " /* background-image: url(../addons/pager/icons/loading.gif) !important; */" + " background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs=') !important;" + "}" + "" + "/* Zebra Widget - row alternating colors */" + ".tablesorter-blue tbody tr.odd td {" + " background-color: #ebf2fa;" + "}" + ".tablesorter-blue tbody tr.even td {" + " background-color: #fff;" + "}" + "" + "/* Column Widget - column sort colors */" + ".tablesorter-blue td.primary," + ".tablesorter-blue tr.odd td.primary {" + " background-color: #99b3e6;" + "}" + ".tablesorter-blue tr.even td.primary {" + " background-color: #c2d1f0;" + "}" + ".tablesorter-blue td.secondary," + ".tablesorter-blue tr.odd td.secondary {" + " background-color: #c2d1f0;" + "}" + ".tablesorter-blue tr.even td.secondary {" + " background-color: #d6e0f5;" + "}" + ".tablesorter-blue td.tertiary," + ".tablesorter-blue tr.odd td.tertiary {" + " background-color: #d6e0f5;" + "}" + ".tablesorter-blue tr.even td.tertiary {" + " background-color: #ebf0fa;" + "}" + ""; GM_addStyle(userStyles3); GM_addStyle(userStyles0); /// REMOVE GOOGLE LINK TRACKING /// // http://google.com/url?q=http://www.imdb.com/title/tt2294449/&sa=X&oi=moviesi&ii=0&usg=AFQjCNGscoN77bL-4GPf_VHQGumRZwQtAg // http://google.com/url?q=http://www.youtube.com/watch%3Fv%3DVQqUOvQKPBg&sa=X&oi=movies&ii=0&usg=AFQjCNE3UMhKGsCv8WI8pgS-LMhnI2e0fA $('a[href^="/url?q="]').each(function() { this.href = this.href.replace(/^https?:\/\/google\.com\/url\?q=/, ''); this.href = this.href.replace(/&(?:sa|oi|ii|usg)=.*/, ''); this.href = decodeURIComponent(this.href); this.href = this.href.replace(/afid=goog&?/, ''); this.href = this.href.replace(/source=google&?/, ''); });