Commits

Mathias Köhler  committed eac6f71

Modularize JS, Add CSS, Some other changes

  • Participants
  • Parent commits 9cb8eeb

Comments (0)

Files changed (29)

 *.pyc
+.sass-cache
+node_modules
 #!/usr/bin/env python
 # coding: utf8
 
+from socketio.server import SocketIOServer
 from flask_script import Manager
 from wetube import app, db
+from wetube.workers import spawn
+from redis import StrictRedis
 
 
 manager = Manager(app)
 
 
 @manager.command
+def worker():
+    spawn()
+
+
+@manager.command
+def clean():
+    db = StrictRedis()
+    db.delete("connected_count")
+    db.delete("connected")
+    db.delete("playlist")
+
+
+@manager.command
 def runserver():
-    from socketio.server import SocketIOServer
     SocketIOServer(('0.0.0.0', 5000), app,
         namespace="socket.io",  policy_server=False).serve_forever()
 

File wetube/__init__.py

 # -*- coding: utf-8 -*-
 
 from flask import Flask, render_template, request, session, redirect, \
-    url_for
+    url_for, send_from_directory
 from passlib.hash import pbkdf2_sha512 as hasher
 from redis import StrictRedis
 from socketio import socketio_manage
 
 @app.route('/logout')
 def logout():
-    del session["user"]
-    return redirect(url_for('index'))
+    if "user" in session:
+        del session["user"]
+    return redirect(url_for('login'))
 
 
 @app.route("/register", methods=["POST"])

File wetube/assets/config.rb

+# Require any additional compass plugins here.
+
+# Set this to the root of your project when deployed:
+http_path = "/"
+css_dir = "css"
+sass_dir = "sass"
+images_dir = "images"
+javascripts_dir = "javascripts"
+
+# You can select your preferred output style here (can be overridden via the command line):
+output_style = :compact
+
+# To enable relative paths to assets via compass helper functions. Uncomment:
+# relative_assets = true
+
+# To disable debugging comments that display the original location of your selectors. Uncomment:
+# line_comments = false
+
+
+# If you prefer the indented syntax, you might want to regenerate this
+# project again passing --syntax sass, or you can uncomment this:
+preferred_syntax = :sass
+# and then run:
+# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass

File wetube/assets/css/main.css

+/* line 17, ../../../../../.gem/ruby/2.0.0/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font: inherit; font-size: 100%; vertical-align: baseline; }
+
+/* line 22, ../../../../../.gem/ruby/2.0.0/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+html { line-height: 1; }
+
+/* line 24, ../../../../../.gem/ruby/2.0.0/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+ol, ul { list-style: none; }
+
+/* line 26, ../../../../../.gem/ruby/2.0.0/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+table { border-collapse: collapse; border-spacing: 0; }
+
+/* line 28, ../../../../../.gem/ruby/2.0.0/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+caption, th, td { text-align: left; font-weight: normal; vertical-align: middle; }
+
+/* line 30, ../../../../../.gem/ruby/2.0.0/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+q, blockquote { quotes: none; }
+/* line 103, ../../../../../.gem/ruby/2.0.0/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+q:before, q:after, blockquote:before, blockquote:after { content: ""; content: none; }
+
+/* line 32, ../../../../../.gem/ruby/2.0.0/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+a img { border: none; }
+
+/* line 116, ../../../../../.gem/ruby/2.0.0/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary { display: block; }
+
+/* line 6, ../sass/main.sass */
+html, body { background: #1b1b1b; position: relative; height: 100%; overflow: hidden; }
+
+/* line 12, ../sass/main.sass */
+header { width: 100%; height: 55px; background: #272727; color: white; z-index: 3; }
+/* line 19, ../sass/main.sass */
+header h1, header h2, header h3 { width: 40%; font-size: 1.3em; font-family: Arial; padding: 17px; float: left; color: #cccccc; }
+/* line 26, ../sass/main.sass */
+header h1:first-child, header h2:first-child, header h3:first-child { font-size: 1.5em; color: white; }
+/* line 30, ../sass/main.sass */
+header nav { height: 100%; }
+/* line 32, ../sass/main.sass */
+header nav > ul { float: right; text-align: right; width: auto; padding-right: 10px; height: 100%; }
+/* line 38, ../sass/main.sass */
+header nav > ul > li { position: relative; height: 15px; padding: 20px 5px 20px 5px; display: inline-block; float: left; }
+/* line 44, ../sass/main.sass */
+header nav > ul > li:hover { color: #ff3333; }
+/* line 46, ../sass/main.sass */
+header .userlist { position: absolute; top: 100%; right: 50%; color: #cccccc; -webkit-transform: translate(50%, 0); -moz-transform: translate(50%, 0); -ms-transform: translate(50%, 0); -o-transform: translate(50%, 0); transform: translate(50%, 0); z-index: 1; background: #333333; border: 6px solid #222222; border-top: 0; padding: 10px; text-align: left; -webkit-transition: 0.2s ease-out all; -moz-transition: 0.2s ease-out all; -o-transition: 0.2s ease-out all; transition: 0.2s ease-out all; opacity: 1; }
+/* line 60, ../sass/main.sass */
+header .userlist.ng-hide { opacity: 0; }
+/* line 62, ../sass/main.sass */
+header .userlist.ng-hide-remove, header .userlist.ng-hide-add { display: block !important; }
+/* line 64, ../sass/main.sass */
+header .userlist li { padding: 5px; }
+/* line 67, ../sass/main.sass */
+header a { color: white; text-decoration: none; }
+/* line 70, ../sass/main.sass */
+header a:hover { color: #ff3333; }
+
+/* line 73, ../sass/main.sass */
+#party { position: absolute; top: 55px; bottom: 0; right: 0; left: 0; }
+
+/* line 80, ../sass/main.sass */
+#video { width: 50%; height: 100%; position: relative; float: left; background: #131313; -webkit-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; -moz-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; }
+/* line 88, ../sass/main.sass */
+#video #playlist { list-style: none; height: -webkit-calc(100vh - 28.125vw - 55px); height: -moz-calc(100vh - 28.125vw - 55px); height: -ms-calc(100vh - 28.125vw - 55px); height: -o-calc(100vh - 28.125vw - 55px); height: calc(100vh - 28.125vw - 55px); overflow: auto; }
+/* line 11, ../../../../../.gem/ruby/2.0.0/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/typography/lists/_bullets.scss */
+#video #playlist li { list-style-image: none; list-style-type: none; margin-left: 0; }
+/* line 92, ../sass/main.sass */
+#video #playlist li { position: relative; color: #bbbbbb; padding: 8px; }
+/* line 96, ../sass/main.sass */
+#video #playlist li:not(:last-child) { border-bottom: 1px solid #050505; -webkit-box-shadow: #222222 0px 1px 0px; -moz-box-shadow: #222222 0px 1px 0px; box-shadow: #222222 0px 1px 0px; }
+/* line 99, ../sass/main.sass */
+#video #playlist span.title { position: absolute; left: 105px; top: 50%; width: -webkit-calc(50vw - 160px); width: -moz-calc(50vw - 160px); width: -ms-calc(50vw - 160px); width: -o-calc(50vw - 160px); width: calc(50vw - 160px); -webkit-transform: translate(0, -50%); -moz-transform: translate(0, -50%); -ms-transform: translate(0, -50%); -o-transform: translate(0, -50%); transform: translate(0, -50%); }
+/* line 105, ../sass/main.sass */
+#video #playlist span.duration { position: absolute; top: 50%; right: 10px; -webkit-transform: translate(0, -50%); -moz-transform: translate(0, -50%); -ms-transform: translate(0, -50%); -o-transform: translate(0, -50%); transform: translate(0, -50%); }
+/* line 110, ../sass/main.sass */
+#video #playlist .crop { margin-right: 10px; height: 47.8125px; width: 85px; overflow: hidden; }
+/* line 118, ../sass/main.sass */
+#video #playlist .crop img { width: 85px; margin-top: -7.96875px; margin-bottom: -7.96875px; }
+/* line 122, ../sass/main.sass */
+#video form { margin-left: 66px; height: 42px; }
+/* line 125, ../sass/main.sass */
+#video input { position: absolute; top: 50%; -webkit-transform: translate(0, -50%); -moz-transform: translate(0, -50%); -ms-transform: translate(0, -50%); -o-transform: translate(0, -50%); transform: translate(0, -50%); }
+/* line 129, ../sass/main.sass */
+#video input[type="submit"] { right: 10px; }
+/* line 131, ../sass/main.sass */
+#video input:not([type="submit"]) { width: -webkit-calc(50vw - 200px); width: -moz-calc(50vw - 200px); width: -ms-calc(50vw - 200px); width: -o-calc(50vw - 200px); width: calc(50vw - 200px); }
+
+/* line 134, ../sass/main.sass */
+#videowrapper { height: 28.125vw; position: relative; background: black; }
+
+/* line 139, ../sass/main.sass */
+#chat { float: right; height: 100%; width: 50%; color: #bbbbbb; background: #191919; line-height: 1.5; font-family: Arial; position: relative; -webkit-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0, #111111 -1px 0 0; -moz-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0, #111111 -1px 0 0; box-shadow: #353535 0 -1px 0, #111111 0 -2px 0, #111111 -1px 0 0; }
+/* line 149, ../sass/main.sass */
+#chat ul { position: absolute; left: 0; right: 0; bottom: 55px; padding: 20px; }
+/* line 155, ../sass/main.sass */
+#chat li { margin: 5px; padding: 2px; width: -webkit-calc(100% - 20px); width: -moz-calc(100% - 20px); width: -ms-calc(100% - 20px); width: -o-calc(100% - 20px); width: calc(100% - 20px); display: inline-block; position: relative; }
+/* line 161, ../sass/main.sass */
+#chat li .user { padding-right: 10px; font-weight: bold; color: #777777; display: block; float: left; height: 100%; }
+/* line 162, ../sass/main.sass */
+#chat li .user:after { content: " >"; }
+/* line 170, ../sass/main.sass */
+#chat form { height: 35px; position: absolute; bottom: 0px; background: #272727; width: 100%; padding: 10px; -webkit-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; -moz-box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; box-shadow: #353535 0 -1px 0, #111111 0 -2px 0; }
+/* line 178, ../sass/main.sass */
+#chat input { background: #494949; border: 1px solid #222222; color: black; }
+/* line 183, ../sass/main.sass */
+#chat input:not([type="submit"]) { width: -webkit-calc(100% - 120px); width: -moz-calc(100% - 120px); width: -ms-calc(100% - 120px); width: -o-calc(100% - 120px); width: calc(100% - 120px); }
+
+/* line 188, ../sass/main.sass */
+input { padding: 7px; font-size: 0.8em; margin: 0; border: 0; background: #222222; outline: none; color: #bbbbbb; border: 1px solid #080808; -webkit-box-shadow: #222222 0px 0px 3px; -moz-box-shadow: #222222 0px 0px 3px; box-shadow: #222222 0px 0px 3px; }
+/* line 198, ../sass/main.sass */
+input[type="submit"]::-moz-focus-inner { padding: 0; border: 0; }
+
+/* line 204, ../sass/main.sass */
+::-webkit-scrollbar { width: 12px; height: 12px; }
+
+/* line 208, ../sass/main.sass */
+::-webkit-scrollbar-track { background: #444444; }
+
+/* line 211, ../sass/main.sass */
+::-webkit-scrollbar-thumb { background: #090909; }

File wetube/assets/gulpfile.js

+var server = require('tiny-lr')(),
+    gulp = require('gulp'),
+    gutil = require('gulp-util'),
+    ngmin = require('gulp-ngmin'),
+    livereload = require('gulp-livereload'),
+    watch = require('gulp-watch'),
+    compass = require('gulp-compass'),
+    minifyCSS = require('gulp-minify-css'),
+    uglify = require('gulp-uglify'),
+    concat = require('gulp-concat'),
+    path = require('path');
+
+
+gulp.task('listen', function(next) {
+  server.listen(35729, function(err) {
+    if (err) return console.error(err);
+    next();
+  });
+});
+
+
+gulp.task('styles', ['listen'], function() {
+  gulp.src('./sass/*.sass')
+      .pipe(compass({
+        config_file: './config.rb',
+        css: './css',
+        sass: './sass',
+        project: __dirname
+      }))
+      .pipe(minifyCSS())
+      .pipe(gulp.dest('../static/css'))
+      .pipe(livereload(server));
+});
+
+
+gulp.task('javascript', function() {
+  gulp.src('./js/**/*.js')
+      .pipe(ngmin())
+      .pipe(uglify())
+      .pipe(concat('dist.js'))
+      .pipe(gulp.dest('../static/js'))
+});
+
+
+gulp.task('watch', ['listen'], function() {
+  gulp.watch('./js/**/*.js', ['javascript']);
+  gulp.watch('./sass/*.sass', ['styles'])
+
+});
+
+gulp.task('default', ['watch']);

File wetube/assets/js/app.js

+angular.module("wetube.services", [
+  "wetube.services.socket",
+  "wetube.services.youtube",
+  "wetube.services.sync_time"
+]);
+
+angular.module("wetube.directives", [
+  "wetube.directives.player",
+  "wetube.directives.userlist"
+]);
+
+angular.module("wetube.filters", [
+  "wetube.filters.duration"
+]);
+
+angular.module("wetube.controllers", [
+  "wetube.controllers.UserController",
+  "wetube.controllers.ChatController",
+  "wetube.controllers.PlaylistController",
+  "wetube.controllers.VideoController"  
+]);
+
+
+angular.module('wetube', [
+    "ngAnimate",
+    "wetube.controllers",
+    "wetube.services",
+    "wetube.directives",
+    "wetube.filters"
+  ])
+  .run(function(socket, sync_time) {
+    socket.emit("join")
+    sync_time.initiate()
+  });
+

File wetube/assets/js/controllers/ChatController.js

+angular.module("wetube.controllers.ChatController", [
+    "wetube.services.socket"  
+  ])
+  .controller('ChatController', function($scope, socket, $log) {
+    $scope.messages = [];
+
+    socket.on('message', function(message) {
+      $scope.messages.push(message);
+    });
+    $scope.addMessage = function() {
+      var message = $scope.message_input;
+      socket.emit("message", message);
+      $scope.message_input = "";
+    };
+  });

File wetube/assets/js/controllers/PlaylistController.js

+angular.module("wetube.controllers.PlaylistController", [
+    "wetube.services.socket"
+  ])
+  .controller('PlaylistController', function($scope, socket) {
+    socket.on("playlist", function(data) {
+      $scope.playlist = data;
+    });
+
+    $scope.addVideo = function() {
+      var url = $scope.video_input;
+      socket.emit("add_video", url);
+      $scope.video_input = "";
+    };
+  });

File wetube/assets/js/controllers/UserController.js

+angular.module("wetube.controllers.UserController", [
+    "wetube.services.socket"  
+  ])
+  .controller('UserController', function($scope, socket) {
+    $scope.showUserlist = false;
+    $scope.toggleUserlist = function() {
+      $scope.showUserlist = !$scope.showUserlist;
+    }
+    socket.on('connected', function(data) {
+      $scope.users = data;
+    });
+    socket.on('user_leaved', function(leaved_user) {
+      $scope.users = $scope.users.filter(function(user) {
+        return user.name !== leaved_user.name;
+      });
+    });
+    socket.on('user_joined', function(user) {
+      $scope.users.push(user);
+    });
+  });

File wetube/assets/js/controllers/VideoController.js

+angular.module("wetube.controllers.VideoController", [
+    "wetube.services.socket",
+    "wetube.services.youtube"  
+  ])
+  .controller('VideoController', function($scope, socket, youtube) {
+    socket.on("playback", function(data) {
+      youtube.load(data.id, data.started)
+    }); 
+  });

File wetube/assets/js/directives/player.js

+angular.module("wetube.directives.player", [
+    "wetube.services.youtube"
+  ])
+  .directive('player', function(youtube) {
+    return {
+      restrict: 'AE',
+      link: function(scope, element) {
+        youtube.bind(element[0]);
+      }
+    };
+  });

File wetube/assets/js/directives/userlist.js

+angular.module("wetube.directives.userlist", [])
+  .directive('userlist', function(youtube) {
+    return {
+      restrict: 'AE',
+      templateUrl: "static/templates/userlist.html",
+      replace: true
+    };
+  });

File wetube/assets/js/filters/duration.js

+angular.module("wetube.filters.duration", [])
+  .filter('duration', function() {
+    return function(date) {
+      if (angular.isNumber(Math.round(date / 1000))) {
+        date = new Date(Math.abs(date));
+      }
+      return new Date(
+          date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(),
+          date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(),
+          date.getUTCMilliseconds()
+      );
+    };
+  });

File wetube/assets/js/services/socket.js

+angular.module("wetube.services.socket", [])
+  .factory('socket', function ($rootScope) {
+    var socket = io.connect("/app");
+    return {
+      on: function(eventName, callback) {
+        socket.on(eventName, function () {  
+          var args = arguments;
+          $rootScope.$apply(function () {
+            callback.apply(socket, args);
+          });
+        });
+      },
+      emit: function(eventName, data, callback) {
+        socket.emit(eventName, data, function () {
+          var args = arguments;
+          $rootScope.$apply(function () {
+            if (callback) {
+              callback.apply(socket, args);
+            }
+          });
+        });
+      }
+    };
+  });

File wetube/assets/js/services/sync_time.js

+angular.module("wetube.services.sync_time", [
+    "wetube.services.socket"
+  ])
+  .service('sync_time', function ($rootScope, $interval, socket) {
+    var round_trip, response_diff, time_diff_queue = [];
+
+    this.time_diff = 0;
+    this.latency = 0;
+    $rootScope.time_diff = this.time_diff;
+    $rootScope.latency = this.latency;
+
+    socket.on('sync_time', function(request_diff, response_time) {
+      response_diff = (new Date()).getTime() - response_time;
+
+      round_trip = Math.ceil((request_diff + response_diff) / 2);
+
+      time_diff_queue.push(response_diff - round_trip);
+      if (time_diff_queue.length > 100) {
+        time_diff_queue.shift();
+      }
+
+      var index = Math.ceil(time_diff_queue.length / 2) - 1;
+      this.time_diff = time_diff_queue.slice(0).sort(
+        function (a, b) {
+          return a - b;
+        }
+      )[index];
+      this.latency = round_trip;
+      $rootScope.time_diff = this.time_diff;
+      $rootScope.latency = this.latency;
+    });
+
+    this.initiate = function() {
+      socket.emit('sync_time', (new Date()).getTime());
+      $interval(function() {
+        socket.emit('sync_time', (new Date()).getTime());
+      }, 1000); 
+    };
+  });

File wetube/assets/js/services/youtube.js

+angular.module("wetube.services.youtube", [])
+  .factory('youtube', function(
+        $rootScope, $window, $timeout, $interval, $log, sync_time) {
+    var service = $rootScope.$new(true);
+  
+    service.ready = false;
+    service.create = false;
+    service.player = null;
+    service.id = null;
+    service.video = null;
+    service.started = null;
+
+    $window.onYouTubeIframeAPIReady = function () {
+      $log.info('Youtube API is ready');
+      service.ready = true;
+    };
+
+    $interval(function() {
+      if (service.player && service.created) {
+        var now = (new Date()).getTime() - sync_time.time_diff;
+        var position = ((now - service.started) / 1000)
+        var position_diff = position - (service.player.getCurrentTime());
+        if (position_diff > 2 || position_diff < -2) {
+          service.player.seekTo(position);
+        }
+      }
+    }, 200);
+
+    return {
+      bind: function(element) {
+        $log.info("Bind youtube to player element");
+        element.id = "youtube_player";
+        service.id = element.id;
+      },
+      load: function(video_id, started) {
+        service.video = video_id;
+        service.started = started;
+        if (service.player && service.created) {
+          var now = (new Date()).getTime() - sync_time.time_diff;
+          var time = (now - started) / 1000;
+          service.player.loadVideoById(video_id, time);
+        } else {
+          this.create_player()
+        }
+      },
+      create_player: function() {
+        if (!service.ready) {
+          $timeout(this.create_player, 100);
+        }
+        if (service.id && service.video) {
+          $log.info('Create player!');
+          if (service.player) {
+            service.player.destroy();
+          }
+          service.player = new YT.Player(service.id, {
+            videoId: service.video,
+            width: "100%",
+            height: "100%",
+            playerVars: {
+              modestbranding: 1
+            },
+            events: {
+              'onReady': function() {
+                service.created = true;
+                var now = (new Date()).getTime() - sync_time.time_diff;
+                var start = (now - service.started) / 1000;
+                service.player.seekTo(start, true);
+              }
+            }
+          });
+        }
+      },
+      player: function() {
+        return service.player;
+      }
+    };
+  });

File wetube/assets/sass/main.sass

+@import compass/css3
+@import compass/reset
+@import compass/utilities
+
+
+html, body
+  background: #1b1b1b
+  position: relative
+  height: 100%
+  overflow: hidden
+
+header
+  width: 100%
+  height: 55px
+  background: #272727
+  color: #fff
+  z-index: 3
+
+  #{headings(1,3)}
+    width: 40%
+    font-size: 1.3em
+    font-family: Arial
+    padding: 17px
+    float: left
+    color: #ccc
+    &:first-child
+      font-size: 1.5em
+      color: #fff
+
+  nav
+    height: 100%
+    &> ul
+      float: right
+      text-align: right
+      width: auto
+      padding-right: 10px
+      height: 100%
+      &> li
+        position: relative
+        height: 15px
+        padding: 20px 5px 20px 5px
+        display: inline-block
+        float: left
+        &:hover
+          color: #f33
+  .userlist
+    position: absolute
+    top: 100%
+    right: 50%
+    color: #ccc
+    +translate(50%, 0)
+    z-index: 1
+    background: #333
+    border: 6px solid #222
+    border-top: 0
+    padding: 10px
+    text-align: left
+    +transition(0.2s ease-out all)
+    opacity: 1
+    &.ng-hide
+      opacity: 0
+    &.ng-hide-remove, &.ng-hide-add
+      display: block !important
+    li
+      padding: 5px
+
+  a
+    color: #fff
+    text-decoration: none
+    &:hover
+      color: #f33
+
+#party
+  position: absolute
+  top: 55px
+  bottom: 0
+  right: 0
+  left: 0
+
+#video
+  width: 50%
+  height: 100%
+  position: relative
+  float: left
+  background: #131313
+  +box-shadow(#353535 0 -1px 0, #111 0 -2px 0)
+
+  #playlist
+    +no-bullets
+    +experimental-value(height, calc(100vh - 28.125vw - 55px))
+    overflow: auto
+    li
+      position: relative
+      color: #bbb
+      padding: 8px
+      &:not(:last-child)
+        border-bottom: 1px solid #050505
+        +box-shadow(#222 0px 1px 0px)
+    span.title
+      position: absolute
+      left: 105px
+      top: 50%
+      +experimental-value(width, calc(50vw - 160px))
+      +translate(0, -50%)
+    span.duration
+      position: absolute
+      top: 50%
+      right: 10px
+      +translate(0, -50%)
+    .crop
+      $width: 85px
+      $height: $width * 0.5625
+      $crop: $width * 0.75 - $height
+      margin-right: 10px
+      height: $height
+      width: $width
+      overflow: hidden
+      img
+        width: $width
+        margin-top: -$crop/2
+        margin-bottom: -$crop/2
+  form
+    margin-left: 66px
+    height: 42px
+  input
+    position: absolute
+    top: 50%
+    +translate(0, -50%)
+  input[type="submit"]
+    right: 10px
+  input:not([type="submit"])
+    +experimental-value(width, calc(50vw - 200px))
+
+#videowrapper
+  height: 28.125vw
+  position: relative
+  background: #000
+
+#chat
+  float: right
+  height: 100%
+  width: 50%
+  color: #bbb
+  background: #191919
+  line-height: 1.5
+  font-family: Arial
+  position: relative
+  +box-shadow(#353535 0 -1px 0, #111 0 -2px 0, #111 -1px 0 0)
+  ul
+    position: absolute
+    left: 0
+    right: 0
+    bottom: 55px
+    padding: 20px
+  li
+    margin: 5px
+    padding: 2px
+    +experimental-value(width, calc(100% - 20px))
+    display: inline-block
+    position: relative
+    .user
+      &:after
+        content: " >"
+      padding-right: 10px
+      font-weight: bold
+      color: #777
+      display: block
+      float: left
+      height: 100%
+  form
+    height: 35px
+    position: absolute
+    bottom: 0px
+    background: #272727
+    width: 100%
+    padding: 10px
+    +box-shadow(#353535 0 -1px 0, #111 0 -2px 0)
+  input
+    background: #494949
+    border: 1px solid #222
+    color: #000
+
+    &:not([type="submit"])
+      +experimental-value(width, calc(100% - 120px))
+
+// Common Tags
+
+input
+  padding: 7px
+  font-size: 0.8em
+  margin: 0
+  border: 0
+  background: #222
+  outline: none
+  color: #bbb
+  border: 1px solid #080808
+  +box-shadow(#222 0px 0px 3px)
+  &[type="submit"]::-moz-focus-inner
+    padding: 0
+    border: 0
+
+// Browser specific
+
+::-webkit-scrollbar
+  width: 12px
+  height: 12px
+
+::-webkit-scrollbar-track
+  background: #444
+
+::-webkit-scrollbar-thumb
+  background: #090909

File wetube/forms.py

         if not rv:
             return False
 
-        user = db.hgetall("users/%s" % self.username.data)
+        user = db.hgetall("users:%s" % self.username.data)
         if not user:
             self.username.errors.append(u'Unbekannter Username')
             return False
         if not rv:
             return False
 
-        if db.exists("users/%s" % self.username.data):
+        if db.exists("users:%s" % self.username.data):
             self.username.errors.append(u"Name bereits vergeben")
             return False
 

File wetube/session_interface.py

         expires = self.get_expiration_time(app, session)
 
         self.redis.hmset(self.prefix + session.sid, session)
+        self.redis.expireat(self.prefix + session.sid, expires)
         response.set_cookie(name, session.sid, expires=expires,
                             httponly=True, domain=domain)

File wetube/static/css/main.css

+html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:400;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary{display:block}html,body{background:#1b1b1b;position:relative;height:100%;overflow:hidden}header{width:100%;height:55px;background:#272727;color:#fff;z-index:3}header h1,header h2,header h3{width:40%;font-size:1.3em;font-family:Arial;padding:17px;float:left;color:#ccc}header h1:first-child,header h2:first-child,header h3:first-child{font-size:1.5em;color:#fff}header nav{height:100%}header nav>ul{float:right;text-align:right;width:auto;padding-right:10px;height:100%}header nav>ul>li{position:relative;height:15px;padding:20px 5px;display:inline-block;float:left}header nav>ul>li:hover{color:#f33}header .userlist{position:absolute;top:100%;right:50%;color:#ccc;-webkit-transform:translate(50%,0);-moz-transform:translate(50%,0);-ms-transform:translate(50%,0);-o-transform:translate(50%,0);transform:translate(50%,0);z-index:1;background:#333;border:6px solid #222;border-top:0;padding:10px;text-align:left;-webkit-transition:.2s ease-out all;-moz-transition:.2s ease-out all;-o-transition:.2s ease-out all;transition:.2s ease-out all;opacity:1}header .userlist.ng-hide{opacity:0}header .userlist.ng-hide-remove,header .userlist.ng-hide-add{display:block!important}header .userlist li{padding:5px}header a{color:#fff;text-decoration:none}header a:hover{color:#f33}#party{position:absolute;top:55px;bottom:0;right:0;left:0}#video{width:50%;height:100%;position:relative;float:left;background:#131313;-webkit-box-shadow:#353535 0 -1px 0,#111 0 -2px 0;-moz-box-shadow:#353535 0 -1px 0,#111 0 -2px 0;box-shadow:#353535 0 -1px 0,#111 0 -2px 0}#video #playlist{list-style:none;height:-webkit-calc(100vh - 28.125vw - 55px);height:-moz-calc(100vh - 28.125vw - 55px);height:-ms-calc(100vh - 28.125vw - 55px);height:-o-calc(100vh - 28.125vw - 55px);height:calc(100vh - 28.125vw - 55px);overflow:auto}#video #playlist li{list-style-image:none;list-style-type:none;margin-left:0}#video #playlist li{position:relative;color:#bbb;padding:8px}#video #playlist li:not(:last-child){border-bottom:1px solid #050505;-webkit-box-shadow:#222 0 1px 0;-moz-box-shadow:#222 0 1px 0;box-shadow:#222 0 1px 0}#video #playlist span.title{position:absolute;left:105px;top:50%;width:-webkit-calc(50vw - 160px);width:-moz-calc(50vw - 160px);width:-ms-calc(50vw - 160px);width:-o-calc(50vw - 160px);width:calc(50vw - 160px);-webkit-transform:translate(0,-50%);-moz-transform:translate(0,-50%);-ms-transform:translate(0,-50%);-o-transform:translate(0,-50%);transform:translate(0,-50%)}#video #playlist span.duration{position:absolute;top:50%;right:10px;-webkit-transform:translate(0,-50%);-moz-transform:translate(0,-50%);-ms-transform:translate(0,-50%);-o-transform:translate(0,-50%);transform:translate(0,-50%)}#video #playlist .crop{margin-right:10px;height:47.8125px;width:85px;overflow:hidden}#video #playlist .crop img{width:85px;margin-top:-7.96875px;margin-bottom:-7.96875px}#video form{margin-left:66px;height:42px}#video input{position:absolute;top:50%;-webkit-transform:translate(0,-50%);-moz-transform:translate(0,-50%);-ms-transform:translate(0,-50%);-o-transform:translate(0,-50%);transform:translate(0,-50%)}#video input[type=submit]{right:10px}#video input:not([type=submit]){width:-webkit-calc(50vw - 200px);width:-moz-calc(50vw - 200px);width:-ms-calc(50vw - 200px);width:-o-calc(50vw - 200px);width:calc(50vw - 200px)}#videowrapper{height:28.125vw;position:relative;background:#000}#chat{float:right;height:100%;width:50%;color:#bbb;background:#191919;line-height:1.5;font-family:Arial;position:relative;-webkit-box-shadow:#353535 0 -1px 0,#111 0 -2px 0,#111 -1px 0 0;-moz-box-shadow:#353535 0 -1px 0,#111 0 -2px 0,#111 -1px 0 0;box-shadow:#353535 0 -1px 0,#111 0 -2px 0,#111 -1px 0 0}#chat ul{position:absolute;left:0;right:0;bottom:55px;padding:20px}#chat li{margin:5px;padding:2px;width:-webkit-calc(100% - 20px);width:-moz-calc(100% - 20px);width:-ms-calc(100% - 20px);width:-o-calc(100% - 20px);width:calc(100% - 20px);display:inline-block;position:relative}#chat li .user{padding-right:10px;font-weight:700;color:#777;display:block;float:left;height:100%}#chat li .user:after{content:" >"}#chat form{height:35px;position:absolute;bottom:0;background:#272727;width:100%;padding:10px;-webkit-box-shadow:#353535 0 -1px 0,#111 0 -2px 0;-moz-box-shadow:#353535 0 -1px 0,#111 0 -2px 0;box-shadow:#353535 0 -1px 0,#111 0 -2px 0}#chat input{background:#494949;border:1px solid #222;color:#000}#chat input:not([type=submit]){width:-webkit-calc(100% - 120px);width:-moz-calc(100% - 120px);width:-ms-calc(100% - 120px);width:-o-calc(100% - 120px);width:calc(100% - 120px)}input{padding:7px;font-size:.8em;margin:0;border:0;background:#222;outline:0;color:#bbb;border:1px solid #080808;-webkit-box-shadow:#222 0 0 3px;-moz-box-shadow:#222 0 0 3px;box-shadow:#222 0 0 3px}input[type=submit]::-moz-focus-inner{padding:0;border:0}::-webkit-scrollbar{width:12px;height:12px}::-webkit-scrollbar-track{background:#444}::-webkit-scrollbar-thumb{background:#090909}

File wetube/static/js/app.js

-var app = angular.module('wetube', [])
-  .factory('socket', function ($rootScope) {
-    var socket = io.connect("/app");
-    return {
-      on: function (eventName, callback) {
-        socket.on(eventName, function () {  
-          var args = arguments;
-          $rootScope.$apply(function () {
-            callback.apply(socket, args);
-          });
-        });
-      },
-      emit: function (eventName, data, callback) {
-        socket.emit(eventName, data, function () {
-          var args = arguments;
-          $rootScope.$apply(function () {
-            if (callback) {
-              callback.apply(socket, args);
-            }
-          });
-        });
-      }
-    }
-  })
-  .controller('VideoController', function($scope, socket) {
-
-  })
-  .controller('PlaylistController', function($scope, socket) {
-  
-  })
-  .controller('ChatController', function($scope, socket) {
-    socket.on('connected_users', function(data) {
-      $scope.users = data;
-    });
-    socket.on('user_leaved', function(username) {
-      $scope.users = $scope.users.filter(function(user) {
-        return user.name !== username;
-      });
-    });
-    socket.on('user_joined', function(user) {
-      $scope.users.push(user);
-    });
-  })
-  .run(function($rootScope, socket) {
-    var request_time, time_diff, round_trip, response_diff, now, index;
-    var time_diff_queue = [];
-
-    socket.on('sync_time', function(request_diff, response_time) {
-      now = (new Date()).getTime();
-      response_diff = now - response_time;
-
-      round_trip = Math.ceil((request_diff + response_diff) / 2);
-      time_diff = response_diff - round_trip;
-
-      time_diff_queue.push(time_diff);
-      if (time_diff_queue.length > 300) {
-        time_diff_queue.shift();
-      }
-
-      index = Math.ceil(time_diff_queue.length / 2) - 1;
-      $rootScope.time_diff = time_diff_queue.slice(0).sort()[index];
-      $rootScope.latency = round_trip;
-      
-      setTimeout(function(){
-        request_time = (new Date()).getTime();
-        socket.emit('sync_time', request_time);
-      }, 1000);
-    });
-
-    socket.emit('join');
-    request_time = (new Date()).getTime();
-    socket.emit('sync_time', request_time);
-  });
-

File wetube/static/js/dist.js

+angular.module("wetube.services",["wetube.services.socket","wetube.services.youtube","wetube.services.sync_time"]),angular.module("wetube.directives",["wetube.directives.player","wetube.directives.userlist"]),angular.module("wetube.filters",["wetube.filters.duration"]),angular.module("wetube.controllers",["wetube.controllers.UserController","wetube.controllers.ChatController","wetube.controllers.PlaylistController","wetube.controllers.VideoController"]),angular.module("wetube",["ngAnimate","wetube.controllers","wetube.services","wetube.directives","wetube.filters"]).run(["socket","sync_time",function(e,t){e.emit("join"),t.initiate()}]);
+angular.module("wetube.controllers.ChatController",["wetube.services.socket"]).controller("ChatController",["$scope","socket","$log",function(e,s){e.messages=[],s.on("message",function(s){e.messages.push(s)}),e.addMessage=function(){var o=e.message_input;s.emit("message",o),e.message_input=""}}]);
+angular.module("wetube.controllers.PlaylistController",["wetube.services.socket"]).controller("PlaylistController",["$scope","socket",function(o,e){e.on("playlist",function(e){o.playlist=e}),o.addVideo=function(){var l=o.video_input;e.emit("add_video",l),o.video_input=""}}]);
+angular.module("wetube.controllers.UserController",["wetube.services.socket"]).controller("UserController",["$scope","socket",function(e,s){e.showUserlist=!1,e.toggleUserlist=function(){e.showUserlist=!e.showUserlist},s.on("connected",function(s){e.users=s}),s.on("user_leaved",function(s){e.users=e.users.filter(function(e){return e.name!==s.name})}),s.on("user_joined",function(s){e.users.push(s)})}]);
+angular.module("wetube.controllers.VideoController",["wetube.services.socket","wetube.services.youtube"]).controller("VideoController",["$scope","socket","youtube",function(e,o,t){o.on("playback",function(e){t.load(e.id,e.started)})}]);
+angular.module("wetube.directives.player",["wetube.services.youtube"]).directive("player",["youtube",function(e){return{restrict:"AE",link:function(t,u){e.bind(u[0])}}}]);
+angular.module("wetube.directives.userlist",[]).directive("userlist",["youtube",function(){return{restrict:"AE",templateUrl:"static/templates/userlist.html",replace:!0}}]);
+angular.module("wetube.filters.duration",[]).filter("duration",function(){return function(e){return angular.isNumber(Math.round(e/1e3))&&(e=new Date(Math.abs(e))),new Date(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),e.getUTCHours(),e.getUTCMinutes(),e.getUTCSeconds(),e.getUTCMilliseconds())}});
+angular.module("wetube.services.socket",[]).factory("socket",["$rootScope",function(n){var o=io.connect("/app");return{on:function(t,c){o.on(t,function(){var t=arguments;n.$apply(function(){c.apply(o,t)})})},emit:function(t,c,e){o.emit(t,c,function(){var t=arguments;n.$apply(function(){e&&e.apply(o,t)})})}}}]);
+angular.module("wetube.services.sync_time",["wetube.services.socket"]).service("sync_time",["$rootScope","$interval","socket",function(e,t,i){var n,c,s=[];this.time_diff=0,this.latency=0,e.time_diff=this.time_diff,e.latency=this.latency,i.on("sync_time",function(t,i){c=(new Date).getTime()-i,n=Math.ceil((t+c)/2),s.push(c-n),s.length>100&&s.shift();var f=Math.ceil(s.length/2)-1;this.time_diff=s.slice(0).sort(function(e,t){return e-t})[f],this.latency=n,e.time_diff=this.time_diff,e.latency=this.latency}),this.initiate=function(){i.emit("sync_time",(new Date).getTime()),t(function(){i.emit("sync_time",(new Date).getTime())},1e3)}}]);
+angular.module("wetube.services.youtube",[]).factory("youtube",["$rootScope","$window","$timeout","$interval","$log","sync_time",function(e,t,r,a,i,n){var d=e.$new(!0);return d.ready=!1,d.create=!1,d.player=null,d.id=null,d.video=null,d.started=null,t.onYouTubeIframeAPIReady=function(){i.info("Youtube API is ready"),d.ready=!0},a(function(){if(d.player&&d.created){var e=(new Date).getTime()-n.time_diff,t=(e-d.started)/1e3,r=t-d.player.getCurrentTime();(r>2||-2>r)&&d.player.seekTo(t)}},200),{bind:function(e){i.info("Bind youtube to player element"),e.id="youtube_player",d.id=e.id},load:function(e,t){if(d.video=e,d.started=t,d.player&&d.created){var r=(new Date).getTime()-n.time_diff,a=(r-t)/1e3;d.player.loadVideoById(e,a)}else this.create_player()},create_player:function(){d.ready||r(this.create_player,100),d.id&&d.video&&(i.info("Create player!"),d.player&&d.player.destroy(),d.player=new YT.Player(d.id,{videoId:d.video,width:"100%",height:"100%",playerVars:{modestbranding:1},events:{onReady:function(){d.created=!0;var e=(new Date).getTime()-n.time_diff,t=(e-d.started)/1e3;d.player.seekTo(t,!0)}}}))},player:function(){return d.player}}}]);

File wetube/static/templates/userlist.html

+<ul class="userlist" ng-show="showUserlist">
+  <li ng-repeat="user in users">{{ user.name }}</li>
+</ul>

File wetube/templates/base.html

 <!doctype html>
 <html ng-app="wetube">
 <meta charset=utf-8>
-<link rel=stylesheet type=text/css href="{{ url_for('static', filename='main.css') }}">
+<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/main.css') }}">
 
 <title>{% block title %}Wetube{% endblock %}</title>
 
 <header>
   <h1>{{ self.title() }}</h1>
 
+  {% block header %}
   <nav>
     <ul>
       {%- if logged_in %}
       {%- endif %}
     </ul>
   </nav>
+  {% endblock %}
 </header>
 
 <main>
   {% block content %}{% endblock %}
 </main>
 
-<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular.js"></script>
+<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.11/angular.js"></script>
+<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.11/angular-animate.js"></script>
 <script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/0.9.16/socket.io.min.js"></script>
 <script src="//www.youtube.com/iframe_api"></script>
-<script src="{{ url_for('static', filename='js/app.js') }}"></script>
+<script src="{{ url_for('static', filename='js/dist.js') }}"></script>
 </html>

File wetube/templates/index.html

 
 {% block title %}{{ super() }}{% endblock %}
 
+{% block header %}
+  <nav>
+    <ul>
+      {%- raw %}
+      <li ng-controller="UserController" ng-click="toggleUserlist()">{{ users.length }} Users online
+        <userlist></userlist>
+      </li>
+      <li><stats></stats></li>
+      {%- endraw %}
+      <li>{{ link("logout", "Logout") }}</li>
+    </ul>
+  </nav>
+{% endblock %}
+
 {% block content -%}
   <section id=party>
     {%- raw %}
-    <section ng-controller="VideoController">
-      {{ video }}
-    </section>
-    <section ng-controller="PlaylistController">
-      {{ latency }} || {{ time_diff }}
-      <ul id=playlist>
-        <li ng-repeat="video in playlist">
-          {{ video.name }}<span class=duration>{{ video.duration }}</span>
-        </li>
-      </ul>
+    <section id="video" ng-controller="VideoController">
+      <div id="videowrapper">
+        <player></player>
+      </div>
+      <section ng-controller="PlaylistController">
+        <ul id=playlist>
+          <li ng-repeat="video in playlist">
+            <div class=crop><img ng-src="{{ video.thumbnail }}" /></div>
+            <span class=title>{{ video.title }}</span>
+            <span class=duration>{{ video.duration | duration | date:'H:mm:ss'  }}</span>
+          </li>
+          <li>
+            <form ng-submit="addVideo()">
+              <input type="text" ng-model="video_input" placeholder="Youtube Video URL">
+              <input type="submit" value="Hinzufügen">
+            </form>
+          </li>
+        </ul>
+      </section>
     </section>
-    <section ng-controller="ChatController">
+
+    <section id="chat" ng-controller="ChatController">
       <div id=messages>
-        <h1>Nachrichten</h1>
         <ul>
           <li ng-repeat="message in messages">
-            <span class=user>{{ message.user }}</span>{{ message.content }}
+            <span class=user>{{ message.user }}</span>{{ message.msg }}
           </li>
         </ul>
       </div>
-      <div id=users>
-        <h1>Users</h1>
-        <ul>
-          <li ng-repeat="user in users">{{ user.name }}</li>
-        </ul>
-      </div>
+      <form ng-submit="addMessage()">
+        <input type="text" ng-model="message_input" placeholder="Nachricht">
+        <input type="submit" value="Hinzufügen">
+      </form>
     </section>
     {%- endraw %}
   </section>

File wetube/utils.py

 from uuid import UUID
 from functools import wraps
 
+from redis import StrictRedis
 from flask import abort, session, redirect, url_for
 
 
 
 
 def logged_in():
-    return "user" in session and session["user"] is not None
+    if "user" in session and session["user"] is not None:
+        db = StrictRedis()
+        user = db.hgetall("users:%s" % session["user"])
+        return bool(user)
 
 
 def login_required(func):
             return func(*args, **kwargs)
         return redirect(url_for('login'))
     return wrapper
+
+
+def extract(d, keys):
+    return dict((k, d[k]) for k in keys if k in d)

File wetube/websockets.py

 
 import math
 from gevent import monkey; monkey.patch_all()
-from gevent.pool import Pool
-from time import time, sleep
+from time import time
+from json import loads, dumps
 
 from socketio import socketio_manage
 from socketio.server import SocketIOServer
 from flask import request
 from redis import StrictRedis
 from . import app
-
-
-# TODO: Remove global state
-sockets = []
-worker_tasks = Pool(1)
-
-
-def playback_worker():
-    db = StrictRedis()
-    id = db.lindex('playlist', 0)
-    while not id:
-        id = db.lindex('playlist', 0)
-        sleep(2)
-    video = db.hgetall('video:%s' % id)
-    while True:
-        if time() > video['started'] + video['duration']:
-            app.logger("Automatic skip to next video")
-            # Sort the current playlist
-            db.lpop('playlist')
-            db.rpush('playlist', id)
-
-            # Get current playlist and set start
-            # time of the first (current) video
-            video_ids = db.lrange('playlist', 0, -1)
-            id = video_ids[0]
-            key = 'video:{}'.format(id)
-            db.hset(key, 'started', time() * 1000)
-            video = db.hgetall(key)
-
-            # Send full playlist with metadata and current video
-            videos = []
-            for video_id in video_ids:
-                videos.append(db.hgetall('video:%s' % video_id))
-            for socket in sockets:
-                if len(videos) > 1:
-                    socket.emit('playlist', videos[1:])
-                if videos:
-                    socket.emit('playback', videos[0])
-        sleep(0.3)
+from .utils import extract
 
 
 class WetubeSocket(BaseNamespace, BroadcastMixin):
 
     def __init__(self, *args, **kwargs):
         self.db = StrictRedis()
+        self.r = self.db.pubsub()
+        self.listen = {}
         super(WetubeSocket, self).__init__(*args, **kwargs)
+        self.session["user"] = None
 
     def get_initial_acl(self):
         return set(['recv_connect'])
 
-    @property
-    def sockets(self):
-        return self.socket.server.sockets.values()
-
-    def recv_connect(self):
-        try:
-            with app.app_context():
-                with app.request_context(self.environ):
-                    sid = request.cookies.get('session')
-            data = self.db.hgetall('session:' + sid)
-            key = 'users:' + data['user']
-            self.session['user'] = user = self.db.hgetall(key)
-            if 'role' in user and user['role'] == 'mod':
-                self.lift_acl_restrictions()
-            else:
-                self.allowed_methods = set(['recv_connect', 'on_join'])
-        except:
-            self.disconnect()
+    def send_connected(self, data=None):
+        users = []
+        usernames = self.db.smembers("connected")
+        for name in usernames:
+            user = self.db.hgetall("users:%s" % name)
+            users.append(extract(user, ('name', 'role')))
+        self.emit("connected", users or [])
 
-    def on_join(self, data):
-        """Join the party and send initial data"""
-        self.allowed_methods.update(('on_message', 'on_sync_time'))
-        print self.allowed_methods
-        joined = {k: self.session['user'][k] for k in ('name', 'role')}
-        self.broadcast_event_not_me('user_joined', joined)
-        if self not in sockets:
-            sockets.append(self)
-
-        # Send client who is connected
-        connected = []
-        for socket in sockets:
-            if not socket.socket.connected:
-                continue
-            user = {k: socket.session["user"][k] for k in ('name', 'role')}
-            if user not in connected:
-                connected.append(user)
-        self.emit('connected_users', connected)
-
-        # Send playlist and current video
+    def send_playlist(self, data=None):
         videos = []
-        ids = self.db.lrange('playlist', 0, -1)
+        ids = self.db.lrange('playlist', 1, -1)
         for id in ids:
             videos.append(self.db.hgetall('video:%s' % id))
-        if len(videos) > 1:
-            self.emit('playlist', videos[1:])
         if videos:
-            self.emit('playback', videos[0])
-
-        # Start video_worker if neccessary
-        if not worker_tasks.full():
-            worker_tasks.spawn(playback_worker)
-
-    def recv_disconnect(self):
-        connected = []
-        sockets.remove(self)
-        username = self.session['user']['name']
-        # TODO: Create a new method for that
-        for socket in sockets:
-            if not socket.socket.connected:
-                continue
-            user = socket.session["user"]["name"]
-            if user not in connected:
-                connected.append(user)
-        if username not in connected:
-            self.broadcast_event_not_me('user_leaved', username)
-        self.emit('connected_users', connected)
-
-        if not len(sockets):
-            worker_tasks.kill()
-        self.disconnect(silent=True)
-
-    def on_message(self, msg):
-        username = self.session['user']['name']
-        self.broadcast_event('message', msg, username)
+            self.emit('playlist', videos)
+
+    def send_playback(self, data=None):
+        id = self.db.lindex('playlist', 0)
+        if not id:
+            return
+        video = self.db.hgetall('video:%s' % id)
+        self.emit("playback", video)
+
+    def send_message(self, data=None):
+        print "loool"
+        self.emit('message', loads(data))
+
+    def subscribe(self, event, func):
+        if event not in self.listen:
+            self.listen[event] = func
+            self.r.subscribe(event)
+
+    def listener(self):
+        for event in self.r.listen():
+            channel = event["channel"]
+            if channel in self.listen and event["type"] == "message":
+                func = self.listen[channel]
+                func(event["data"])
 
     def on_sync_time(self, client_time):
         server_time = int(math.ceil(time() * 1000))
         difference = server_time - client_time
+
+        key = "users:%s" % self.session["user"]
         self.emit('sync_time', difference, server_time)
 
-    def on_ban(self, user):
-        # TODO: Implement Admin Functions
-        pass
+    def on_join(self, data):
+        if self.allowed_methods:
+            self.allowed_methods.update(('on_message', ))
+
+        connected = self.db.smembers("connected")
+        if self.session["user"] in connected:
+            return
+
+        self.db.incr("connected_count")
+        self.db.sadd("connected", self.session["user"])
+
+        key = "users:%s" % self.session["user"]
+        user = self.db.hgetall(key)
+        user = extract(user, ('name', 'role'))
+        self.db.publish("user_joined", dumps(user))
+        self.broadcast_event("user_joined", user)
+
+    def on_add_video(self, url):
+        self.db.publish("new_video_url", url)
+
+    def on_message(self, msg):
+        print "lol"
+        data = {'user': self.session["user"], 'msg': msg}
+        self.db.publish("new_message", dumps(data))
+
+    def recv_connect(self):
+        with app.app_context():
+            with app.request_context(self.environ):
+                sid = request.cookies.get('session')
+        try:
+            data = self.db.hgetall('session:' + sid)
+            self.session["user"] = data["user"]
+        except (TypeError, KeyError):
+            self.disconnect()
+            return
+
+        self.allowed_methods.update(('on_sync_time', 'on_join'))
+        key = "users:%s" % self.session["user"]
+        user = self.db.hgetall(key)
+        if 'role' in user and user['role'] == 'mod':
+            self.lift_acl_restrictions()
 
-    def recv_message(self, message):
-        print 'PING!!!', message
+        self.send_connected()
+        self.send_playlist()
+        self.send_playback()
+
+        self.subscribe("playlist_changed", self.send_playlist)
+        self.subscribe("playback_changed", self.send_playback)
+        self.subscribe("new_message", self.send_message)
+        self.spawn(self.listener)
+
+    def recv_disconnect(self):
+        username = self.session['user']
+        sockets = self.socket.server.sockets.iteritems()
+        users = map(lambda x: x[1].session["user"], sockets)
+        user_connections = len(filter(lambda x: x == username, users))
+
+        if user_connections < 2:
+            self.db.decr("connected_count")
+            self.db.srem("connected", username)
+
+            user = self.db.hgetall("users:%s" % username)
+            user = extract(user, ('name', 'role'))
+            self.db.publish("user_leaved", dumps(user))
+            self.broadcast_event_not_me("user_leaved", user)
+        self.disconnect(silent=True)

File wetube/workers.py

+#!/usr/bin/env python
+# encoding: utf-8
+
+import gevent
+from gevent import monkey; monkey.patch_all()
+from gevent.pool import Pool
+from urlparse import urlparse, parse_qs
+
+import requests
+from json import dumps, loads
+from logbook import Logger
+from time import time, sleep
+from redis import StrictRedis
+from isodate import parse_duration
+
+
+def wait_till(event):
+    db = StrictRedis()
+    r = db.pubsub()
+    r.subscribe(event)
+    for e in r.listen():
+        print "hmm"
+        if e['type'] == 'message':
+            r.unsubscribe()
+            data = e['data']
+            break
+    return data
+
+
+def rotate_worker():
+    log = Logger('Rotate')
+    log.info("Start worker")
+    db = StrictRedis()
+
+    id = db.lindex('playlist', 0)
+    if not id:
+        log.info('Wait until a video is in playlist')
+        id = loads(wait_till('video_added'))
+    video = db.hgetall('video:%s' % id)
+
+    while True:
+        # Wait until somebody is connected
+        connected = int(db.get('connected_count'))
+        if not connected:
+            log.info('Wait until a user is connected')
+            wait_till('user_joined')
+
+        now = int(time() * 1000)
+        started = int(video['started'])
+        duration = int(video['duration'])
+        if now > (started + duration):
+            # Sort the current playlist
+            db.lpop('playlist')
+            db.rpush('playlist', id)
+
+            # Get current playlist and set start
+            # time of the first (current) video
+            id = db.lindex('playlist', 0)
+            key = 'video:%s' % id
+            db.hset(key, 'started', int(time() * 1000))
+            video = db.hgetall(key)
+            log.info('Next video: %s' % video['title'])
+
+            # Publish that the video has changed
+            db.publish('playlist_changed', dumps(id))
+            db.publish('playback_changed', dumps(video))
+        sleep(0.3)
+
+
+def load_video_info(db, id):
+    log = Logger("YoutubeLoader")
+
+    api_url = 'https://www.googleapis.com/youtube/v3/videos'
+    params = {
+        'part': 'snippet,contentDetails',
+        'key': 'AIzaSyCHyfH7Mq7Hn2FtHKZfAXWJFfGiC0n3j50',
+        'id': id
+    }
+    result = requests.get(api_url, params=params)
+    data = result.json()
+    data = data["items"][0]
+
+    duration = parse_duration(data["contentDetails"]["duration"])
+    video = {
+        'id': id,
+        'title': data["snippet"]["title"],
+        'thumbnail': data["snippet"]["thumbnails"]["default"]["url"],
+        'duration': int(duration.total_seconds() * 1000),
+        'started': int(time() * 1000)
+    }
+
+    log.info("Loaded: %s" % video['title'])
+    db.hmset('video:%s' % id, video)
+    db.rpush("playlist", id)
+
+    videos = len(db.lrange("playlist", 0, -1))
+    if videos > 1:
+        db.publish('playlist_changed', dumps(id))
+    else:
+        db.publish('playback_changed', dumps(video))
+    db.publish("video_added", dumps(id))
+
+
+def video_info_worker():
+    log = Logger('VideoInfo')
+    log.info("Start Worker")
+
+    db = StrictRedis()
+    r = db.pubsub()
+    r.subscribe('new_video_url')
+    for event in r.listen():
+        if event['type'] == 'message':
+            log.info("Add new video")
+            url = urlparse(event["data"])
+            query = parse_qs(url.query)
+            load_video_info(db, query['v'][0])
+
+
+def spawn():
+    gevent.joinall([
+        gevent.spawn(rotate_worker),
+        gevent.spawn(video_info_worker)
+    ])
+
+
+if __name__ == '__main__':
+    spawn()