<file_sep>
from flask import Flask
from flask_peewee.db import Database
import config
import grooveshark

app = Flask(__name__)
app.config.from_envvar("PIDJ_SETTINGS")

db = Database(app)
gs = grooveshark.Client(app)
<file_sep>
# PiDJ

A crowd-sourced DJ platform that allows anyone in the room to search for
and queue up songs.

Users visit a flask-powered website and can search (using the Grooveshark
api in the backend) and add a song of their choosing to the queue. Songs are played
in FIFO order using mpd. PiDJ is designed to be run on a raspberry pi that is connected
to speakers, although all software used is cross platform and can be run on x86 linux or osx.

## Technology

 * Flask website
 * gevent for IO and player control
 * mpd for music control and streaming
 * Facebook for user auth and identification
 * Grooveshark api for song search and streaming
 * peewee/sqlite for relational database
 * redis for key-value store and song caching
<file_sep>Flask==0.9
Jinja2==2.6
WTForms==1.0.2
Werkzeug==0.8.3
distribute==0.6.27
facebook-sdk==0.4.0
flask-peewee==0.6.1
gevent==0.13.8
greenlet==0.4.0
peewee==2.0.4
pytz==2012h
requests==0.14.2
simplejson==2.6.2
wsgiref==0.1.2
wtf-peewee==0.2.0
<file_sep>
from app import app, db
from peewee import *
from pytz import timezone
import datetime

__all__ = ["Song", "Vote", "create_tables", "STATUS_PLAYED", "STATUS_PLAYING", "STATUS_QUEUED"]

STATUS_PLAYED, STATUS_PLAYING, STATUS_QUEUED = range(3)

mytz = timezone(app.config["TIMEZONE"])

class Song(db.Model):
	user_id = CharField(max_length = 15)
	user_name = CharField(max_length = 100)
	song_name = CharField()
	song_id = IntegerField()
	album_name = CharField()
	album_id = IntegerField()
	artist_name = CharField()

	added = DateTimeField(default = datetime.datetime.now)
	status = IntegerField(default = STATUS_QUEUED)
	finish = DateTimeField(default = datetime.datetime.now)
	duration = IntegerField(default = 0)

	def __unicode__(self):
		return self.song_name

	@staticmethod
	def getPlaying():
		try:
			return Song.get(status = STATUS_PLAYING)
		except Song.DoesNotExist:
			return None

	@staticmethod
	def getNext():
		try:
			return Song.select().where(Song.status == STATUS_QUEUED).order_by(Song.added).get()
		except Song.DoesNotExist:
			return None

	def serialize(self):
		return {
			"id": self.id,
			"user_id": self.user_id,
			"user_name": self.user_name,
			"song_name": self.song_name,
			"song_id": str(self.song_id),
			"album_name": self.album_name,
			"album_id": str(self.album_id),
			"artist_name": self.artist_name,
			"added": mytz.localize(self.added).isoformat(),
			"status": self.status,
			"finish": mytz.localize(self.finish).isoformat(),
			"duration": self.duration
		}

class Vote(db.Model):
	user_id = CharField(max_length = 15, index = True)
	song = ForeignKeyField(Song)

def create_tables():
	Song.create_table()
	Vote.create_table()
<file_sep>
__all__ = ["run", "models"]

from gevent import monkey
monkey.patch_all()

import gevent

from app import app
from views import *
import models

from gevent.pywsgi import WSGIServer
from werkzeug.wsgi import SharedDataMiddleware
from werkzeug.debug import DebuggedApplication

import os

def run_debug(port):
	global app
	app = DebuggedApplication(app, evalex = True)
	WSGIServer(('', port), app).serve_forever()

def run_production(port):
	global app
	app = SharedDataMiddleware(app, {
			'/': os.path.join(os.path.dirname(__file__), 'static')
	})
	WSGIServer(('', port), app).serve_forever()

def run(port):

	import player
	g1 = player.start()

	if app.config["DEBUG"]:
		g2 = gevent.spawn(run_debug, port)
	else:
		g2 = gevent.spawn(run_production, port)

	gevent.joinall([g1, g2])
<file_sep>#!/usr/bin/env python

import sys; sys.dont_write_bytecode = True
import os
os.environ["PIDJ_SETTINGS"] = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.py")

import pidj
import sys

port = 5000
if len(sys.argv) >= 2:
   port = int(sys.argv[1])

pidj.run(port)
<file_sep>
import functools
from flask import jsonify
from app import app

def api_route(action, **options):
	def decorator(fn):
		@functools.wraps(fn)
		def wrapper(*args, **kwargs):
			ret = fn(*args, **kwargs)
			obj = {"status": "success"}
			if ret:
				obj.update(ret)
			return jsonify(obj)
		return app.route("/ajax/" + action, methods=["POST"])(wrapper)
	return decorator
<file_sep>
from models import *
import gevent
from app import gs
import datetime
from gevent import socket

def log(s):
	return
	print s

class MPDClient(object):
	def __init__(self):
		self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self.host = None
		self.port = None

	def close(self):
		try:
			self.socket.close()
		except:
			pass

	def _reconn(self):
		self.socket.connect((self.host, self.port))
		self.read_until("\n")

	def _tryfunc(self, f, *args, **kwargs):
		try:
			return f(*args, **kwargs)
		except IOError:
			self.close()
			self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
			self._reconn()
			return f(*args, **kwargs)

	def connect(self, host = "localhost", port = 6600):
		self.host = host
		self.port = int(port)
		self._reconn()

	def send(self, msg):
		return self._tryfunc(self.socket.send, msg)

	def recv(self, amt):
		return self._tryfunc(self.socket.recv, amt)

	def read_until(self, delim):
		data = ""
		while 1:
			data += self.recv(1024)
			if data.endswith(delim):
				return data[:-len(delim)]

	def status(self):
		self.send("status\n")
		data = self.read_until("OK\n")
		d = {}
		for line in data.split("\n"):
			i = line.split(": ")
			if len(i) < 2:
				continue
			d[i[0]] = i[1]
		return d

	def clear(self):
		self.send("clear\n")
		self.read_until("OK\n")

	def consume(self, v):
		self.send("consume " + str(v) + "\n")
		self.read_until("OK\n")

	def play(self):
		self.send("play\n")
		self.read_until("OK\n")

	def add(self, song):
		self.send("add " + song + "\n")
		self.read_until("OK\n")

class Player(object):
	def __init__(self):
		self.client = None

	def reconnect(self):
		if self.client:
			self.client.close()
		self.client = MPDClient()
		self.client.connect()
		self.client.clear()
		self.client.consume(1)

	def addSongInner(self, r):
		self.client.add(r["result"]["url"])
		self.client.play()

	def addSong(self, song):
		if not gs.has_init:
			gs.init()
		r = gs.api("getSubscriberStreamKey", {"songID": song.song_id})
		try:
			self.addSongInner(r)
		except ConnectionError:
			self.reconnect()
			self.addSongInner(r)

	def start(self):
		self.reconnect()
		curSong = None
		curUpdated = False

		sp = Song.getPlaying()
		if sp is not None:
			sp.delete()

		while 1:

			if curSong == None:
				log("curSong == none")
				song = Song.getNext()
				if song is not None:
					log("found song")
					self.addSong(song)
					curUpdated = False
					curSong = song
					gevent.sleep(1.0)
				else:
					log("no next song found")
					gevent.sleep(3.0)

			else:
				log("curSong != None")

				if not curUpdated:
					log("curUpdated = false")
					s = self.client.status()

					if "time" in s and s["playlistlength"] == "1" and s["time"] != "0:0":

						duration = int(s["time"].split(":")[1])
						print s

						curSong.duration = duration
						curSong.status = STATUS_PLAYING
						curSong.finish = datetime.datetime.now() \
							+ datetime.timedelta(seconds=duration) \
							- datetime.timedelta(seconds=float(s["elapsed"]))
						curSong.save()
						curUpdated = True

						sleep = (curSong.finish - datetime.datetime.now()).total_seconds() - 2
						log("Sleeping: " + str(sleep))
						gevent.sleep(sleep)
						curSong.status = STATUS_PLAYED
						curSong.save()

						song = Song.getNext()
						if song is not None:
							log("Next song")
							self.addSong(song)
							curUpdated = False
							curSong = song
						else:
							curSong = None

						gevent.sleep(4.0)
					else:
						gevent.sleep(2.0)

				else:
					log("curUpdated = true")
					gevent.sleep(5.0)

def start():
	p = Player()
	return gevent.spawn(p.start)

if __name__ == "__main__":
	m = MPDClient()
	m.connect("localhost", "6600")
	print m.status()
<file_sep>
from app import app, gs
from models import *
from flask import render_template, request, session
from util import api_route

import requests
import facebook

def get_graph_api(reset = False):

	if not reset and "fb_access_token" in session:
		return facebook.GraphAPI(session["fb_access_token"])

	user = facebook.get_user_from_cookie(
		request.cookies,
		app.config["FACEBOOK_APP_ID"],
		app.config["FACEBOOK_APP_SECRET"]
	)

	if not user:
		return None

	session["fb_access_token"] = user["access_token"]
	return facebook.GraphAPI(session["fb_access_token"])


@app.route('/')
def index():
	return render_template("index.html", app_id = app.config["FACEBOOK_APP_ID"])

@api_route("search")
def ajax_search():
	res = requests.get("http://tinysong.com/s/%s?format=json&limit=20&key=%s" %
					   (request.form["query"], app.config["TINYSONG_KEY"]))
	return {"songs": res.json}

@api_route("queue")
def ajax_queue():
	q = Song.select().where(Song.status == STATUS_QUEUED).order_by(Song.added)
	obj = {"songs": [s.serialize() for s in q]}

	try:
		obj["playing"] = Song.select().where(Song.status == STATUS_PLAYING).order_by(Song.added.desc()).get().serialize()
	except Song.DoesNotExist:
		pass

	return obj

@api_route("add")
def ajax_add():
	songid = request.form["songid"]

	graph = get_graph_api()

	if not graph:
		return {"status": "error", "error": "no facebook user"}

	try:
		profile = graph.get_object("me")
	except facebook.GraphAPIError:
		graph = get_graph_api(True)
		profile = graph.get_object("me")

	sinfo = gs.api("getSongsInfo", {"songIDs": [songid]})
	songs = sinfo["result"]["songs"]

	if len(songs) == 0:
		return {"status": "error", "error": "song not found in database"}

	song = songs[0]

	song = Song(
		user_id = profile["id"],
		user_name = profile["name"],
		song_name = song["SongName"],
		song_id = song["SongID"],
		album_name = song["AlbumName"],
		album_id = song["AlbumID"],
		artist_name = song["ArtistName"],
	)

	song.save()

	return song.serialize()
<file_sep>#!/bin/sh
rm -f pidj.sqlite3
python -c "import pidj; pidj.models.create_tables()"
<file_sep>
import hashlib
import hmac
import simplejson
import requests

COUNTRY = {'ID': 221, 'CC1': 0, 'CC2': 0, 'CC3': 0, 'CC4': 0, 'DMA': 0, 'IPR': 0}

class Client(object):
	def __init__(self, app):
		self.key    = app.config["GROOVESHARK_KEY"]
		self.secret = app.config["GROOVESHARK_SECRET"]
		self.user   = app.config["GROOVESHARK_USER"]
		self.passw  = app.config["GROOVESHARK_PASS"]

		self.session = None
		self.has_init = False

		app.before_first_request(lambda: self.init())

	def signature(self, data):
		return hmac.new(self.secret, data).hexdigest()

	def api(self, method, params = {}):
		params["country"] = COUNTRY
		data = {
			"method": method,
			"parameters": params,
			"header": {"wsKey": self.key}
		}
		if self.session is not None:
			data["header"]["sessionID"] = self.session

		data = simplejson.dumps(data)
		sig = self.signature(data)

		r = requests.post("https://api.grooveshark.com/ws3.php?sig=%s" % sig, data = data)
		print r.json

		return r.json

	def init(self):
		if self.has_init:
			return

		response = self.api("startSession")
		if response["result"]["success"] == True:
			self.session = response["result"]["sessionID"]
		else:
			raise Exception("Could not start session")

		response = self.api("authenticate", {
				"login": self.user,
				"password": <PASSWORD>
		})
		self.has_init = True
<file_sep>

var userID;
var token;
var compiledTemplates = {};
var songData;

function getTemplate(tid) {
    if (compiledTemplates[tid])
        return compiledTemplates[tid];
    var source = $("#" + tid).html();
    var template = Handlebars.compile(source);
    compiledTemplates[tid] = template;
    return template;
}

function putTpl(tid, targ, params)
{
    $(targ).html(getTemplate(tid)(params));
}

function loggedIn(auth) {
    userID = auth.userID;
    token = auth.accessToken;
    putTpl("tpl-main", "#content");
    setInterval(reloadQueue, 5000);
}

var stInterval = null;
var curSong = null;

function updateTime() {
    var seconds = Number(curSong.duration) - Math.round((curSong.finish - new Date()) / 1000);
    if (seconds >= curSong.duration) {
        clearInterval(stInterval);
        $("#playing").html("");
        return;
    }
    var minutes = Math.floor(seconds / 60);
    seconds = seconds % 60;
    if (seconds < 10)
        seconds = "0" + seconds;
    $("#song-minutes").text(minutes);
    $("#song-seconds").text(seconds);
}

var oldQueue = "";
function reloadQueue() {
    $.post("/ajax/queue", {}, function(response) {
        var str = JSON.stringify(response);
        if (str == oldQueue)
            return;

        oldQueue = str;
        putTpl("tpl-queue", "#queue", response);

        if (response.playing) {

            var song = response.playing;
            song.added = new Date(song.added);
            song.finish = new Date(song.finish);
            if (Number(new Date()) > Number(song.finish))
                return;
            var mins = Math.floor(Number(song.duration) / 60);
            var secs = song.duration % 60;
            if (secs < 10)
                secs = "0" + secs;
            song.durationStr = mins + ":" + secs;
            console.log(song);
            curSong = song;
            stInterval = setInterval(updateTime, 100);
            putTpl("tpl-playing", "#playing", song);
        }

    });
}

$(function() {

    FB.init({
        appId: FB_APP_ID,
        status: true,
        cookie: true,
        xfbml: true
    });

    FB.getLoginStatus(function(response) {
        if (response.authResponse)
            loggedIn(response.authResponse);
        else
            putTpl("tpl-login", "#content");
    });

    $(document).on("click", "#login-btn", function() {
        FB.login(function (response) {
            if (response.authResponse)
                loggedIn(response.authResponse);
        });
        return false;
    });

    $(document).on("submit", "#search-form", function() {
        var inpDiv = $("#search-form .search-input");
        var val = inpDiv.val();
        inpDiv.val("");

        putTpl("tpl-modal", "#search-modal", {query: val});
        $("#search-modal").modal("show");

        $.post("/ajax/search", {query: val}, function(response) {
            songData = response.songs;
            for (var i = 0; i < songData.length; i++)
                songData[i]["id"] = i;
            putTpl("tpl-searchlist", "#search-modal .search-list", {songs: songData});
        });

        return false;
    });

    $(document).on("click", ".song-add-button", function() {
        var idx = $(this).attr("data-id");
        var sid = songData[idx].SongID;
        $("#search-modal").modal("hide");
        $.post("/ajax/add", {songid: sid}, function(response) {
            reloadQueue();
        });
        return false;
    });

    $(document).on("click", "a[rel=external]", function() {
        window.open(this.href, "pidjGsWindow", "width=800, height=600");
        return false;
    });
});
<file_sep>
# move this file to config.py

DEBUG = True
SECRET_KEY = "_none_"

FACEBOOK_APP_ID = ""
FACEBOOK_APP_SECRET = ""

GROOVESHARK_KEY = ""
GROOVESHARK_SECRET = ""
GROOVESHARK_USER = ""
GROOVESHARK_PASS = ""

TINYSONG_KEY = ""

TIMEZONE = "US/Central"

DATABASE = {
	'name': 'pidj.sqlite3',
	'engine': 'peewee.SqliteDatabase',
	'check_same_thread': False
}
