");
+
+ //refresh the UI to update the elapsed/remaining time
+ setTimeout(updateWidget, 1000);
+ }
+
+ function processData(data){
+ sd = new ScheduleData(data);
+ updateWidget();
+ }
+
+ function getServerData(){
+ $.ajax({ url: options.sourceDomain + "api/live-info/", dataType:"jsonp", success:function(data){
+ processData(data);
+ }, error:function(jqXHR, textStatus, errorThrown){}});
+ setTimeout(getServerData, defaults.updatePeriod*1000);
+ }
+ });
+ };
+})(jQuery);
+
+/* ScheduleData class BEGIN */
+function ScheduleData(data){
+ this.data = data;
+ this.estimatedSchedulePosixTime;
+
+ this.currentShow = new Array();
+ for (var i=0; i< data.currentShow.length; i++){
+ this.currentShow[i] = new Show(data.currentShow[i]);
+ }
+
+ this.nextShows = new Array();
+ for (var i=0; i< data.nextShow.length; i++){
+ this.nextShows[i] = new Show(data.nextShow[i]);
+ }
+
+
+ this.schedulePosixTime = convertDateToPosixTime(data.schedulerTime);
+ this.schedulePosixTime += parseInt(data.timezoneOffset)*1000;
+ var date = new Date();
+ this.localRemoteTimeOffset = date.getTime() - this.schedulePosixTime;
+}
+
+
+ScheduleData.prototype.secondsTimer = function(){
+ var date = new Date();
+ this.estimatedSchedulePosixTime = date.getTime() - this.localRemoteTimeOffset;
+}
+
+ScheduleData.prototype.getCurrentShow = function(){
+ return this.currentShow;
+}
+
+ScheduleData.prototype.getNextShows = function() {
+ return this.nextShows;
+}
+
+ScheduleData.prototype.getShowTimeElapsed = function(show) {
+ this.secondsTimer();
+
+ var showStart = convertDateToPosixTime(show.getStartTimestamp());
+ return convertToHHMMSS(this.estimatedSchedulePosixTime - showStart);
+};
+
+ScheduleData.prototype.getShowTimeRemaining = function(show) {
+ this.secondsTimer();
+
+ var showEnd = convertDateToPosixTime(show.getEndTimestamp());
+ return convertToHHMMSS(showEnd - this.estimatedSchedulePosixTime);
+};
+/* ScheduleData class END */
+
+/* Show class BEGIN */
+function Show(showData){
+ this.showData = showData;
+}
+
+Show.prototype.getName = function(){
+ return this.showData.name;
+}
+Show.prototype.getRange = function(){
+ return getTime(this.showData.start_timestamp) + " - " + getTime(this.showData.end_timestamp);
+}
+Show.prototype.getStartTimestamp = function(){
+ return this.showData.start_timestamp;
+}
+Show.prototype.getEndTimestamp = function(){
+ return this.showData.end_timestamp;
+}
+/* Show class END */
+
+
+function getTime(timestamp) {
+ var time = timestamp.split(" ")[1].split(":");
+ return time[0] + ":" + time[1];
+};
+
+/* Takes an input parameter of milliseconds and converts these into
+ * the format HH:MM:SS */
+function convertToHHMMSS(timeInMS){
+ var time = parseInt(timeInMS);
+
+ var hours = parseInt(time / 3600000);
+ time -= 3600000*hours;
+
+ var minutes = parseInt(time / 60000);
+ time -= 60000*minutes;
+
+ var seconds = parseInt(time / 1000);
+
+ hours = hours.toString();
+ minutes = minutes.toString();
+ seconds = seconds.toString();
+
+ if (hours.length == 1)
+ hours = "0" + hours;
+ if (minutes.length == 1)
+ minutes = "0" + minutes;
+ if (seconds.length == 1)
+ seconds = "0" + seconds;
+ if (hours == "00")
+ return minutes + ":" + seconds;
+ else
+ return hours + ":" + minutes + ":" + seconds;
+}
+
+/* Takes in a string of format similar to 2011-02-07 02:59:57,
+ * and converts this to epoch/posix time. */
+function convertDateToPosixTime(s){
+ var datetime = s.split(" ");
+
+ var date = datetime[0].split("-");
+ var time = datetime[1].split(":");
+
+ var year = date[0];
+ var month = date[1];
+ var day = date[2];
+ var hour = time[0];
+ var minute = time[1];
+ var sec = 0;
+ var msec = 0;
+
+ if (time[2].indexOf(".") != -1){
+ var temp = time[2].split(".");
+ sec = temp[0];
+ msec = temp[1];
+ } else
+ sec = time[2];
+
+ return Date.UTC(year, month, day, hour, minute, sec, msec);
+}
diff --git a/public/css/images/cue_playlist.png b/public/css/images/cue_playlist.png
new file mode 100644
index 000000000..b928ccfae
Binary files /dev/null and b/public/css/images/cue_playlist.png differ
diff --git a/public/css/images/icon_rebroadcast.png b/public/css/images/icon_rebroadcast.png
new file mode 100644
index 000000000..3a6c19571
Binary files /dev/null and b/public/css/images/icon_rebroadcast.png differ
diff --git a/public/css/images/icon_rebroadcast_m.png b/public/css/images/icon_rebroadcast_m.png
new file mode 100644
index 000000000..585a7d31f
Binary files /dev/null and b/public/css/images/icon_rebroadcast_m.png differ
diff --git a/public/css/images/icon_record.png b/public/css/images/icon_record.png
new file mode 100644
index 000000000..020ebe283
Binary files /dev/null and b/public/css/images/icon_record.png differ
diff --git a/public/css/images/icon_record_m.png b/public/css/images/icon_record_m.png
new file mode 100644
index 000000000..50b3ebe15
Binary files /dev/null and b/public/css/images/icon_record_m.png differ
diff --git a/public/css/playlist_builder.css b/public/css/playlist_builder.css
index dd1ad24b1..bd0ecc44f 100644
--- a/public/css/playlist_builder.css
+++ b/public/css/playlist_builder.css
@@ -15,7 +15,11 @@
#spl_sortable > li,
#side_playlist > div,
#spl_editor,
-.spl_artist {
+.spl_artist,
+.spl_cue_in,
+.spl_fade_in,
+.spl_cue_out,
+.spl_fade_out {
clear: left;
}
@@ -35,8 +39,8 @@
#spl_sortable {
list-style: none;
padding:0;
- height: 400px;
- overflow-y: scroll;
+ height: 300px;
+ overflow: auto;
width:100%;
margin-top:0;
}
@@ -52,6 +56,10 @@
border: none;
}
+#spl_name {
+
+}
+
.ui-icon-closethick {
margin-top: 7px;
}
@@ -72,10 +80,18 @@
font-size:12px;
}
+/*#spl_editor {
+ height: 50px;
+}*/
+
+#spl_editor > div > span {
+/* display: inline-block;
+ width: 150px;*/
+}
+
.ui-icon-closethick,
.ui-icon-play,
.spl_fade_control,
-.spl_playlength,
.spl_text_input {
cursor: pointer;
}
@@ -239,13 +255,12 @@
margin: 0;
}
-dd.edit-error {
+.edit-error {
color:#b80000;
margin:0;
padding-bottom:0;
font-size:12px;
display:none;
- clear: left;
}
/*.edit-error:last-child {
@@ -276,3 +291,26 @@ dd.edit-error {
top: 3px;
z-index: 3;
}
+
+#spl_sortable li .spl_cue {
+ background-color: transparent;
+ float:right;
+ font-size: 9px;
+ height: 15px;
+ right: 35px;
+ width: 33px;
+ margin-top:2px;
+ cursor:pointer;
+}
+#spl_sortable li .spl_cue.ui-state-default {
+ background: transparent url(images/cue_playlist.png) no-repeat 0 0;
+ border:none;
+}
+#spl_sortable li .spl_cue.ui-state-default:hover {
+ background: transparent url(images/cue_playlist.png) no-repeat 0 -15px;
+ border:none;
+}
+#spl_sortable li .spl_cue.ui-state-active, #spl_sortable li .spl_cue.ui-state-active:hover {
+ background: transparent url(images/cue_playlist.png) no-repeat 0 -30px;
+ border:none;
+}
\ No newline at end of file
diff --git a/public/css/styles.css b/public/css/styles.css
index 4e5e20a0c..7d0dc8690 100644
--- a/public/css/styles.css
+++ b/public/css/styles.css
@@ -184,7 +184,7 @@ select {
.progressbar .progress-show-error {
background:#d40000 url(images/progressbar_show_error.png) repeat-x 0 0;
}
-.now-playing-info .lenght {
+.now-playing-info .show-length {
color:#c4c4c4;
padding-left:6px;
}
@@ -196,6 +196,7 @@ select {
.time-info-block {
padding:0 14px 0 2px;
background:url(images/masterpanel_spacer.png) no-repeat right 0;
+ min-width:105px;
}
.time-info-block ul {
margin:0;
@@ -825,7 +826,6 @@ div.ui-datepicker {
#schedule_playlist_chosen li > h3 > span.ui-icon.ui-icon-triangle-1-e,
#schedule_playlist_chosen li > h3 > span.ui-icon.ui-icon-triangle-1-s {
float:left;
-
margin-right: 8px;
}
#schedule_playlist_chosen li > h3 > span.ui-icon.ui-icon-close {
@@ -1219,6 +1219,16 @@ ul.errors li {
margin-bottom:2px;
border:1px solid #c83f3f;
}
+
+div.success{
+ color:#3B5323;
+ font-size:11px;
+ padding:2px 4px;
+ background:#93DB70;
+ margin-bottom:2px;
+ border:1px solid #488214;
+}
+
.collapsible-header {
border: 1px solid #8f8f8f;
background-color: #cccccc;
@@ -1474,9 +1484,35 @@ ul.errors li {
margin:-4px 3px -3px 0;
float:right;
}
-
.time-flow {
- float:right;
- margin-right:4px;
+ float:right;
+ margin-right:4px;
+}
+.small-icon {
+ display:block;
+ width:20px;
+ height:10px;
+ float:right;
+ margin-left:3px;
+}
+.small-icon.recording {
+ background:url(images/icon_record.png) no-repeat 0 0;
+}
+.small-icon.rebroadcast {
+ background:url(images/icon_rebroadcast.png) no-repeat 0 0;
}
+.medium-icon {
+ display:block;
+ width:25px;
+ height:12px;
+ float:right;
+ margin-left:4px;
+}
+.medium-icon.recording {
+ background:url(images/icon_record_m.png) no-repeat 0 0;
+}
+.medium-icon.rebroadcast {
+ background:url(images/icon_rebroadcast_m.png) no-repeat 0 0;
+}
+
diff --git a/public/js/airtime/library/spl.js b/public/js/airtime/library/spl.js
index e32e83f62..89203600d 100644
--- a/public/js/airtime/library/spl.js
+++ b/public/js/airtime/library/spl.js
@@ -205,13 +205,15 @@ function openFadeEditor(event) {
function openCueEditor(event) {
event.stopPropagation();
- var pos, url, li;
+ var pos, url, li, icon;
li = $(this).parent().parent().parent();
+ icon = $(this);
pos = li.attr("id").split("_").pop();
if(li.hasClass("ui-state-active")) {
li.removeClass("ui-state-active");
+ icon.attr("class", "spl_cue ui-state-default");
$("#cues_"+pos)
.empty()
@@ -220,6 +222,7 @@ function openCueEditor(event) {
return;
}
+ icon.attr("class", "spl_cue ui-state-default ui-state-active");
url = '/Playlist/set-cue';
highlightActive(li);
@@ -253,7 +256,8 @@ function setSPLContent(json) {
$("#spl_sortable .ui-icon-closethick").click(deleteSPLItem);
$(".spl_fade_control").click(openFadeEditor);
- $(".spl_playlength").click(openCueEditor);
+ //$(".spl_playlength").click(openCueEditor);
+ $(".spl_cue").click(openCueEditor);
return false;
}
@@ -487,7 +491,8 @@ function setUpSPL() {
$("#spl_sortable .ui-icon-closethick").click(deleteSPLItem);
$(".spl_fade_control").click(openFadeEditor);
- $(".spl_playlength").click(openCueEditor);
+ //$(".spl_playlength").click(openCueEditor);
+ $(".spl_cue").click(openCueEditor);
$("#spl_sortable").droppable();
$("#spl_sortable" ).bind( "drop", addSPLItem);
diff --git a/public/js/airtime/schedule/add-show.js b/public/js/airtime/schedule/add-show.js
index 50ca4058a..329391086 100644
--- a/public/js/airtime/schedule/add-show.js
+++ b/public/js/airtime/schedule/add-show.js
@@ -182,7 +182,6 @@ function setAddShowEvents() {
});
form.find("#add-show-submit")
- .button()
.click(function(event){
event.preventDefault();
diff --git a/public/js/airtime/schedule/full-calendar-functions.js b/public/js/airtime/schedule/full-calendar-functions.js
index 33df795b7..ce01e4583 100644
--- a/public/js/airtime/schedule/full-calendar-functions.js
+++ b/public/js/airtime/schedule/full-calendar-functions.js
@@ -165,6 +165,25 @@ function eventRender(event, element, view) {
}
$(element).find(".fc-event-title").after(div);
+ }
+
+ //add the record/rebroadcast icons if needed.
+ if((view.name === 'agendaDay' || view.name === 'agendaWeek') && event.record === 1) {
+
+ $(element).find(".fc-event-time").after('');
+ }
+ if(view.name === 'month' && event.record === 1) {
+
+ $(element).find(".fc-event-title").after('');
+ }
+
+ if((view.name === 'agendaDay' || view.name === 'agendaWeek') && event.rebroadcast === 1) {
+
+ $(element).find(".fc-event-time").after('');
+ }
+ if(view.name === 'month' && event.rebroadcast === 1) {
+
+ $(element).find(".fc-event-title").after('');
}
if(event.backgroundColor !== "") {
diff --git a/public/js/airtime/user/user.js b/public/js/airtime/user/user.js
index 4add2d4b1..6a518fdc1 100644
--- a/public/js/airtime/user/user.js
+++ b/public/js/airtime/user/user.js
@@ -2,6 +2,7 @@ function populateForm(entries){
//$('#user_details').show();
$('.errors').remove();
+ $('.success').remove();
$('#user_id').val(entries.id);
$('#login').val(entries.login);
diff --git a/public/js/playlist/playlist.js b/public/js/playlist/playlist.js
index 82f277d1c..a72e94dc2 100644
--- a/public/js/playlist/playlist.js
+++ b/public/js/playlist/playlist.js
@@ -165,11 +165,12 @@ function updatePlaybar(){
/* Column 1 update */
$('#playlist').text("Current Show:");
+ var recElem = $('.recording-show');
if (currentShow.length > 0){
$('#playlist').text(currentShow[0].name);
-
- var recElem = $('.recording-show');
- currentShow[0].record ? recElem.show(): recElem.hide();
+ (currentShow[0].record == "1") ? recElem.show(): recElem.hide();
+ } else {
+ recElem.hide();
}
$('#show-length').empty();
diff --git a/pypo/api_clients/api_client_factory.py b/pypo/api_clients/api_client_factory.py
deleted file mode 100644
index 4762b0fc7..000000000
--- a/pypo/api_clients/api_client_factory.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import airtime_api_client
-import obp_api_client
-
-def create_api_client(config):
- if config["api_client"] == "airtime":
- return campcaster_api_client.AirtimeApiClient(config)
- elif config["api_client"] == "obp":
- return obp_api_client.ObpApiClient(config)
-
diff --git a/pypo/debug.log b/pypo/debug.log
deleted file mode 100644
index e69de29bb..000000000
diff --git a/pypo/error.log b/pypo/error.log
deleted file mode 100644
index e69de29bb..000000000
diff --git a/pypo/install/pypo-daemontools-fetch.sh b/pypo/install/pypo-daemontools-fetch.sh
deleted file mode 100644
index 6e7bed55d..000000000
--- a/pypo/install/pypo-daemontools-fetch.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/sh
-pypo_user="pypo"
-export HOME="/home/pypo/"
-# Location of pypo_cli.py Python script
-pypo_path="/opt/pypo/bin/"
-pypo_script="pypo-cli.py"
-echo "*** Daemontools: starting daemon"
-cd ${pypo_path}
-exec 2>&1
-# Note the -u when calling python! we need it to get unbuffered binary stdout and stderr
-exec setuidgid ${pypo_user} \
- python -u ${pypo_path}${pypo_script} \
- -f
-# EOF
diff --git a/pypo/install/pypo-stop.py b/pypo/install/pypo-stop.py
deleted file mode 100644
index a27032313..000000000
--- a/pypo/install/pypo-stop.py
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-import os
-import sys
-
-if os.geteuid() != 0:
- print "Please run this as root."
- sys.exit(1)
-
-try:
- print "Stopping daemontool script pypo-fetch"
- os.system("svc -dx /etc/service/pypo-fetch 2>/dev/null")
-
- print "Stopping daemontool script pypo-push"
- os.system("svc -dx /etc/service/pypo-push 2>/dev/null")
-
- print "Stopping daemontool script pypo-liquidsoap"
- os.system("svc -dx /etc/service/pypo-liquidsoap 2>/dev/null")
- os.system("killall liquidsoap")
-
-except Exception, e:
- print "exception:" + str(e)
diff --git a/pypo/logging.cfg b/pypo/logging.cfg
deleted file mode 100644
index ee3cd2bee..000000000
--- a/pypo/logging.cfg
+++ /dev/null
@@ -1,60 +0,0 @@
-[loggers]
-keys=root
-
-[handlers]
-keys=consoleHandler,fileHandlerERROR,fileHandlerDEBUG,nullHandler
-
-[formatters]
-keys=simpleFormatter
-
-[logger_root]
-level=DEBUG
-handlers=consoleHandler,fileHandlerERROR,fileHandlerDEBUG
-
-[logger_libs]
-handlers=nullHandler
-level=DEBUG
-qualname="process"
-propagate=0
-
-
-[handler_consoleHandler]
-class=StreamHandler
-level=DEBUG
-formatter=simpleFormatter
-args=(sys.stdout,)
-
-[handler_fileHandlerERROR]
-class=FileHandler
-level=WARNING
-formatter=simpleFormatter
-args=("./error.log",)
-
-[handler_fileHandlerDEBUG]
-class=FileHandler
-level=DEBUG
-formatter=simpleFormatter
-args=("./debug.log",)
-
-[handler_nullHandler]
-class=FileHandler
-level=DEBUG
-formatter=simpleFormatter
-args=("/dev/null",)
-
-
-[formatter_simpleFormatter]
-format=%(asctime)s %(levelname)s - [%(filename)s : %(funcName)s() : line %(lineno)d] - %(message)s
-datefmt=
-
-
-## multitail color sheme
-## pyml / python
-# colorscheme:pyml:www.obp.net
-# cs_re:blue:\[[^ ]*\]
-# cs_re:red:CRITICAL:*
-# cs_re:red,black,blink:ERROR:*
-# cs_re:blue:NOTICE:*
-# cs_re:cyan:INFO:*
-# cs_re:green:DEBUG:*
-
diff --git a/pypo/scripts/silence.mp3 b/pypo/scripts/silence.mp3
deleted file mode 100644
index 374dfe286..000000000
Binary files a/pypo/scripts/silence.mp3 and /dev/null differ
diff --git a/pypo/api_clients/__init__.py b/python_apps/api_clients/__init__.py
similarity index 100%
rename from pypo/api_clients/__init__.py
rename to python_apps/api_clients/__init__.py
diff --git a/pypo/api_clients/api_client.py b/python_apps/api_clients/api_client.py
similarity index 91%
rename from pypo/api_clients/api_client.py
rename to python_apps/api_clients/api_client.py
index 6994cf3fd..cd2a6e7d7 100644
--- a/pypo/api_clients/api_client.py
+++ b/python_apps/api_clients/api_client.py
@@ -13,6 +13,7 @@
import sys
import time
import urllib
+import urllib2
import logging
import json
import os
@@ -90,6 +91,12 @@ class ApiClientInterface:
# You will be able to use this data in update_start_playing
def get_liquidsoap_data(self, pkey, schedule):
pass
+
+ def get_shows_to_record(self):
+ pass
+
+ def upload_recorded_show(self):
+ pass
# Put here whatever tests you want to run to make sure your API is working
def test(self):
@@ -189,30 +196,10 @@ class AirTimeApiClient(ApiClientInterface):
def get_schedule(self, start=None, end=None):
logger = logging.getLogger()
-
- """
- calculate start/end time range (format: YYYY-DD-MM-hh-mm-ss,YYYY-DD-MM-hh-mm-ss)
- (seconds are ignored, just here for consistency)
- """
- tnow = time.localtime(time.time())
- if (not start):
- tstart = time.localtime(time.time() - 3600 * int(self.config["cache_for"]))
- start = "%04d-%02d-%02d-%02d-%02d" % (tstart[0], tstart[1], tstart[2], tstart[3], tstart[4])
-
- if (not end):
- tend = time.localtime(time.time() + 3600 * int(self.config["prepare_ahead"]))
- end = "%04d-%02d-%02d-%02d-%02d" % (tend[0], tend[1], tend[2], tend[3], tend[4])
-
- range = {}
- range['start'] = start
- range['end'] = end
-
+
# Construct the URL
export_url = self.config["base_url"] + self.config["api_base"] + self.config["export_url"]
- # Insert the start and end times into the URL
- export_url = export_url.replace('%%from%%', range['start'])
- export_url = export_url.replace('%%to%%', range['end'])
logger.info("Fetching schedule from %s", export_url)
export_url = export_url.replace('%%api_key%%', self.config["api_key"])
@@ -225,24 +212,6 @@ class AirTimeApiClient(ApiClientInterface):
except Exception, e:
print e
- #schedule = response["playlists"]
- #scheduleKeys = sorted(schedule.iterkeys())
- #
- ## Remove all playlists that have passed current time
- #try:
- # tnow = time.localtime(time.time())
- # str_tnow_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tnow[0], tnow[1], tnow[2], tnow[3], tnow[4], tnow[5])
- # toRemove = []
- # for pkey in scheduleKeys:
- # if (str_tnow_s > schedule[pkey]['end']):
- # toRemove.append(pkey)
- # else:
- # break
- # for index in toRemove:
- # del schedule[index]
- #except Exception, e:
- #response["playlists"] = schedule
-
return status, response
@@ -316,6 +285,41 @@ class AirTimeApiClient(ApiClientInterface):
except Exception, e:
data["schedule_id"] = 0
return data
+
+ def get_shows_to_record(self):
+ logger = logging.getLogger()
+ response = ''
+ try:
+ url = self.config["base_url"] + self.config["api_base"] + self.config["show_schedule_url"]
+ logger.debug(url)
+ url = url.replace("%%api_key%%", self.config["api_key"])
+
+ response = urllib.urlopen(url)
+ response = json.loads(response.read())
+ logger.info("shows %s", response)
+
+ except Exception, e:
+ logger.error("Exception: %s", e)
+
+ return response[u'shows']
+
+ def upload_recorded_show(self, data, headers):
+ logger = logging.getLogger()
+ response = ''
+ try:
+ url = self.config["base_url"] + self.config["api_base"] + self.config["upload_file_url"]
+ logger.debug(url)
+ url = url.replace("%%api_key%%", self.config["api_key"])
+
+ request = urllib2.Request(url, data, headers)
+ response = urllib2.urlopen(request).read().strip()
+
+ logger.info("uploaded show result %s", response)
+
+ except Exception, e:
+ logger.error("Exception: %s", e)
+
+ return response
diff --git a/python_apps/eggs/poster-0.8.0-py2.6.egg b/python_apps/eggs/poster-0.8.0-py2.6.egg
deleted file mode 100644
index 7af5c8f5e..000000000
Binary files a/python_apps/eggs/poster-0.8.0-py2.6.egg and /dev/null differ
diff --git a/pypo/AUTHORS b/python_apps/pypo/AUTHORS
similarity index 100%
rename from pypo/AUTHORS
rename to python_apps/pypo/AUTHORS
diff --git a/pypo/LICENSE b/python_apps/pypo/LICENSE
similarity index 100%
rename from pypo/LICENSE
rename to python_apps/pypo/LICENSE
diff --git a/pypo/config.cfg b/python_apps/pypo/config.cfg
similarity index 86%
rename from pypo/config.cfg
rename to python_apps/pypo/config.cfg
index 638558fbf..2c0fce6f0 100644
--- a/pypo/config.cfg
+++ b/python_apps/pypo/config.cfg
@@ -26,6 +26,13 @@ base_url = 'http://localhost/'
ls_host = '127.0.0.1'
ls_port = '1234'
+############################################
+# RabbitMQ settings #
+############################################
+rabbitmq_host = 'localhost'
+rabbitmq_user = 'guest'
+rabbitmq_password = 'guest'
+
############################################
# pypo preferences #
############################################
@@ -34,15 +41,13 @@ cache_for = 24 #how long to hold the cache, in hours
# Poll interval in seconds.
#
+# This will rarely need to be changed because any schedule changes are
+# automatically sent to pypo immediately.
+#
# This is how often the poll script downloads new schedules and files from the
-# server.
+# server in the event that no changes are made to the schedule.
#
-# For production use, this number depends on whether you plan on making any
-# last-minute changes to your schedule. This number should be set to half of
-# the time you expect to "lock-in" your schedule. So if your schedule is set
-# 24 hours in advance, this can be set to poll every 12 hours.
-#
-poll_interval = 5 # in seconds.
+poll_interval = 3600 # in seconds.
# Push interval in seconds.
@@ -52,7 +57,7 @@ poll_interval = 5 # in seconds.
#
# It's hard to imagine a situation where this should be more than 1 second.
#
-push_interval = 2 # in seconds
+push_interval = 1 # in seconds
# 'pre' or 'otf'. 'pre' cues while playlist preparation
# while 'otf' (on the fly) cues while loading into ls
@@ -80,7 +85,7 @@ version_url = 'version/api_key/%%api_key%%'
# Schedule export path.
# %%from%% - starting date/time in the form YYYY-MM-DD-hh-mm
# %%to%% - starting date/time in the form YYYY-MM-DD-hh-mm
-export_url = 'schedule/api_key/%%api_key%%/from/%%from%%/to/%%to%%'
+export_url = 'schedule/api_key/%%api_key%%'
# Update whether a schedule group has begun playing.
update_item_url = 'notify-schedule-group-play/api_key/%%api_key%%/schedule_id/%%schedule_id%%'
diff --git a/pypo/config.cfg.dist b/python_apps/pypo/config.cfg.dist
similarity index 100%
rename from pypo/config.cfg.dist
rename to python_apps/pypo/config.cfg.dist
diff --git a/pypo/dls/__init__.py b/python_apps/pypo/dls/__init__.py
similarity index 100%
rename from pypo/dls/__init__.py
rename to python_apps/pypo/dls/__init__.py
diff --git a/pypo/dls/dls_client.py b/python_apps/pypo/dls/dls_client.py
similarity index 100%
rename from pypo/dls/dls_client.py
rename to python_apps/pypo/dls/dls_client.py
diff --git a/pypo/install/pypo-daemontools-liquidsoap.sh b/python_apps/pypo/install/pypo-daemontools-liquidsoap.sh
similarity index 61%
rename from pypo/install/pypo-daemontools-liquidsoap.sh
rename to python_apps/pypo/install/pypo-daemontools-liquidsoap.sh
index 6f9061bfa..cd6177164 100644
--- a/pypo/install/pypo-daemontools-liquidsoap.sh
+++ b/python_apps/pypo/install/pypo-daemontools-liquidsoap.sh
@@ -1,10 +1,12 @@
#!/bin/sh
ls_user="pypo"
export HOME="/home/pypo/"
+api_client_path="/opt/pypo/"
ls_path="/opt/pypo/bin/liquidsoap/liquidsoap"
ls_param="/opt/pypo/bin/scripts/ls_script.liq"
echo "*** Daemontools: starting liquidsoap"
exec 2>&1
-echo "exec sudo -u ${ls_user} ${ls_path} ${ls_param} "
-cd /opt/pypo/bin/scripts && sudo -u ${ls_user} ${ls_path} ${ls_param}
+
+cd /opt/pypo/bin/scripts
+sudo PYTHONPATH=${api_client_path} -u ${ls_user} ${ls_path} ${ls_param}
# EOF
diff --git a/pypo/install/pypo-daemontools-logger.sh b/python_apps/pypo/install/pypo-daemontools-logger.sh
similarity index 100%
rename from pypo/install/pypo-daemontools-logger.sh
rename to python_apps/pypo/install/pypo-daemontools-logger.sh
diff --git a/pypo/install/pypo-daemontools-push.sh b/python_apps/pypo/install/pypo-daemontools.sh
similarity index 70%
rename from pypo/install/pypo-daemontools-push.sh
rename to python_apps/pypo/install/pypo-daemontools.sh
index 4c5cc9f7c..1f276cc4e 100644
--- a/pypo/install/pypo-daemontools-push.sh
+++ b/python_apps/pypo/install/pypo-daemontools.sh
@@ -3,12 +3,16 @@ pypo_user="pypo"
export HOME="/home/pypo/"
# Location of pypo_cli.py Python script
pypo_path="/opt/pypo/bin/"
+api_client_path="/opt/pypo/"
pypo_script="pypo-cli.py"
echo "*** Daemontools: starting daemon"
cd ${pypo_path}
exec 2>&1
+
+PYTHONPATH=${api_client_path}:$PYTHONPATH
+export PYTHONPATH
+
# Note the -u when calling python! we need it to get unbuffered binary stdout and stderr
exec setuidgid ${pypo_user} \
- python -u ${pypo_path}${pypo_script} \
- -p
+ python -u ${pypo_path}${pypo_script}
# EOF
diff --git a/pypo/install/pypo-install.py b/python_apps/pypo/install/pypo-install.py
similarity index 71%
rename from pypo/install/pypo-install.py
rename to python_apps/pypo/install/pypo-install.py
index 52746f009..1171dfe23 100644
--- a/pypo/install/pypo-install.py
+++ b/python_apps/pypo/install/pypo-install.py
@@ -36,14 +36,16 @@ def create_user(username):
os.system("adduser --system --quiet --group --shell /bin/bash "+username)
#set pypo password
- p = os.popen('/usr/bin/passwd pypo', 'w')
+ p = os.popen('/usr/bin/passwd pypo 1>/dev/null 2>&1', 'w')
p.write('pypo\n')
p.write('pypo\n')
p.close()
else:
print "User already exists."
#add pypo to audio group
- os.system("adduser " + username + " audio 2>&1 1>/dev/null")
+ os.system("adduser " + username + " audio 1>/dev/null 2>&1")
+ #add pypo to pulse-access group
+ #os.system("adduser " + username + " pulse-access 1>/dev/null 2>&1")
def copy_dir(src_dir, dest_dir):
if (os.path.exists(dest_dir)) and (dest_dir != "/"):
@@ -63,7 +65,7 @@ def get_current_script_dir():
try:
current_script_dir = get_current_script_dir()
print "Checking and removing any existing pypo processes"
- os.system("python %s/pypo-uninstall.py 2>&1 1>/dev/null"% current_script_dir)
+ os.system("python %s/pypo-uninstall.py 1>/dev/null 2>&1"% current_script_dir)
time.sleep(5)
# Create users
@@ -79,14 +81,8 @@ try:
create_path(BASE_PATH+"cache")
create_path(BASE_PATH+"files")
create_path(BASE_PATH+"tmp")
- create_path(BASE_PATH+"files/basic")
- create_path(BASE_PATH+"files/fallback")
- create_path(BASE_PATH+"files/jingles")
create_path(BASE_PATH+"archive")
-
- print "Copying pypo files"
- shutil.copy("%s/../scripts/silence.mp3"%current_script_dir, BASE_PATH+"files/basic")
-
+
if platform.architecture()[0] == '64bit':
print "Installing 64-bit liquidsoap binary"
shutil.copy("%s/../liquidsoap/liquidsoap64"%current_script_dir, "%s/../liquidsoap/liquidsoap"%current_script_dir)
@@ -98,28 +94,21 @@ try:
sys.exit(1)
copy_dir("%s/.."%current_script_dir, BASE_PATH+"bin/")
+ copy_dir("%s/../../api_clients"%current_script_dir, BASE_PATH+"api_clients/")
print "Setting permissions"
os.system("chmod -R 755 "+BASE_PATH)
os.system("chown -R pypo:pypo "+BASE_PATH)
- print "Installing daemontool script pypo-fetch"
- create_path("/etc/service/pypo-fetch")
- create_path("/etc/service/pypo-fetch/log")
- shutil.copy("%s/pypo-daemontools-fetch.sh"%current_script_dir, "/etc/service/pypo-fetch/run")
- shutil.copy("%s/pypo-daemontools-logger.sh"%current_script_dir, "/etc/service/pypo-fetch/log/run")
- os.system("chmod -R 755 /etc/service/pypo-fetch")
- os.system("chown -R pypo:pypo /etc/service/pypo-fetch")
+ print "Installing pypo daemon"
+ create_path("/etc/service/pypo")
+ create_path("/etc/service/pypo/log")
+ shutil.copy("%s/pypo-daemontools.sh"%current_script_dir, "/etc/service/pypo/run")
+ shutil.copy("%s/pypo-daemontools-logger.sh"%current_script_dir, "/etc/service/pypo/log/run")
+ os.system("chmod -R 755 /etc/service/pypo")
+ os.system("chown -R pypo:pypo /etc/service/pypo")
- print "Installing daemontool script pypo-push"
- create_path("/etc/service/pypo-push")
- create_path("/etc/service/pypo-push/log")
- shutil.copy("%s/pypo-daemontools-push.sh"%current_script_dir, "/etc/service/pypo-push/run")
- shutil.copy("%s/pypo-daemontools-logger.sh"%current_script_dir, "/etc/service/pypo-push/log/run")
- os.system("chmod -R 755 /etc/service/pypo-push")
- os.system("chown -R pypo:pypo /etc/service/pypo-push")
-
- print "Installing daemontool script pypo-liquidsoap"
+ print "Installing liquidsoap daemon"
create_path("/etc/service/pypo-liquidsoap")
create_path("/etc/service/pypo-liquidsoap/log")
shutil.copy("%s/pypo-daemontools-liquidsoap.sh"%current_script_dir, "/etc/service/pypo-liquidsoap/run")
@@ -134,16 +123,12 @@ try:
found = True
- p = Popen('svstat /etc/service/pypo-fetch', shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
+ p = Popen('svstat /etc/service/pypo', shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
output = p.stdout.read()
if (output.find("unable to open supervise/ok: file does not exist") >= 0):
found = False
print output
-
- p = Popen('svstat /etc/service/pypo-push', shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
- output = p.stdout.read()
- print output
-
+
p = Popen('svstat /etc/service/pypo-liquidsoap', shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
output = p.stdout.read()
print output
@@ -152,6 +137,7 @@ try:
print "Pypo install has completed, but daemontools is not running, please make sure you have it installed and then reboot."
except Exception, e:
print "exception:" + str(e)
+ sys.exit(1)
diff --git a/pypo/install/pypo-start.py b/python_apps/pypo/install/pypo-start.py
similarity index 61%
rename from pypo/install/pypo-start.py
rename to python_apps/pypo/install/pypo-start.py
index f23794af1..9d5811638 100644
--- a/pypo/install/pypo-start.py
+++ b/python_apps/pypo/install/pypo-start.py
@@ -9,12 +9,9 @@ if os.geteuid() != 0:
sys.exit(1)
try:
- print "Starting daemontool script pypo-fetch"
- os.system("svc -u /etc/service/pypo-fetch")
-
- print "Starting daemontool script pypo-push"
- os.system("svc -u /etc/service/pypo-push")
-
+ print "Starting daemontool script pypo"
+ os.system("svc -u /etc/service/pypo")
+
print "Starting daemontool script pypo-liquidsoap"
os.system("svc -u /etc/service/pypo-liquidsoap")
diff --git a/python_apps/pypo/install/pypo-stop.py b/python_apps/pypo/install/pypo-stop.py
new file mode 100644
index 000000000..388c0bc4b
--- /dev/null
+++ b/python_apps/pypo/install/pypo-stop.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+
+if os.geteuid() != 0:
+ print "Please run this as root."
+ sys.exit(1)
+
+try:
+ print "Stopping daemontool script pypo"
+ os.system("svc -dx /etc/service/pypo 1>/dev/null 2>&1")
+
+ if os.path.exists("/etc/service/pypo-fetch"):
+ os.system("svc -dx /etc/service/pypo-fetch 1>/dev/null 2>&1")
+ if os.path.exists("/etc/service/pypo-push"):
+ os.system("svc -dx /etc/service/pypo-push 1>/dev/null 2>&1")
+
+ print "Stopping daemontool script pypo-liquidsoap"
+ os.system("svc -dx /etc/service/pypo-liquidsoap 1>/dev/null 2>&1")
+ os.system("killall liquidsoap")
+
+except Exception, e:
+ print "exception:" + str(e)
diff --git a/pypo/install/pypo-uninstall.py b/python_apps/pypo/install/pypo-uninstall.py
similarity index 69%
rename from pypo/install/pypo-uninstall.py
rename to python_apps/pypo/install/pypo-uninstall.py
index 0b225cc7e..cd22e9c4c 100644
--- a/pypo/install/pypo-uninstall.py
+++ b/python_apps/pypo/install/pypo-uninstall.py
@@ -15,13 +15,13 @@ def remove_path(path):
os.system("rm -rf " + path)
def remove_user(username):
- os.system("killall -u %s 2>&1 1>/dev/null" % username)
+ os.system("killall -u %s 1>/dev/null 2>&1" % username)
#allow all process to be completely closed before we attempt to delete user
print "Waiting for processes to close..."
time.sleep(5)
- os.system("deluser --remove-home " + username + " 1>/dev/null")
+ os.system("deluser --remove-home " + username + " 1>/dev/null 2>&1")
def get_current_script_dir():
current_script_dir = os.path.realpath(__file__)
@@ -37,16 +37,19 @@ try:
print "Removing pypo files"
remove_path(BASE_PATH)
- print "Removing daemontool script pypo-fetch"
- remove_path("rm -rf /etc/service/pypo-fetch")
-
- print "Removing daemontool script pypo-push"
- remove_path("rm -rf /etc/service/pypo-push")
-
+ print "Removing daemontool script pypo"
+ remove_path("/etc/service/pypo")
+
+ if os.path.exists("/etc/service/pypo-fetch"):
+ remove_path("/etc/service/pypo-fetch")
+
+ if os.path.exists("/etc/service/pypo-push"):
+ remove_path("/etc/service/pypo-push")
+
print "Removing daemontool script pypo-liquidsoap"
- remove_path("rm -rf /etc/service/pypo-liquidsoap")
+ remove_path("/etc/service/pypo-liquidsoap")
remove_user("pypo")
- print "Uninstall complete."
+ print "Pypo uninstall complete."
except Exception, e:
print "exception:" + str(e)
diff --git a/pypo/liquidsoap/liquidsoap32 b/python_apps/pypo/liquidsoap/liquidsoap32
similarity index 100%
rename from pypo/liquidsoap/liquidsoap32
rename to python_apps/pypo/liquidsoap/liquidsoap32
diff --git a/pypo/liquidsoap/liquidsoap64 b/python_apps/pypo/liquidsoap/liquidsoap64
similarity index 100%
rename from pypo/liquidsoap/liquidsoap64
rename to python_apps/pypo/liquidsoap/liquidsoap64
diff --git a/pypo/logging-api-validator.cfg b/python_apps/pypo/logging-api-validator.cfg
similarity index 100%
rename from pypo/logging-api-validator.cfg
rename to python_apps/pypo/logging-api-validator.cfg
diff --git a/python_apps/pypo/logging.cfg b/python_apps/pypo/logging.cfg
new file mode 100644
index 000000000..7b679d40c
--- /dev/null
+++ b/python_apps/pypo/logging.cfg
@@ -0,0 +1,34 @@
+[loggers]
+keys=root,fetch,push
+
+[handlers]
+keys=consoleHandler
+
+[formatters]
+keys=simpleFormatter
+
+[logger_root]
+level=DEBUG
+handlers=consoleHandler
+
+[logger_fetch]
+level=DEBUG
+handlers=consoleHandler
+qualname=fetch
+propagate=0
+
+[logger_push]
+level=DEBUG
+handlers=consoleHandler
+qualname=push
+propagate=0
+
+[handler_consoleHandler]
+class=StreamHandler
+level=DEBUG
+formatter=simpleFormatter
+args=(sys.stdout,)
+
+[formatter_simpleFormatter]
+format=%(asctime)s %(levelname)s - [%(filename)s : %(funcName)s() : line %(lineno)d] - %(message)s
+datefmt=
diff --git a/pypo/pypo-api-validator.py b/python_apps/pypo/pypo-api-validator.py
similarity index 100%
rename from pypo/pypo-api-validator.py
rename to python_apps/pypo/pypo-api-validator.py
diff --git a/pypo/pypo-cli.py b/python_apps/pypo/pypo-cli.py
similarity index 79%
rename from pypo/pypo-cli.py
rename to python_apps/pypo/pypo-cli.py
index 3c0402908..208049d7b 100755
--- a/pypo/pypo-cli.py
+++ b/python_apps/pypo/pypo-cli.py
@@ -7,14 +7,13 @@ Python part of radio playout (pypo)
The main functions are "fetch" (./pypo_cli.py -f) and "push" (./pypo_cli.py -p)
"""
-# python defaults (debian default)
import time
#import calendar
-
-
#import traceback
from optparse import *
import sys
+import os
+import signal
#import datetime
import logging
import logging.config
@@ -27,11 +26,11 @@ import logging.config
#import string
#import operator
#import inspect
+from Queue import Queue
from pypopush import PypoPush
from pypofetch import PypoFetch
-# additional modules (should be checked)
from configobj import ConfigObj
# custom imports
@@ -53,7 +52,6 @@ parser.add_option("-v", "--compat", help="Check compatibility with server API ve
parser.add_option("-t", "--test", help="Do a test to make sure everything is working properly.", default=False, action="store_true", dest="test")
parser.add_option("-f", "--fetch-scheduler", help="Fetch the schedule from server. This is a polling process that runs forever.", default=False, action="store_true", dest="fetch_scheduler")
parser.add_option("-p", "--push-scheduler", help="Push the schedule to Liquidsoap. This is a polling process that runs forever.", default=False, action="store_true", dest="push_scheduler")
-
parser.add_option("-b", "--cleanup", help="Cleanup", default=False, action="store_true", dest="cleanup")
parser.add_option("-c", "--check", help="Check the cached schedule and exit", default=False, action="store_true", dest="check")
@@ -66,10 +64,6 @@ logging.config.fileConfig("logging.cfg")
# loading config file
try:
config = ConfigObj('config.cfg')
- POLL_INTERVAL = float(config['poll_interval'])
- PUSH_INTERVAL = float(config['push_interval'])
- LS_HOST = config['ls_host']
- LS_PORT = config['ls_port']
except Exception, e:
print 'Error loading config file: ', e
sys.exit()
@@ -121,55 +115,44 @@ class Global:
for media in playlist['medias']:
print media
+def keyboardInterruptHandler(signum, frame):
+ print "\nKeyboard Interrupt\n"
+ sys.exit();
if __name__ == '__main__':
print '###########################################'
print '# *** pypo *** #'
- print '# Liquidsoap + External Scheduler #'
- print '# Playout System #'
+ print '# Liquidsoap Scheduled Playout System #'
print '###########################################'
+ signal.signal(signal.SIGINT, keyboardInterruptHandler)
+
# initialize
g = Global()
g.selfcheck()
logger = logging.getLogger()
- loops = 0
if options.test:
g.test_api()
sys.exit()
-
- if options.fetch_scheduler:
- pf = PypoFetch()
- while True:
- try: pf.fetch('scheduler')
- except Exception, e:
- print e
- sys.exit()
+ q = Queue()
- if (loops%2 == 0):
- logger.info("heartbeat")
- loops += 1
- time.sleep(POLL_INTERVAL)
+ pp = PypoPush(q)
+ pp.daemon = True
+ pp.start()
- if options.push_scheduler:
- pp = PypoPush()
- while True:
- try: pp.push('scheduler')
- except Exception, e:
- print 'PUSH ERROR!! WILL EXIT NOW:('
- print e
- sys.exit()
+ pf = PypoFetch(q)
+ pf.daemon = True
+ pf.start()
- if (loops%60 == 0):
- logger.info("heartbeat")
-
- loops += 1
- time.sleep(PUSH_INTERVAL)
+ while True: time.sleep(3600)
+ #pp.join()
+ #pf.join()
+"""
if options.check:
try: g.check_schedule()
except Exception, e:
@@ -179,4 +162,4 @@ if __name__ == '__main__':
try: pf.cleanup('scheduler')
except Exception, e:
print e
- sys.exit()
+"""
diff --git a/pypo/pypo-cue-in-validator.py b/python_apps/pypo/pypo-cue-in-validator.py
similarity index 100%
rename from pypo/pypo-cue-in-validator.py
rename to python_apps/pypo/pypo-cue-in-validator.py
diff --git a/pypo/pypo-dls.py b/python_apps/pypo/pypo-dls.py
similarity index 100%
rename from pypo/pypo-dls.py
rename to python_apps/pypo/pypo-dls.py
diff --git a/pypo/pypo-log.sh b/python_apps/pypo/pypo-log.sh
similarity index 100%
rename from pypo/pypo-log.sh
rename to python_apps/pypo/pypo-log.sh
diff --git a/pypo/pypo-notify.py b/python_apps/pypo/pypo-notify.py
similarity index 100%
rename from pypo/pypo-notify.py
rename to python_apps/pypo/pypo-notify.py
diff --git a/pypo/pypofetch.py b/python_apps/pypo/pypofetch.py
similarity index 50%
rename from pypo/pypofetch.py
rename to python_apps/pypo/pypofetch.py
index aaeda54f5..2958c6bd4 100644
--- a/pypo/pypofetch.py
+++ b/python_apps/pypo/pypofetch.py
@@ -9,146 +9,151 @@ import random
import string
import json
import telnetlib
+import math
+from threading import Thread
+from subprocess import Popen, PIPE
+
+# For RabbitMQ
+from kombu.connection import BrokerConnection
+from kombu.messaging import Exchange, Queue, Consumer, Producer
from api_clients import api_client
from util import CueFile
from configobj import ConfigObj
+# configure logging
+logging.config.fileConfig("logging.cfg")
+
# loading config file
try:
config = ConfigObj('config.cfg')
- POLL_INTERVAL = float(config['poll_interval'])
- PUSH_INTERVAL = 0.5
- #PUSH_INTERVAL = float(config['push_interval'])
LS_HOST = config['ls_host']
LS_PORT = config['ls_port']
+ POLL_INTERVAL = int(config['poll_interval'])
+
except Exception, e:
print 'Error loading config file: ', e
sys.exit()
-class PypoFetch:
- def __init__(self):
+# Yuk - using a global, i know!
+SCHEDULE_PUSH_MSG = []
+
+"""
+Handle a message from RabbitMQ, put it into our yucky global var.
+Hopefully there is a better way to do this.
+"""
+def handle_message(body, message):
+ logger = logging.getLogger('fetch')
+ global SCHEDULE_PUSH_MSG
+ logger.info("Received schedule from RabbitMQ: " + message.body)
+ SCHEDULE_PUSH_MSG = json.loads(message.body)
+ # ACK the message to take it off the queue
+ message.ack()
+
+
+class PypoFetch(Thread):
+ def __init__(self, q):
+ Thread.__init__(self)
+ logger = logging.getLogger('fetch')
self.api_client = api_client.api_client_factory(config)
self.cue_file = CueFile()
self.set_export_source('scheduler')
+ self.queue = q
+
+ logger.info("Initializing RabbitMQ stuff")
+ schedule_exchange = Exchange("airtime-schedule", "direct", durable=True, auto_delete=True)
+ schedule_queue = Queue("pypo-fetch", exchange=schedule_exchange, key="foo")
+ self.connection = BrokerConnection(config["rabbitmq_host"], config["rabbitmq_user"], config["rabbitmq_password"], "/")
+ channel = self.connection.channel()
+ consumer = Consumer(channel, schedule_queue)
+ consumer.register_callback(handle_message)
+ consumer.consume()
+
+ logger.info("PypoFetch: init complete")
+
def set_export_source(self, export_source):
self.export_source = export_source
self.cache_dir = config["cache_dir"] + self.export_source + '/'
- self.schedule_file = self.cache_dir + 'schedule.pickle'
- self.schedule_tracker_file = self.cache_dir + "schedule_tracker.pickle"
+
+ def check_matching_timezones(self, server_timezone):
+ logger = logging.getLogger('fetch')
+
+ process = Popen(["date", "+%z"], stdout=PIPE)
+ pypo_timezone = (process.communicate()[0]).strip(' \r\n\t')
+
+ if server_timezone != pypo_timezone:
+ logger.error("Server and pypo timezone offsets do not match. Audio playback may not start when expected!")
+ logger.error("Server timezone offset: %s", server_timezone)
+ logger.error("Pypo timezone offset: %s", pypo_timezone)
"""
- Fetching part of pypo
- - Reads the scheduled entries of a given range (actual time +/- "prepare_ahead" / "cache_for")
- - Saves a serialized file of the schedule
- - playlists are prepared. (brought to liquidsoap format) and, if not mounted via nsf, files are copied
- to the cache dir (Folder-structure: cache/YYYY-MM-DD-hh-mm-ss)
- - runs the cleanup routine, to get rid of unused cashed files
+ Process the schedule
+ - Reads the scheduled entries of a given range (actual time +/- "prepare_ahead" / "cache_for")
+ - Saves a serialized file of the schedule
+ - playlists are prepared. (brought to liquidsoap format) and, if not mounted via nsf, files are copied
+ to the cache dir (Folder-structure: cache/YYYY-MM-DD-hh-mm-ss)
+ - runs the cleanup routine, to get rid of unused cashed files
"""
- def fetch(self, export_source):
- """
- wrapper script for fetching the whole schedule (in json)
- """
- logger = logging.getLogger()
+ def process_schedule(self, schedule_data, export_source):
+ logger = logging.getLogger('fetch')
+ self.schedule = schedule_data["playlists"]
- try: os.mkdir(self.cache_dir)
- except Exception, e: pass
-
- # get schedule
+ self.check_matching_timezones(schedule_data["server_timezone"])
+
+ # Push stream metadata to liquidsoap
+ # TODO: THIS LIQUIDSOAP STUFF NEEDS TO BE MOVED TO PYPO-PUSH!!!
+ stream_metadata = schedule_data['stream_metadata']
try:
- while self.get_schedule() != 1:
- logger.warning("failed to read from export url")
- time.sleep(1)
+ tn = telnetlib.Telnet(LS_HOST, LS_PORT)
+ #encode in latin-1 due to telnet protocol not supporting utf-8
+ tn.write(('vars.stream_metadata_type %s\n' % stream_metadata['format']).encode('latin-1'))
+ tn.write(('vars.station_name %s\n' % stream_metadata['station_name']).encode('latin-1'))
+ tn.write('exit\n')
+ tn.read_all()
+ except Exception, e:
+ logger.error("Exception %s", e)
+ status = 0
+ # Download all the media and put playlists in liquidsoap format
+ try:
+ playlists = self.prepare_playlists()
except Exception, e: logger.error("%s", e)
- # prepare the playlists
- if config["cue_style"] == 'pre':
- try: self.prepare_playlists_cue()
- except Exception, e: logger.error("%s", e)
- elif config["cue_style"] == 'otf':
- try: self.prepare_playlists(self.export_source)
- except Exception, e: logger.error("%s", e)
+ # Send the data to pypo-push
+ scheduled_data = dict()
+ scheduled_data['playlists'] = playlists
+ scheduled_data['schedule'] = self.schedule
+ scheduled_data['stream_metadata'] = schedule_data["stream_metadata"]
+ self.queue.put(scheduled_data)
# cleanup
try: self.cleanup(self.export_source)
except Exception, e: logger.error("%s", e)
- def get_schedule(self):
- logger = logging.getLogger()
- status, response = self.api_client.get_schedule()
-
- if status == 1:
- logger.info("dump serialized schedule to %s", self.schedule_file)
- schedule = response['playlists']
- stream_metadata = response['stream_metadata']
- try:
- schedule_file = open(self.schedule_file, "w")
- pickle.dump(schedule, schedule_file)
- schedule_file.close()
-
- tn = telnetlib.Telnet(LS_HOST, LS_PORT)
-
- #encode in latin-1 due to telnet protocol not supporting utf-8
- tn.write(('vars.stream_metadata_type %s\n' % stream_metadata['format']).encode('latin-1'))
- tn.write(('vars.station_name %s\n' % stream_metadata['station_name']).encode('latin-1'))
-
- tn.write('exit\n')
- logger.debug(tn.read_all())
-
- except Exception, e:
- logger.error("Exception %s", e)
- status = 0
-
- return status
-
- #TODO this is a duplicate function!!!
- def load_schedule(self):
- logger = logging.getLogger()
- schedule = None
-
- # create the file if it doesnt exist
- if (not os.path.exists(self.schedule_file)):
- logger.debug('creating file ' + self.schedule_file)
- open(self.schedule_file, 'w').close()
- else:
- # load the schedule from cache
- #logger.debug('loading schedule file '+self.schedule_file)
- try:
- schedule_file = open(self.schedule_file, "r")
- schedule = pickle.load(schedule_file)
- schedule_file.close()
-
- except Exception, e:
- logger.error('%s', e)
-
- return schedule
-
"""
- Alternative version of playout preparation. Every playlist entry is
- pre-cued if neccessary (cue_in/cue_out != 0) and stored in the
- playlist folder.
- file is eg 2010-06-23-15-00-00/17_cue_10.132-123.321.mp3
+ In this function every audio file is cut as necessary (cue_in/cue_out != 0)
+ and stored in a playlist folder.
+ file is e.g. 2010-06-23-15-00-00/17_cue_10.132-123.321.mp3
"""
- def prepare_playlists_cue(self):
- logger = logging.getLogger()
+ def prepare_playlists(self):
+ logger = logging.getLogger('fetch')
- # Load schedule from disk
- schedule = self.load_schedule()
+ schedule = self.schedule
+ playlists = dict()
# Dont do anything if schedule is empty
- if (not schedule):
+ if not schedule:
logger.debug("Schedule is empty.")
- return
+ return playlists
scheduleKeys = sorted(schedule.iterkeys())
try:
for pkey in scheduleKeys:
- logger.info("found playlist at %s", pkey)
+ logger.info("Playlist starting at %s", pkey)
playlist = schedule[pkey]
# create playlist directory
@@ -157,15 +162,15 @@ class PypoFetch:
except Exception, e:
pass
- logger.debug('*****************************************')
- logger.debug('pkey: ' + str(pkey))
- logger.debug('cached at : ' + self.cache_dir + str(pkey))
- logger.debug('subtype: ' + str(playlist['subtype']))
- logger.debug('played: ' + str(playlist['played']))
- logger.debug('schedule id: ' + str(playlist['schedule_id']))
- logger.debug('duration: ' + str(playlist['duration']))
- logger.debug('source id: ' + str(playlist['x_ident']))
- logger.debug('*****************************************')
+ #logger.debug('*****************************************')
+ #logger.debug('pkey: ' + str(pkey))
+ #logger.debug('cached at : ' + self.cache_dir + str(pkey))
+ #logger.debug('subtype: ' + str(playlist['subtype']))
+ #logger.debug('played: ' + str(playlist['played']))
+ #logger.debug('schedule id: ' + str(playlist['schedule_id']))
+ #logger.debug('duration: ' + str(playlist['duration']))
+ #logger.debug('source id: ' + str(playlist['x_ident']))
+ #logger.debug('*****************************************')
if int(playlist['played']) == 1:
logger.info("playlist %s already played / sent to liquidsoap, so will ignore it", pkey)
@@ -173,34 +178,32 @@ class PypoFetch:
elif int(playlist['subtype']) > 0 and int(playlist['subtype']) < 5:
ls_playlist = self.handle_media_file(playlist, pkey)
- # write playlist file
- plfile = open(self.cache_dir + str(pkey) + '/list.lsp', "w")
- plfile.write(json.dumps(ls_playlist))
- plfile.close()
- logger.info('ls playlist file written to %s', self.cache_dir + str(pkey) + '/list.lsp')
-
+ playlists[pkey] = ls_playlist
except Exception, e:
logger.info("%s", e)
+ return playlists
+
+ """
+ Download and cache the media files.
+ This handles both remote and local files.
+ Returns an updated ls_playlist string.
+ """
def handle_media_file(self, playlist, pkey):
- """
- This handles both remote and local files.
- Returns an updated ls_playlist string.
- """
ls_playlist = []
- logger = logging.getLogger()
+ logger = logging.getLogger('fetch')
for media in playlist['medias']:
logger.debug("Processing track %s", media['uri'])
fileExt = os.path.splitext(media['uri'])[1]
try:
if str(media['cue_in']) == '0' and str(media['cue_out']) == '0':
- logger.debug('No cue in/out detected for this file')
+ #logger.debug('No cue in/out detected for this file')
dst = "%s%s/%s%s" % (self.cache_dir, str(pkey), str(media['id']), str(fileExt))
do_cue = False
else:
- logger.debug('Cue in/out detected')
+ #logger.debug('Cue in/out detected')
dst = "%s%s/%s_cue_%s-%s%s" % \
(self.cache_dir, str(pkey), str(media['id']), str(float(media['cue_in']) / 1000), str(float(media['cue_out']) / 1000), str(fileExt))
do_cue = True
@@ -225,7 +228,7 @@ class PypoFetch:
% (str(media['export_source']), media['id'], 0, str(float(media['fade_in']) / 1000), \
str(float(media['fade_out']) / 1000), media['row_id'],dst)
- logger.debug(pl_entry)
+ #logger.debug(pl_entry)
"""
Tracks are only added to the playlist if they are accessible
@@ -239,7 +242,7 @@ class PypoFetch:
entry['show_name'] = playlist['show_name']
ls_playlist.append(entry)
- logger.debug("everything ok, adding %s to playlist", pl_entry)
+ #logger.debug("everything ok, adding %s to playlist", pl_entry)
else:
print 'zero-file: ' + dst + ' from ' + media['uri']
logger.warning("zero-size file - skipping %s. will not add it to playlist", dst)
@@ -251,11 +254,15 @@ class PypoFetch:
return ls_playlist
+ """
+ Download a file from a remote server and store it in the cache.
+ """
def handle_remote_file(self, media, dst, do_cue):
- logger = logging.getLogger()
+ logger = logging.getLogger('fetch')
if do_cue == False:
if os.path.isfile(dst):
- logger.debug("file already in cache: %s", dst)
+ pass
+ #logger.debug("file already in cache: %s", dst)
else:
logger.debug("try to download %s", media['uri'])
self.api_client.get_media(media['uri'], dst)
@@ -296,12 +303,12 @@ class PypoFetch:
logger.error("%s", e)
+ """
+ Cleans up folders in cache_dir. Look for modification date older than "now - CACHE_FOR"
+ and deletes them.
+ """
def cleanup(self, export_source):
- """
- Cleans up folders in cache_dir. Look for modification date older than "now - CACHE_FOR"
- and deletes them.
- """
- logger = logging.getLogger()
+ logger = logging.getLogger('fetch')
offset = 3600 * int(config["cache_for"])
now = time.time()
@@ -323,3 +330,41 @@ class PypoFetch:
print e
logger.error("%s", e)
+
+ """
+ Main loop of the thread:
+ Wait for schedule updates from RabbitMQ, but in case there arent any,
+ poll the server to get the upcoming schedule.
+ """
+ def run(self):
+ logger = logging.getLogger('fetch')
+
+ try: os.mkdir(self.cache_dir)
+ except Exception, e: pass
+
+ # Bootstrap: since we are just starting up, we need to grab the
+ # most recent schedule. After that we can just wait for updates.
+ status, schedule_data = self.api_client.get_schedule()
+ if status == 1:
+ self.process_schedule(schedule_data, "scheduler")
+ logger.info("Bootstrap complete: got initial copy of the schedule")
+
+ loops = 1
+ while True:
+ logger.info("Loop #"+str(loops))
+ try:
+ # Wait for messages from RabbitMQ. Timeout if we
+ # dont get any after POLL_INTERVAL.
+ self.connection.drain_events(timeout=POLL_INTERVAL)
+ # Hooray for globals!
+ schedule_data = SCHEDULE_PUSH_MSG
+ status = 1
+ except:
+ # We didnt get a message for a while, so poll the server
+ # to get an updated schedule.
+ status, schedule_data = self.api_client.get_schedule()
+
+ if status == 1:
+ self.process_schedule(schedule_data, "scheduler")
+ loops += 1
+
diff --git a/pypo/pypopush.py b/python_apps/pypo/pypopush.py
similarity index 63%
rename from pypo/pypopush.py
rename to python_apps/pypo/pypopush.py
index 266443b8f..25bfddbf1 100644
--- a/pypo/pypopush.py
+++ b/python_apps/pypo/pypopush.py
@@ -7,29 +7,38 @@ import pickle
import telnetlib
import calendar
import json
+import math
+from threading import Thread
from api_clients import api_client
from util import CueFile
from configobj import ConfigObj
+# configure logging
+logging.config.fileConfig("logging.cfg")
+
# loading config file
try:
config = ConfigObj('config.cfg')
- POLL_INTERVAL = float(config['poll_interval'])
- PUSH_INTERVAL = 0.5
- #PUSH_INTERVAL = float(config['push_interval'])
LS_HOST = config['ls_host']
LS_PORT = config['ls_port']
+ PUSH_INTERVAL = 2
except Exception, e:
- print 'Error loading config file: ', e
+ logger.error('Error loading config file %s', e)
sys.exit()
-class PypoPush:
- def __init__(self):
+class PypoPush(Thread):
+ def __init__(self, q):
+ Thread.__init__(self)
self.api_client = api_client.api_client_factory(config)
self.cue_file = CueFile()
self.set_export_source('scheduler')
+ self.queue = q
+
+ self.schedule = dict()
+ self.playlists = dict()
+ self.stream_metadata = dict()
"""
push_ahead2 MUST be < push_ahead. The difference in these two values
@@ -42,51 +51,58 @@ class PypoPush:
def set_export_source(self, export_source):
self.export_source = export_source
self.cache_dir = config["cache_dir"] + self.export_source + '/'
- self.schedule_file = self.cache_dir + 'schedule.pickle'
self.schedule_tracker_file = self.cache_dir + "schedule_tracker.pickle"
"""
- The Push Loop - the push loop periodically (minimal 1/2 of the playlist-grid)
- checks if there is a playlist that should be scheduled at the current time.
- If yes, the temporary liquidsoap playlist gets replaced with the corresponding one,
+ The Push Loop - the push loop periodically checks if there is a playlist
+ that should be scheduled at the current time.
+ If yes, the current liquidsoap playlist gets replaced with the corresponding one,
then liquidsoap is asked (via telnet) to reload and immediately play it.
"""
def push(self, export_source):
- logger = logging.getLogger()
+ logger = logging.getLogger('push')
- self.schedule = self.load_schedule()
- playedItems = self.load_schedule_tracker()
+ # get a new schedule from pypo-fetch
+ if not self.queue.empty():
+ scheduled_data = self.queue.get()
+ logger.debug("Received data from pypo-fetch")
+ self.schedule = scheduled_data['schedule']
+ self.playlists = scheduled_data['playlists']
+ self.stream_metadata = scheduled_data['stream_metadata']
- tcoming = time.localtime(time.time() + self.push_ahead)
- tcoming2 = time.localtime(time.time() + self.push_ahead2)
+ schedule = self.schedule
+ playlists = self.playlists
-
- str_tcoming_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tcoming[0], tcoming[1], tcoming[2], tcoming[3], tcoming[4], tcoming[5])
- str_tcoming2_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tcoming2[0], tcoming2[1], tcoming2[2], tcoming2[3], tcoming2[4], tcoming2[5])
-
currently_on_air = False
- if self.schedule == None:
- logger.warn('Unable to loop schedule - maybe write in progress?')
- logger.warn('Will try again in next loop.')
+ if schedule:
+ playedItems = self.load_schedule_tracker()
- else:
- for pkey in self.schedule:
+ timenow = time.time()
+ tcoming = time.localtime(timenow + self.push_ahead)
+ str_tcoming_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tcoming[0], tcoming[1], tcoming[2], tcoming[3], tcoming[4], tcoming[5])
+
+ tcoming2 = time.localtime(timenow + self.push_ahead2)
+ str_tcoming2_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tcoming2[0], tcoming2[1], tcoming2[2], tcoming2[3], tcoming2[4], tcoming2[5])
+
+ tnow = time.localtime(timenow)
+ str_tnow_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tnow[0], tnow[1], tnow[2], tnow[3], tnow[4], tnow[5])
+
+ for pkey in schedule:
plstart = pkey[0:19]
- start = self.schedule[pkey]['start']
- end = self.schedule[pkey]['end']
+ start = schedule[pkey]['start']
+ end = schedule[pkey]['end']
playedFlag = (pkey in playedItems) and playedItems[pkey].get("played", 0)
if plstart == str_tcoming_s or (plstart < str_tcoming_s and plstart > str_tcoming2_s and not playedFlag):
logger.debug('Preparing to push playlist scheduled at: %s', pkey)
- playlist = self.schedule[pkey]
+ playlist = schedule[pkey]
- ptype = playlist['subtype']
currently_on_air = True
# We have a match, replace the current playlist and
# force liquidsoap to refresh.
- if (self.push_liquidsoap(pkey, self.schedule, ptype) == 1):
+ if (self.push_liquidsoap(pkey, schedule, playlists) == 1):
logger.debug("Pushed to liquidsoap, updating 'played' status.")
# Marked the current playlist as 'played' in the schedule tracker
# so it is not called again in the next push loop.
@@ -100,39 +116,28 @@ class PypoPush:
# Call API to update schedule states
logger.debug("Doing callback to server to update 'played' status.")
- self.api_client.notify_scheduled_item_start_playing(pkey, self.schedule)
-
- if self.schedule != None:
- tnow = time.localtime(time.time())
- str_tnow_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tnow[0], tnow[1], tnow[2], tnow[3], tnow[4], tnow[5])
- for pkey in self.schedule:
- start = self.schedule[pkey]['start']
- end = self.schedule[pkey]['end']
+ self.api_client.notify_scheduled_item_start_playing(pkey, schedule)
+
+ start = schedule[pkey]['start']
+ end = schedule[pkey]['end']
if start <= str_tnow_s and str_tnow_s < end:
currently_on_air = True
-
+ else:
+ pass
+ #logger.debug('Empty schedule')
+
if not currently_on_air:
tn = telnetlib.Telnet(LS_HOST, LS_PORT)
- tn.write('source.skip\n'.encode('latin-1'))
+ tn.write('source.skip\n')
tn.write('exit\n')
tn.read_all()
- #logger.info('source.skip')
- #logger.debug(tn.read_all())
- def push_liquidsoap(self, pkey, schedule, ptype):
- logger = logging.getLogger()
- src = self.cache_dir + str(pkey) + '/list.lsp'
+ def push_liquidsoap(self, pkey, schedule, playlists):
+ logger = logging.getLogger('push')
try:
- if True == os.access(src, os.R_OK):
- logger.debug('OK - Can read playlist file')
-
- pl_file = open(src, "r")
- file_content = pl_file.read()
- pl_file.close()
- logger.debug('file content: %s' % (file_content))
- playlist = json.loads(file_content)
+ playlist = playlists[pkey]
#strptime returns struct_time in local time
#mktime takes a time_struct and returns a floating point
@@ -180,43 +185,24 @@ class PypoPush:
except Exception, e:
logger.error('%s', e)
status = 0
-
return status
-
- def load_schedule(self):
- logger = logging.getLogger()
- schedule = None
-
- # create the file if it doesnt exist
- if (not os.path.exists(self.schedule_file)):
- logger.debug('creating file ' + self.schedule_file)
- open(self.schedule_file, 'w').close()
- else:
- # load the schedule from cache
- #logger.debug('loading schedule file '+self.schedule_file)
- try:
- schedule_file = open(self.schedule_file, "r")
- schedule = pickle.load(schedule_file)
- schedule_file.close()
-
- except Exception, e:
- logger.error('%s', e)
-
- return schedule
-
-
def load_schedule_tracker(self):
- logger = logging.getLogger()
+ logger = logging.getLogger('push')
+ #logger.debug('load_schedule_tracker')
playedItems = dict()
# create the file if it doesnt exist
if (not os.path.exists(self.schedule_tracker_file)):
- logger.debug('creating file ' + self.schedule_tracker_file)
- schedule_tracker = open(self.schedule_tracker_file, 'w')
- pickle.dump(playedItems, schedule_tracker)
- schedule_tracker.close()
+ try:
+ logger.debug('creating file ' + self.schedule_tracker_file)
+ schedule_tracker = open(self.schedule_tracker_file, 'w')
+ pickle.dump(playedItems, schedule_tracker)
+ schedule_tracker.close()
+ except Exception, e:
+ logger.error('Error creating schedule tracker file: %s', e)
else:
+ #logger.debug('schedule tracker file exists, opening: ' + self.schedule_tracker_file)
try:
schedule_tracker = open(self.schedule_tracker_file, "r")
playedItems = pickle.load(schedule_tracker)
@@ -226,3 +212,18 @@ class PypoPush:
return playedItems
+ def run(self):
+ loops = 0
+ heartbeat_period = math.floor(30/PUSH_INTERVAL)
+ logger = logging.getLogger('push')
+
+ while True:
+ if loops % heartbeat_period == 0:
+ logger.info("heartbeat")
+ loops = 0
+ try: self.push('scheduler')
+ except Exception, e:
+ logger.error('Pypo Push Error, exiting: %s', e)
+ sys.exit()
+ time.sleep(PUSH_INTERVAL)
+ loops += 1
diff --git a/pypo/scripts/library/Makefile b/python_apps/pypo/scripts/library/Makefile
similarity index 100%
rename from pypo/scripts/library/Makefile
rename to python_apps/pypo/scripts/library/Makefile
diff --git a/pypo/scripts/library/ask-liquidsoap.pl b/python_apps/pypo/scripts/library/ask-liquidsoap.pl
similarity index 100%
rename from pypo/scripts/library/ask-liquidsoap.pl
rename to python_apps/pypo/scripts/library/ask-liquidsoap.pl
diff --git a/pypo/scripts/library/ask-liquidsoap.rb b/python_apps/pypo/scripts/library/ask-liquidsoap.rb
similarity index 100%
rename from pypo/scripts/library/ask-liquidsoap.rb
rename to python_apps/pypo/scripts/library/ask-liquidsoap.rb
diff --git a/pypo/scripts/library/external-todo.liq b/python_apps/pypo/scripts/library/external-todo.liq
similarity index 100%
rename from pypo/scripts/library/external-todo.liq
rename to python_apps/pypo/scripts/library/external-todo.liq
diff --git a/pypo/scripts/library/externals.liq b/python_apps/pypo/scripts/library/externals.liq
similarity index 100%
rename from pypo/scripts/library/externals.liq
rename to python_apps/pypo/scripts/library/externals.liq
diff --git a/pypo/scripts/library/extract-replaygain b/python_apps/pypo/scripts/library/extract-replaygain
similarity index 100%
rename from pypo/scripts/library/extract-replaygain
rename to python_apps/pypo/scripts/library/extract-replaygain
diff --git a/pypo/scripts/library/interactive.screen b/python_apps/pypo/scripts/library/interactive.screen
similarity index 100%
rename from pypo/scripts/library/interactive.screen
rename to python_apps/pypo/scripts/library/interactive.screen
diff --git a/pypo/scripts/library/lastfm.liq b/python_apps/pypo/scripts/library/lastfm.liq
similarity index 100%
rename from pypo/scripts/library/lastfm.liq
rename to python_apps/pypo/scripts/library/lastfm.liq
diff --git a/pypo/scripts/library/liquidsoap.gentoo.initd b/python_apps/pypo/scripts/library/liquidsoap.gentoo.initd
similarity index 100%
rename from pypo/scripts/library/liquidsoap.gentoo.initd
rename to python_apps/pypo/scripts/library/liquidsoap.gentoo.initd
diff --git a/pypo/scripts/library/liquidsoap.gentoo.initd.in b/python_apps/pypo/scripts/library/liquidsoap.gentoo.initd.in
similarity index 100%
rename from pypo/scripts/library/liquidsoap.gentoo.initd.in
rename to python_apps/pypo/scripts/library/liquidsoap.gentoo.initd.in
diff --git a/pypo/scripts/library/liquidsoap.initd b/python_apps/pypo/scripts/library/liquidsoap.initd
similarity index 100%
rename from pypo/scripts/library/liquidsoap.initd
rename to python_apps/pypo/scripts/library/liquidsoap.initd
diff --git a/pypo/scripts/library/liquidsoap.initd.in b/python_apps/pypo/scripts/library/liquidsoap.initd.in
similarity index 100%
rename from pypo/scripts/library/liquidsoap.initd.in
rename to python_apps/pypo/scripts/library/liquidsoap.initd.in
diff --git a/pypo/scripts/library/liquidsoap.logrotate b/python_apps/pypo/scripts/library/liquidsoap.logrotate
similarity index 100%
rename from pypo/scripts/library/liquidsoap.logrotate
rename to python_apps/pypo/scripts/library/liquidsoap.logrotate
diff --git a/pypo/scripts/library/liquidsoap.logrotate.in b/python_apps/pypo/scripts/library/liquidsoap.logrotate.in
similarity index 100%
rename from pypo/scripts/library/liquidsoap.logrotate.in
rename to python_apps/pypo/scripts/library/liquidsoap.logrotate.in
diff --git a/pypo/scripts/library/liquidtts b/python_apps/pypo/scripts/library/liquidtts
similarity index 100%
rename from pypo/scripts/library/liquidtts
rename to python_apps/pypo/scripts/library/liquidtts
diff --git a/pypo/scripts/library/liquidtts.in b/python_apps/pypo/scripts/library/liquidtts.in
similarity index 100%
rename from pypo/scripts/library/liquidtts.in
rename to python_apps/pypo/scripts/library/liquidtts.in
diff --git a/pypo/scripts/library/pervasives.liq b/python_apps/pypo/scripts/library/pervasives.liq
similarity index 100%
rename from pypo/scripts/library/pervasives.liq
rename to python_apps/pypo/scripts/library/pervasives.liq
diff --git a/pypo/scripts/library/shoutcast.liq b/python_apps/pypo/scripts/library/shoutcast.liq
similarity index 100%
rename from pypo/scripts/library/shoutcast.liq
rename to python_apps/pypo/scripts/library/shoutcast.liq
diff --git a/pypo/scripts/library/test.liq b/python_apps/pypo/scripts/library/test.liq
similarity index 100%
rename from pypo/scripts/library/test.liq
rename to python_apps/pypo/scripts/library/test.liq
diff --git a/pypo/scripts/library/typing.liq b/python_apps/pypo/scripts/library/typing.liq
similarity index 100%
rename from pypo/scripts/library/typing.liq
rename to python_apps/pypo/scripts/library/typing.liq
diff --git a/pypo/scripts/library/utils.liq b/python_apps/pypo/scripts/library/utils.liq
similarity index 100%
rename from pypo/scripts/library/utils.liq
rename to python_apps/pypo/scripts/library/utils.liq
diff --git a/pypo/scripts/ls_config.liq b/python_apps/pypo/scripts/ls_config.liq
similarity index 97%
rename from pypo/scripts/ls_config.liq
rename to python_apps/pypo/scripts/ls_config.liq
index ef1caa198..20b1f4215 100644
--- a/pypo/scripts/ls_config.liq
+++ b/python_apps/pypo/scripts/ls_config.liq
@@ -44,5 +44,5 @@ icecast_description = "Airtime Radio!"
icecast_genre = "genre"
output_sound_device = false
-output_icecast_vorbis = true
+output_icecast_vorbis = false
output_icecast_mp3 = true
diff --git a/pypo/scripts/ls_lib.liq b/python_apps/pypo/scripts/ls_lib.liq
similarity index 80%
rename from pypo/scripts/ls_lib.liq
rename to python_apps/pypo/scripts/ls_lib.liq
index 27ed94f06..5bd611878 100644
--- a/pypo/scripts/ls_lib.liq
+++ b/python_apps/pypo/scripts/ls_lib.liq
@@ -1,14 +1,14 @@
def notify(m)
- system("./notify.sh --data='#{!pypo_data}' --media-id=#{m['schedule_table_id']}")
- print("./notify.sh --data='#{!pypo_data}' --media-id=#{m['schedule_table_id']}")
+ system("/opt/pypo/bin/scripts/notify.sh --data='#{!pypo_data}' --media-id=#{m['schedule_table_id']}")
+ print("/opt/pypo/bin/scripts/notify.sh --data='#{!pypo_data}' --media-id=#{m['schedule_table_id']}")
end
# A function applied to each metadata chunk
def append_title(m) =
if !stream_metadata_type == 1 then
- [("artist","#{!show_name} - #{m['title']}")]
- #elsif !stream_metadata_type == 2 then
- # [("artist", ""), ("title", !show_name)]
+ [("artist","#{!show_name} - #{m['artist']}")]
+ #####elsif !stream_metadata_type == 2 then
+ ##### [("artist", ""), ("title", !show_name)]
elsif !stream_metadata_type == 2 then
[("artist",!station_name), ("title", !show_name)]
else
diff --git a/pypo/scripts/ls_script.liq b/python_apps/pypo/scripts/ls_script.liq
similarity index 90%
rename from pypo/scripts/ls_script.liq
rename to python_apps/pypo/scripts/ls_script.liq
index a35495839..5fec857ea 100644
--- a/pypo/scripts/ls_script.liq
+++ b/python_apps/pypo/scripts/ls_script.liq
@@ -24,7 +24,7 @@ server.register(namespace="vars", "show_name", fun (s) -> begin show_name := s s
server.register(namespace="vars", "station_name", fun (s) -> begin station_name := s s end)
-default = single(conservative=true, "/opt/pypo/files/basic/silence.mp3")
+default = amplify(0.00001, noise())
default = rewrite_metadata([("artist","Airtime"), ("title", "offline")],default)
s = fallback(track_sensitive=false, [queue, default])
@@ -69,6 +69,9 @@ if output_icecast_mp3 then
end
if output_icecast_vorbis then
+ #remove metadata from ogg source and merge tracks to fix bug
+ #with vlc and mplayer disconnecting at the end of every track
+ ogg_s = add(normalize=false, [amplify(0.00001, noise()),s])
out_vorbis = output.icecast(%vorbis,
host = icecast_host, port = icecast_port,
password = icecast_pass, mount = mount_point_vorbis,
@@ -78,5 +81,5 @@ if output_icecast_vorbis then
url = icecast_url,
description = icecast_description,
genre = icecast_genre,
- s)
+ ogg_s)
end
diff --git a/pypo/scripts/notify.sh b/python_apps/pypo/scripts/notify.sh
similarity index 55%
rename from pypo/scripts/notify.sh
rename to python_apps/pypo/scripts/notify.sh
index 6298f13dd..07a70f5d0 100755
--- a/pypo/scripts/notify.sh
+++ b/python_apps/pypo/scripts/notify.sh
@@ -4,4 +4,10 @@
# needed here to keep dirs/configs clean #
# and maybe to set user-rights #
############################################
-cd ../ && ./pypo-notify.py $1 $2 $3 $4 $5 $6 $7 $8 &
+
+# Absolute path to this script
+SCRIPT=`readlink -f $0`
+# Absolute path this script is in
+SCRIPTPATH=`dirname $SCRIPT`
+
+cd ${SCRIPTPATH}/../ && ./pypo-notify.py $1 $2 $3 $4 $5 $6 $7 $8 &
diff --git a/pypo/scripts/old_files/README b/python_apps/pypo/scripts/old_files/README
similarity index 100%
rename from pypo/scripts/old_files/README
rename to python_apps/pypo/scripts/old_files/README
diff --git a/pypo/scripts/old_files/cue_file.py b/python_apps/pypo/scripts/old_files/cue_file.py
similarity index 100%
rename from pypo/scripts/old_files/cue_file.py
rename to python_apps/pypo/scripts/old_files/cue_file.py
diff --git a/pypo/scripts/old_files/include_daypart.liq b/python_apps/pypo/scripts/old_files/include_daypart.liq
similarity index 100%
rename from pypo/scripts/old_files/include_daypart.liq
rename to python_apps/pypo/scripts/old_files/include_daypart.liq
diff --git a/pypo/scripts/old_files/include_dynamic_vars.liq b/python_apps/pypo/scripts/old_files/include_dynamic_vars.liq
similarity index 100%
rename from pypo/scripts/old_files/include_dynamic_vars.liq
rename to python_apps/pypo/scripts/old_files/include_dynamic_vars.liq
diff --git a/pypo/scripts/old_files/include_live_in.liq b/python_apps/pypo/scripts/old_files/include_live_in.liq
similarity index 100%
rename from pypo/scripts/old_files/include_live_in.liq
rename to python_apps/pypo/scripts/old_files/include_live_in.liq
diff --git a/pypo/scripts/old_files/include_notify.liq b/python_apps/pypo/scripts/old_files/include_notify.liq
similarity index 100%
rename from pypo/scripts/old_files/include_notify.liq
rename to python_apps/pypo/scripts/old_files/include_notify.liq
diff --git a/pypo/scripts/old_files/include_scheduler.liq b/python_apps/pypo/scripts/old_files/include_scheduler.liq
similarity index 100%
rename from pypo/scripts/old_files/include_scheduler.liq
rename to python_apps/pypo/scripts/old_files/include_scheduler.liq
diff --git a/pypo/scripts/old_files/library.liq b/python_apps/pypo/scripts/old_files/library.liq
similarity index 100%
rename from pypo/scripts/old_files/library.liq
rename to python_apps/pypo/scripts/old_files/library.liq
diff --git a/pypo/scripts/old_files/log_run.sh b/python_apps/pypo/scripts/old_files/log_run.sh
similarity index 100%
rename from pypo/scripts/old_files/log_run.sh
rename to python_apps/pypo/scripts/old_files/log_run.sh
diff --git a/pypo/scripts/old_files/ls_config.liq.dist b/python_apps/pypo/scripts/old_files/ls_config.liq.dist
similarity index 100%
rename from pypo/scripts/old_files/ls_config.liq.dist
rename to python_apps/pypo/scripts/old_files/ls_config.liq.dist
diff --git a/pypo/scripts/old_files/ls_cue.liq b/python_apps/pypo/scripts/old_files/ls_cue.liq
similarity index 100%
rename from pypo/scripts/old_files/ls_cue.liq
rename to python_apps/pypo/scripts/old_files/ls_cue.liq
diff --git a/pypo/scripts/old_files/ls_run.sh b/python_apps/pypo/scripts/old_files/ls_run.sh
similarity index 100%
rename from pypo/scripts/old_files/ls_run.sh
rename to python_apps/pypo/scripts/old_files/ls_run.sh
diff --git a/pypo/scripts/old_files/ls_script.liq b/python_apps/pypo/scripts/old_files/ls_script.liq
similarity index 100%
rename from pypo/scripts/old_files/ls_script.liq
rename to python_apps/pypo/scripts/old_files/ls_script.liq
diff --git a/pypo/scripts/old_files/silence-playlist.lsp b/python_apps/pypo/scripts/old_files/silence-playlist.lsp
similarity index 100%
rename from pypo/scripts/old_files/silence-playlist.lsp
rename to python_apps/pypo/scripts/old_files/silence-playlist.lsp
diff --git a/pypo/test/airtime-schedule-insert.php b/python_apps/pypo/test/airtime-schedule-insert.php
similarity index 98%
rename from pypo/test/airtime-schedule-insert.php
rename to python_apps/pypo/test/airtime-schedule-insert.php
index 6d3e37951..bc229dbd4 100644
--- a/pypo/test/airtime-schedule-insert.php
+++ b/python_apps/pypo/test/airtime-schedule-insert.php
@@ -79,7 +79,7 @@ $endTime = date("Y-m-d H:i:s", time()+(60*60));
echo "Removing everything from the scheduler between $startTime and $endTime...";
// Scheduler: remove any playlists for the next hour
-Schedule::RemoveItemsInRange($startTime, $endTime);
+//Schedule::RemoveItemsInRange($startTime, $endTime);
// Check for succcess
$scheduleClear = Schedule::isScheduleEmptyInRange($startTime, "01:00:00");
if (!$scheduleClear) {
diff --git a/pypo/util/__init__.py b/python_apps/pypo/util/__init__.py
similarity index 100%
rename from pypo/util/__init__.py
rename to python_apps/pypo/util/__init__.py
diff --git a/pypo/util/cue_file.py b/python_apps/pypo/util/cue_file.py
similarity index 97%
rename from pypo/util/cue_file.py
rename to python_apps/pypo/util/cue_file.py
index 0d5f6cab5..8b3a158be 100755
--- a/pypo/util/cue_file.py
+++ b/python_apps/pypo/util/cue_file.py
@@ -64,7 +64,7 @@ class CueFile():
print command
os.system(command + ' > /dev/null 2>&1')
- command = 'lame -b 32 %s %s' % (dst + '.tmp.mp3', dst);
+ command = 'lame -b 128 %s %s' % (dst + '.tmp.mp3', dst);
logger.info("command: %s", command)
print command
os.system(command + ' > /dev/null 2>&1')
diff --git a/pypo/util/status.py b/python_apps/pypo/util/status.py
similarity index 100%
rename from pypo/util/status.py
rename to python_apps/pypo/util/status.py
diff --git a/python_apps/show-recorder/config.cfg b/python_apps/show-recorder/config.cfg
index 274d9d786..ec812b8cf 100644
--- a/python_apps/show-recorder/config.cfg
+++ b/python_apps/show-recorder/config.cfg
@@ -1,9 +1,22 @@
+api_client = "airtime"
+
# Hostname
-base_url = 'http://campcaster.dev/'
-
-show_schedule_url = 'Recorder/get-show-schedule/format/json'
-
-upload_file_url = 'Plupload/upload-recorded/format/json'
+base_url = 'http://localhost/'
# base path to store recordered shows at
-base_recorded_files = '/home/naomi/Music/'
+base_recorded_files = '/home/pypo/Music/'
+
+# Value needed to access the API
+api_key = 'AAA'
+
+# Path to the base of the API
+api_base = 'api/'
+
+# URL to get the version number of the server API
+version_url = 'version/api_key/%%api_key%%'
+
+# URL to get the schedule of shows set to record
+show_schedule_url = 'recorded-shows/format/json/api_key/%%api_key%%'
+
+# URL to upload the recorded show's file to Airtime
+upload_file_url = 'upload-recorded/format/json/api_key/%%api_key%%'
diff --git a/python_apps/show-recorder/install/recorder-daemontools-logger.sh b/python_apps/show-recorder/install/recorder-daemontools-logger.sh
new file mode 100644
index 000000000..9673575db
--- /dev/null
+++ b/python_apps/show-recorder/install/recorder-daemontools-logger.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec setuidgid pypo multilog t ./main
diff --git a/python_apps/show-recorder/install/recorder-daemontools.sh b/python_apps/show-recorder/install/recorder-daemontools.sh
new file mode 100644
index 000000000..c1e0df863
--- /dev/null
+++ b/python_apps/show-recorder/install/recorder-daemontools.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+recorder_user="pypo"
+export HOME="/home/pypo/"
+export TERM=xterm
+
+# Location of pypo_cli.py Python script
+recorder_path="/opt/recorder/bin/"
+recorder_script="testrecordscript.py"
+
+api_client_path="/opt/pypo/"
+cd ${recorder_path}
+
+echo "*** Daemontools: starting daemon"
+exec 2>&1
+# Note the -u when calling python! we need it to get unbuffered binary stdout and stderr
+
+sudo PYTHONPATH=${api_client_path} -u ${recorder_user} python -u ${recorder_path}${recorder_script}
+# EOF
diff --git a/python_apps/show-recorder/install/recorder-install.py b/python_apps/show-recorder/install/recorder-install.py
new file mode 100644
index 000000000..1b8186391
--- /dev/null
+++ b/python_apps/show-recorder/install/recorder-install.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import time
+import os
+import traceback
+from optparse import *
+import sys
+import time
+import datetime
+import logging
+import logging.config
+import shutil
+import string
+import platform
+from subprocess import Popen, PIPE, STDOUT
+
+if os.geteuid() != 0:
+ print "Please run this as root."
+ sys.exit(1)
+
+BASE_PATH = '/opt/recorder/'
+
+def create_path(path):
+ if not (os.path.exists(path)):
+ print "Creating directory " + path
+ os.makedirs(path)
+
+def create_user(username):
+ print "Checking for user "+username
+ p = Popen('id '+username, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
+ output = p.stdout.read()
+ if (output[0:3] != "uid"):
+ # Make the pypo user
+ print "Creating user "+username
+ os.system("adduser --system --quiet --group --shell /bin/bash "+username)
+
+ #set pypo password
+ p = os.popen('/usr/bin/passwd pypo 1>/dev/null 2>&1', 'w')
+ p.write('pypo\n')
+ p.write('pypo\n')
+ p.close()
+ else:
+ print "User already exists."
+ #add pypo to audio group
+ os.system("adduser " + username + " audio 1>/dev/null 2>&1")
+
+def copy_dir(src_dir, dest_dir):
+ if (os.path.exists(dest_dir)) and (dest_dir != "/"):
+ print "Removing old directory "+dest_dir
+ shutil.rmtree(dest_dir)
+ if not (os.path.exists(dest_dir)):
+ print "Copying directory "+src_dir+" to "+dest_dir
+ shutil.copytree(src_dir, dest_dir)
+
+def get_current_script_dir():
+ current_script_dir = os.path.realpath(__file__)
+ index = current_script_dir.rindex('/')
+ print current_script_dir[0:index]
+ return current_script_dir[0:index]
+
+
+try:
+ current_script_dir = get_current_script_dir()
+ print "Checking and removing any existing recorder processes"
+ os.system("python %s/recorder-uninstall.py 1>/dev/null 2>&1"% current_script_dir)
+ time.sleep(5)
+
+ # Create users
+ create_user("pypo")
+
+ print "Creating home directory"
+ create_path("/home/pypo")
+ os.system("chmod -R 755 /home/pypo")
+ os.system("chown -R pypo:pypo /home/pypo")
+
+ print "Creating home directory"
+ create_path("/home/pypo/Music")
+ os.system("chmod -R 755 /home/pypo/Music")
+ os.system("chown -R pypo:pypo /home/pypo/Music")
+
+ print "Creating log directories"
+ create_path("/var/log/recorder")
+ os.system("chmod -R 755 /var/log/recorder")
+ os.system("chown -R pypo:pypo /var/log/recorder")
+
+ create_path(BASE_PATH)
+ create_path(BASE_PATH+"bin")
+ create_path(BASE_PATH+"cache")
+ create_path(BASE_PATH+"files")
+ create_path(BASE_PATH+"tmp")
+ create_path(BASE_PATH+"archive")
+
+ copy_dir("%s/.."%current_script_dir, BASE_PATH+"bin/")
+
+ print "Setting permissions"
+ os.system("chmod -R 755 "+BASE_PATH)
+ os.system("chown -R pypo:pypo "+BASE_PATH)
+
+ print "Installing recorder daemon"
+ create_path("/etc/service/recorder")
+ create_path("/etc/service/recorder/log")
+ shutil.copy("%s/recorder-daemontools.sh"%current_script_dir, "/etc/service/recorder/run")
+ shutil.copy("%s/recorder-daemontools-logger.sh"%current_script_dir, "/etc/service/recorder/log/run")
+ os.system("chmod -R 755 /etc/service/recorder")
+ os.system("chown -R pypo:pypo /etc/service/recorder")
+
+ print "Waiting for processes to start..."
+ time.sleep(5)
+ os.system("python %s/recorder-start.py" % (get_current_script_dir()))
+ time.sleep(2)
+
+ found = True
+
+ p = Popen('svstat /etc/service/recorder', shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
+ output = p.stdout.read()
+ if (output.find("unable to open supervise/ok: file does not exist") >= 0):
+ found = False
+ print output
+
+ if not found:
+ print "Recorder install has completed, but daemontools is not running, please make sure you have it installed and then reboot."
+except Exception, e:
+ print "exception:" + str(e)
+ sys.exit(1)
+
+
+
diff --git a/python_apps/show-recorder/install/recorder-start.py b/python_apps/show-recorder/install/recorder-start.py
new file mode 100644
index 000000000..084b2b1ad
--- /dev/null
+++ b/python_apps/show-recorder/install/recorder-start.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+
+if os.geteuid() != 0:
+ print "Please run this as root."
+ sys.exit(1)
+
+try:
+ print "Starting daemontool script recorder"
+ os.system("svc -u /etc/service/recorder")
+
+except Exception, e:
+ print "exception:" + str(e)
diff --git a/python_apps/show-recorder/install/recorder-stop.py b/python_apps/show-recorder/install/recorder-stop.py
new file mode 100644
index 000000000..c8eb45562
--- /dev/null
+++ b/python_apps/show-recorder/install/recorder-stop.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+
+if os.geteuid() != 0:
+ print "Please run this as root."
+ sys.exit(1)
+
+try:
+ print "Stopping daemontool script recorder"
+ os.system("svc -dx /etc/service/recorder 1>/dev/null 2>&1")
+
+except Exception, e:
+ print "exception:" + str(e)
diff --git a/python_apps/show-recorder/install/recorder-uninstall.py b/python_apps/show-recorder/install/recorder-uninstall.py
new file mode 100644
index 000000000..f8ab96432
--- /dev/null
+++ b/python_apps/show-recorder/install/recorder-uninstall.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+import time
+
+if os.geteuid() != 0:
+ print "Please run this as root."
+ sys.exit(1)
+
+BASE_PATH = '/opt/recorder/'
+
+def remove_path(path):
+ os.system("rm -rf " + path)
+
+def remove_user(username):
+ os.system("killall -u %s 1>/dev/null 2>&1" % username)
+
+ #allow all process to be completely closed before we attempt to delete user
+ print "Waiting for processes to close..."
+ time.sleep(5)
+
+ os.system("deluser --remove-home " + username + " 1>/dev/null 2>&1")
+
+def get_current_script_dir():
+ current_script_dir = os.path.realpath(__file__)
+ index = current_script_dir.rindex('/')
+ return current_script_dir[0:index]
+
+try:
+ os.system("python %s/recorder-stop.py" % get_current_script_dir())
+
+ print "Removing log directories"
+ remove_path("/var/log/recorder")
+
+ print "Removing recorder files"
+ remove_path(BASE_PATH)
+
+ print "Removing daemontool script recorder"
+ remove_path("rm -rf /etc/service/recorder")
+
+ remove_user("pypo")
+ print "Uninstall complete."
+except Exception, e:
+ print "exception:" + str(e)
diff --git a/python_apps/show-recorder/logging.cfg b/python_apps/show-recorder/logging.cfg
new file mode 100644
index 000000000..251fce8d7
--- /dev/null
+++ b/python_apps/show-recorder/logging.cfg
@@ -0,0 +1,22 @@
+[loggers]
+keys=root
+
+[handlers]
+keys=consoleHandler
+
+[formatters]
+keys=simpleFormatter
+
+[logger_root]
+level=DEBUG
+handlers=consoleHandler
+
+[handler_consoleHandler]
+class=StreamHandler
+level=DEBUG
+formatter=simpleFormatter
+args=(sys.stdout,)
+
+[formatter_simpleFormatter]
+format=%(asctime)s %(levelname)s - [%(filename)s : %(funcName)s() : line %(lineno)d] - %(message)s
+datefmt=
diff --git a/python_apps/show-recorder/testrecordscript.py b/python_apps/show-recorder/testrecordscript.py
index b5cda7409..c7fb2691d 100644
--- a/python_apps/show-recorder/testrecordscript.py
+++ b/python_apps/show-recorder/testrecordscript.py
@@ -1,10 +1,12 @@
#!/usr/local/bin/python
import urllib
import logging
+import logging.config
import json
import time
import datetime
import os
+import sys
from configobj import ConfigObj
@@ -13,6 +15,20 @@ from poster.streaminghttp import register_openers
import urllib2
from subprocess import call
+from threading import Thread
+
+# For RabbitMQ
+from kombu.connection import BrokerConnection
+from kombu.messaging import Exchange, Queue, Consumer, Producer
+
+from api_clients import api_client
+
+# configure logging
+try:
+ logging.config.fileConfig("logging.cfg")
+except Exception, e:
+ print 'Error configuring logging: ', e
+ sys.exit()
# loading config file
try:
@@ -21,95 +37,127 @@ except Exception, e:
print 'Error loading config file: ', e
sys.exit()
-shows_to_record = {}
-
-
-def record_show(filelength, filename, filetype="mp3"):
-
- length = str(filelength)+".0"
- filename = filename.replace(" ", "-")
- filepath = "%s%s.%s" % (config["base_recorded_files"], filename, filetype)
-
- command = "ecasound -i alsa -o %s -t:%s" % (filepath, filelength)
-
- call(command, shell=True)
-
- return filepath
-
-
def getDateTimeObj(time):
timeinfo = time.split(" ")
date = timeinfo[0].split("-")
time = timeinfo[1].split(":")
- return datetime.datetime(int(date[0]), int(date[1]), int(date[2]), int(time[0]), int(time[1]), int(time[2]))
+ return datetime.datetime(int(date[0]), int(date[1]), int(date[2]), int(time[0]), int(time[1]), int(time[2]))
-def process_shows(shows):
-
- for show in shows:
- show_starts = getDateTimeObj(show[u'starts'])
- show_end = getDateTimeObj(show[u'ends'])
- time_delta = show_end - show_starts
+class ShowRecorder(Thread):
+
+ def __init__ (self, show_instance, filelength, filename, filetype):
+ Thread.__init__(self)
+ self.api_client = api_client.api_client_factory(config)
+ self.filelength = filelength
+ self.filename = filename
+ self.filetype = filetype
+ self.show_instance = show_instance
+
+ def record_show(self):
+
+ length = str(self.filelength)+".0"
+ filename = self.filename.replace(" ", "-")
+ filepath = "%s%s.%s" % (config["base_recorded_files"], filename, self.filetype)
+
+ command = "ecasound -i alsa -o %s -t:%s" % (filepath, length)
+ args = command.split(" ")
+
+ print "starting record"
+
+ code = call(args)
+
+ print "finishing record, return code %s" % (code)
+
+ return code, filepath
+
+ def upload_file(self, filepath):
+
+ filename = os.path.split(filepath)[1]
+
+ # Register the streaming http handlers with urllib2
+ register_openers()
+
+ # headers contains the necessary Content-Type and Content-Length
+ # datagen is a generator object that yields the encoded parameters
+ datagen, headers = multipart_encode({"file": open(filepath, "rb"), 'name': filename, 'show_instance': self.show_instance})
+
+ self.api_client.upload_recorded_show(datagen, headers)
+
+ def run(self):
+ code, filepath = self.record_show()
+
+ if code == 0:
+ self.upload_file(filepath)
+ else:
+ print "problem recording show"
+
+
+class Record():
+
+ def __init__(self):
+ self.api_client = api_client.api_client_factory(config)
+ self.shows_to_record = {}
+
+ def process_shows(self, shows):
+
+ self.shows_to_record = {}
- shows_to_record[show[u'starts']] = time_delta
+ for show in shows:
+ show_starts = getDateTimeObj(show[u'starts'])
+ show_end = getDateTimeObj(show[u'ends'])
+ time_delta = show_end - show_starts
+
+ self.shows_to_record[show[u'starts']] = [time_delta, show[u'instance_id'], show[u'name']]
-def check_record():
-
- tnow = datetime.datetime.now()
- sorted_show_keys = sorted(shows_to_record.keys())
- start_time = sorted_show_keys[0]
- next_show = getDateTimeObj(start_time)
+ def check_record(self):
+
+ tnow = datetime.datetime.now()
+ sorted_show_keys = sorted(self.shows_to_record.keys())
+
+ start_time = sorted_show_keys[0]
+ next_show = getDateTimeObj(start_time)
- delta = next_show - tnow
+ print next_show
+ print tnow
- if delta <= datetime.timedelta(seconds=60):
- time.sleep(delta.seconds)
-
- show_length = shows_to_record[start_time]
- filepath = record_show(show_length.seconds, start_time)
- upload_file(filepath)
-
+ delta = next_show - tnow
+ min_delta = datetime.timedelta(seconds=60)
-def get_shows():
+ if delta <= min_delta:
+ print "sleeping %s seconds until show" % (delta.seconds)
+ time.sleep(delta.seconds)
+
+ show_length = self.shows_to_record[start_time][0]
+ show_instance = self.shows_to_record[start_time][1]
+ show_name = self.shows_to_record[start_time][2]
+ filename = show_name+"-"+start_time
- url = config["base_url"] + config["show_schedule_url"]
- response = urllib.urlopen(url)
- data = response.read()
- print data
+ show = ShowRecorder(show_instance, show_length.seconds, filename, filetype="mp3")
+ show.start()
+
+ #remove show from shows to record.
+ del self.shows_to_record[start_time]
+
- response_json = json.loads(data)
- shows = response_json[u'shows']
- print shows
+ def get_shows(self):
- if len(shows):
- process_shows(shows)
- check_record()
+ shows = self.api_client.get_shows_to_record()
-def upload_file(filepath):
-
- filename = os.path.split(filepath)[1]
-
- # Register the streaming http handlers with urllib2
- register_openers()
-
- # headers contains the necessary Content-Type and Content-Length
- # datagen is a generator object that yields the encoded parameters
- datagen, headers = multipart_encode({"file": open(filepath, "rb"), 'name': filename})
-
- url = config["base_url"] + config["upload_file_url"]
-
- req = urllib2.Request(url, datagen, headers)
- response = urllib2.urlopen(req).read().strip()
- print response
+ if len(shows):
+ self.process_shows(shows)
+ self.check_record()
if __name__ == '__main__':
+ recorder = Record()
+
while True:
- get_shows()
- time.sleep(30)
+ recorder.get_shows()
+ time.sleep(5)
diff --git a/python_apps/show-recorder/testsoundcloud.py b/python_apps/show-recorder/testsoundcloud.py
deleted file mode 100644
index 0ba6fd20d..000000000
--- a/python_apps/show-recorder/testsoundcloud.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import webbrowser
-import scapi
-
-# the host to connect to. Normally, this
-# would be api.soundcloud.com
-API_HOST = "api.soundcloud.com"
-
-# This needs to be the consumer ID you got from
-# http://soundcloud.com/settings/applications/new
-CONSUMER = "2CLCxcSXYzx7QhhPVHN4A"
-# This needs to be the consumer secret password you got from
-# http://soundcloud.com/settings/applications/new
-CONSUMER_SECRET = "pZ7beWmF06epXLHVUP1ufOg2oEnIt9XhE8l8xt0bBs"
-
-# first, we create an OAuthAuthenticator that only knows about consumer
-# credentials. This is done so that we can get an request-token as
-# first step.
-oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER,
- CONSUMER_SECRET,
- None,
- None)
-
-# The connector works with the authenticator to create and sign the requests. It
-# has some helper-methods that allow us to do the OAuth-dance.
-connector = scapi.ApiConnector(host=API_HOST, authenticator=oauth_authenticator)
-
-# First step is to get a request-token, and to let the user authorize that
-# via the browser.
-token, secret = connector.fetch_request_token()
-authorization_url = connector.get_request_token_authorization_url(token)
-webbrowser.open(authorization_url)
-oauth_verifier = raw_input("please enter verifier code as seen in the browser:")
-
-# Now we create a new authenticator with the temporary token & secret we got from
-# the request-token. This will give us the access-token
-oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER,
- CONSUMER_SECRET,
- token,
- secret)
-
-# we need a new connector with the new authenticator!
-connector = scapi.ApiConnector(API_HOST, authenticator=oauth_authenticator)
-token, secret = connector.fetch_access_token(oauth_verifier)
-
-
-# now we are finally ready to go - with all four parameters OAuth requires,
-# we can setup an authenticator that allows for actual API-calls.
-oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER,
- CONSUMER_SECRET,
- token,
- secret)
-
-# we pass the connector to a Scope - a Scope is essentially a path in the REST-url-space.
-# Without any path-component, it's the root from which we can then query into the
-# resources.
-root = scapi.Scope(scapi.ApiConnector(host=API_HOST, authenticator=oauth_authenticator))
-
-# Hey, nice meeting you! Connected to SoundCloud using OAuth will allow you to access protected resources, like the current user's name.
-print "Hello, %s" % root.me().username
diff --git a/python_apps/soundcloud-api/AUTHORS b/python_apps/soundcloud-api/AUTHORS
deleted file mode 100644
index ae92c1d26..000000000
--- a/python_apps/soundcloud-api/AUTHORS
+++ /dev/null
@@ -1,5 +0,0 @@
-Authors
--------
-
-Diez B. Roggisch, deets@web.de
-
diff --git a/python_apps/soundcloud-api/ChangeLog b/python_apps/soundcloud-api/ChangeLog
deleted file mode 100644
index 9b5bb4679..000000000
--- a/python_apps/soundcloud-api/ChangeLog
+++ /dev/null
@@ -1,9 +0,0 @@
-2009-09-10 Diez Roggisch
-
- * OAuth 1.0a working
- * Query-Parameters for GET-requests to allow e.g. filtering
- * Setting file-objects as attributes working.
- * share to emails working.
- * groups
- * downloading/streaming private tracks
-
diff --git a/python_apps/soundcloud-api/LICENSE b/python_apps/soundcloud-api/LICENSE
deleted file mode 100644
index 3b473dbfc..000000000
--- a/python_apps/soundcloud-api/LICENSE
+++ /dev/null
@@ -1,458 +0,0 @@
- GNU LESSER GENERAL PUBLIC LICENSE
- Version 2.1, February 1999
-
- Copyright (C) 1991, 1999 Free Software Foundation, Inc.
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-[This is the first released version of the Lesser GPL. It also counts
- as the successor of the GNU Library Public License, version 2, hence
- the version number 2.1.]
-
- Preamble
-
- The licenses for most software are designed to take away your
-freedom to share and change it. By contrast, the GNU General Public
-Licenses are intended to guarantee your freedom to share and change
-free software--to make sure the software is free for all its users.
-
- This license, the Lesser General Public License, applies to some
-specially designated software packages--typically libraries--of the
-Free Software Foundation and other authors who decide to use it. You
-can use it too, but we suggest you first think carefully about whether
-this license or the ordinary General Public License is the better
-strategy to use in any particular case, based on the explanations below.
-
- When we speak of free software, we are referring to freedom of use,
-not price. Our General Public Licenses are designed to make sure that
-you have the freedom to distribute copies of free software (and charge
-for this service if you wish); that you receive source code or can get
-it if you want it; that you can change the software and use pieces of
-it in new free programs; and that you are informed that you can do
-these things.
-
- To protect your rights, we need to make restrictions that forbid
-distributors to deny you these rights or to ask you to surrender these
-rights. These restrictions translate to certain responsibilities for
-you if you distribute copies of the library or if you modify it.
-
- For example, if you distribute copies of the library, whether gratis
-or for a fee, you must give the recipients all the rights that we gave
-you. You must make sure that they, too, receive or can get the source
-code. If you link other code with the library, you must provide
-complete object files to the recipients, so that they can relink them
-with the library after making changes to the library and recompiling
-it. And you must show them these terms so they know their rights.
-
- We protect your rights with a two-step method: (1) we copyright the
-library, and (2) we offer you this license, which gives you legal
-permission to copy, distribute and/or modify the library.
-
- To protect each distributor, we want to make it very clear that
-there is no warranty for the free library. Also, if the library is
-modified by someone else and passed on, the recipients should know
-that what they have is not the original version, so that the original
-author's reputation will not be affected by problems that might be
-introduced by others.
-
- Finally, software patents pose a constant threat to the existence of
-any free program. We wish to make sure that a company cannot
-effectively restrict the users of a free program by obtaining a
-restrictive license from a patent holder. Therefore, we insist that
-any patent license obtained for a version of the library must be
-consistent with the full freedom of use specified in this license.
-
- Most GNU software, including some libraries, is covered by the
-ordinary GNU General Public License. This license, the GNU Lesser
-General Public License, applies to certain designated libraries, and
-is quite different from the ordinary General Public License. We use
-this license for certain libraries in order to permit linking those
-libraries into non-free programs.
-
- When a program is linked with a library, whether statically or using
-a shared library, the combination of the two is legally speaking a
-combined work, a derivative of the original library. The ordinary
-General Public License therefore permits such linking only if the
-entire combination fits its criteria of freedom. The Lesser General
-Public License permits more lax criteria for linking other code with
-the library.
-
- We call this license the "Lesser" General Public License because it
-does Less to protect the user's freedom than the ordinary General
-Public License. It also provides other free software developers Less
-of an advantage over competing non-free programs. These disadvantages
-are the reason we use the ordinary General Public License for many
-libraries. However, the Lesser license provides advantages in certain
-special circumstances.
-
- For example, on rare occasions, there may be a special need to
-encourage the widest possible use of a certain library, so that it becomes
-a de-facto standard. To achieve this, non-free programs must be
-allowed to use the library. A more frequent case is that a free
-library does the same job as widely used non-free libraries. In this
-case, there is little to gain by limiting the free library to free
-software only, so we use the Lesser General Public License.
-
- In other cases, permission to use a particular library in non-free
-programs enables a greater number of people to use a large body of
-free software. For example, permission to use the GNU C Library in
-non-free programs enables many more people to use the whole GNU
-operating system, as well as its variant, the GNU/Linux operating
-system.
-
- Although the Lesser General Public License is Less protective of the
-users' freedom, it does ensure that the user of a program that is
-linked with the Library has the freedom and the wherewithal to run
-that program using a modified version of the Library.
-
- The precise terms and conditions for copying, distribution and
-modification follow. Pay close attention to the difference between a
-"work based on the library" and a "work that uses the library". The
-former contains code derived from the library, whereas the latter must
-be combined with the library in order to run.
-
- GNU LESSER GENERAL PUBLIC LICENSE
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
- 0. This License Agreement applies to any software library or other
-program which contains a notice placed by the copyright holder or
-other authorized party saying it may be distributed under the terms of
-this Lesser General Public License (also called "this License").
-Each licensee is addressed as "you".
-
- A "library" means a collection of software functions and/or data
-prepared so as to be conveniently linked with application programs
-(which use some of those functions and data) to form executables.
-
- The "Library", below, refers to any such software library or work
-which has been distributed under these terms. A "work based on the
-Library" means either the Library or any derivative work under
-copyright law: that is to say, a work containing the Library or a
-portion of it, either verbatim or with modifications and/or translated
-straightforwardly into another language. (Hereinafter, translation is
-included without limitation in the term "modification".)
-
- "Source code" for a work means the preferred form of the work for
-making modifications to it. For a library, complete source code means
-all the source code for all modules it contains, plus any associated
-interface definition files, plus the scripts used to control compilation
-and installation of the library.
-
- Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope. The act of
-running a program using the Library is not restricted, and output from
-such a program is covered only if its contents constitute a work based
-on the Library (independent of the use of the Library in a tool for
-writing it). Whether that is true depends on what the Library does
-and what the program that uses the Library does.
-
- 1. You may copy and distribute verbatim copies of the Library's
-complete source code as you receive it, in any medium, provided that
-you conspicuously and appropriately publish on each copy an
-appropriate copyright notice and disclaimer of warranty; keep intact
-all the notices that refer to this License and to the absence of any
-warranty; and distribute a copy of this License along with the
-Library.
-
- You may charge a fee for the physical act of transferring a copy,
-and you may at your option offer warranty protection in exchange for a
-fee.
-
- 2. You may modify your copy or copies of the Library or any portion
-of it, thus forming a work based on the Library, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
- a) The modified work must itself be a software library.
-
- b) You must cause the files modified to carry prominent notices
- stating that you changed the files and the date of any change.
-
- c) You must cause the whole of the work to be licensed at no
- charge to all third parties under the terms of this License.
-
- d) If a facility in the modified Library refers to a function or a
- table of data to be supplied by an application program that uses
- the facility, other than as an argument passed when the facility
- is invoked, then you must make a good faith effort to ensure that,
- in the event an application does not supply such function or
- table, the facility still operates, and performs whatever part of
- its purpose remains meaningful.
-
- (For example, a function in a library to compute square roots has
- a purpose that is entirely well-defined independent of the
- application. Therefore, Subsection 2d requires that any
- application-supplied function or table used by this function must
- be optional: if the application does not supply it, the square
- root function must still compute square roots.)
-
-These requirements apply to the modified work as a whole. If
-identifiable sections of that work are not derived from the Library,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works. But when you
-distribute the same sections as part of a whole which is a work based
-on the Library, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote
-it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Library.
-
-In addition, mere aggregation of another work not based on the Library
-with the Library (or with a work based on the Library) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
- 3. You may opt to apply the terms of the ordinary GNU General Public
-License instead of this License to a given copy of the Library. To do
-this, you must alter all the notices that refer to this License, so
-that they refer to the ordinary GNU General Public License, version 2,
-instead of to this License. (If a newer version than version 2 of the
-ordinary GNU General Public License has appeared, then you can specify
-that version instead if you wish.) Do not make any other change in
-these notices.
-
- Once this change is made in a given copy, it is irreversible for
-that copy, so the ordinary GNU General Public License applies to all
-subsequent copies and derivative works made from that copy.
-
- This option is useful when you wish to copy part of the code of
-the Library into a program that is not a library.
-
- 4. You may copy and distribute the Library (or a portion or
-derivative of it, under Section 2) in object code or executable form
-under the terms of Sections 1 and 2 above provided that you accompany
-it with the complete corresponding machine-readable source code, which
-must be distributed under the terms of Sections 1 and 2 above on a
-medium customarily used for software interchange.
-
- If distribution of object code is made by offering access to copy
-from a designated place, then offering equivalent access to copy the
-source code from the same place satisfies the requirement to
-distribute the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
- 5. A program that contains no derivative of any portion of the
-Library, but is designed to work with the Library by being compiled or
-linked with it, is called a "work that uses the Library". Such a
-work, in isolation, is not a derivative work of the Library, and
-therefore falls outside the scope of this License.
-
- However, linking a "work that uses the Library" with the Library
-creates an executable that is a derivative of the Library (because it
-contains portions of the Library), rather than a "work that uses the
-library". The executable is therefore covered by this License.
-Section 6 states terms for distribution of such executables.
-
- When a "work that uses the Library" uses material from a header file
-that is part of the Library, the object code for the work may be a
-derivative work of the Library even though the source code is not.
-Whether this is true is especially significant if the work can be
-linked without the Library, or if the work is itself a library. The
-threshold for this to be true is not precisely defined by law.
-
- If such an object file uses only numerical parameters, data
-structure layouts and accessors, and small macros and small inline
-functions (ten lines or less in length), then the use of the object
-file is unrestricted, regardless of whether it is legally a derivative
-work. (Executables containing this object code plus portions of the
-Library will still fall under Section 6.)
-
- Otherwise, if the work is a derivative of the Library, you may
-distribute the object code for the work under the terms of Section 6.
-Any executables containing that work also fall under Section 6,
-whether or not they are linked directly with the Library itself.
-
- 6. As an exception to the Sections above, you may also combine or
-link a "work that uses the Library" with the Library to produce a
-work containing portions of the Library, and distribute that work
-under terms of your choice, provided that the terms permit
-modification of the work for the customer's own use and reverse
-engineering for debugging such modifications.
-
- You must give prominent notice with each copy of the work that the
-Library is used in it and that the Library and its use are covered by
-this License. You must supply a copy of this License. If the work
-during execution displays copyright notices, you must include the
-copyright notice for the Library among them, as well as a reference
-directing the user to the copy of this License. Also, you must do one
-of these things:
-
- a) Accompany the work with the complete corresponding
- machine-readable source code for the Library including whatever
- changes were used in the work (which must be distributed under
- Sections 1 and 2 above); and, if the work is an executable linked
- with the Library, with the complete machine-readable "work that
- uses the Library", as object code and/or source code, so that the
- user can modify the Library and then relink to produce a modified
- executable containing the modified Library. (It is understood
- that the user who changes the contents of definitions files in the
- Library will not necessarily be able to recompile the application
- to use the modified definitions.)
-
- b) Use a suitable shared library mechanism for linking with the
- Library. A suitable mechanism is one that (1) uses at run time a
- copy of the library already present on the user's computer system,
- rather than copying library functions into the executable, and (2)
- will operate properly with a modified version of the library, if
- the user installs one, as long as the modified version is
- interface-compatible with the version that the work was made with.
-
- c) Accompany the work with a written offer, valid for at
- least three years, to give the same user the materials
- specified in Subsection 6a, above, for a charge no more
- than the cost of performing this distribution.
-
- d) If distribution of the work is made by offering access to copy
- from a designated place, offer equivalent access to copy the above
- specified materials from the same place.
-
- e) Verify that the user has already received a copy of these
- materials or that you have already sent this user a copy.
-
- For an executable, the required form of the "work that uses the
-Library" must include any data and utility programs needed for
-reproducing the executable from it. However, as a special exception,
-the materials to be distributed need not include anything that is
-normally distributed (in either source or binary form) with the major
-components (compiler, kernel, and so on) of the operating system on
-which the executable runs, unless that component itself accompanies
-the executable.
-
- It may happen that this requirement contradicts the license
-restrictions of other proprietary libraries that do not normally
-accompany the operating system. Such a contradiction means you cannot
-use both them and the Library together in an executable that you
-distribute.
-
- 7. You may place library facilities that are a work based on the
-Library side-by-side in a single library together with other library
-facilities not covered by this License, and distribute such a combined
-library, provided that the separate distribution of the work based on
-the Library and of the other library facilities is otherwise
-permitted, and provided that you do these two things:
-
- a) Accompany the combined library with a copy of the same work
- based on the Library, uncombined with any other library
- facilities. This must be distributed under the terms of the
- Sections above.
-
- b) Give prominent notice with the combined library of the fact
- that part of it is a work based on the Library, and explaining
- where to find the accompanying uncombined form of the same work.
-
- 8. You may not copy, modify, sublicense, link with, or distribute
-the Library except as expressly provided under this License. Any
-attempt otherwise to copy, modify, sublicense, link with, or
-distribute the Library is void, and will automatically terminate your
-rights under this License. However, parties who have received copies,
-or rights, from you under this License will not have their licenses
-terminated so long as such parties remain in full compliance.
-
- 9. You are not required to accept this License, since you have not
-signed it. However, nothing else grants you permission to modify or
-distribute the Library or its derivative works. These actions are
-prohibited by law if you do not accept this License. Therefore, by
-modifying or distributing the Library (or any work based on the
-Library), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Library or works based on it.
-
- 10. Each time you redistribute the Library (or any work based on the
-Library), the recipient automatically receives a license from the
-original licensor to copy, distribute, link with or modify the Library
-subject to these terms and conditions. You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties with
-this License.
-
- 11. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Library at all. For example, if a patent
-license would not permit royalty-free redistribution of the Library by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Library.
-
-If any portion of this section is held invalid or unenforceable under any
-particular circumstance, the balance of the section is intended to apply,
-and the section as a whole is intended to apply in other circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system which is
-implemented by public license practices. Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
- 12. If the distribution and/or use of the Library is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Library under this License may add
-an explicit geographical distribution limitation excluding those countries,
-so that distribution is permitted only in or among countries not thus
-excluded. In such case, this License incorporates the limitation as if
-written in the body of this License.
-
- 13. The Free Software Foundation may publish revised and/or new
-versions of the Lesser General Public License from time to time.
-Such new versions will be similar in spirit to the present version,
-but may differ in detail to address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Library
-specifies a version number of this License which applies to it and
-"any later version", you have the option of following the terms and
-conditions either of that version or of any later version published by
-the Free Software Foundation. If the Library does not specify a
-license version number, you may choose any version ever published by
-the Free Software Foundation.
-
- 14. If you wish to incorporate parts of the Library into other free
-programs whose distribution conditions are incompatible with these,
-write to the author to ask for permission. For software which is
-copyrighted by the Free Software Foundation, write to the Free
-Software Foundation; we sometimes make exceptions for this. Our
-decision will be guided by the two goals of preserving the free status
-of all derivatives of our free software and of promoting the sharing
-and reuse of software generally.
-
- NO WARRANTY
-
- 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
-WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
-EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
-OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
-KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
-LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
-THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
-WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
-AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
-FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
-CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
-LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
-RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
-FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
-SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
-DAMAGES.
-
- END OF TERMS AND CONDITIONS
diff --git a/python_apps/soundcloud-api/README b/python_apps/soundcloud-api/README
deleted file mode 100644
index 729a8faac..000000000
--- a/python_apps/soundcloud-api/README
+++ /dev/null
@@ -1,45 +0,0 @@
-Running tests
-=============
-
-The **SCAPI** comes with a small testsuite. It can be run automatically through either setuptools_
-or nose_.
-
-Configuring tests
------------------
-
-Before you can run the tests, you need to configure them. You do this using the `test.ini` file in the
-root of python **SCAPI** workingcopy.
-
-Running tests through setuptools
---------------------------------
-
-You can run the whole testsuite through setuptools_ by doing ::
-
- host:~/SoundCloudAPI deets$ python setup.py test
-
-Running tests through nose
---------------------------
-
-If you want a more fine-grained control over which tests to run, you can use the `nosetests`-commandline tool.
-
-Then to run individual tests, you can e.g. do::
-
- host:~/SoundCloudAPI deets$ nosetests -s scapi.tests.scapi_tests:SCAPITests.test_setting_permissions
-
-
-See the nose_-website for more options.
-
-
-
-.. _nose: http://somethingaboutorange.com/mrl/projects/nose/
-.. _setuptools: http://peak.telecommunity.com/DevCenter/setuptools
-.. _ConfigObj: http://www.voidspace.org.uk/python/configobj.html
-
-
-Creating the API-docs
-=====================
-
-Do::
- epydoc -v --name="SoundCloud API" --html -o docs/api scapi --exclude="os|mimetypes|urllib2|exceptions|mimetools"
-
-
diff --git a/python_apps/soundcloud-api/bootstrap.py b/python_apps/soundcloud-api/bootstrap.py
deleted file mode 100644
index c04723c5b..000000000
--- a/python_apps/soundcloud-api/bootstrap.py
+++ /dev/null
@@ -1,58 +0,0 @@
-import webbrowser
-import scapi
-
-# the host to connect to. Normally, this
-# would be api.soundcloud.com
-API_HOST = "api.sandbox-soundcloud.com"
-
-# This needs to be the consumer ID you got from
-# http://soundcloud.com/settings/applications/new
-CONSUMER = "gLnhFeUBnBCZF8a6Ngqq7w"
-# This needs to be the consumer secret password you got from
-# http://soundcloud.com/settings/applications/new
-CONSUMER_SECRET = "nbWRdG5X9xUb63l4nIeFYm3nmeVJ2v4s1ROpvRSBvU8"
-
-# first, we create an OAuthAuthenticator that only knows about consumer
-# credentials. This is done so that we can get an request-token as
-# first step.
-oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER,
- CONSUMER_SECRET,
- None,
- None)
-
-# The connector works with the authenticator to create and sign the requests. It
-# has some helper-methods that allow us to do the OAuth-dance.
-connector = scapi.ApiConnector(host=API_HOST, authenticator=oauth_authenticator)
-
-# First step is to get a request-token, and to let the user authorize that
-# via the browser.
-token, secret = connector.fetch_request_token()
-authorization_url = connector.get_request_token_authorization_url(token)
-webbrowser.open(authorization_url)
-oauth_verifier = raw_input("please enter verifier code as seen in the browser:")
-
-# Now we create a new authenticator with the temporary token & secret we got from
-# the request-token. This will give us the access-token
-oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER,
- CONSUMER_SECRET,
- token,
- secret)
-
-# we need a new connector with the new authenticator!
-connector = scapi.ApiConnector(API_HOST, authenticator=oauth_authenticator)
-token, secret = connector.fetch_access_token(oauth_verifier)
-
-# now we are finally ready to go - with all four parameters OAuth requires,
-# we can setup an authenticator that allows for actual API-calls.
-oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER,
- CONSUMER_SECRET,
- token,
- secret)
-
-# we pass the connector to a Scope - a Scope is essentiall a path in the REST-url-space.
-# Without any path-component, it's the root from which we can then query into the
-# resources.
-root = scapi.Scope(scapi.ApiConnector(host=API_HOST, authenticator=oauth_authenticator))
-
-# Hey, nice meeting you!
-print "Hello, %s" % root.me().username
diff --git a/python_apps/soundcloud-api/docs/api/api-objects.txt b/python_apps/soundcloud-api/docs/api/api-objects.txt
deleted file mode 100644
index 8f3e21555..000000000
--- a/python_apps/soundcloud-api/docs/api/api-objects.txt
+++ /dev/null
@@ -1,333 +0,0 @@
-scapi scapi-module.html
-scapi.escape scapi.util-module.html#escape
-scapi.USE_PROXY scapi-module.html#USE_PROXY
-scapi.REQUEST_TOKEN_URL scapi-module.html#REQUEST_TOKEN_URL
-scapi.PROXY scapi-module.html#PROXY
-scapi.ACCESS_TOKEN_URL scapi-module.html#ACCESS_TOKEN_URL
-scapi.register_classes scapi-module.html#register_classes
-scapi.logger scapi-module.html#logger
-scapi.AUTHORIZATION_URL scapi-module.html#AUTHORIZATION_URL
-scapi.authentication scapi.authentication-module.html
-scapi.authentication.USE_DOUBLE_ESCAPE_HACK scapi.authentication-module.html#USE_DOUBLE_ESCAPE_HACK
-scapi.authentication.escape scapi.util-module.html#escape
-scapi.authentication.logger scapi.authentication-module.html#logger
-scapi.config scapi.config-module.html
-scapi.json scapi.json-module.html
-scapi.json.read scapi.json-module.html#read
-scapi.json.write scapi.json-module.html#write
-scapi.multidict scapi.multidict-module.html
-scapi.tests scapi.tests-module.html
-scapi.tests.scapi_tests scapi.tests.scapi_tests-module.html
-scapi.tests.scapi_tests.api_logger scapi.tests.scapi_tests-module.html#api_logger
-scapi.tests.scapi_tests.logger scapi.tests.scapi_tests-module.html#logger
-scapi.tests.test_connect scapi.tests.test_connect-module.html
-scapi.tests.test_connect.test_me_having_stress scapi.tests.test_connect-module.html#test_me_having_stress
-scapi.tests.test_connect.test_scoped_track_creation scapi.tests.test_connect-module.html#test_scoped_track_creation
-scapi.tests.test_connect.test_permissions scapi.tests.test_connect-module.html#test_permissions
-scapi.tests.test_connect.CONSUMER_SECRET scapi.tests.test_connect-module.html#CONSUMER_SECRET
-scapi.tests.test_connect.CONSUMER scapi.tests.test_connect-module.html#CONSUMER
-scapi.tests.test_connect.load_config scapi.tests.test_connect-module.html#load_config
-scapi.tests.test_connect.test_upload scapi.tests.test_connect-module.html#test_upload
-scapi.tests.test_connect.USER scapi.tests.test_connect-module.html#USER
-scapi.tests.test_connect.test_load_config scapi.tests.test_connect-module.html#test_load_config
-scapi.tests.test_connect.test_access_token_acquisition scapi.tests.test_connect-module.html#test_access_token_acquisition
-scapi.tests.test_connect.test_track_creation scapi.tests.test_connect-module.html#test_track_creation
-scapi.tests.test_connect.test_setting_comments scapi.tests.test_connect-module.html#test_setting_comments
-scapi.tests.test_connect.test_track_update scapi.tests.test_connect-module.html#test_track_update
-scapi.tests.test_connect.SECRET scapi.tests.test_connect-module.html#SECRET
-scapi.tests.test_connect.test_contact_add_and_removal scapi.tests.test_connect-module.html#test_contact_add_and_removal
-scapi.tests.test_connect.logger scapi.tests.test_connect-module.html#logger
-scapi.tests.test_connect.test_connect scapi.tests.test_connect-module.html#test_connect
-scapi.tests.test_connect.ROOT scapi.tests.test_connect-module.html#ROOT
-scapi.tests.test_connect.test_contact_list scapi.tests.test_connect-module.html#test_contact_list
-scapi.tests.test_connect.test_playlists scapi.tests.test_connect-module.html#test_playlists
-scapi.tests.test_connect.API_HOST scapi.tests.test_connect-module.html#API_HOST
-scapi.tests.test_connect._logger scapi.tests.test_connect-module.html#_logger
-scapi.tests.test_connect.TOKEN scapi.tests.test_connect-module.html#TOKEN
-scapi.tests.test_connect.test_events scapi.tests.test_connect-module.html#test_events
-scapi.tests.test_connect.test_non_global_api scapi.tests.test_connect-module.html#test_non_global_api
-scapi.tests.test_connect.test_setting_permissions scapi.tests.test_connect-module.html#test_setting_permissions
-scapi.tests.test_connect.PASSWORD scapi.tests.test_connect-module.html#PASSWORD
-scapi.tests.test_connect.test_setting_comments_the_way_shawn_says_its_correct scapi.tests.test_connect-module.html#test_setting_comments_the_way_shawn_says_its_correct
-scapi.tests.test_connect.test_large_list scapi.tests.test_connect-module.html#test_large_list
-scapi.tests.test_connect.CONNECTOR scapi.tests.test_connect-module.html#CONNECTOR
-scapi.tests.test_connect.CONFIG_NAME scapi.tests.test_connect-module.html#CONFIG_NAME
-scapi.tests.test_connect.RUN_INTERACTIVE_TESTS scapi.tests.test_connect-module.html#RUN_INTERACTIVE_TESTS
-scapi.tests.test_connect.setup scapi.tests.test_connect-module.html#setup
-scapi.tests.test_connect.test_favorites scapi.tests.test_connect-module.html#test_favorites
-scapi.tests.test_connect.USE_OAUTH scapi.tests.test_connect-module.html#USE_OAUTH
-scapi.tests.test_oauth scapi.tests.test_oauth-module.html
-scapi.tests.test_oauth._logger scapi.tests.test_oauth-module.html#_logger
-scapi.tests.test_oauth.test_oauth_connect scapi.tests.test_oauth-module.html#test_oauth_connect
-scapi.tests.test_oauth.TOKEN scapi.tests.test_oauth-module.html#TOKEN
-scapi.tests.test_oauth.test_base64_connect scapi.tests.test_oauth-module.html#test_base64_connect
-scapi.tests.test_oauth.CONSUMER_SECRET scapi.tests.test_oauth-module.html#CONSUMER_SECRET
-scapi.tests.test_oauth.SECRET scapi.tests.test_oauth-module.html#SECRET
-scapi.tests.test_oauth.logger scapi.tests.test_oauth-module.html#logger
-scapi.tests.test_oauth.CONSUMER scapi.tests.test_oauth-module.html#CONSUMER
-scapi.util scapi.util-module.html
-scapi.util.escape scapi.util-module.html#escape
-exceptions.AssertionError exceptions.AssertionError-class.html
-exceptions.AssertionError.__init__ exceptions.AssertionError-class.html#__init__
-exceptions.AssertionError.__new__ exceptions.AssertionError-class.html#__new__
-scapi.ApiConnector scapi.ApiConnector-class.html
-scapi.ApiConnector.fetch_access_token scapi.ApiConnector-class.html#fetch_access_token
-scapi.ApiConnector.LIST_LIMIT scapi.ApiConnector-class.html#LIST_LIMIT
-scapi.ApiConnector.LIST_LIMIT_PARAMETER scapi.ApiConnector-class.html#LIST_LIMIT_PARAMETER
-scapi.ApiConnector.fetch_request_token scapi.ApiConnector-class.html#fetch_request_token
-scapi.ApiConnector.get_request_token_authorization_url scapi.ApiConnector-class.html#get_request_token_authorization_url
-scapi.ApiConnector.normalize_method scapi.ApiConnector-class.html#normalize_method
-scapi.ApiConnector.__init__ scapi.ApiConnector-class.html#__init__
-scapi.ApiConnector.LIST_OFFSET_PARAMETER scapi.ApiConnector-class.html#LIST_OFFSET_PARAMETER
-scapi.Comment scapi.Comment-class.html
-scapi.RESTBase._scope scapi.RESTBase-class.html#_scope
-scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__
-scapi.Comment.KIND scapi.Comment-class.html#KIND
-scapi.RESTBase.create scapi.RESTBase-class.html#create
-scapi.RESTBase.get scapi.RESTBase-class.html#get
-scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__
-scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES
-scapi.RESTBase.new scapi.RESTBase-class.html#new
-scapi.RESTBase.ALIASES scapi.RESTBase-class.html#ALIASES
-scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__
-scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments
-scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value
-scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__
-scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton
-scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY
-scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__
-scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__
-scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__
-scapi.Event scapi.Event-class.html
-scapi.RESTBase._scope scapi.RESTBase-class.html#_scope
-scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__
-scapi.Event.KIND scapi.Event-class.html#KIND
-scapi.RESTBase.create scapi.RESTBase-class.html#create
-scapi.RESTBase.get scapi.RESTBase-class.html#get
-scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__
-scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES
-scapi.RESTBase.new scapi.RESTBase-class.html#new
-scapi.RESTBase.ALIASES scapi.RESTBase-class.html#ALIASES
-scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__
-scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments
-scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value
-scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__
-scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton
-scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY
-scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__
-scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__
-scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__
-scapi.Group scapi.Group-class.html
-scapi.RESTBase._scope scapi.RESTBase-class.html#_scope
-scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__
-scapi.Group.KIND scapi.Group-class.html#KIND
-scapi.RESTBase.create scapi.RESTBase-class.html#create
-scapi.RESTBase.get scapi.RESTBase-class.html#get
-scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__
-scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES
-scapi.RESTBase.new scapi.RESTBase-class.html#new
-scapi.RESTBase.ALIASES scapi.RESTBase-class.html#ALIASES
-scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__
-scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments
-scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value
-scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__
-scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton
-scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY
-scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__
-scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__
-scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__
-scapi.InvalidMethodException scapi.InvalidMethodException-class.html
-scapi.InvalidMethodException.__repr__ scapi.InvalidMethodException-class.html#__repr__
-scapi.InvalidMethodException.__init__ scapi.InvalidMethodException-class.html#__init__
-scapi.NoResultFromRequest scapi.NoResultFromRequest-class.html
-scapi.Playlist scapi.Playlist-class.html
-scapi.RESTBase._scope scapi.RESTBase-class.html#_scope
-scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__
-scapi.Playlist.KIND scapi.Playlist-class.html#KIND
-scapi.RESTBase.create scapi.RESTBase-class.html#create
-scapi.RESTBase.get scapi.RESTBase-class.html#get
-scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__
-scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES
-scapi.RESTBase.new scapi.RESTBase-class.html#new
-scapi.RESTBase.ALIASES scapi.RESTBase-class.html#ALIASES
-scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__
-scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments
-scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value
-scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__
-scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton
-scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY
-scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__
-scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__
-scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__
-scapi.RESTBase scapi.RESTBase-class.html
-scapi.RESTBase._scope scapi.RESTBase-class.html#_scope
-scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__
-scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__
-scapi.RESTBase.create scapi.RESTBase-class.html#create
-scapi.RESTBase.get scapi.RESTBase-class.html#get
-scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__
-scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES
-scapi.RESTBase.new scapi.RESTBase-class.html#new
-scapi.RESTBase.ALIASES scapi.RESTBase-class.html#ALIASES
-scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__
-scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments
-scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value
-scapi.RESTBase.KIND scapi.RESTBase-class.html#KIND
-scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton
-scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY
-scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__
-scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__
-scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__
-scapi.SCRedirectHandler scapi.SCRedirectHandler-class.html
-scapi.SCRedirectHandler.alternate_method scapi.SCRedirectHandler-class.html#alternate_method
-scapi.SCRedirectHandler.http_error_303 scapi.SCRedirectHandler-class.html#http_error_303
-scapi.SCRedirectHandler.http_error_201 scapi.SCRedirectHandler-class.html#http_error_201
-scapi.Scope scapi.Scope-class.html
-scapi.Scope.oauth_sign_get_request scapi.Scope-class.html#oauth_sign_get_request
-scapi.Scope._map scapi.Scope-class.html#_map
-scapi.Scope.__str__ scapi.Scope-class.html#__str__
-scapi.Scope.__getattr__ scapi.Scope-class.html#__getattr__
-scapi.Scope._call scapi.Scope-class.html#_call
-scapi.Scope._create_query_string scapi.Scope-class.html#_create_query_string
-scapi.Scope._create_request scapi.Scope-class.html#_create_request
-scapi.Scope._get_connector scapi.Scope-class.html#_get_connector
-scapi.Scope.__init__ scapi.Scope-class.html#__init__
-scapi.Scope.__repr__ scapi.Scope-class.html#__repr__
-scapi.Track scapi.Track-class.html
-scapi.RESTBase._scope scapi.RESTBase-class.html#_scope
-scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__
-scapi.Track.KIND scapi.Track-class.html#KIND
-scapi.RESTBase.create scapi.RESTBase-class.html#create
-scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments
-scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__
-scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES
-scapi.RESTBase.new scapi.RESTBase-class.html#new
-scapi.Track.ALIASES scapi.Track-class.html#ALIASES
-scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__
-scapi.RESTBase.get scapi.RESTBase-class.html#get
-scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value
-scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__
-scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton
-scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY
-scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__
-scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__
-scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__
-scapi.UnknownContentType scapi.UnknownContentType-class.html
-scapi.UnknownContentType.__str__ scapi.UnknownContentType-class.html#__str__
-scapi.UnknownContentType.__repr__ scapi.UnknownContentType-class.html#__repr__
-scapi.UnknownContentType.__init__ scapi.UnknownContentType-class.html#__init__
-scapi.User scapi.User-class.html
-scapi.RESTBase._scope scapi.RESTBase-class.html#_scope
-scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__
-scapi.User.KIND scapi.User-class.html#KIND
-scapi.RESTBase.create scapi.RESTBase-class.html#create
-scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments
-scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__
-scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES
-scapi.RESTBase.new scapi.RESTBase-class.html#new
-scapi.User.ALIASES scapi.User-class.html#ALIASES
-scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__
-scapi.RESTBase.get scapi.RESTBase-class.html#get
-scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value
-scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__
-scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton
-scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY
-scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__
-scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__
-scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__
-scapi.authentication.BasicAuthenticator scapi.authentication.BasicAuthenticator-class.html
-scapi.authentication.BasicAuthenticator.augment_request scapi.authentication.BasicAuthenticator-class.html#augment_request
-scapi.authentication.BasicAuthenticator.__init__ scapi.authentication.BasicAuthenticator-class.html#__init__
-scapi.authentication.OAuthAuthenticator scapi.authentication.OAuthAuthenticator-class.html
-scapi.authentication.OAuthAuthenticator.augment_request scapi.authentication.OAuthAuthenticator-class.html#augment_request
-scapi.authentication.OAuthAuthenticator.generate_nonce scapi.authentication.OAuthAuthenticator-class.html#generate_nonce
-scapi.authentication.OAuthAuthenticator.OAUTH_API_VERSION scapi.authentication.OAuthAuthenticator-class.html#OAUTH_API_VERSION
-scapi.authentication.OAuthAuthenticator.generate_timestamp scapi.authentication.OAuthAuthenticator-class.html#generate_timestamp
-scapi.authentication.OAuthAuthenticator.AUTHORIZATION_HEADER scapi.authentication.OAuthAuthenticator-class.html#AUTHORIZATION_HEADER
-scapi.authentication.OAuthAuthenticator.__init__ scapi.authentication.OAuthAuthenticator-class.html#__init__
-scapi.authentication.OAuthSignatureMethod_HMAC_SHA1 scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html
-scapi.authentication.OAuthSignatureMethod_HMAC_SHA1.FORBIDDEN scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html#FORBIDDEN
-scapi.authentication.OAuthSignatureMethod_HMAC_SHA1.get_name scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html#get_name
-scapi.authentication.OAuthSignatureMethod_HMAC_SHA1.get_normalized_http_method scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html#get_normalized_http_method
-scapi.authentication.OAuthSignatureMethod_HMAC_SHA1.get_normalized_http_url scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html#get_normalized_http_url
-scapi.authentication.OAuthSignatureMethod_HMAC_SHA1.get_normalized_parameters scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html#get_normalized_parameters
-scapi.authentication.OAuthSignatureMethod_HMAC_SHA1.build_signature scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html#build_signature
-scapi.json.JsonReader scapi.json.JsonReader-class.html
-scapi.json.JsonReader.escapes scapi.json.JsonReader-class.html#escapes
-scapi.json.JsonReader._readObject scapi.json.JsonReader-class.html#_readObject
-scapi.json.JsonReader._assertNext scapi.json.JsonReader-class.html#_assertNext
-scapi.json.JsonReader._readNumber scapi.json.JsonReader-class.html#_readNumber
-scapi.json.JsonReader._peek scapi.json.JsonReader-class.html#_peek
-scapi.json.JsonReader._readNull scapi.json.JsonReader-class.html#_readNull
-scapi.json.JsonReader._hexDigitToInt scapi.json.JsonReader-class.html#_hexDigitToInt
-scapi.json.JsonReader._readTrue scapi.json.JsonReader-class.html#_readTrue
-scapi.json.JsonReader._readDoubleSolidusComment scapi.json.JsonReader-class.html#_readDoubleSolidusComment
-scapi.json.JsonReader.read scapi.json.JsonReader-class.html#read
-scapi.json.JsonReader._readFalse scapi.json.JsonReader-class.html#_readFalse
-scapi.json.JsonReader.hex_digits scapi.json.JsonReader-class.html#hex_digits
-scapi.json.JsonReader._readComment scapi.json.JsonReader-class.html#_readComment
-scapi.json.JsonReader._readArray scapi.json.JsonReader-class.html#_readArray
-scapi.json.JsonReader._eatWhitespace scapi.json.JsonReader-class.html#_eatWhitespace
-scapi.json.JsonReader._read scapi.json.JsonReader-class.html#_read
-scapi.json.JsonReader._readString scapi.json.JsonReader-class.html#_readString
-scapi.json.JsonReader._readCStyleComment scapi.json.JsonReader-class.html#_readCStyleComment
-scapi.json.JsonReader._next scapi.json.JsonReader-class.html#_next
-scapi.json.JsonWriter scapi.json.JsonWriter-class.html
-scapi.json.JsonWriter.write scapi.json.JsonWriter-class.html#write
-scapi.json.JsonWriter._append scapi.json.JsonWriter-class.html#_append
-scapi.json.JsonWriter._write scapi.json.JsonWriter-class.html#_write
-scapi.json.ReadException scapi.json.ReadException-class.html
-scapi.json.WriteException scapi.json.WriteException-class.html
-scapi.json._StringGenerator scapi.json._StringGenerator-class.html
-scapi.json._StringGenerator.peek scapi.json._StringGenerator-class.html#peek
-scapi.json._StringGenerator.all scapi.json._StringGenerator-class.html#all
-scapi.json._StringGenerator.next scapi.json._StringGenerator-class.html#next
-scapi.json._StringGenerator.__init__ scapi.json._StringGenerator-class.html#__init__
-scapi.tests.scapi_tests.SCAPITests scapi.tests.scapi_tests.SCAPITests-class.html
-scapi.tests.scapi_tests.SCAPITests._load_config scapi.tests.scapi_tests.SCAPITests-class.html#_load_config
-scapi.tests.scapi_tests.SCAPITests.test_track_creation_with_artwork scapi.tests.scapi_tests.SCAPITests-class.html#test_track_creation_with_artwork
-scapi.tests.scapi_tests.SCAPITests.test_upload scapi.tests.scapi_tests.SCAPITests-class.html#test_upload
-scapi.tests.scapi_tests.SCAPITests.test_scoped_track_creation scapi.tests.scapi_tests.SCAPITests-class.html#test_scoped_track_creation
-scapi.tests.scapi_tests.SCAPITests.test_permissions scapi.tests.scapi_tests.SCAPITests-class.html#test_permissions
-scapi.tests.scapi_tests.SCAPITests.CONSUMER_SECRET scapi.tests.scapi_tests.SCAPITests-class.html#CONSUMER_SECRET
-scapi.tests.scapi_tests.SCAPITests.CONSUMER scapi.tests.scapi_tests.SCAPITests-class.html#CONSUMER
-scapi.tests.scapi_tests.SCAPITests.test_me_having_stress scapi.tests.scapi_tests.SCAPITests-class.html#test_me_having_stress
-scapi.tests.scapi_tests.SCAPITests.USER scapi.tests.scapi_tests.SCAPITests-class.html#USER
-scapi.tests.scapi_tests.SCAPITests.CONFIGSPEC scapi.tests.scapi_tests.SCAPITests-class.html#CONFIGSPEC
-scapi.tests.scapi_tests.SCAPITests.test_track_creation_with_email_sharers scapi.tests.scapi_tests.SCAPITests-class.html#test_track_creation_with_email_sharers
-scapi.tests.scapi_tests.SCAPITests.test_setting_comments scapi.tests.scapi_tests.SCAPITests-class.html#test_setting_comments
-scapi.tests.scapi_tests.SCAPITests.test_access_token_acquisition scapi.tests.scapi_tests.SCAPITests-class.html#test_access_token_acquisition
-scapi.tests.scapi_tests.SCAPITests.test_track_creation scapi.tests.scapi_tests.SCAPITests-class.html#test_track_creation
-scapi.tests.scapi_tests.SCAPITests.test_groups scapi.tests.scapi_tests.SCAPITests-class.html#test_groups
-scapi.tests.scapi_tests.SCAPITests.test_track_update scapi.tests.scapi_tests.SCAPITests-class.html#test_track_update
-scapi.tests.scapi_tests.SCAPITests.test_modifying_playlists scapi.tests.scapi_tests.SCAPITests-class.html#test_modifying_playlists
-scapi.tests.scapi_tests.SCAPITests.SECRET scapi.tests.scapi_tests.SCAPITests-class.html#SECRET
-scapi.tests.scapi_tests.SCAPITests.test_oauth_get_signing scapi.tests.scapi_tests.SCAPITests-class.html#test_oauth_get_signing
-scapi.tests.scapi_tests.SCAPITests.test_contact_add_and_removal scapi.tests.scapi_tests.SCAPITests-class.html#test_contact_add_and_removal
-scapi.tests.scapi_tests.SCAPITests.test_connect scapi.tests.scapi_tests.SCAPITests-class.html#test_connect
-scapi.tests.scapi_tests.SCAPITests.test_large_list scapi.tests.scapi_tests.SCAPITests-class.html#test_large_list
-unittest.TestCase.failureException exceptions.AssertionError-class.html
-scapi.tests.scapi_tests.SCAPITests.test_contact_list scapi.tests.scapi_tests.SCAPITests-class.html#test_contact_list
-scapi.tests.scapi_tests.SCAPITests.test_playlists scapi.tests.scapi_tests.SCAPITests-class.html#test_playlists
-scapi.tests.scapi_tests.SCAPITests.API_HOST scapi.tests.scapi_tests.SCAPITests-class.html#API_HOST
-scapi.tests.scapi_tests.SCAPITests.setUp scapi.tests.scapi_tests.SCAPITests-class.html#setUp
-scapi.tests.scapi_tests.SCAPITests.test_downloadable scapi.tests.scapi_tests.SCAPITests-class.html#test_downloadable
-scapi.tests.scapi_tests.SCAPITests.TOKEN scapi.tests.scapi_tests.SCAPITests-class.html#TOKEN
-scapi.tests.scapi_tests.SCAPITests.test_events scapi.tests.scapi_tests.SCAPITests-class.html#test_events
-scapi.tests.scapi_tests.SCAPITests.test_non_global_api scapi.tests.scapi_tests.SCAPITests-class.html#test_non_global_api
-scapi.tests.scapi_tests.SCAPITests.PASSWORD scapi.tests.scapi_tests.SCAPITests-class.html#PASSWORD
-scapi.tests.scapi_tests.SCAPITests.test_setting_comments_the_way_shawn_says_its_correct scapi.tests.scapi_tests.SCAPITests-class.html#test_setting_comments_the_way_shawn_says_its_correct
-scapi.tests.scapi_tests.SCAPITests.CONFIG_NAME scapi.tests.scapi_tests.SCAPITests-class.html#CONFIG_NAME
-scapi.tests.scapi_tests.SCAPITests.test_setting_permissions scapi.tests.scapi_tests.SCAPITests-class.html#test_setting_permissions
-scapi.tests.scapi_tests.SCAPITests.RUN_INTERACTIVE_TESTS scapi.tests.scapi_tests.SCAPITests-class.html#RUN_INTERACTIVE_TESTS
-scapi.tests.scapi_tests.SCAPITests.test_favorites scapi.tests.scapi_tests.SCAPITests-class.html#test_favorites
-scapi.tests.scapi_tests.SCAPITests.test_track_creation_with_updated_artwork scapi.tests.scapi_tests.SCAPITests-class.html#test_track_creation_with_updated_artwork
-scapi.tests.scapi_tests.SCAPITests.AUTHENTICATOR scapi.tests.scapi_tests.SCAPITests-class.html#AUTHENTICATOR
-scapi.tests.scapi_tests.SCAPITests.test_streaming scapi.tests.scapi_tests.SCAPITests-class.html#test_streaming
-scapi.tests.scapi_tests.SCAPITests.test_playlist_creation scapi.tests.scapi_tests.SCAPITests-class.html#test_playlist_creation
-scapi.tests.scapi_tests.SCAPITests.test_track_deletion scapi.tests.scapi_tests.SCAPITests-class.html#test_track_deletion
-scapi.tests.scapi_tests.SCAPITests.test_filtered_list scapi.tests.scapi_tests.SCAPITests-class.html#test_filtered_list
-scapi.tests.scapi_tests.SCAPITests.root scapi.tests.scapi_tests.SCAPITests-class.html#root
-scapi.util.MultiDict scapi.util.MultiDict-class.html
-scapi.util.MultiDict.add scapi.util.MultiDict-class.html#add
-scapi.util.MultiDict.iteritemslist scapi.util.MultiDict-class.html#iteritemslist
diff --git a/python_apps/soundcloud-api/docs/api/class-tree.html b/python_apps/soundcloud-api/docs/api/class-tree.html
deleted file mode 100644
index b2473382f..000000000
--- a/python_apps/soundcloud-api/docs/api/class-tree.html
+++ /dev/null
@@ -1,216 +0,0 @@
-
-
-
-
- Class Hierarchy
-
-
-
-
-
-
-
-
-
-
-
diff --git a/python_apps/soundcloud-api/docs/api/crarr.png b/python_apps/soundcloud-api/docs/api/crarr.png
deleted file mode 100644
index 26b43c524..000000000
Binary files a/python_apps/soundcloud-api/docs/api/crarr.png and /dev/null differ
diff --git a/python_apps/soundcloud-api/docs/api/epydoc.css b/python_apps/soundcloud-api/docs/api/epydoc.css
deleted file mode 100644
index 86d417068..000000000
--- a/python_apps/soundcloud-api/docs/api/epydoc.css
+++ /dev/null
@@ -1,322 +0,0 @@
-
-
-/* Epydoc CSS Stylesheet
- *
- * This stylesheet can be used to customize the appearance of epydoc's
- * HTML output.
- *
- */
-
-/* Default Colors & Styles
- * - Set the default foreground & background color with 'body'; and
- * link colors with 'a:link' and 'a:visited'.
- * - Use bold for decision list terms.
- * - The heading styles defined here are used for headings *within*
- * docstring descriptions. All headings used by epydoc itself use
- * either class='epydoc' or class='toc' (CSS styles for both
- * defined below).
- */
-body { background: #ffffff; color: #000000; }
-p { margin-top: 0.5em; margin-bottom: 0.5em; }
-a:link { color: #0000ff; }
-a:visited { color: #204080; }
-dt { font-weight: bold; }
-h1 { font-size: +140%; font-style: italic;
- font-weight: bold; }
-h2 { font-size: +125%; font-style: italic;
- font-weight: bold; }
-h3 { font-size: +110%; font-style: italic;
- font-weight: normal; }
-code { font-size: 100%; }
-/* N.B.: class, not pseudoclass */
-a.link { font-family: monospace; }
-
-/* Page Header & Footer
- * - The standard page header consists of a navigation bar (with
- * pointers to standard pages such as 'home' and 'trees'); a
- * breadcrumbs list, which can be used to navigate to containing
- * classes or modules; options links, to show/hide private
- * variables and to show/hide frames; and a page title (using
- *
). The page title may be followed by a link to the
- * corresponding source code (using 'span.codelink').
- * - The footer consists of a navigation bar, a timestamp, and a
- * pointer to epydoc's homepage.
- */
-h1.epydoc { margin: 0; font-size: +140%; font-weight: bold; }
-h2.epydoc { font-size: +130%; font-weight: bold; }
-h3.epydoc { font-size: +115%; font-weight: bold;
- margin-top: 0.2em; }
-td h3.epydoc { font-size: +115%; font-weight: bold;
- margin-bottom: 0; }
-table.navbar { background: #a0c0ff; color: #000000;
- border: 2px groove #c0d0d0; }
-table.navbar table { color: #000000; }
-th.navbar-select { background: #70b0ff;
- color: #000000; }
-table.navbar a { text-decoration: none; }
-table.navbar a:link { color: #0000ff; }
-table.navbar a:visited { color: #204080; }
-span.breadcrumbs { font-size: 85%; font-weight: bold; }
-span.options { font-size: 70%; }
-span.codelink { font-size: 85%; }
-td.footer { font-size: 85%; }
-
-/* Table Headers
- * - Each summary table and details section begins with a 'header'
- * row. This row contains a section title (marked by
- * 'span.table-header') as well as a show/hide private link
- * (marked by 'span.options', defined above).
- * - Summary tables that contain user-defined groups mark those
- * groups using 'group header' rows.
- */
-td.table-header { background: #70b0ff; color: #000000;
- border: 1px solid #608090; }
-td.table-header table { color: #000000; }
-td.table-header table a:link { color: #0000ff; }
-td.table-header table a:visited { color: #204080; }
-span.table-header { font-size: 120%; font-weight: bold; }
-th.group-header { background: #c0e0f8; color: #000000;
- text-align: left; font-style: italic;
- font-size: 115%;
- border: 1px solid #608090; }
-
-/* Summary Tables (functions, variables, etc)
- * - Each object is described by a single row of the table with
- * two cells. The left cell gives the object's type, and is
- * marked with 'code.summary-type'. The right cell gives the
- * object's name and a summary description.
- * - CSS styles for the table's header and group headers are
- * defined above, under 'Table Headers'
- */
-table.summary { border-collapse: collapse;
- background: #e8f0f8; color: #000000;
- border: 1px solid #608090;
- margin-bottom: 0.5em; }
-td.summary { border: 1px solid #608090; }
-code.summary-type { font-size: 85%; }
-table.summary a:link { color: #0000ff; }
-table.summary a:visited { color: #204080; }
-
-
-/* Details Tables (functions, variables, etc)
- * - Each object is described in its own div.
- * - A single-row summary table w/ table-header is used as
- * a header for each details section (CSS style for table-header
- * is defined above, under 'Table Headers').
- */
-table.details { border-collapse: collapse;
- background: #e8f0f8; color: #000000;
- border: 1px solid #608090;
- margin: .2em 0 0 0; }
-table.details table { color: #000000; }
-table.details a:link { color: #0000ff; }
-table.details a:visited { color: #204080; }
-
-/* Fields */
-dl.fields { margin-left: 2em; margin-top: 1em;
- margin-bottom: 1em; }
-dl.fields dd ul { margin-left: 0em; padding-left: 0em; }
-dl.fields dd ul li ul { margin-left: 2em; padding-left: 0em; }
-div.fields { margin-left: 2em; }
-div.fields p { margin-bottom: 0.5em; }
-
-/* Index tables (identifier index, term index, etc)
- * - link-index is used for indices containing lists of links
- * (namely, the identifier index & term index).
- * - index-where is used in link indices for the text indicating
- * the container/source for each link.
- * - metadata-index is used for indices containing metadata
- * extracted from fields (namely, the bug index & todo index).
- */
-table.link-index { border-collapse: collapse;
- background: #e8f0f8; color: #000000;
- border: 1px solid #608090; }
-td.link-index { border-width: 0px; }
-table.link-index a:link { color: #0000ff; }
-table.link-index a:visited { color: #204080; }
-span.index-where { font-size: 70%; }
-table.metadata-index { border-collapse: collapse;
- background: #e8f0f8; color: #000000;
- border: 1px solid #608090;
- margin: .2em 0 0 0; }
-td.metadata-index { border-width: 1px; border-style: solid; }
-table.metadata-index a:link { color: #0000ff; }
-table.metadata-index a:visited { color: #204080; }
-
-/* Function signatures
- * - sig* is used for the signature in the details section.
- * - .summary-sig* is used for the signature in the summary
- * table, and when listing property accessor functions.
- * */
-.sig-name { color: #006080; }
-.sig-arg { color: #008060; }
-.sig-default { color: #602000; }
-.summary-sig { font-family: monospace; }
-.summary-sig-name { color: #006080; font-weight: bold; }
-table.summary a.summary-sig-name:link
- { color: #006080; font-weight: bold; }
-table.summary a.summary-sig-name:visited
- { color: #006080; font-weight: bold; }
-.summary-sig-arg { color: #006040; }
-.summary-sig-default { color: #501800; }
-
-/* Subclass list
- */
-ul.subclass-list { display: inline; }
-ul.subclass-list li { display: inline; }
-
-/* To render variables, classes etc. like functions */
-table.summary .summary-name { color: #006080; font-weight: bold;
- font-family: monospace; }
-table.summary
- a.summary-name:link { color: #006080; font-weight: bold;
- font-family: monospace; }
-table.summary
- a.summary-name:visited { color: #006080; font-weight: bold;
- font-family: monospace; }
-
-/* Variable values
- * - In the 'variable details' sections, each varaible's value is
- * listed in a 'pre.variable' box. The width of this box is
- * restricted to 80 chars; if the value's repr is longer than
- * this it will be wrapped, using a backslash marked with
- * class 'variable-linewrap'. If the value's repr is longer
- * than 3 lines, the rest will be ellided; and an ellipsis
- * marker ('...' marked with 'variable-ellipsis') will be used.
- * - If the value is a string, its quote marks will be marked
- * with 'variable-quote'.
- * - If the variable is a regexp, it is syntax-highlighted using
- * the re* CSS classes.
- */
-pre.variable { padding: .5em; margin: 0;
- background: #dce4ec; color: #000000;
- border: 1px solid #708890; }
-.variable-linewrap { color: #604000; font-weight: bold; }
-.variable-ellipsis { color: #604000; font-weight: bold; }
-.variable-quote { color: #604000; font-weight: bold; }
-.variable-group { color: #008000; font-weight: bold; }
-.variable-op { color: #604000; font-weight: bold; }
-.variable-string { color: #006030; }
-.variable-unknown { color: #a00000; font-weight: bold; }
-.re { color: #000000; }
-.re-char { color: #006030; }
-.re-op { color: #600000; }
-.re-group { color: #003060; }
-.re-ref { color: #404040; }
-
-/* Base tree
- * - Used by class pages to display the base class hierarchy.
- */
-pre.base-tree { font-size: 80%; margin: 0; }
-
-/* Frames-based table of contents headers
- * - Consists of two frames: one for selecting modules; and
- * the other listing the contents of the selected module.
- * - h1.toc is used for each frame's heading
- * - h2.toc is used for subheadings within each frame.
- */
-h1.toc { text-align: center; font-size: 105%;
- margin: 0; font-weight: bold;
- padding: 0; }
-h2.toc { font-size: 100%; font-weight: bold;
- margin: 0.5em 0 0 -0.3em; }
-
-/* Syntax Highlighting for Source Code
- * - doctest examples are displayed in a 'pre.py-doctest' block.
- * If the example is in a details table entry, then it will use
- * the colors specified by the 'table pre.py-doctest' line.
- * - Source code listings are displayed in a 'pre.py-src' block.
- * Each line is marked with 'span.py-line' (used to draw a line
- * down the left margin, separating the code from the line
- * numbers). Line numbers are displayed with 'span.py-lineno'.
- * The expand/collapse block toggle button is displayed with
- * 'a.py-toggle' (Note: the CSS style for 'a.py-toggle' should not
- * modify the font size of the text.)
- * - If a source code page is opened with an anchor, then the
- * corresponding code block will be highlighted. The code
- * block's header is highlighted with 'py-highlight-hdr'; and
- * the code block's body is highlighted with 'py-highlight'.
- * - The remaining py-* classes are used to perform syntax
- * highlighting (py-string for string literals, py-name for names,
- * etc.)
- */
-pre.py-doctest { padding: .5em; margin: 1em;
- background: #e8f0f8; color: #000000;
- border: 1px solid #708890; }
-table pre.py-doctest { background: #dce4ec;
- color: #000000; }
-pre.py-src { border: 2px solid #000000;
- background: #f0f0f0; color: #000000; }
-.py-line { border-left: 2px solid #000000;
- margin-left: .2em; padding-left: .4em; }
-.py-lineno { font-style: italic; font-size: 90%;
- padding-left: .5em; }
-a.py-toggle { text-decoration: none; }
-div.py-highlight-hdr { border-top: 2px solid #000000;
- border-bottom: 2px solid #000000;
- background: #d8e8e8; }
-div.py-highlight { border-bottom: 2px solid #000000;
- background: #d0e0e0; }
-.py-prompt { color: #005050; font-weight: bold;}
-.py-more { color: #005050; font-weight: bold;}
-.py-string { color: #006030; }
-.py-comment { color: #003060; }
-.py-keyword { color: #600000; }
-.py-output { color: #404040; }
-.py-name { color: #000050; }
-.py-name:link { color: #000050 !important; }
-.py-name:visited { color: #000050 !important; }
-.py-number { color: #005000; }
-.py-defname { color: #000060; font-weight: bold; }
-.py-def-name { color: #000060; font-weight: bold; }
-.py-base-class { color: #000060; }
-.py-param { color: #000060; }
-.py-docstring { color: #006030; }
-.py-decorator { color: #804020; }
-/* Use this if you don't want links to names underlined: */
-/*a.py-name { text-decoration: none; }*/
-
-/* Graphs & Diagrams
- * - These CSS styles are used for graphs & diagrams generated using
- * Graphviz dot. 'img.graph-without-title' is used for bare
- * diagrams (to remove the border created by making the image
- * clickable).
- */
-img.graph-without-title { border: none; }
-img.graph-with-title { border: 1px solid #000000; }
-span.graph-title { font-weight: bold; }
-span.graph-caption { }
-
-/* General-purpose classes
- * - 'p.indent-wrapped-lines' defines a paragraph whose first line
- * is not indented, but whose subsequent lines are.
- * - The 'nomargin-top' class is used to remove the top margin (e.g.
- * from lists). The 'nomargin' class is used to remove both the
- * top and bottom margin (but not the left or right margin --
- * for lists, that would cause the bullets to disappear.)
- */
-p.indent-wrapped-lines { padding: 0 0 0 7em; text-indent: -7em;
- margin: 0; }
-.nomargin-top { margin-top: 0; }
-.nomargin { margin-top: 0; margin-bottom: 0; }
-
-/* HTML Log */
-div.log-block { padding: 0; margin: .5em 0 .5em 0;
- background: #e8f0f8; color: #000000;
- border: 1px solid #000000; }
-div.log-error { padding: .1em .3em .1em .3em; margin: 4px;
- background: #ffb0b0; color: #000000;
- border: 1px solid #000000; }
-div.log-warning { padding: .1em .3em .1em .3em; margin: 4px;
- background: #ffffb0; color: #000000;
- border: 1px solid #000000; }
-div.log-info { padding: .1em .3em .1em .3em; margin: 4px;
- background: #b0ffb0; color: #000000;
- border: 1px solid #000000; }
-h2.log-hdr { background: #70b0ff; color: #000000;
- margin: 0; padding: 0em 0.5em 0em 0.5em;
- border-bottom: 1px solid #000000; font-size: 110%; }
-p.log { font-weight: bold; margin: .5em 0 .5em 0; }
-tr.opt-changed { color: #000000; font-weight: bold; }
-tr.opt-default { color: #606060; }
-pre.log { margin: 0; padding: 0; padding-left: 1em; }
diff --git a/python_apps/soundcloud-api/docs/api/epydoc.js b/python_apps/soundcloud-api/docs/api/epydoc.js
deleted file mode 100644
index e787dbcf4..000000000
--- a/python_apps/soundcloud-api/docs/api/epydoc.js
+++ /dev/null
@@ -1,293 +0,0 @@
-function toggle_private() {
- // Search for any private/public links on this page. Store
- // their old text in "cmd," so we will know what action to
- // take; and change their text to the opposite action.
- var cmd = "?";
- var elts = document.getElementsByTagName("a");
- for(var i=0; i";
- s += " ";
- for (var i=0; i... ";
- elt.innerHTML = s;
- }
-}
-
-function toggle(id) {
- elt = document.getElementById(id+"-toggle");
- if (elt.innerHTML == "-")
- collapse(id);
- else
- expand(id);
- return false;
-}
-
-function highlight(id) {
- var elt = document.getElementById(id+"-def");
- if (elt) elt.className = "py-highlight-hdr";
- var elt = document.getElementById(id+"-expanded");
- if (elt) elt.className = "py-highlight";
- var elt = document.getElementById(id+"-collapsed");
- if (elt) elt.className = "py-highlight";
-}
-
-function num_lines(s) {
- var n = 1;
- var pos = s.indexOf("\n");
- while ( pos > 0) {
- n += 1;
- pos = s.indexOf("\n", pos+1);
- }
- return n;
-}
-
-// Collapse all blocks that mave more than `min_lines` lines.
-function collapse_all(min_lines) {
- var elts = document.getElementsByTagName("div");
- for (var i=0; i 0)
- if (elt.id.substring(split, elt.id.length) == "-expanded")
- if (num_lines(elt.innerHTML) > min_lines)
- collapse(elt.id.substring(0, split));
- }
-}
-
-function expandto(href) {
- var start = href.indexOf("#")+1;
- if (start != 0 && start != href.length) {
- if (href.substring(start, href.length) != "-") {
- collapse_all(4);
- pos = href.indexOf(".", start);
- while (pos != -1) {
- var id = href.substring(start, pos);
- expand(id);
- pos = href.indexOf(".", pos+1);
- }
- var id = href.substring(start, href.length);
- expand(id);
- highlight(id);
- }
- }
-}
-
-function kill_doclink(id) {
- var parent = document.getElementById(id);
- parent.removeChild(parent.childNodes.item(0));
-}
-function auto_kill_doclink(ev) {
- if (!ev) var ev = window.event;
- if (!this.contains(ev.toElement)) {
- var parent = document.getElementById(this.parentID);
- parent.removeChild(parent.childNodes.item(0));
- }
-}
-
-function doclink(id, name, targets_id) {
- var elt = document.getElementById(id);
-
- // If we already opened the box, then destroy it.
- // (This case should never occur, but leave it in just in case.)
- if (elt.childNodes.length > 1) {
- elt.removeChild(elt.childNodes.item(0));
- }
- else {
- // The outer box: relative + inline positioning.
- var box1 = document.createElement("div");
- box1.style.position = "relative";
- box1.style.display = "inline";
- box1.style.top = 0;
- box1.style.left = 0;
-
- // A shadow for fun
- var shadow = document.createElement("div");
- shadow.style.position = "absolute";
- shadow.style.left = "-1.3em";
- shadow.style.top = "-1.3em";
- shadow.style.background = "#404040";
-
- // The inner box: absolute positioning.
- var box2 = document.createElement("div");
- box2.style.position = "relative";
- box2.style.border = "1px solid #a0a0a0";
- box2.style.left = "-.2em";
- box2.style.top = "-.2em";
- box2.style.background = "white";
- box2.style.padding = ".3em .4em .3em .4em";
- box2.style.fontStyle = "normal";
- box2.onmouseout=auto_kill_doclink;
- box2.parentID = id;
-
- // Get the targets
- var targets_elt = document.getElementById(targets_id);
- var targets = targets_elt.getAttribute("targets");
- var links = "";
- target_list = targets.split(",");
- for (var i=0; i" +
- target[0] + "";
- }
-
- // Put it all together.
- elt.insertBefore(box1, elt.childNodes.item(0));
- //box1.appendChild(box2);
- box1.appendChild(shadow);
- shadow.appendChild(box2);
- box2.innerHTML =
- "Which "+name+" do you want to see documentation for?" +
- "
This document contains the API (Application Programming Interface)
-documentation for SoundCloud API. Documentation for the Python
-objects defined by the project is divided into separate pages for each
-package, module, and class. The API documentation also includes two
-pages containing information about the project as a whole: a trees
-page, and an index page.
-
-
Object Documentation
-
-
Each Package Documentation page contains:
-
-
A description of the package.
-
A list of the modules and sub-packages contained by the
- package.
-
A summary of the classes defined by the package.
-
A summary of the functions defined by the package.
-
A summary of the variables defined by the package.
-
A detailed description of each function defined by the
- package.
-
A detailed description of each variable defined by the
- package.
-
-
-
Each Module Documentation page contains:
-
-
A description of the module.
-
A summary of the classes defined by the module.
-
A summary of the functions defined by the module.
-
A summary of the variables defined by the module.
-
A detailed description of each function defined by the
- module.
-
A detailed description of each variable defined by the
- module.
-
-
-
Each Class Documentation page contains:
-
-
A class inheritance diagram.
-
A list of known subclasses.
-
A description of the class.
-
A summary of the methods defined by the class.
-
A summary of the instance variables defined by the class.
-
A summary of the class (static) variables defined by the
- class.
-
A detailed description of each method defined by the
- class.
-
A detailed description of each instance variable defined by the
- class.
-
A detailed description of each class (static) variable defined
- by the class.
-
-
-
Project Documentation
-
-
The Trees page contains the module and class hierarchies:
-
-
The module hierarchy lists every package and module, with
- modules grouped into packages. At the top level, and within each
- package, modules and sub-packages are listed alphabetically.
-
The class hierarchy lists every class, grouped by base
- class. If a class has more than one base class, then it will be
- listed under each base class. At the top level, and under each base
- class, classes are listed alphabetically.
-
-
-
The Index page contains indices of terms and
- identifiers:
-
-
The term index lists every term indexed by any object's
- documentation. For each term, the index provides links to each
- place where the term is indexed.
-
The identifier index lists the (short) name of every package,
- module, class, method, function, variable, and parameter. For each
- identifier, the index provides a short description, and a link to
- its documentation.
-
-
-
The Table of Contents
-
-
The table of contents occupies the two frames on the left side of
-the window. The upper-left frame displays the project
-contents, and the lower-left frame displays the module
-contents:
-
-
-
-
- Project Contents...
-
- API Documentation Frame
-
-
-
-
- Module Contents ...
-
-
-
-
-
The project contents frame contains a list of all packages
-and modules that are defined by the project. Clicking on an entry
-will display its contents in the module contents frame. Clicking on a
-special entry, labeled "Everything," will display the contents of
-the entire project.
-
-
The module contents frame contains a list of every
-submodule, class, type, exception, function, and variable defined by a
-module or package. Clicking on an entry will display its
-documentation in the API documentation frame. Clicking on the name of
-the module, at the top of the frame, will display the documentation
-for the module itself.
-
-
The "frames" and "no frames" buttons below the top
-navigation bar can be used to control whether the table of contents is
-displayed or not.
-
-
The Navigation Bar
-
-
A navigation bar is located at the top and bottom of every page.
-It indicates what type of page you are currently viewing, and allows
-you to go to related pages. The following table describes the labels
-on the navigation bar. Note that not some labels (such as
-[Parent]) are not displayed on all pages.
-
-
-
-
Label
-
Highlighted when...
-
Links to...
-
-
[Parent]
-
(never highlighted)
-
the parent of the current package
-
[Package]
-
viewing a package
-
the package containing the current object
-
-
[Module]
-
viewing a module
-
the module containing the current object
-
-
[Class]
-
viewing a class
-
the class containing the current object
-
[Trees]
-
viewing the trees page
-
the trees page
-
[Index]
-
viewing the index page
-
the index page
-
[Help]
-
viewing the help page
-
the help page
-
-
-
The "show private" and "hide private" buttons below
-the top navigation bar can be used to control whether documentation
-for private objects is displayed. Private objects are usually defined
-as objects whose (short) names begin with a single underscore, but do
-not end with an underscore. For example, "_x",
-"__pprint", and "epydoc.epytext._tokenize"
-are private objects; but "re.sub",
-"__init__", and "type_" are not. However,
-if a module defines the "__all__" variable, then its
-contents are used to decide which objects are private.
-
-
A timestamp below the bottom navigation bar indicates when each
-page was last updated.
When javascript is enabled, this page will redirect URLs of
-the form redirect.html#dotted.name to the
-documentation for the object with the given fully-qualified
-dotted name.
- 1## SouncCloudAPI implements a Python wrapper around the SoundCloud RESTful
- 2## API
- 3##
- 4## Copyright (C) 2008 Diez B. Roggisch
- 5## Contact mailto:deets@soundcloud.com
- 6##
- 7## This library is free software; you can redistribute it and/or
- 8## modify it under the terms of the GNU Lesser General Public
- 9## License as published by the Free Software Foundation; either
- 10## version 2.1 of the License, or (at your option) any later version.
- 11##
- 12## This library is distributed in the hope that it will be useful,
- 13## but WITHOUT ANY WARRANTY; without even the implied warranty of
- 14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- 15## Lesser General Public License for more details.
- 16##
- 17## You should have received a copy of the GNU Lesser General Public
- 18## License along with this library; if not, write to the Free Software
- 19## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
- 20
- 21importurllib
- 22importurllib2
- 23
- 24importlogging
- 25importsimplejson
- 26importcgi
- 27fromscapi.MultipartPostHandlerimportMultipartPostHandler
- 28frominspectimportisclass
- 29importurlparse
- 30fromscapi.authenticationimportBasicAuthenticator
- 31fromscapi.utilimport(
- 32escape,
- 33MultiDict,
- 34)
- 35
- 36logging.basicConfig()
- 37logger=logging.getLogger(__name__)
- 38
- 39USE_PROXY=False
- 40"""
- 41Something like http://127.0.0.1:10000/
- 42"""
- 43PROXY=''
- 44
- 45
- 46
- 47"""
- 48The url Soundcould offers to obtain request-tokens
- 49"""
- 50REQUEST_TOKEN_URL='http://api.soundcloud.com/oauth/request_token'
- 51"""
- 52The url Soundcould offers to exchange access-tokens for request-tokens.
- 53"""
- 54ACCESS_TOKEN_URL='http://api.soundcloud.com/oauth/access_token'
- 55"""
- 56The url Soundcould offers to make users authorize a concrete request token.
- 57"""
- 58AUTHORIZATION_URL='http://api.soundcloud.com/oauth/authorize'
- 59
- 60__all__=['SoundCloudAPI','USE_PROXY','PROXY','REQUEST_TOKEN_URL','ACCESS_TOKEN_URL','AUTHORIZATION_URL']
-
93"""
- 94 The ApiConnector holds all the data necessary to authenticate against
- 95 the soundcloud-api. You can instantiate several connectors if you like, but usually one
- 96 should be sufficient.
- 97 """
- 98
- 99"""
-100 SoundClound imposes a maximum on the number of returned items. This value is that
-101 maximum.
-102 """
-103LIST_LIMIT=50
-104
-105"""
-106 The query-parameter that is used to request results beginning from a certain offset.
-107 """
-108LIST_OFFSET_PARAMETER='offset'
-109"""
-110 The query-parameter that is used to request results being limited to a certain amount.
-111
-112 Currently this is of no use and just for completeness sake.
-113 """
-114LIST_LIMIT_PARAMETER='limit'
-115
-
117"""
-118 Constructor for the API-Singleton. Use it once with parameters, and then the
-119 subsequent calls internal to the API will work.
-120
-121 @type host: str
-122 @param host: the host to connect to, e.g. "api.soundcloud.com". If a port is needed, use
-123 "api.soundcloud.com:1234"
-124 @type user: str
-125 @param user: if given, the username for basic HTTP authentication
-126 @type password: str
-127 @param password: if the user is given, you have to give a password as well
-128 @type authenticator: OAuthAuthenticator | BasicAuthenticator
-129 @param authenticator: the authenticator to use, see L{scapi.authentication}
-130 """
-131self.host=host
-132ifauthenticatorisnotNone:
-133self.authenticator=authenticator
-134elifuserisnotNoneandpasswordisnotNone:
-135self.authenticator=BasicAuthenticator(user,password)
-136self._base=base
-137self.collapse_scope=collapse_scope
-
140"""
-141 This method will take a method that has been part of a redirect of some sort
-142 and see if it's valid, which means that it's located beneath our base.
-143 If yes, we return it normalized without that very base.
-144 """
-145_,_,path,_,_,_=urlparse.urlparse(method)
-146ifpath.startswith("/"):
-147path=path[1:]
-148# if the base is "", we return the whole path,
-149# otherwise normalize it away
-150ifself._base=="":
-151returnpath
-152ifpath.startswith(self._base):
-153returnpath[len(self._base)-1:]
-154raiseInvalidMethodException("Not a valid API method: %s"%method)
-
214"""
-215 Simple helper function to generate the url needed
-216 to ask a user for request token authorization.
-217
-218 See also L{fetch_request_token}.
-219
-220 Possible usage:
-221
-222 >>> import webbrowser
-223 >>> sca = scapi.ApiConnector()
-224 >>> authorization_url = sca.get_request_token_authorization_url(token)
-225 >>> webbrowser.open(authorization_url)
-226 """
-227return"%s?oauth_token=%s"%(AUTHORIZATION_URL,token)
-
238"""
-239 In case of return-code 303 (See-other), we have to store the location we got
-240 because that will determine the actual type of resource returned.
-241 """
-242self.alternate_method=hdrs['location']
-243# for oauth, we need to re-create the whole header-shizzle. This
-244# does it - it recreates a full url and signs the request
-245new_url=self.alternate_method
-246# if USE_PROXY:
-247# import pdb; pdb.set_trace()
-248# old_url = req.get_full_url()
-249# protocol, host, _, _, _, _ = urlparse.urlparse(old_url)
-250# new_url = urlparse.urlunparse((protocol, host, self.alternate_method, None, None, None))
-251req=req.recreate_request(new_url)
-252returnurllib2.HTTPRedirectHandler.http_error_303(self,req,fp,code,msg,hdrs)
-
255"""
-256 We fake a 201 being a 303 so that our redirection-scheme takes place
-257 for the 201 the API throws in case we created something. If the location is
-258 not available though, that means that whatever we created has succeded - without
-259 being a named resource. Assigning an asset to a track is an example of such
-260 case.
-261 """
-262if'location'notinhdrs:
-263raiseNoResultFromRequest()
-264returnself.http_error_303(req,fp,303,msg,hdrs)
-
267"""
-268 The basic means to query and create resources. The Scope uses the L{ApiConnector} to
-269 create the proper URIs for querying or creating resources.
-270
-271 For accessing resources from the root level, you explcitly create a Scope and pass it
-272 an L{ApiConnector}-instance. Then you can query it
-273 or create new resources like this:
-274
-275 >>> connector = scapi.ApiConnector(host='host', user='user', password='password') # initialize the API
-276 >>> scope = scapi.Scope(connector) # get the root scope
-277 >>> users = list(scope.users())
-278 [<scapi.User object at 0x12345>, ...]
-279
-280 Please not that all resources that are lists are returned as B{generator}. So you need
-281 to either iterate over them, or call list(resources) on them.
-282
-283 When accessing resources that belong to another resource, like contacts of a user, you access
-284 the parent's resource scope implicitly through the resource instance like this:
-285
-286 >>> user = scope.users().next()
-287 >>> list(user.contacts())
-288 [<scapi.Contact object at 0x12345>, ...]
-289
-290 """
-
292"""
-293 Create the Scope. It can have a resource as scope, and possibly a parent-scope.
-294
-295 @param connector: The connector to use.
-296 @type connector: ApiConnector
-297 @type scope: scapi.RESTBase
-298 @param scope: the resource to make this scope belong to
-299 @type parent: scapi.Scope
-300 @param parent: the parent scope of this scope
-301 """
-302
-303ifscopeisNone:
-304scope=()
-305else:
-306scope=scope,
-307ifparentisnotNone:
-308scope=parent._scope+scope
-309self._scope=scope
-310self._connector=connector
-
317"""
-318 This method will take an arbitrary url, and rewrite it
-319 so that the current authenticator's oauth-headers are appended
-320 as query-parameters.
-321
-322 This is used in streaming and downloading, because those content
-323 isn't served from the SoundCloud servers themselves.
-324
-325 A usage example would look like this:
-326
-327 >>> sca = scapi.Scope(connector)
-328 >>> track = sca.tracks(params={
-329 "filter" : "downloadable",
-330 }).next()
-331
-332
-333 >>> download_url = track.download_url
-334 >>> signed_url = track.oauth_sign_get_request(download_url)
-335 >>> data = urllib2.urlopen(signed_url).read()
-336
-337 """
-338scheme,netloc,path,params,query,fragment=urlparse.urlparse(url)
-339
-340req=urllib2.Request(url)
-341
-342all_params={}
-343ifquery:
-344all_params.update(cgi.parse_qs(query))
-345
-346ifnotall_params:
-347all_params=None
-348
-349self._connector.authenticator.augment_request(req,all_params,False)
-350
-351auth_header=req.get_header("Authorization")
-352auth_header=auth_header[len("OAuth "):]
-353
-354query_params=[]
-355ifquery:
-356query_params.append(query)
-357
-358forpartinauth_header.split(","):
-359key,value=part.split("=")
-360assertkey.startswith("oauth")
-361value=value[1:-1]
-362query_params.append("%s=%s"%(key,value))
-363
-364query="&".join(query_params)
-365url=urlparse.urlunparse((scheme,netloc,path,params,query,fragment))
-366returnurl
-
370"""
-371 This method returnes the urllib2.Request to perform the actual HTTP-request.
-372
-373 We return a subclass that overload the get_method-method to return a custom method like "PUT".
-374 Additionally, the request is enhanced with the current authenticators authorization scheme
-375 headers.
-376
-377 @param url: the destination url
-378 @param connector: our connector-instance
-379 @param parameters: the POST-parameters to use.
-380 @type parameters: None|dict<str, basestring|list<basestring>>
-381 @param queryparams: the queryparams to use
-382 @type queryparams: None|dict<str, basestring|list<basestring>>
-383 @param alternate_http_method: an alternate HTTP-method to use
-384 @type alternate_http_method: str
-385 @return: the fully equipped request
-386 @rtype: urllib2.Request
-387 """
-388classMyRequest(urllib2.Request):
-389defget_method(self):
-390ifalternate_http_methodisnotNone:
-391returnalternate_http_method
-392returnurllib2.Request.get_method(self)
-
418"""
-419 Small helpermethod to create the querystring from a dict.
-420
-421 @type queryparams: None|dict<str, basestring|list<basestring>>
-422 @param queryparams: the queryparameters.
-423 @return: either the empty string, or a "?" followed by the parameters joined by "&"
-424 @rtype: str
-425 """
-426ifnotqueryparams:
-427return""
-428h=[]
-429forkey,valuesinqueryparams.iteritems():
-430ifisinstance(values,(int,long,float)):
-431values=str(values)
-432ifisinstance(values,basestring):
-433values=[values]
-434forvinvalues:
-435v=v.encode("utf-8")
-436h.append("%s=%s"%(key,escape(v)))
-437return"?"+"&".join(h)
-
441"""
-442 The workhorse. It's complicated, convoluted and beyond understanding of a mortal being.
-443
-444 You have been warned.
-445 """
-446
-447queryparams={}
-448__offset__=ApiConnector.LIST_LIMIT
-449if"__offset__"inkwargs:
-450offset=kwargs.pop("__offset__")
-451queryparams['offset']=offset
-452__offset__=offset+ApiConnector.LIST_LIMIT
-453
-454if"params"inkwargs:
-455queryparams.update(kwargs.pop("params"))
-456
-457# create a closure to invoke this method again with a greater offset
-458_cl_method=method
-459_cl_args=tuple(args)
-460_cl_kwargs={}
-461_cl_kwargs.update(kwargs)
-462_cl_kwargs["__offset__"]=__offset__
-463defcontinue_list_fetching():
-464returnself._call(method,*_cl_args,**_cl_kwargs)
-
465connector=self._get_connector()
-466deffilelike(v):
-467ifisinstance(v,file):
-468returnTrue
-469ifhasattr(v,"read"):
-470returnTrue
-471returnFalse
-472alternate_http_method=None
-473if"_alternate_http_method"inkwargs:
-474alternate_http_method=kwargs.pop("_alternate_http_method")
-475urlparams=kwargsifkwargselseNone
-476use_multipart=False
-477ifurlparamsisnotNone:
-478fileargs=dict((key,value)forkey,valueinurlparams.iteritems()iffilelike(value))
-479use_multipart=bool(fileargs)
-480
-481# ensure the method has a trailing /
-482ifmethod[-1]!="/":
-483method=method+"/"
-484ifargs:
-485method="%s%s"%(method,"/".join(str(a)forainargs))
-486
-487scope=''
-488ifself._scope:
-489scopes=self._scope
-490ifconnector.collapse_scope:
-491scopes=scopes[-1:]
-492scope="/".join([sc._scope()forscinscopes])+"/"
-493url="http://%(host)s/%(base)s%(scope)s%(method)s%(queryparams)s"%dict(host=connector.host,method=method,base=connector._base,scope=scope,queryparams=self._create_query_string(queryparams))
-494
-495# we need to install SCRedirectHandler
-496# to gather possible See-Other redirects
-497# so that we can exchange our method
-498redirect_handler=SCRedirectHandler()
-499handlers=[redirect_handler]
-500ifUSE_PROXY:
-501handlers.append(urllib2.ProxyHandler({'http':PROXY}))
-502req=self._create_request(url,connector,urlparams,queryparams,alternate_http_method,use_multipart)
-503
-504http_method=req.get_method()
-505ifurlparamsisnotNone:
-506logger.debug("Posting url: %s, method: %s",url,http_method)
-507else:
-508logger.debug("Fetching url: %s, method: %s",url,http_method)
-509
-510
-511ifuse_multipart:
-512handlers.extend([MultipartPostHandler])
-513else:
-514ifurlparamsisnotNone:
-515urlparams=urllib.urlencode(urlparams.items(),True)
-516opener=urllib2.build_opener(*handlers)
-517try:
-518handle=opener.open(req,urlparams)
-519exceptNoResultFromRequest:
-520returnNone
-521excepturllib2.HTTPError,e:
-522ifhttp_method=="GET"ande.code==404:
-523returnNone
-524raise
-525
-526info=handle.info()
-527ct=info['Content-Type']
-528content=handle.read()
-529logger.debug("Content-type:%s",ct)
-530logger.debug("Request Content:\n%s",content)
-531ifredirect_handler.alternate_methodisnotNone:
-532method=connector.normalize_method(redirect_handler.alternate_method)
-533logger.debug("Method changed through redirect to: <%s>",method)
-534
-535try:
-536if"application/json"inct:
-537content=content.strip()
-538ifnotcontent:
-539content="{}"
-540try:
-541res=simplejson.loads(content)
-542except:
-543logger.error("Couldn't decode returned json")
-544logger.error(content)
-545raise
-546res=self._map(res,method,continue_list_fetching)
-547returnres
-548eliflen(content)<=1:
-549# this might be the famous SeeOtherSpecialCase which means that
-550# all that matters is just the method
-551pass
-552raiseUnknownContentType("%s, returned:\n%s"%(ct,content))
-553finally:
-554handle.close()
-555
-
557"""
-558 This method will take the JSON-result of a HTTP-call and return our domain-objects.
-559
-560 It's also deep magic, don't look.
-561 """
-562pathparts=reversed(method.split("/"))
-563stack=[]
-564forpartinpathparts:
-565stack.append(part)
-566ifpartinRESTBase.REGISTRY:
-567cls=RESTBase.REGISTRY[part]
-568# multiple objects
-569ifisinstance(res,list):
-570defresult_gen():
-571count=0
-572foriteminres:
-573yieldcls(item,self,stack)
-574count+=1
-575ifcount==ApiConnector.LIST_LIMIT:
-576foritemincontinue_list_fetching():
-577yielditem
-
578returnresult_gen()
-579else:
-580returncls(res,self,stack)
-581logger.debug("don't know how to handle result")
-582logger.debug(res)
-583returnres
-584
-
586"""
-587 Retrieve an API-method or a scoped domain-class.
-588
-589 If the former, result is a callable that supports the following invocations:
-590
-591 - calling (...), with possible arguments (positional/keyword), return the resulting resource or list of resources.
-592 When calling, you can pass a keyword-argument B{params}. This must be a dict or L{MultiDict} and will be used to add additional query-get-parameters.
-593
-594 - invoking append(resource) on it will PUT the resource, making it part of the current resource. Makes
-595 sense only if it's a collection of course.
-596
-597 - invoking remove(resource) on it will DELETE the resource from it's container. Also only usable on collections.
-598
-599 TODO: describe the latter
-600 """
-601scope=self
-602
-603classapi_call(object):
-604def__call__(selfish,*args,**kwargs):
-605returnself._call(_name,*args,**kwargs)
-
606
-607defnew(self,**kwargs):
-608"""
-609 Will invoke the new method on the named resource _name, with
-610 self as scope.
-611 """
-612cls=RESTBase.REGISTRY[_name]
-613returncls.new(scope,**kwargs)
-614
-615defappend(selfish,resource):
-616"""
-617 If the current scope is
-618 """
-619self._call(_name,str(resource.id),_alternate_http_method="PUT")
-620
-621defremove(selfish,resource):
-622self._call(_name,str(resource.id),_alternate_http_method="DELETE")
-623
-624if_nameinRESTBase.ALL_DOMAIN_CLASSES:
-625cls=RESTBase.ALL_DOMAIN_CLASSES[_name]
-626
-627classScopeBinder(object):
-628defnew(self,*args,**data):
-629
-630d=MultiDict()
-631name=cls._singleton()
-632
-633defunfold_value(key,value):
-634ifisinstance(value,(basestring,file)):
-635d.add(key,value)
-636elifisinstance(value,dict):
-637forsub_key,sub_valueinvalue.iteritems():
-638unfold_value("%s[%s]"%(key,sub_key),sub_value)
-639else:
-640# assume iteration else
-641forsub_valueinvalue:
-642unfold_value(key+"[]",sub_value)
-643
-644
-645forkey,valueindata.iteritems():
-646unfold_value("%s[%s]"%(name,key),value)
-647
-648returnscope._call(cls.KIND,**d)
-649
-650defcreate(self,**data):
-651returncls.create(scope,**data)
-652
-653defget(self,id):
-654returncls.get(scope,id)
-655
-656
-657returnScopeBinder()
-658returnapi_call()
-659
-
695self.__data=data
-696self.__scope=scope
-697# try and see if we can/must create an id out of our path
-698logger.debug("path_stack: %r",path_stack)
-699ifpath_stack:
-700try:
-701id=int(path_stack[0])
-702self.__data['id']=id
-703exceptValueError:
-704pass
-
722"""
-723 This method is used to set a property, a resource or a list of resources as property of the resource the
-724 method is invoked on.
-725
-726 For example, to set a comment on a track, do
-727
-728 >>> sca = scapi.Scope(connector)
-729 >>> track = scapi.Track.new(title='bar', sharing="private")
-730 >>> comment = scapi.Comment.create(body="This is the body of my comment", timestamp=10)
-731 >>> track.comments = comment
-732
-733 To set a list of users as permissions, do
-734
-735 >>> sca = scapi.Scope(connector)
-736 >>> me = sca.me()
-737 >>> track = scapi.Track.new(title='bar', sharing="private")
-738 >>> users = sca.users()
-739 >>> users_to_set = [user for user in users[:10] if user != me]
-740 >>> track.permissions = users_to_set
-741
-742 And finally, to simply change the title of a track, do
-743
-744 >>> sca = scapi.Scope(connector)
-745 >>> track = sca.Track.get(track_id)
-746 >>> track.title = "new_title"
-747
-748 @param name: the property name
-749 @type name: str
-750 @param value: the property, resource or resources to set
-751 @type value: RESTBase | list<RESTBase> | basestring | long | int | float
-752 @return: None
-753 """
-754
-755# update "private" data, such as __data
-756if"_RESTBase__"inname:
-757self.__dict__[name]=value
-758else:
-759ifisinstance(value,list)andlen(value):
-760# the parametername is something like
-761# permissions[user_id][]
-762# so we try to infer that.
-763parameter_name="%s[%s_id][]"%(name,value[0]._singleton())
-764values=[o.idforoinvalue]
-765kwargs={"_alternate_http_method":"PUT",
-766parameter_name:values}
-767self.__scope._call(self.KIND,self.id,name,**kwargs)
-768elifisinstance(value,RESTBase):
-769# we got a single instance, so make that an argument
-770self.__scope._call(self.KIND,self.id,name,**value._as_arguments())
-771else:
-772# we have a simple property
-773parameter_name="%s[%s]"%(self._singleton(),name)
-774kwargs={"_alternate_http_method":"PUT",
-775parameter_name:self._convert_value(value)}
-776self.__scope._call(self.KIND,self.id,**kwargs)
-
779"""
-780 Converts a resource to a argument-string the way Rails expects it.
-781 """
-782res={}
-783forkey,valueinself.__data.items():
-784value=self._convert_value(value)
-785res["%s[%s]"%(self._singleton(),key)]=value
-786returnres
-
799"""
-800 This is a convenience-method for creating an object that will be passed
-801 as parameter - e.g. a comment. A usage would look like this:
-802
-803 >>> sca = scapi.Scope(connector)
-804 >>> track = sca.Track.new(title='bar', sharing="private")
-805 >>> comment = sca.Comment.create(body="This is the body of my comment", timestamp=10)
-806 >>> track.comments = comment
-807
-808 """
-809returncls(data,scope)
-
813"""
-814 Create a new resource inside a given Scope. The actual values are in data.
-815
-816 So for creating new resources, you have two options:
-817
-818 - create an instance directly using the class:
-819
-820 >>> scope = scapi.Scope(connector)
-821 >>> scope.User.new(...)
-822 <scapi.User object at 0x1234>
-823
-824 - create a instance in a certain scope:
-825
-826 >>> scope = scapi.Scope(connector)
-827 >>> user = scapi.User("1")
-828 >>> track = user.tracks.new()
-829 <scapi.Track object at 0x1234>
-830
-831 @param scope: if not empty, a one-element tuple containing the Scope
-832 @type scope: tuple<Scope>[1]
-833 @param data: the data
-834 @type data: dict
-835 @return: new instance of the resource
-836 """
-837returngetattr(scope,cls.__name__).new(**data)
-
841"""
-842 Fetch a resource by id.
-843
-844 Simply pass a known id as argument. For example
-845
-846 >>> sca = scapi.Scope(connector)
-847 >>> track = sca.Track.get(id)
-848
-849 """
-850returngetattr(scope,cls.KIND)(id)
-
854"""
-855 Return the scope this resource lives in, which is the KIND and id
-856
-857 @return: "<KIND>/<id>"
-858 """
-859return"%s/%s"%(self.KIND,str(self.id))
-
863"""
-864 This method will take a resource name like "users" and
-865 return the single-case, in the example "user".
-866
-867 Currently, it's not very sophisticated, only strips a trailing s.
-868 """
-869name=cls.KIND
-870ifname[-1]=='s':
-871returnname[:-1]
-872raiseValueError("Can't make %s to a singleton"%name)
-
891"""
-892 Test for equality.
-893
-894 Resources are considered equal if the have the same kind and id.
-895 """
-896ifnotisinstance(other,RESTBase):
-897returnFalse
-898res=self.KIND==other.KINDandself.id==other.id
-899returnres
-
937"""
-938 A group domain object/resource
-939 """
-940KIND='groups'
-
941
-
942
-943
-944# this registers all the RESTBase subclasses.
-945# One day using a metaclass will make this a tad
-946# less ugly.
-947-defregister_classes():
-
The ApiConnector holds all the data necessary to authenticate against
- the soundcloud-api. You can instantiate several connectors if you like,
- but usually one should be sufficient.
normalize_method(self,
- method)
- This method will take a method that has been part of a redirect of
- some sort and see if it's valid, which means that it's located
- beneath our base.
fetch_request_token(self,
- url=None,
- oauth_callback='oob',
- oauth_verifier=None)
- Helper-function for a registered consumer to obtain a request token,
- as used by oauth.
get_request_token_authorization_url(self,
- token)
- Simple helper function to generate the url needed to ask a user for
- request token authorization.
This method will take a method that has been part of a redirect of
- some sort and see if it's valid, which means that it's located beneath
- our base. If yes, we return it normalized without that very base.
__setattr__(self,
- name,
- value)
- This method is used to set a property, a resource or a list of
- resources as property of the resource the method is invoked on.
This method is used to set a property, a resource or a list of
- resources as property of the resource the method is invoked on.
-
For example, to set a comment on a track, do
-
->>> sca = scapi.Scope(connector)
->>> track = scapi.Track.new(title='bar', sharing="private")
->>> comment = scapi.Comment.create(body="This is the body of my comment", timestamp=10)
->>> track.comments = comment
-
To set a list of users as permissions, do
-
->>> sca = scapi.Scope(connector)
->>> me = sca.me()
->>> track = scapi.Track.new(title='bar', sharing="private")
->>> users = sca.users()
->>> users_to_set = [user for user in users[:10] if user != me]
->>> track.permissions = users_to_set
-
And finally, to simply change the title of a track, do
This is a convenience-method for creating an object that will be
- passed as parameter - e.g. a comment. A usage would look like this:
-
->>> sca = scapi.Scope(connector)
->>> track = sca.Track.new(title='bar', sharing="private")
->>> comment = sca.Comment.create(body="This is the body of my comment", timestamp=10)
->>> track.comments = comment
http_error_303(self,
- req,
- fp,
- code,
- msg,
- hdrs)
- In case of return-code 303 (See-other), we have to store the location
- we got because that will determine the actual type of resource
- returned.
http_error_201(self,
- req,
- fp,
- code,
- msg,
- hdrs)
- We fake a 201 being a 303 so that our redirection-scheme takes place
- for the 201 the API throws in case we created something.
We fake a 201 being a 303 so that our redirection-scheme takes place
- for the 201 the API throws in case we created something. If the location
- is not available though, that means that whatever we created has succeded
- - without being a named resource. Assigning an asset to a track is an
- example of such case.
The basic means to query and create resources. The Scope uses the ApiConnector to create the proper URIs for
- querying or creating resources.
-
For accessing resources from the root level, you explcitly create a
- Scope and pass it an ApiConnector-instance. Then you can query
- it or create new resources like this:
-
->>> connector = scapi.ApiConnector(host='host', user='user', password='password') # initialize the API
->>> scope = scapi.Scope(connector) # get the root scope
->>> users = list(scope.users())
-[<scapi.User object at 0x12345>, ...]
-
Please not that all resources that are lists are returned as
- generator. So you need to either iterate over them, or call
- list(resources) on them.
-
When accessing resources that belong to another resource, like
- contacts of a user, you access the parent's resource scope implicitly
- through the resource instance like this:
-
->>> user = scope.users().next()
->>> list(user.contacts())
-[<scapi.Contact object at 0x12345>, ...]
oauth_sign_get_request(self,
- url)
- This method will take an arbitrary url, and rewrite it so that the
- current authenticator's oauth-headers are appended as
- query-parameters.
_create_request(self,
- url,
- connector,
- parameters,
- queryparams,
- alternate_http_method=None,
- use_multipart=False)
- This method returnes the urllib2.Request to perform the actual
- HTTP-request.
This method returnes the urllib2.Request to perform the actual
- HTTP-request.
-
We return a subclass that overload the get_method-method to return a
- custom method like "PUT". Additionally, the request is enhanced
- with the current authenticators authorization scheme headers.
-
-
Parameters:
-
-
url - the destination url
-
connector - our connector-instance
-
parameters (None|dict<str, basestring|list<basestring>>) - the POST-parameters to use.
-
queryparams (None|dict<str, basestring|list<basestring>>) - the queryparams to use
-
alternate_http_method (str) - an alternate HTTP-method to use
If the former, result is a callable that supports the following
- invocations:
-
-
- calling (...), with possible arguments (positional/keyword), return
- the resulting resource or list of resources. When calling, you can
- pass a keyword-argument params. This must be a dict or MultiDict and
- will be used to add additional query-get-parameters.
-
-
- invoking append(resource) on it will PUT the resource, making it part
- of the current resource. Makes sense only if it's a collection of
- course.
-
-
- invoking remove(resource) on it will DELETE the resource from it's
- container. Also only usable on collections.
-
- 1## SouncCloudAPI implements a Python wrapper around the SoundCloud RESTful
- 2## API
- 3##
- 4## Copyright (C) 2008 Diez B. Roggisch
- 5## Contact mailto:deets@soundcloud.com
- 6##
- 7## This library is free software; you can redistribute it and/or
- 8## modify it under the terms of the GNU Lesser General Public
- 9## License as published by the Free Software Foundation; either
- 10## version 2.1 of the License, or (at your option) any later version.
- 11##
- 12## This library is distributed in the hope that it will be useful,
- 13## but WITHOUT ANY WARRANTY; without even the implied warranty of
- 14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- 15## Lesser General Public License for more details.
- 16##
- 17## You should have received a copy of the GNU Lesser General Public
- 18## License along with this library; if not, write to the Free Software
- 19## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
- 20
- 21importbase64
- 22importtime,random
- 23importurlparse
- 24importhmac
- 25importhashlib
- 26fromscapi.utilimportescape
- 27importlogging
- 28
- 29
- 30USE_DOUBLE_ESCAPE_HACK=True
- 31"""
- 32There seems to be an uncertainty on the way
- 33parameters are to be escaped. For now, this
- 34variable switches between two escaping mechanisms.
- 35
- 36If True, the passed parameters - GET or POST - are
- 37escaped *twice*.
- 38"""
- 39
- 40logger=logging.getLogger(__name__)
- 41
-
97ifparamsisNone:
- 98params={}
- 99try:
-100# exclude the signature if it exists
-101delparams['oauth_signature']
-102except:
-103pass
-104key_values=[]
-105
-106forkey,valuesinparams.iteritems():
-107ifisinstance(values,file):
-108continue
-109ifisinstance(values,(int,long,float)):
-110values=str(values)
-111ifisinstance(values,(list,tuple)):
-112values=[str(v)forvinvalues]
-113ifisinstance(values,basestring):
-114values=[values]
-115ifUSE_DOUBLE_ESCAPE_HACKandnotkey.startswith("ouath"):
-116key=escape(key)
-117forvinvalues:
-118v=v.encode("utf-8")
-119key=key.encode("utf-8")
-120ifUSE_DOUBLE_ESCAPE_HACKandnotkey.startswith("oauth"):
-121# this is a dirty hack to make the
-122# thing work with the current server-side
-123# implementation. Or is it by spec?
-124v=escape(v)
-125key_values.append(escape("%s=%s"%(key,v)))
-126# sort lexicographically, first after key, then after value
-127key_values.sort()
-128# combine key value pairs in string
-129returnescape('&').join(key_values)
-
- 1importstring
- 2importtypes
- 3
- 4## json.py implements a JSON (http://json.org) reader and writer.
- 5## Copyright (C) 2005 Patrick D. Logan
- 6## Contact mailto:patrickdlogan@stardecisions.com
- 7##
- 8## This library is free software; you can redistribute it and/or
- 9## modify it under the terms of the GNU Lesser General Public
- 10## License as published by the Free Software Foundation; either
- 11## version 2.1 of the License, or (at your option) any later version.
- 12##
- 13## This library is distributed in the hope that it will be useful,
- 14## but WITHOUT ANY WARRANTY; without even the implied warranty of
- 15## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- 16## Lesser General Public License for more details.
- 17##
- 18## You should have received a copy of the GNU Lesser General Public
- 19## License along with this library; if not, write to the Free Software
- 20## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
- 21
- 22
-
153try:
-154result=self.hex_digits[ch.upper()]
-155exceptKeyError:
-156try:
-157result=int(ch)
-158exceptValueError:
-159raiseReadException,"The character %s is not a hex digit."%ch
-160returnresult
-
281sca=self.root
-282user=sca.me()
-283track=sca.Track.new(title='bar',sharing="private",asset_data=self.data)
-284comment=sca.Comment.create(body="This is the body of my comment",timestamp=10)
-285track.comments=comment
-286asserttrack.comments().next().body==comment.body
-
290sca=self.root
-291track=sca.Track.new(title='bar',sharing="private",asset_data=self.data)
-292cbody="This is the body of my comment"
-293track.comments.new(body=cbody,timestamp=10)
-294assertlist(track.comments())[0].body==cbody
-
224sca=ROOT
-225user=sca.me()
-226track=sca.Track.new(title='bar',sharing="private")
-227comment=sca.Comment.create(body="This is the body of my comment",timestamp=10)
-228track.comments=comment
-229asserttrack.comments().next().body==comment.body
-
233sca=ROOT
-234track=sca.Track.new(title='bar',sharing="private")
-235cbody="This is the body of my comment"
-236track.comments.new(body=cbody,timestamp=10)
-237assertlist(track.comments())[0].body==cbody
-
312root=scapi.Scope(CONNECTOR)
-313me=root.me()
-314assertisinstance(me,scapi.User)
-315
-316# now get something *from* that user
-317favorites=list(me.favorites())
-318assertfavorites
-
- 1## SouncCloudAPI implements a Python wrapper around the SoundCloud RESTful
- 2## API
- 3##
- 4## Copyright (C) 2008 Diez B. Roggisch
- 5## Contact mailto:deets@soundcloud.com
- 6##
- 7## This library is free software; you can redistribute it and/or
- 8## modify it under the terms of the GNU Lesser General Public
- 9## License as published by the Free Software Foundation; either
-10## version 2.1 of the License, or (at your option) any later version.
-11##
-12## This library is distributed in the hope that it will be useful,
-13## but WITHOUT ANY WARRANTY; without even the implied warranty of
-14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-15## Lesser General Public License for more details.
-16##
-17## You should have received a copy of the GNU Lesser General Public
-18## License along with this library; if not, write to the Free Software
-19## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-20
-21importurllib
-22
-