Python: Difference between revisions

From kJams Wiki
Jump to navigation Jump to search
No edit summary
No edit summary
 
(49 intermediate revisions by the same user not shown)
Line 1: Line 1:
<pre>
NOTE: Python does not yet work in 64bit!
// CPython.cpp


#include "kVersion.h"
If you're looking for the open-sourced [[Python/Code|C++ Embedding Code]].


#if kUsePython
==How to Use==
#if __WIN32__
# On Mac: it's built in.  On Windows: requires Python 2.7, 32bit.  You can [https://karaoke.kjams.com/downloads/python_installer.msi download it here].
#include "python.h"
# Run kJams 2. Go to the Advanced menu and see the Python submenu
#else
# in your user's Music/kJams folder there is now a Python folder. Put your scripts in there to see them in the Python menu (re-start kJams)
#include <Python/Python.h>
#endif
#endif


#include "stdafx.h"
Note if you find it stops working on mac, got to Terminal and issue this:
#include "CApp.h"
tccutil reset AppleEvents
#include "CPython.h"
This will reset the "are you sure this is okay" security prompt, and the OS should ask if it's okay that kJams control iTunes, be sure to say OK and not deny the request


bool CPython_UsePython()
==Tips==
{
* Pick "Advanced->Python->Reveal “kjams.py”", this will give you a list of enums for commands, server, and reveal other commands you can call
return kUsePython;
* A script named "startup.py" will, if present, be run on startup, so if eg: you always want to ensure some prefs are set correctly, regardless of what someone may have changed, you can use this script to set some prefs the way you like them.
}
* Picking anything from the Python menu will run it.  Holding the alt/option key when picking it will reveal (open) the file instead
* You should open each of the files and look at them for examples
* you can edit your scripts live and re-run them, no need to quit and re-run kJams
* if you create new scripts or remove old ones, or rename one, you must restart kJams to see them in the menu
* always edit your scripts as UTF8, and be sure to include the header "# -*- coding: utf-8 -*- " as the first line.  that way, you can just enter any unicode characters you wish as string literals.  no need to prefix with u, tho that is optional and will work.
* when passing parameters, tuples and lists are considered the same thing to kJams (an array), and are interchangeable.


#if kUsePython
==What's working==
* All the [[Scripting]] commands
* Access to every menu item (including sub menus)
* Access to all preferences including secret prefs (prefs that otherwise have no user interface)
* ability to show a progress bar in the activity window
* ability to kill off a script (stop sign in activity window)
* song meta data editing
* get/set selected playlist
* get/set selection within playlist
* message dialog, with title and message, up to 3 buttons, and 1 check box
* interactive string dialog, title, message, default string, OK, Cancel
* set keyboard shortcuts
* add song to any playlist
* reorder songs within a playlist (including singer)
* new playlist
* ability to show ShowScreens
* ability so script iTunes (get play state, get/set volume)
* Much of the [[Server]] functionality including:
** fetching playlists (eg: Library, Rotation, Venue (list of singers)) etc
** fetching info including venue name
** singer creation and/or login
** getting singers' lists (tonight, history, faves)
** add songs to singer
* [[Python/Crossfade|crossfade with iTunes]]


#include "CTaskMgr.h"
==What's Coming==
* remove song from playlist
* whatever else you need


#define kEmbeddedModuleName "kjams"
==Ideas==
* auto cross fade with your favorite DJ app
* Twitter feed - Current singer and song
* a Rate the singer app for iPhone or 'Droid
* update web site with current stats
* load some videos playing behind the kJams window (with transparency), with VLC or QuickTime Player
* fix prefs on startup
* make your own iOS or Droid app that interacts with kJams
==How to modify an existing script==
The TL;DR is: make a copy of the existing script and modify that


#define kCustomPrint "custom_print"
# Hold the option/alt key and pick the script to reveal it
#define kCustomErr "custom_err"
# Mac:
#define kCommand "do_command"
## revealing will open your script in a text editor
#define kStringCommand "do_command_str"
# Windows:
#define kDoMenuCommand "do_menu_command"
## revealing will show the script file in Windows Explorer
#define kDoMenuName "do_menu_name"
## open that file in a text editor
 
# in the File menu of your text editor, pick "Save As..."
#define kStartupScriptName "startup.py"
# go to your Python folder here: /Users/<you>/Music/kJams/Python/
#define kUserAbortStr "User Abort"
# Re-name the file so you can distinguish it from the built-in one (eg: prepend "My " to it's name)
 
# save it in that location with the new name
/**********************************************/
class ScEnsureGIL {
PyGILState_STATE i_state;
public:
ScEnsureGIL() :
i_state(PyGILState_Ensure())
{ }
~ScEnsureGIL() {
PyGILState_Release(i_state);
}
};
 
class ScReleaseGIL {
public:
PyThreadState *i_stateP;
 
ScReleaseGIL() :
i_stateP(PyEval_SaveThread())
{ }
~ScReleaseGIL() {
PyEval_RestoreThread(i_stateP);
}
};
 
class ScPyObject {
PyObject *i_objP;
public:
ScPyObject(PyObject *objP) : i_objP(objP) {}
~ScPyObject() {
Py_DECREF(i_objP);
}
operator PyObject*() {
return i_objP;
}
};
 
/**********************************************/
class CPython;
class CT_RunScript;
 
typedef std::vector<CT_RunScript *> ScriptVec;
 
/*
this thread runs in the background, serving as the
"main event loop" for all python scripts
it runs an "idle" on each script every 1/4 second
to look for user-aborted threads, and if found,
causes an exception to be thrown within that thread
*/
class CPython_RunLoop : public CT_Preemptive {
public:
CPython *thiz;
CMutex_bool i_abortB;
CMutexT<ScriptVec *> i_scriptVecP;
CPython_RunLoop(CPython *in_thiz);
virtual OSStatus operator()(OSStatus err);
void RunScript(const char *unf8NameZ, const char *utf8ScriptZ);
void AddScript(CT_RunScript *scriptP);
void RemoveScript(CT_RunScript *scriptP);
void IdleScripts();
size_t CountScripts();
};
 
/*
this is the main Python object the app uses to
communicate with the python runloop above
*/
class CPython {
friend class CPython_RunLoop;
static CPython *s_pythonP;
CMutexT<bool> i_inittedB;
std::string i_appName;
CMutexT<CPython_RunLoop*> i_runLoopP;
 
public:
CPython(const char *appNameZ);
~CPython();
static CPython* Get(const char *appNameZ = NULL);
/****************************************************/
void Startup();
void Test();
};
 
//static
CPython* CPython::s_pythonP = NULL;
 
/*********************************************************************/
struct ThisRec {
CT_RunScript *i_scriptP;
ThisRec(CT_RunScript *scriptP) : i_scriptP(scriptP) {}
};
 
boost::thread_specific_ptr<ThisRec> g_threadP;
 
/*
this is a script-running thread
if any "error" statements are "printed", they are gathered up into
a single string and presented to the user as a dialog
*/
class CT_RunScript : public CT_Preemptive {
public:
SuperString i_name;
SuperString i_script;
CPython_RunLoop *i_runLoopP;
long i_thread_id;
 
class CShowErrorTimer;
CMutexT<CShowErrorTimer *> i_errTimerP;
/*****************************
this gathers all errors printed out:
if it gets some error text, it waits a half second.
if more error prints come in within that time, they are appended
when the timer expires, all the error messages gathered are
shown to the user in a dialog
*/
class CShowErrorTimer : public CT_Timer {
friend class CT_RunScript;
SuperString i_errStr;
CMutex_bool i_abortB;
CT_RunScript *i_scriptP;
bool i_doneB;
public:
CShowErrorTimer(CT_RunScript *scriptP) :
i_doneB(false),
i_scriptP(scriptP),
CT_Timer(NO_KILL "CShowErrorTimer", kEventDurationSecond / 2)
{
call();
}
~CShowErrorTimer() {
i_scriptP->i_errTimerP.Set(NULL);
}
virtual OSStatus operator()()
{
CCritical sc(&i_scriptP->i_errTimerP);
 
i_doneB = true;
if (!i_errStr.Contains(kUserAbortStr)) {
if (i_errStr.GetIndCharR() == '\r') {
i_errStr.pop_back();
}
i_errStr.Replace("<string>", i_scriptP->i_name);
PostAlert("Python Error:", i_errStr.utf8Z());
}
return threadTimerTerminate;
}
void append(const char *utf8Z) {
if (!i_doneB) {
i_errStr.append(utf8Z);
prime();
}
}
};
/*****************************/
CPW_TaskRec *i_taskRecP;
CPW_ProgData i_progData;
CT_RunScript(
CPython_RunLoop *runLoopP,
const SuperString& name,
const SuperString& script
) :
i_runLoopP(runLoopP),
i_name(name),
i_script(script)
{
SuperString verb1("Python: ");
 
verb1.append(name);
i_taskRecP = gApp->NewTask(verb1.ref(), NULL);
i_runLoopP->AddScript(this);
call(NO_KILL "CPython::CT_RunScript");
}
~CT_RunScript()
{
CF_ASSERT(i_errTimerP.Get() == NULL);
i_taskRecP->Delete();
i_runLoopP->RemoveScript(this);
}
// called from CPython_RunLoop thread, NOT from "this" thread
void Idle()
{
OSStatus err = noErr;
ERR(i_runLoopP->i_abortB.Get());
ERR(i_taskRecP->MT_UpdateData(&i_progData));
// an error here means the user has aborted the script
if (err) {
err = noErr;
ERR_XTE_START {
ScEnsureGIL sc;
ScPyObject exceptionP(PyString_FromString(kUserAbortStr));
int countI(PyThreadState_SetAsyncExc(i_thread_id, exceptionP));
// during shut down it is reasonable that countI may be 0
// but it should never be greater than 1
CF_ASSERT(countI == 0 || countI == 1);
} ERR_XTE_END;
if (err) {
ReportErr("Python: Exception when attempting to kill thread", err);
}
}
}
virtual OSStatus operator()(OSStatus err)
{
SetThis(this);
{
ScEnsureGIL sc;
// gather my thread ID so i can be killed later if
// the user hits cancel on my thread
i_thread_id = PyThreadState_Get()->thread_id;
ERR(PyRun_SimpleString(i_script.utf8Z()));
}
 
// now wait until errors have already been shown, if any
while (i_errTimerP.Get()) {
IdleDuration(0.1f, kDurationForever_Idle);
}
return err;
}
/***************************/
// extensions to python for use within scripts
static CT_RunScript* GetThis() {
CT_RunScript *thiz = NULL;
ThisRec *thisRecP = g_threadP.get();
if (thisRecP) {
thiz = thisRecP->i_scriptP;
}
return thiz;
}
static void SetThis(CT_RunScript *scriptP) {
g_threadP.reset(new ThisRec(scriptP));
}
static PyObject* emb_print_err(PyObject *self, PyObject *args) {
return GetThis()->print_err(args);
}
 
PyObject* print_err(PyObject *args)
{
PyObject *resultObjP = NULL;
const char *utf8_strZ = NULL;
static SuperString s_err_str;
if (PyArg_ParseTuple(args, "s", &utf8_strZ)) {
CCritical sc(&i_errTimerP);
if (i_errTimerP.Get() == NULL) {
i_errTimerP.Set(new CShowErrorTimer(this));
}
i_errTimerP.Get()->append(utf8_strZ);
resultObjP = Py_None;
Py_INCREF(resultObjP);
}
return resultObjP;
}
 
/***************************/
static PyObject* emb_print(PyObject *self, PyObject *args) {
return GetThis()->print(args);
}
 
PyObject* print(PyObject *args)
{
PyObject *resultObjP = NULL;
const char *utf8_strZ = NULL;
if (PyArg_ParseTuple(args, "s", &utf8_strZ)) {
Log(utf8_strZ, false);
 
resultObjP = Py_None;
Py_INCREF(resultObjP);
}
return resultObjP;
}
 
/***************************/
static PyObject* emb_do_command(PyObject *self, PyObject *args) {
return GetThis()->do_command(args);
}
 
PyObject* do_command(PyObject *args)
{
PyObject *resultObjP = NULL;
int commandID = kScriptCommand_NONE;
if (PyArg_ParseTuple(args, "i", &commandID)) {
double resultF = Scripting_Command((SInt32)commandID);
 
resultObjP = PyFloat_FromDouble(resultF);
}
return resultObjP;
}
 
/***************************/
static PyObject* emb_do_command_str(PyObject *self, PyObject *args) {
return GetThis()->do_command_str(args);
}
 
PyObject* do_command_str(PyObject *args)
{
PyObject *resultObjP = NULL;
int commandID = kScriptCommand_NONE;
if (PyArg_ParseTuple(args, "i", &commandID)) {
SuperString resultStr(Scripting_CommandStr((SInt32)commandID), true);
 
resultObjP = PyString_FromString(resultStr.utf8Z());
}
return resultObjP;
}
 
/***************************/
static PyObject* emb_do_menu_command(PyObject *self, PyObject *args) {
return GetThis()->do_menu_command(args);
}
 
PyObject* do_menu_command(PyObject *args)
{
PyObject *resultObjP = NULL;
short menuI = 0;
short menu_itemI = 0;
short sub_menu_itemI = 0;
if (PyArg_ParseTuple(args, "hh|h", &menuI, &menu_itemI, &sub_menu_itemI)) {
SInt16Vec intVec;
intVec.push_back(menuI);
intVec.push_back(menu_itemI);
if (sub_menu_itemI) {
intVec.push_back(sub_menu_itemI);
}
DoMenuCommand(intVec);
resultObjP = Py_None;
Py_INCREF(resultObjP);
}
return resultObjP;
}
 
/***************************/
static PyObject* emb_do_menu_name(PyObject *self, PyObject *args) {
return GetThis()->do_menu_name(args);
}
 
PyObject* do_menu_name(PyObject *args)
{
PyObject *resultObjP = NULL;
const char *menuZ = NULL;
const char *menu_itemZ = NULL;
const char *sub_menu_itemZ = NULL;
if (PyArg_ParseTuple(args, "ss|s", &menuZ, &menu_itemZ, &sub_menu_itemZ)) {
SStringVec stringVec;
stringVec.push_back(menuZ);
stringVec.push_back(menu_itemZ);
if (sub_menu_itemZ) {
stringVec.push_back(sub_menu_itemZ);
}
DoMenuCommand(stringVec);
 
resultObjP = Py_None;
Py_INCREF(resultObjP);
}
return resultObjP;
}
};
 
/*****************************************************/
 
static const PyMethodDef EmbMethods[] = {
    {kCustomPrint, CT_RunScript::emb_print, METH_VARARGS, "Calls custom print function."},
    {kCustomErr, CT_RunScript::emb_print_err, METH_VARARGS, "Calls custom error function."},
    {kCommand, CT_RunScript::emb_do_command, METH_VARARGS, "calls scripting command (float result)."},
    {kStringCommand, CT_RunScript::emb_do_command_str, METH_VARARGS, "calls scripting command (string result)."},
    {kDoMenuCommand, CT_RunScript::emb_do_menu_command, METH_VARARGS, "calls menu command by index"},
    {kDoMenuName, CT_RunScript::emb_do_menu_name, METH_VARARGS, "calls menu command by name"},
    {NULL, NULL, 0, NULL}
};
 
static const char *s_RedirectPrint =
"import " kEmbeddedModuleName "\n"
"import sys\n"
"\n"
"class CustomPrintClass:\n"
" def write(self, stuff):\n"
" " kEmbeddedModuleName "." kCustomPrint "(stuff)\n"
"class CustomErrClass:\n"
" def write(self, stuff):\n"
" " kEmbeddedModuleName "." kCustomErr "(stuff)\n"
"sys.stdout = CustomPrintClass()\n"
"sys.stderr = CustomErrClass()\n";
 
static const char *s_PrintTime =
"import time\n"
"print 'Today is', time.ctime(time.time())\n";
 
static const char *s_AllowPendingCalls =
"pass\n";
 
/*****************************************************************/
CPython_RunLoop::CPython_RunLoop(CPython *in_thiz) :
thiz(in_thiz)
{
i_scriptVecP.Set(new ScriptVec());
call(NO_KILL "CPython_RunLoop");
}
 
OSStatus CPython_RunLoop::operator()(OSStatus err)
{
XTE_START {
bool continueB = true;
 
#if OPT_WINOS
{
// don't crash if python is not installed, simply bail
HMODULE pythonH = LoadLibrary(L"pywintypes27");
continueB = pythonH != NULL;
}
#endif
if (continueB) {
Log("Python: about to set program name");
Py_SetProgramName(const_cast<char *>(thiz->i_appName.c_str()));
 
Log("Python: about to init");
Py_Initialize();
 
{
Log("Python: about to create " kEmbeddedModuleName " module");
PyObject *myModuleP = Py_InitModule(
kEmbeddedModuleName, const_cast<PyMethodDef*>(EmbMethods));
ETX(myModuleP == NULL);
// the owner of myModuleP is now the python interpreter
// it will auto-decref during Py_Finalize()
}
// redirect stdout and stderr to my own logging functions
Log("Python: about to run log redirect script");
ETX(PyRun_SimpleString(s_RedirectPrint));
PyEval_InitThreads();
thiz->i_inittedB.Set(true);
}
} XTE_END;
 
LogYesOrNo("Python Initted", thiz->i_inittedB.Get());
if (thiz->i_inittedB.Get()) {
bool abortB = false;
bool doneB = false;
 
{
ScReleaseGIL sc;
/*
run the "loop" that will handle killing of
any scripts canceled by the user
*/
do {
// IdleScripts will kill any scripts that the user has hit cancel on
IdleScripts();
// if you want to use Py_AddPendingCall() to send a message to THIS
// thread, then you'd need to uncomment this line
#if 0
if (PyRun_SimpleString(s_AllowPendingCalls) != 0) {
PostAlert("Python: s_AllowPendingCalls failed");
i_abortB.Set();
}
#endif
 
if (!abortB) {
// this gets set when quitting the app
abortB = i_abortB.Get();
}
if (abortB) {
doneB = CountScripts() == 0;
}
if (!doneB) {
IdleDuration(kQuarterSecond);
}
} while (!doneB);
}
 
Py_Finalize();
thiz->i_inittedB.Set(false);
}
thiz->i_runLoopP.Set(NULL);
{
ScriptVec *vecP = i_scriptVecP.Get();
CF_ASSERT(vecP);
CF_ASSERT(vecP->empty());
delete vecP;
}
i_scriptVecP.Set(NULL);
return err;
}
 
void CPython_RunLoop::RunScript(const char *unf8NameZ, const char *utf8ScriptZ)
{
new CT_RunScript(this, unf8NameZ, utf8ScriptZ);
}
 
void CPython_RunLoop::AddScript(CT_RunScript *scriptP)
{
CCritical sc(&i_scriptVecP);
 
i_scriptVecP.Get()->push_back(scriptP);
}
 
void CPython_RunLoop::RemoveScript(CT_RunScript *scriptP)
{
CCritical sc(&i_scriptVecP);
ScriptVec& scriptVec(*i_scriptVecP.Get());
ScriptVec::iterator it(std::find(scriptVec.begin(), scriptVec.end(), scriptP));
CF_ASSERT(it != scriptVec.end());
if (it != scriptVec.end()) {
scriptVec.erase(it);
}
}
 
void CPython_RunLoop::IdleScripts()
{
ScriptVec iter_scriptVec;
 
{
CCritical sc(&i_scriptVecP);
ScriptVec& orig_scriptVec(*i_scriptVecP.Get());
// make a copy to iterate over, cuz during the iterate
// we may actually delete the current script
iter_scriptVec = orig_scriptVec;
}
BOOST_FOREACH(CT_RunScript *scriptP, iter_scriptVec) {
{
CCritical sc(&i_scriptVecP);
ScriptVec& scriptVec(*i_scriptVecP.Get());
ScriptVec::iterator it(std::find(scriptVec.begin(), scriptVec.end(), scriptP));
// we still have to check to see if this script still exists before calling it
if (it != scriptVec.end()) {
scriptP->Idle();
}
}
}
}
 
size_t CPython_RunLoop::CountScripts()
{
CCritical sc(&i_scriptVecP);
 
return i_scriptVecP.Get()->size();
}
 
/**************************************************************/
CPython::CPython(const char *appNameZ) :
i_appName(appNameZ)
{
i_runLoopP.Set(new CPython_RunLoop(this));
}
 
CPython::~CPython()
{
s_pythonP = NULL;
{
CCritical sc(&i_runLoopP);
CPython_RunLoop *runLoopP(i_runLoopP.Get());
if (runLoopP) {
runLoopP->i_abortB.Set(true);
}
}
while (i_runLoopP.Get()) {
IdleDuration(0.1f, kDurationForever_Idle);
}
}
 
// static
CPython* CPython::Get(const char *appNameZ)
{
if (s_pythonP == NULL) {
CF_ASSERT(appNameZ);
s_pythonP = new CPython(appNameZ);
}
return s_pythonP;
}
 
/****************************************************/
void CPython::Startup()
{
if (i_inittedB.Get()) XTE_START {
CharVec charVec;
CFileRef pythonRef(kFolder_KJAMS);
ETX(pythonRef.Descend("Python/" kStartupScriptName));
pythonRef.Load(&charVec);
charVec.push_back(0);
i_runLoopP.Get()->RunScript(kStartupScriptName, &charVec[0]);
} XTE_END;
}
 
void CPython::Test()
{
if (i_inittedB.Get()) {
i_runLoopP.Get()->RunScript("print_time.py", s_PrintTime);
}
}
/*****************************************************************/
 
#endif // kUsePython
 
// the entire public interface is here
// called on startup to init
OSStatus CPython_PreAlloc(const char *utf8Z)
{
OSStatus err = noErr;
#if kUsePython
if (CPython::Get(utf8Z) == NULL) {
ERR(tsmUnsupScriptLanguageErr);
}
#endif
return err;
}
 
// called on shutdown
void CPython_PostDispose()
{
#if kUsePython
CPython *pyP(CPython::Get());
if (pyP) {
delete pyP;
}
#endif
}
 
// very simple unit test
void CPython_Test()
{
#if kUsePython
CPython *pyP(CPython::Get());
if (pyP) {
pyP->Test();
}
#endif
}
 
// called when startup is complete
void CPython_Startup()
{
#if kUsePython
CPython *pyP(CPython::Get());
if (pyP) {
pyP->Startup();
}
#endif
}
</pre>

Latest revision as of 06:03, 8 October 2025

NOTE: Python does not yet work in 64bit!

If you're looking for the open-sourced C++ Embedding Code.

How to Use

  1. On Mac: it's built in. On Windows: requires Python 2.7, 32bit. You can download it here.
  2. Run kJams 2. Go to the Advanced menu and see the Python submenu
  3. in your user's Music/kJams folder there is now a Python folder. Put your scripts in there to see them in the Python menu (re-start kJams)

Note if you find it stops working on mac, got to Terminal and issue this:

tccutil reset AppleEvents

This will reset the "are you sure this is okay" security prompt, and the OS should ask if it's okay that kJams control iTunes, be sure to say OK and not deny the request

Tips

  • Pick "Advanced->Python->Reveal “kjams.py”", this will give you a list of enums for commands, server, and reveal other commands you can call
  • A script named "startup.py" will, if present, be run on startup, so if eg: you always want to ensure some prefs are set correctly, regardless of what someone may have changed, you can use this script to set some prefs the way you like them.
  • Picking anything from the Python menu will run it. Holding the alt/option key when picking it will reveal (open) the file instead
  • You should open each of the files and look at them for examples
  • you can edit your scripts live and re-run them, no need to quit and re-run kJams
  • if you create new scripts or remove old ones, or rename one, you must restart kJams to see them in the menu
  • always edit your scripts as UTF8, and be sure to include the header "# -*- coding: utf-8 -*- " as the first line. that way, you can just enter any unicode characters you wish as string literals. no need to prefix with u, tho that is optional and will work.
  • when passing parameters, tuples and lists are considered the same thing to kJams (an array), and are interchangeable.

What's working

  • All the Scripting commands
  • Access to every menu item (including sub menus)
  • Access to all preferences including secret prefs (prefs that otherwise have no user interface)
  • ability to show a progress bar in the activity window
  • ability to kill off a script (stop sign in activity window)
  • song meta data editing
  • get/set selected playlist
  • get/set selection within playlist
  • message dialog, with title and message, up to 3 buttons, and 1 check box
  • interactive string dialog, title, message, default string, OK, Cancel
  • set keyboard shortcuts
  • add song to any playlist
  • reorder songs within a playlist (including singer)
  • new playlist
  • ability to show ShowScreens
  • ability so script iTunes (get play state, get/set volume)
  • Much of the Server functionality including:
    • fetching playlists (eg: Library, Rotation, Venue (list of singers)) etc
    • fetching info including venue name
    • singer creation and/or login
    • getting singers' lists (tonight, history, faves)
    • add songs to singer
  • crossfade with iTunes

What's Coming

  • remove song from playlist
  • whatever else you need

Ideas

  • auto cross fade with your favorite DJ app
  • Twitter feed - Current singer and song
  • a Rate the singer app for iPhone or 'Droid
  • update web site with current stats
  • load some videos playing behind the kJams window (with transparency), with VLC or QuickTime Player
  • fix prefs on startup
  • make your own iOS or Droid app that interacts with kJams

How to modify an existing script

The TL;DR is: make a copy of the existing script and modify that

  1. Hold the option/alt key and pick the script to reveal it
  2. Mac:
    1. revealing will open your script in a text editor
  3. Windows:
    1. revealing will show the script file in Windows Explorer
    2. open that file in a text editor
  4. in the File menu of your text editor, pick "Save As..."
  5. go to your Python folder here: /Users/<you>/Music/kJams/Python/
  6. Re-name the file so you can distinguish it from the built-in one (eg: prepend "My " to it's name)
  7. save it in that location with the new name