YouTube autoplay - try something new

By Michael Z Last update Aug 21, 2009 — Installed 650 times.

There are 7 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// -----------------------------------------------------
//
// 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, or (at your option)
// any later 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.
//
// -----------------------------------------------------
// Author: Michael Zangl, michael@jautis.net
// Version 1.0pre7
// Date: 2009-08-21
//
// -----------------------------------------------------
//
// ==UserScript==
// @name            Youtube autoplay
// @namespace       http://www.jautis.net/
// @version         1.0pre7
// @description     plays related youtube videos automaticaly
// @include         http://www.youtube.com/watch*
// ==/UserScript==

/**
 * preferences:
 * <id>.watchedVideos = []
 * <id>.proposedVideos = [{id:..., name:..., rating: ...}]
 * <id>.lastAccess = unixTimestamp
 * <id>.settingVersion = settingVersion
 * settingVersion (random int after settings were changed);
 *
 * Video objects:
 *  id
 *  name
 *  compare      see getCompareString
 *  distance     Distance to the start video
 *  unliked      The number of times a video that has this one in its related
 *               list was marked as unliked.
 *  linked       the number of times the video was linked.
 *  rank         the advantage of ranks the video had. The first video has 1;
 *  rating       a temporary variable of the rating. Is automatically calculated
 *               by the proposed videos heap.
 */
const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const DEBUG = false;


i18n_data = {};
i18n_data["en"] = {
  "start autoplay" : "autoplay",
  "stop autoplay" : "stop autoplay",
  "play next" : "next",
  "settings" : "settings",
  "mark as  unliked": "skip"
}
i18n_data["de"] = {
  "start autoplay" : "Autoplay starten",
  "stop autoplay" : "Autoplay stoppen",
  "play next" : "Nächstes",
  "settings" : "Einstellungen",
  "mark as  unliked": "Video überspringen",
  "next video: %s": "Nächstes Video: «%s»",
  "autoplay not started yet": "Autoplay noch nicht gestartet. " +
          "Klicken zu starten und zum springen zum nächsten Video.",
  "hate this": "Video hassen",
  "go to the next video and do not play the related videos of this one":
       "Spiele das nächste Video und werte die ähnlichen Videos von diesem ab.",
  //settings
  "Rating": "Bewertung",
  "video with the best rating will be played next":
      "Das Video mit der besten Bewertung wird als nächstes abgespielt.",
  "close to the start video": "Nahe am Startvideo.",
  "high in the related videos list": "Weit oben in der Ähnliche-Videos-Liste",
  "often related": "Oft verlinkt",
  "Play options": "Weitere Optionen",
  "what else affects the order of videos":
      "Was die Bewertung der Videos sonst noch beeinflusst",
  "randomisation": "Zufälligkeit",
  "do not play unliked videos": "Ungewollte Videos abwerten",
  "do not play videos with the same title twice":
      "Kein Video mit einem öhnlichen Titel nochmal spielen",
  "About": "Über",
  "%s by %s": "%s von %s",
  "Current Version: %s": "Aktuelle Version: %s",
  "Language code (selected by Youtube): %s": "Sprachcode (von Youtube): %s",
  "settings are automatically saved": "Einstellungen werden automatisch gespeichert.",
  "close": "Schließen"
}

function i18n(orginal) {
  var lang = document.documentElement.getAttribute("lang");
  if (!lang || typeof i18n_data[lang] == "undefined"
        || typeof i18n_data[lang][orginal] == "undefined") {
    var text = orginal;
  } else {
    var text = i18n_data[lang][orginal];
  }
  for (var i = 1; i < arguments.length; i++) {
    if (typeof arguments[i] == "string" && text.indexOf("%s") > -1) {
      text = text.replace("%s", arguments[i]);
    } else if (typeof arguments[i] == "number" && text.indexOf("%d") > -1) {
      text = text.replace("%d", arguments[i]);
    } else {
      text += arguments[i];
    }
  }
  return text;
}

/**
 * constructs a heap that allows you to quickly chose the best rated videos
 */
function VideoHeap() {
  if (arguments[0]) {
    youtubeAutoplay.debug("constructed with " + arguments[0].length + " elements");
    this.array = arguments[0];
  }
}
//invariant: array[i] <= array [i*2 + 1] && array[i] <= array [i*2 + 2]
VideoHeap.prototype.array = [];
//checks the invariant
VideoHeap.prototype.check = function() {
  var correct = true;
  for (var i = 1; i < this.array.length; i++) {
    if (this.array[i].rating > this.array[Math.ceil(i/2) - 1].rating) {
      youtubeAutoplay.debug("Error in Heap: " + i + " should be smaller than " + (Math.ceil(i/2) - 1))
      correct = false;
    }
  }
  return correct;
}
/**
 * rebuilds the heap. Use it if ratings ettings have changed.
 */
VideoHeap.prototype.rebuild = function() {
  for (var i = 0; i < this.array.length; i++) {
    this.array[i].rating = youtubeAutoplay.getRating(this.array[i]);
  }
  youtubeAutoplay.debug("rebuilding " + this.array.length + " elements");
  this.array.sort(function(v1, v2) {
    return v1.rating < v2.rating ? 1
         : v1.rating > v2.rating ? -1
         :                         0});
}
/**
 * sets the array of the heap
 * @param array must be a valid heap array
 */
VideoHeap.prototype.setArray = function(array) {
  this.array = array;
}
/**
 * gets the current heap array.
 * use getFirst or hasVide/getVideo instead.
 */
/*VideoHeap.prototype.getArray = function() {
  return this.array;
}*/
/**
 * gets the current number of videos in the heap
 */
VideoHeap.prototype.getLength = function() {
  return this.array.length;
}
/**
 * removes the first element
 * @return the first element
 */
VideoHeap.prototype.getFirst = function(id) {
  return this.array[0];
}
/**
 * removes the first element
 * @return the first element
 */
VideoHeap.prototype.removeFirst = function(id) {
  var first = this.array[0];
  this.array[0] = this.array.pop();
  this.siftDown(0);
  return first;
}
/**
 * gets an video by its id
 * @return the video or null if it does not exist.
 */
VideoHeap.prototype.getVideoById = function(id) {
  for (var i = 0; i < this.array.length; i++) {
    if (this.array[i].id == id) {
      return this.array[i];
    }
  }
  return null;
}
/**
 * checks if a video with an id is in the heap
 */
VideoHeap.prototype.hasVideoWithId = function(id) {
  for (var i = 0; i < this.array.length; i++) {
    if (this.array[i].id == id) {
      return true;
    }
  }
  return false;
}
/**
 * gets an video by its id
 * @return the video or null if it does not exist.
 */
/*VideoHeap.prototype.getVideoByIndex = function(index) {
  return this.array[index];
}*/

/**
 *
 */
VideoHeap.prototype.getVideo = function(video) {
  for (var i = 0; i < this.array.length; i++) {
    if (youtubeAutoplay.compareVideos(video, this.array[i])) {
      return this.array[i];
    }
  }
  return null;
}
/**
 * gets an video by its id
 * @return true if the video was updatet, false if it was not found
 */
VideoHeap.prototype.updateVideo = function(id, video) {
  for (var i = 0; i < this.array.length; i++) {
    if (this.array[i].id == id) {
      this.array[i] = video;
      this.array[i].rating = youtubeAutoplay.getRating(video);
      this.sift(i);
      return true
    }
  }
  return false;
}

/**
 * gets an video by its id
 * @return true if the video was updatet, false if it was not found
 */
VideoHeap.prototype.removeVideoById = function(id) {
  youtubeAutoplay.debug("removing id " + id + "")
  for (var i = 0; i < this.array.length; i++) {
    if (this.array[i].id == id) {
      var oldRating = this.array[i].rating;
      if (i < this.array.length - 1) {
        this.array[i] = this.array.pop();
        if (oldRating > this.array[i].rating) {
          this.siftDown(i);
        } else {
          this.siftUp(i);
        }
        youtubeAutoplay.debug("id " + id + " removed")
      }
      return true
    }
  }
  youtubeAutoplay.debug("id " + id + " not found")
  return false;
}

/**
 * gets an video by its id
 */
VideoHeap.prototype.insertVideo = function(video) {
  video.rating = youtubeAutoplay.getRating(video);
  this.array.push(video);
  this.siftUp(this.array.length - 1);
}

//private
VideoHeap.prototype.switch = function(index1, index2) {
  youtubeAutoplay.debug("switch: " + index1 + "←→" + index2)
  var video = this.array[index1];
  this.array[index1] = this.array[index2];
  this.array[index2] = video;
}

/**
 * let a video shift, if the invariant is not correct around it.
 * the direction is automaticaly calculated.
 */
VideoHeap.prototype.sift = function(index) {
  if (index > 0 && this.array[index].rating
          > this.array[Math.ceil(index / 2) - 1].rating) {
    this.siftUp(index);
  } else {
    this.siftDown(index);
  }
}
/* private. reconstructs the invariant by moving an element down the tree */
VideoHeap.prototype.siftDown = function(index) {
  //switch with the subtree element that is bigger
  if ( index * 2 + 2  < this.array.length
      && this.array[index * 2 + 1].rating < this.array[index * 2 + 2].rating) {
    //switch with the right one
    // only switch if the right child is bigger than the current element
    if (this.array[index * 2 + 2].rating > this.array[index].rating) {
      this.switch(index, index * 2 + 1);
      this.siftDown(index * 2 + 2);
    }
  } else if (index * 2 + 1 < this.array.length) {
    //switch with the left one
    // only switch if the left child is bigger than the current element
    if (this.array[index * 2 + 1].rating > this.array[index].rating) {
      this.switch(index, index * 2 + 1);
      this.siftDown(index * 2 + 1);
    }
  }
}
VideoHeap.prototype.siftUp = function(index) {
  if (index < 1) {
    return;
  }
  var newIndex = Math.ceil(index / 2) - 1;
  //sift up if the rating of the current element (lower) is bigger than the one
  //of the upper element
  if (this.array[index].rating > this.array[newIndex].rating) {
    this.switch(index, newIndex)
    this.siftUp(newIndex);
  }
}
/**
 * gets the source
 * @return the source of this object
 */
VideoHeap.prototype.toSource = function() {
  return "new VideoHeap(" + this.array.toSource() + ")";
}

var youtubeAutoplay = {
  /* how long is a session saved ?*/
  SAVE_DAYS : 2,

  /* the id of the autoplay session, or 0 if no session is set. */
  autoplaySession : 0,
  proposedVideos : new VideoHeap(),
  watchedVideos : [],

  startLinkTextNode : null,

  currentVideo : {},

  /* calculate once, so if getNextVideo is called twice there are no problems */
  randomConstant : Math.random(),

  /* initializes the text */
  init: function() {
    youtubeAutoplay.addLinks();
    var autoplayFound = location.href.match(/#autoplay(\d+)/);
    if (autoplayFound) {
      youtubeAutoplay.resumeAutoplay(autoplayFound[1]);
      youtubeAutoplay.checkForError();
    }
  },

  debug : function(data) {
    if (!DEBUG) {
    } else if (typeof unsafeWindow.console == "object") {
      unsafeWindow.console.debug(data)
    } else {
      GM_log(data)
    }
  },

  /* SESSION MANAGEMENT */

  /** loads the videos for the current session from the storage, and overwrites the old ones. */
  loadVideoSession : function() {
    //assert autoplaySession != 0
    var videos = GM_getValue(youtubeAutoplay.autoplaySession + ".proposedVideos", null);
    try {
      youtubeAutoplay.debug("Video data:  " + videos)
      var videos = eval(videos);
    } catch(e) {
      youtubeAutoplay.debug("Eval-Error: " + e)
      var videos = new VideoHeap();
    }
    if (typeof videos.getFirst == "undefined") {
      var videos = new VideoHeap();
    }
    //if settings were changed
    if (GM_getValue(youtubeAutoplay.autoplaySession + ".settingVersion", 0)
        != GM_getValue("settingVersion", 1)) {
      videos.rebuild();
    }

    youtubeAutoplay.proposedVideos = videos;

    // watched videos
    var videos = GM_getValue(youtubeAutoplay.autoplaySession + ".watchedVideos", "[]");
    try {
      youtubeAutoplay.watchedVideos = eval(videos);
    } catch(e) {
      youtubeAutoplay.debug("Eval-Error: " + e)
      youtubeAutoplay.watchedVideos = [];
    }
    GM_setValue(youtubeAutoplay.autoplaySession + ".lastAccess", (new Date()).getTime()+"");
  },

  /**  saves the videos of the current session */
  saveVideoSession : function() {
    //assert autoplaySession != 0
    GM_setValue(youtubeAutoplay.autoplaySession + ".proposedVideos", youtubeAutoplay.proposedVideos.toSource());
    GM_setValue(youtubeAutoplay.autoplaySession + ".watchedVideos", youtubeAutoplay.watchedVideos.toSource());
    GM_setValue(youtubeAutoplay.autoplaySession + ".lastAccess", (new Date()).getTime()+"");
    GM_setValue(youtubeAutoplay.autoplaySession + ".settingVersion",
      GM_getValue("settingVersion", 0));
  },

  /* cleans up the sessions */
  deleteOldSessions : function() {
    var allValues = GM_listValues();
    var minimalTimestamp = (new Date()).getTime() - youtubeAutoplay.SAVE_DAYS * 24 * 60 * 60 * 1000;
    for (var i = 0; i < allValues.length; i++) {
      if (allValues[i].indexOf(".lastAccess")> -1
        && GM_getValue(allValues[i]) * 1 < minimalTimestamp) {
        youtubeAutoplay.deleteSession(allValues[i].replace(".lastAccess", ""));
      }
    }
  },

  deleteSession: function(sessionId) {
    GM_deleteValue(sessionId + ".proposedVideos");
    GM_deleteValue(sessionId + ".watchedVideos");
    GM_deleteValue(sessionId + ".lastAccess");
  },

  /*       VIDEO LISTS */

  getRating : function(video) {
    youtubeAutoplay.debug(
        "rank: " + video.rank + "=>" + Math.max(0, 1 - video.rank / 20) + "*" +  youtubeAutoplay.getSetting("ratingRank")
        + "\nlinked: " + video.linked + "=>" + (-1 / (video.linked) + 1) * youtubeAutoplay.getSetting("ratingLinked")
        + "\ndistance: " + video.distance + "=>" + (-1 / (video.distance) + 1) * youtubeAutoplay.getSetting("ratingDistance")
        + "\n*unliked: " + video.unliked + "=>" + Math.pow(youtubeAutoplay.getSetting("unlikedFactor"), video.unliked));
    var rating = ( 1 / (video.distance) * youtubeAutoplay.getSetting("ratingDistance")
          + (-1 / (video.linked) + 1) * youtubeAutoplay.getSetting("ratingLinked")
          + Math.max(0, 1 - video.rank / 20) *  youtubeAutoplay.getSetting("ratingRank"))
      * Math.pow(1-youtubeAutoplay.getSetting("unlikedFactor"), video.unliked);
   youtubeAutoplay.debug(rating);
   rating *= 1 + (Math.random() - 0.5) * youtubeAutoplay.getSetting("randomisationFactor");
   return rating;
  },

  recalculateRatings : function() {
    GM_getValue("settingVersion", Math.floor(Math.random() * 10000));
    youtubeAutoplay.proposedVideos.rebuild();
    youtubeAutoplay.saveVideoSession();
  },
  /**
   * recalculates all ratings like recalculateRatings, but waits a moment before
   * doing so.
   */
  recalculateRatings_timeout : false,
  recalculateRatings2 : function() {
    if (youtubeAutoplay.recalculateRatings_timeout !==  false) {
      clearTimeout(youtubeAutoplay.recalculateRatings_timeout);
    }
    youtubeAutoplay.recalculateRatings_timeout = setTimeout(youtubeAutoplay.recalculateRatings, 500);
  },

  // Todo: use a hash table of compare strings and move this to rating?
  compareVideos : function(video1, video2) {
    return GM_getValue("useCompareString", false) == true
              ? video1.compare == video2.compare
              : video1.id == video2.id;
  },

  getNextVideo : function() {
    /**
     * TODO: (?)
     * for randomisation, we use a linear function. its highest point is at x=0
     * it reaches zero at |proposedVideos| / randomisationFactor. f(x) is the
     * possibility with wich a video is played.
     *
     * currently we just use a simple x^2-function
     */
    //TODO: fix unsafeWindow problem!
    /*var randomisationFactor = youtubeAutoplay.getSetting("randomisationFactor");
    randomisationFactor = Math.min(1, Math.abs(randomisationFactor));

    var maxIndex = (youtubeAutoplay.proposedVideos.getLength() - 1) * randomisationFactor;
    var temporaryRandomConstant = (youtubeAutoplay.randomConstant
        + randomisationFactor * 13 * youtubeAutoplay.randomConstant) % 1;
    //f(rand) = floor(c * x^2); f(0) = 0; f(1) = maxIndex; c = maxIndex
    var index = Math.floor(temporaryRandomConstant * temporaryRandomConstant * maxIndex);
    if (index == NaN || !youtubeAutoplay.proposedVideos.getVideoByIndex(index)) {
      youtubeAutoplay.debug("wrong index: " + index)
      index = 0;
    }*/
    return youtubeAutoplay.proposedVideos.getFirst();
  },

  // loads the current video variable
  loadCurrentVideo : function() {
    var id = unsafeWindow.pageVideoId;
    if (!id) {
      return;
    }
    
    var name = document.title.replace("YouTube - ", "");
    var video = youtubeAutoplay.proposedVideos.getVideoById(id);
    if (video) {
      youtubeAutoplay.currentVideo = video;
      youtubeAutoplay.proposedVideos.removeVideoById(id)
    } else {
      //watrching a video that was never seen bevore
      youtubeAutoplay.currentVideo = {
          id        : id,
          name      : name,
          compare   : youtubeAutoplay.getCompareStringForName(name),
          distance  : 0, //start
          unliked   : 0,
          linked    : 1,
          rank      : 1
      };
    }
    youtubeAutoplay.watchedVideos.push(youtubeAutoplay.currentVideo);
  },
  
  /** adds a possible video to the list or increases the rank of a given one */
  addPossibleVideo : function(video, currentRank) {
    var oldVideo = youtubeAutoplay.proposedVideos.getVideoById(video.id);
    if (oldVideo != null) {
      youtubeAutoplay.debug("Updating possible video " + video.id)
      youtubeAutoplay.debug(youtubeAutoplay.currentVideo);

      oldVideo.distance = Math.min(oldVideo.distance, youtubeAutoplay.currentVideo.distance + 1);
      oldVideo.rank = (oldVideo.rank * oldVideo.linked + currentRank) / (oldVideo.linked + 1);
      oldVideo.linked++;
      youtubeAutoplay.proposedVideos.updateVideo(oldVideo.id, oldVideo);
    } else {
      youtubeAutoplay.debug("Adding possible video " + video.id)
      //add it
      video.distance = youtubeAutoplay.currentVideo.distance + 1;
      video.unliked = 0;
      video.linked = 1;
      video.rank = currentRank;
      youtubeAutoplay.proposedVideos.insertVideo(video);
    }
  },

  videoWasWatched : function(video) {
    var video1 = video;
    return youtubeAutoplay.watchedVideos.some(function(video2) {
      return youtubeAutoplay.compareVideos(video1, video2);
    });
  },

  loadPageVideos : function() {
    //search related videos
    var videos = youtubeAutoplay.getVideosOnPage();
    for (var index = 0; index < videos.length; index++) {
      if (youtubeAutoplay.videoWasWatched(videos[index]) || !videos[index].id) {
        continue;
      } else {
        youtubeAutoplay.addPossibleVideo(videos[index], index);
      }
    }

  },


  /*
   * checks if youtube shows an error, and skips the next movie. Because we
   possibly came to this error while trying to open the next video TODO!!
   * @rturn if there was an error*/
  checkForError : function() {
    if (document.getElementById("error-box")) {
      return true;
    } else {
      return false;
    }
  },

  /**
   * @return an array of videos that are on the page.
   */
  getVideosOnPage : function() {
     //search related videos
    var panel = document.getElementById("watch-related-discoverbox");
    if (!panel) {
      return [];
    }

    var possibleDivs = panel.getElementsByTagName("div");
          panel.addEventListener('mouseover', function(evt) {
        var as = evt.target.getElementsByTagName("a");
        Array.forEach(as, function(a) {
          if (a.href.indexOf("v=")) {
            a.href = a.href.replace(/#autoplay\d+/, "");
            if (youtubeAutoplay.autoplaySession != 0) {
              a.href += "#autoplay" + youtubeAutoplay.autoplaySession;
            }
          }
        })
      }, true)
    var videos = [];

    for (var divIndex = 0; divIndex < possibleDivs.length; divIndex++) {
      if (!possibleDivs[divIndex].className
        || possibleDivs[divIndex].className.indexOf("video-entry") < 0) {
        continue;
      }


      var data = youtubeAutoplay.getVideoByDiv(possibleDivs[divIndex]);
      videos.push(data);
    }
    return videos;
  },

  /**
   * gets a video for a given div of a related videos list
   * @return an array of small video objects with the properties name, id and compare
   */
  getVideoByDiv : function(div) {
    var a = div.getElementsByTagName("a")[3];
    var idMatch = /v=([\w-]+)/.exec(a.href)
    return {name: a.title,
            id: idMatch ? idMatch[1] : "",
            compare: youtubeAutoplay.getCompareStringForName(a.title)}
  },

  /**
   * returns a string that represents the title of the video. If two videos have
   * the same CompareString, they are most likely the same.
   */
  getCompareStringForName : function(name) {
    var match = /\"([^\"]+)\"/.exec(name + "");
    var realname = match ? match[1] : (name + "").replace(/^[^\-]+\-|\([^\(]*\)/g, "");
    return realname.replace(/\W/g, "").toLowerCase();
  },

  /** marks all related videos as unliked */
  unlikeThisVideo : function() {
    var videos = youtubeAutoplay.getVideosOnPage();
    for (var i = 0; i < videos.length; i++) {
      youtubeAutoplay.debug("video " + i + "; id=" + videos[i].id)
      var video = youtubeAutoplay.proposedVideos.getVideoById(videos[i].id);
      if(video) {
        youtubeAutoplay.debug("video: " + video);
        video.unliked += 2 - i * 0.05;
        youtubeAutoplay.proposedVideos.updateVideo(videos[i].id, video);
      }
    }
    youtubeAutoplay.saveVideoSession();
  },


  /*           INTERACTIVITY     */
  // ads an "autoplay"-link to the related videos list
  addLinks : function() {
    var panel = document.getElementById("watch-related-videos-panel");
    if (!panel) {
      return false;
    }
    var linkDiv = document.createElement("div");
    linkDiv.className = "expand-content floatR";
    panel.insertBefore(linkDiv, panel.firstChild);

    var link = document.createElement("a");
    youtubeAutoplay.startLinkTextNode = document.createTextNode(i18n("start autoplay"));
    link.appendChild(youtubeAutoplay.startLinkTextNode);
    link.href = "javascript:;"
    link.addEventListener("click", function() {
      if (youtubeAutoplay.autoplaySession == 0) {
        youtubeAutoplay.startAutoplay();
      } else {
        youtubeAutoplay.stopAutoplay();
      }
    }, true);
    linkDiv.appendChild(link);

    var seperator = document.createElement("a");
    seperator.appendChild(document.createTextNode(" | "));
    seperator.className = "smallText grayText";
    linkDiv.appendChild(seperator);

    var link = document.createElement("a");
    link.appendChild(document.createTextNode(i18n("play next")));
    link.href = "javascript:;"
    link.addEventListener("click", function() {
      if (youtubeAutoplay.autoplaySession == 0) {
        youtubeAutoplay.startAutoplay();
      }
      youtubeAutoplay.playNextVideo();
    }, true);
    link.addEventListener("mouseover", function(evt) {
      if (youtubeAutoplay.autoplaySession && youtubeAutoplay.getNextVideo()) {
        evt.target.title = i18n("next video: %s", youtubeAutoplay.getNextVideo().name);
      } else {
        evt.target.title = i18n("autoplay not started yet");
      }
    }, true);
    linkDiv.appendChild(link);

    linkDiv.appendChild(document.createElement("br"));

    var link = document.createElement("a");
    link.appendChild(document.createTextNode(i18n("settings")));
    link.href = "javascript:;"
    link.addEventListener("click", function() {
      youtubeAutoplay.showSettings();
    }, true);
    linkDiv.appendChild(link);

    var seperator = document.createElement("a");
    seperator.appendChild(document.createTextNode(" | "));
    seperator.className = "smallText grayText";
    linkDiv.appendChild(seperator);

    var link = document.createElement("a");
    link.appendChild(document.createTextNode(i18n("hate this")));
    link.href = "javascript:;"
    link.addEventListener("click", function() {
      if (youtubeAutoplay.autoplaySession == 0) {
        youtubeAutoplay.startAutoplay();
      }
      youtubeAutoplay.unlikeThisVideo();
      youtubeAutoplay.playNextVideo();
    }, true);
    link.title = i18n("go to the next video and do not play the related videos of this one");
    youtubeAutoplay.playNextLink = link;
    linkDiv.appendChild(link);

    // clean right
    GM_addStyle("#watch-related-vids-body {clear:right}");
  },

  /* resumes the autoplay session, and adds the listeners for the next video */
  resumeAutoplay : function(sessionID) {
    if (youtubeAutoplay.startLinkTextNode) {
      youtubeAutoplay.startLinkTextNode.data = i18n("stop autoplay");
    }

    youtubeAutoplay.autoplaySession = sessionID;
    location.href = location.href.replace(/#+(autoplay\d+)?/, "")
        + "#autoplay" + youtubeAutoplay.autoplaySession;
    youtubeAutoplay.loadVideoSession();
    youtubeAutoplay.loadCurrentVideo();
    youtubeAutoplay.loadPageVideos();
    youtubeAutoplay.saveVideoSession();
    youtubeAutoplay.startNextVideoInterval();
  },

  /* start the autoplaying, and delete the old session */
  startAutoplay : function() {
    if (youtubeAutoplay.startLinkTextNode) {
      youtubeAutoplay.startLinkTextNode.data = i18n("stop autoplay")
    }
    youtubeAutoplay.autoplaySession = Math.ceil(Math.random() * 10000);
    location.href = location.href.replace(/#+(autoplay\d+)?/, "")
        + "#autoplay" + youtubeAutoplay.autoplaySession;
    youtubeAutoplay.proposedVideos = new VideoHeap();
    youtubeAutoplay.loadCurrentVideo();
    youtubeAutoplay.loadPageVideos();
    youtubeAutoplay.saveVideoSession();

    youtubeAutoplay.startNextVideoInterval();
    youtubeAutoplay.deleteOldSessions();
  },

  /* stop autoplaying */
  stopAutoplay : function() {
    if (youtubeAutoplay.startLinkTextNode) {
      youtubeAutoplay.startLinkTextNode.data = i18n("start autoplay");
    }
    location.href = location.href.replace(/#+(autoplay\d+)?/, "#")
    youtubeAutoplay.autoplaySession = 0;
    youtubeAutoplay.stopNextVideoInterval();
    youtubeAutoplay.deleteOldSessions();
  },

  /**
   * opens the site of the next video
   */
  playNextVideo : function() {
    if (youtubeAutoplay.autoplaySession == 0) {
      return;
    }
    youtubeAutoplay.stopNextVideoInterval();
    nextVideo = youtubeAutoplay.getNextVideo();
    location.href = "/watch?v=" + nextVideo.id + "&feature=related#autoplay"
      + youtubeAutoplay.autoplaySession;

  },


  /* check all 100ms if the video has terminated. */
  /*
   * this bloody flash does not allow access out of safe window, and we dont
   * have access to GM function in unsafeWindow. so we need this dirty trick :-(
   */
  finishedPlayback  : false,
  nextVideoInterval : -1,
  unsafeNextVideoInterval : -1,
  /**
   * starts an interval that checks if the video was finished yet
   * and plays the next video if it was finished.
   */
  startNextVideoInterval : function() {
    youtubeAutoplay.stopNextVideoInterval();

    //this bloody flash...
    // use a string => executed in unsafe Window
    youtubeAutoplay.unsafeNextVideoInterval = setInterval(
        'var p = document.getElementById("movie_player");'
      + 'var finishedPlayback = false;'
      + 'try {var isPlaying = p.GetVariable("movie.is_playing");'
      + 'if (isPlaying == null) {'
      + 'isPlaying = p.GetVariable("is_playing");'
      + '}'
      + 'var restart = p.GetVariable("movie.restart");'
      + 'if (restart == null) {'
      + 'restart = p.GetVariable("restart");'
      + '}'
        //export for save window
      + 'youtubeAutoplay.finishedPlayback = isPlaying == "false" && restart == "true";'
      + '} catch (err) {'
        //export for save window
      + 'youtubeAutoplay.finishedPlayback = p.GetVariable("is_playing") == "false" && p.GetVariable("restart") == "true";'
      + '}'
    , 100);
    youtubeAutoplay.nextVideoInterval = setInterval(function() {
      if (youtubeAutoplay.finishedPlayback && youtubeAutoplay.autoplaySession != 0) {
        youtubeAutoplay.playNextVideo();
      }
    }, 50)
  },

  /**
   * stops the interval that checks if the video was finished.
   * call it after the finihing of the video was handeled
   */
  stopNextVideoInterval : function() {
    if (youtubeAutoplay.nextVideoInterval > -1) {
      clearInterval(youtubeAutoplay.nextVideoInterval);
      youtubeAutoplay.nextVideoInterval = -1
    }
    if (youtubeAutoplay.unsafeNextVideoInterval > -1) {
      clearInterval(youtubeAutoplay.unsafeNextVideoInterval);
      youtubeAutoplay.unsafeNextVideoInterval = -1
    }
  },


  /* SETTINGS */
  /*
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window title="' + getText('settings') + '"
        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
        xmlns:html="http://www.w3.org/1999/xhtml">

<dialogheader title="' + getText('Rating') + '" description="' + getText('video with the best rating will be played next') + '"/>
<vbox style="padding:0 1em 1em">
  <hbox id="ratingParts">
    <box value="" style="background: #3191FF;height:40px;" flex="1" id="ratingDistance"/>
    <splitter collapse="both" id="ratingSplitter1"/>
    <box value="" style="background: #1F35FF;height:40px;" flex="1" id="ratingRank"/>
    <splitter collapse="both" id="ratingSplitter2"/>
    <box value="" style="background: #001AFF;height:40px" flex="1" id="ratingLinked"/>
  </hbox>
  <hbox>
    <box flex="1">
      <box style="background:#3191FF;width:1.5em;height:1em;margin:.3em;"/>
      <label value="' + getText('close to the start video') + '"/>
    </box>
    <box flex="1">
      <box style="background:#1F35FF;width:1.5em;height:1em;margin:.3em;"/>
      <label value="' + getText('mostly high in the related videos list') + '"/>
    </box>
    <box flex="1">
      <box style="background: #001AFF;width:1.5em;height:1em;margin:.3em;"/>
      <label value="' + getText('often related') + '"/>
    </box>
  </hbox>
</vbox>

<dialogheader title="' + getText('Play options') + '" description="' + getText('what else affects the order of videos') + '"/>
<vbox style="padding:0 1em 1em">
  <grid>
    <columns>
      <column/>
      <column flex="1"/>
    </columns>
    <rows flex="1">
      <row>
        <label value="' + getText('randomisation') + '"/>
        <scale min="0" max="100" id="randomisationScale"/>
      </row>
      <row>
        <label value="' + getText('do not play unliked videos') + '"/>
        <scale min="0" max="100" id="unlikedScale"/>
      </row>
    </rows>
  </grid>
  <checkbox id="useCompareString" label="' + getText('do not play videos with the same title twice') + '"/>
</vbox>

<dialogheader title="' + getText('About') + '" description="Youtube autoplay 1.0"/>
  <vbox style="padding:0 1em 1em">
  <label value="' + getText('%s by %s', 'Youtube Autoplay', 'Michael Z.') + '"/>
  <label value="' + getText('Current Version: %s', '1.0pre7') + '"/>
+  <label value="' + getText('Language code (selected by Youtube): %s', document.documentElement.getAttribute('lang')) + '"/>  <description>
    More information at the scripts
    <html:a href="http://userscripts.org/scripts/show/54901" target="_blank">homepage</html:a>
  </description>
</vbox>
<spacer flex="1"/>
<hbox>
  <label value="' + getText('settings are automatically saved') + '"/>
  <spacer flex="1"/><button label="' + getText('close') + '" oncommand="close()"/>
</hbox>
</window>*/
  settingsWindow : null,
  /**
   * shows a popup with the settings
   */
  showSettings : function() {
    if (youtubeAutoplay.settingsWindow != null && !youtubeAutoplay.settingsWindow.closed) {
      return;
    }

    function getText(text) {
      return i18n.apply(window, arguments).replace(/\"/g, "&quot;");
    }

    var settingsCode = 'data:application/vnd.mozilla.xul+xml,'
       + encodeURIComponent('<?xml version="1.0"?> <?xml-stylesheet href="chrome://global/skin/" type="text/css"?> <window title="' + getText('settings') + '" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"> <dialogheader title="' + getText('Rating') + '" description="' + getText('video with the best rating will be played next') + '"/> <vbox style="padding:0 1em 1em"> <hbox id="ratingParts"> <box value="" style="background: #3191FF;height:40px;" flex="1" id="ratingDistance"/> <splitter collapse="both" id="ratingSplitter1"/> <box value="" style="background: #1F35FF;height:40px;" flex="1" id="ratingRank"/> <splitter collapse="both" id="ratingSplitter2"/> <box value="" style="background: #001AFF;height:40px" flex="1" id="ratingLinked"/> </hbox> <hbox> <box flex="1"> <box style="background:#3191FF;width:1.5em;height:1em;margin:.3em;"/> <label value="' + getText('close to the start video') + '"/> </box> <box flex="1"> <box style="background:#1F35FF;width:1.5em;height:1em;margin:.3em;"/> <label value="' + getText('mostly high in the related videos list') + '"/> </box> <box flex="1"> <box style="background: #001AFF;width:1.5em;height:1em;margin:.3em;"/> <label value="' + getText('often related') + '"/> </box> </hbox> </vbox> <dialogheader title="' + getText('Play options') + '" description="' + getText('what else affects the order of videos') + '"/> <vbox style="padding:0 1em 1em"> <grid> <columns> <column/> <column flex="1"/> </columns> <rows flex="1"> <row> <label value="' + getText('randomisation') + '"/> <scale min="0" max="100" id="randomisationScale"/> </row> <row> <label value="' + getText('do not play unliked videos') + '"/> <scale min="0" max="100" id="unlikedScale"/> </row> </rows> </grid> <checkbox id="useCompareString" label="' + getText('do not play videos with the same title twice') + '"/> </vbox> <dialogheader title="' + getText('About') + '" description="Youtube autoplay 1.0"/> <vbox style="padding:0 1em 1em"> <label value="' + getText('%s by %s', 'Youtube Autoplay', 'Michael Z.') + '"/> <label value="' + getText('Current Version: %s', '1.0pre7') + '"/> <label value="' + getText('Language code (selected by Youtube): %s', document.documentElement.getAttribute('lang')) + '"/> <description> More information at the scripts <html:a href="http://userscripts.org/scripts/show/54901" target="_blank">homepage</html:a> </description> </vbox> <spacer flex="1"/> <hbox> <label value="' + getText('settings are automatically saved') + '"/> <spacer flex="1"/><button label="' + getText('close') + '" oncommand="close()"/> </hbox> </window>');    youtubeAutoplay.settingsWindow = window.open(settingsCode);
    youtubeAutoplay.settingsWindow.addEventListener("load", youtubeAutoplay.settingsWindowLoad, true);
    youtubeAutoplay.settingsWindow.addEventListener("close", function() {
      youtubeAutoplay.settingsWindow = null;
    }, true);
  },

  /**
   * adds the events to the settings window and initiates all settings
   */
  settingsWindowLoad : function() {
    var doc = youtubeAutoplay.settingsWindow.document;
    /*youtubeAutoplay.settingsRegisterScale(doc.getElementById("distanceScale"), "distanceFactor");
    youtubeAutoplay.settingsRegisterScale(doc.getElementById("rankScale"), "rankFactor");
    youtubeAutoplay.settingsRegisterScale(doc.getElementById("linkedScale"), "linkedFactor");*/
    youtubeAutoplay.settingsRegisterScale(doc.getElementById("unlikedScale"), "unlikedFactor");
    youtubeAutoplay.settingsRegisterScale(doc.getElementById("randomisationScale"), "randomisationFactor");

    //rating scales
    doc.getElementById("ratingParts").addEventListener("command", youtubeAutoplay.settingsChangedRatings, true);
    var totalWidth = doc.getElementById("ratingParts").boxObject.width
                   - 2 * doc.getElementById("ratingSplitter1").boxObject.width;
    doc.getElementById("ratingDistance").setAttribute("width", youtubeAutoplay.getSetting("ratingDistance") * totalWidth)
    doc.getElementById("ratingRank").setAttribute("width", youtubeAutoplay.getSetting("ratingRank") * totalWidth)
    doc.getElementById("ratingLinked").setAttribute("width", youtubeAutoplay.getSetting("ratingLinked") * totalWidth)

    var checkbox = doc.getElementById("useCompareString");
    checkbox.setAttribute("checked", GM_getValue("useCompareString", false));
    checkbox.addEventListener("command", function(evt) {
      GM_setValue("useCompareString", evt.target.getAttribute("checked") == "true");
    }, true);
  },

  /*
   all values are recalculated using the getValue method!
   */
  settingsRegisterScale : function(scale, settingName) {
    scale.setAttribute("value", GM_getValue(settingName, 50));
    var settingName = settingName;
    scale.addEventListener("change", function(evt) {
      GM_setValue(settingName, evt.target.getAttribute("value") * 1);
      youtubeAutoplay.recalculateRatings2();
    }, true);
  },

  /**
   * TODO ?
   */
  settingsShowTooltipp : function() {
  },

  /**
   * recalculates the rating settings by the current position of the splitters
   */
  settingsChangedRatings : function() {
    var doc = youtubeAutoplay.settingsWindow.document;

    var ratingDistance = doc.getElementById("ratingDistance").boxObject.width;
    var ratingRank = doc.getElementById("ratingRank").boxObject.width;
    var ratingLinked = doc.getElementById("ratingLinked").boxObject.width;
    youtubeAutoplay.debug("Bar dinemsions: " + ratingDistance + "-" + ratingRank + "-" + ratingLinked)
    var wholeWidth = ratingDistance + ratingRank + ratingLinked;
    ratingDistance = Math.floor(ratingDistance / wholeWidth * 100);
    ratingRank = Math.floor(ratingRank / wholeWidth * 100);
    ratingLinked = 100 - ratingRank - ratingDistance;
    youtubeAutoplay.debug("Result (%): " + ratingDistance + "-" + ratingRank + "-" + ratingLinked)
    GM_setValue("ratingDistance", ratingDistance);
    GM_setValue("ratingRank", ratingRank);
    GM_setValue("ratingLinked", ratingLinked);

    youtubeAutoplay.recalculateRatings2();
  },
  /*
   * gets a GM - setting value (0 .. 100) and recalculates it to our setting values (0 => 0, 100=>1)
   * @return a value (0...1) depending on the user setting
   */
  getSetting : function(settingName)  {
    try {
      var value = GM_getValue(settingName, 33) / 100;
    } catch(e) {
      var value = 0.33;
    }
    if (settingName == "ratingRank") {
      value = Math.min(value, 1 - youtubeAutoplay.getSetting("ratingDistance"));
    } else if (settingName == "ratingLinked") {
      value = Math.min(value, 1 - youtubeAutoplay.getSetting("ratingDistance")
                                - youtubeAutoplay.getSetting("ratingRank"));
    }
    if (value == NaN) {
      value = 0.33;
    }
    return value * 1;
  }
}
unsafeWindow.youtubeAutoplay = youtubeAutoplay;

//initialize
youtubeAutoplay.init();
GM_registerMenuCommand(i18n("start autoplay"), youtubeAutoplay.startAutoplay)
GM_registerMenuCommand(i18n("stop autoplay"), youtubeAutoplay.stopAutoplay)