Remove obsolete Python scripts and doc generator.

This commit is contained in:
John Maguire 2011-11-22 16:59:08 +01:00
parent fd9b03cbcf
commit a047a30265
61 changed files with 0 additions and 2279 deletions

View File

@ -1,41 +0,0 @@
cmake_minimum_required(VERSION 2.6)
include_directories(${LIBGPOD_INCLUDE_DIRS})
include_directories(${PYTHON_INCLUDE_DIRS})
include_directories(${CMAKE_SOURCE_DIR}/3rdparty/gmock/gtest/include)
include_directories(${CMAKE_SOURCE_DIR}/src)
include_directories(${CMAKE_BINARY_DIR}/src)
set(SOURCES
generate_python_docs.cpp
)
set(RESOURCES
generate_python_docs.qrc
)
qt4_add_resources(QRC ${RESOURCES})
add_executable(generate_python_docs EXCLUDE_FROM_ALL ${SOURCES} ${QRC})
target_link_libraries(generate_python_docs clementine_lib)
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/epydoc.css
${CMAKE_CURRENT_BINARY_DIR}/epydoc.css
COPYONLY
)
file(GLOB EPYDOC_PAGES ${CMAKE_CURRENT_SOURCE_DIR}/*.epydoc)
foreach(file ${EPYDOC_PAGES})
get_filename_component(filename ${file} NAME)
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/${filename}
${CMAKE_CURRENT_BINARY_DIR}/${filename}
COPYONLY
)
endforeach(file)
add_custom_target(pythondocs
generate_python_docs
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)

View File

@ -1,102 +0,0 @@
Hello world!
============
Clementine allows users to load scripts written in Python. These scripts run
in a Python interpreter inside Clementine itself and have access to
Clementine's internals through a special L{clementine} module. Scripts can
create graphical user interfaces using the Python Qt bindings.
Let's begin by creating a script that shows a dialog box when loaded.
Creating a simple script
========================
Create a directory called C{helloworld}. All files for this script will live
in this directory.
Inside the C{helloworld} directory create a file called C{main.py}. This will
contain all the Python source code for this script.
>>> from PyQt4.QtGui import QMessageBox
...
... QMessageBox.information(None, "Hello world!", "My script was loaded!")
B{Note:} It is possible (and recommended) to split large scripts up into multiple
classes stored in separate files, and C{import} them as in a normal Python
application. For now we will put all the code into a single file.
This code imports the C{PyQt4.QtGui.QMessageBox} class from the Python Qt
bindings and displays a simple information message box when the script is
loaded.
The script.ini
==============
You now have some Python source code in a directory, but Clementine won't
recognise it as being a loadable script. You need to create a C{script.ini}
file with some information about your script:
>>> [Script]
... name=Hello world!
... description=My first script
... author=Fred <fred@example.com>
... url=http://www.example.com
... icon=:/icon.png
...
... language=python
... script_file=main.py
Let's look at what each of these fields is for:
- B{name} - The short name of your script that is displayed to the user in
bold in the Script Manager dialog.
- B{description} - A longer description of your script. Try to explain
briefly what the script does and what the user should do to start using it.
- B{author} I{[optional]} - Your name and email address in the format
C{Name <email@address>}. This is not currently used, but may be in the
future.
- B{url} I{[optional]} - A URL of this script's homepage. This is not
currently used, but may be in the future.
- B{icon} - The filename (relative to the script's directory) of an icon to
display next to the script in the Script Manager dialog. You can put an
image (.png or .jpg) into the C{helloworld} directory and refer to that
here. The image should be at least 64x64 pixels big. Notice in this
example we use a filename starting with C{:/}. This is a Qt resource path,
and refers to one of the icons embedded within Clementine.
- B{language} - The language the script is written in. Currently this must
be set to C{python}, but more languages may be supported in the future.
- B{script_file} - The Python source file to load for this script. If your
script consists of multiple files then the one specified here is the main
one that will be loaded first.
Adding the script to Clementine
===============================
Clementine searches for its scripts in a couple of different places depending
on the operating system:
B{On Windows:}
- C{%UserProfile%\.config\Clementine\scripts} (where C{%UserProfile%} might be
C{C:\Users\YourUsername} on Windows Vista or Windows 7, or
C{C:\Documents and Settings\YourUsername} on XP).
- C{C:\Program Files\Clementine\scripts}
B{On Linux:}
- C{~/.config/Clementine/scripts}
- C{/usr/share/clementine/scripts}
- C{/usr/local/share/clementine/scripts}
- C{$PREFIX/share/clementine/scripts} (if Clementine was installed into a
different prefix).
B{On Mac OS X:}
- C{~/Library/Application Support/Clementine/scripts}
For development you should copy your C{helloworld} directory (or create a
symbolic link) into one of these locations. Clementine should notice the new
script straight away and add it to the Script Manager dialog.
Load your script by clicking on it in the Script Manager dialog and clicking
the C{Enable} button.

View File

@ -1,352 +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;
font-family: Helvetica,Arial,sans-serif;
font-size: small; }
p { margin-top: 0.5em; margin-bottom: 0.5em; }
a:link { color: #0000ff; }
a:visited { color: #551A8B; }
dt { font-weight: bold; }
h1 { font-size: 140%;
font-weight: bold;
margin: 0.5em 0 0.5em 0;
padding: 1px 3px;
position: relative;
border-top: 1px solid #36C;
background-color: #E5ECF9; }
h2 { font-size: +125%;
font-weight: bold; }
h3 { font-size: +110%; font-style: italic;
font-weight: normal; }
code { font-family: monospace;
color: #007000; }
hr { height: 1px; background-color: #BBB; border: 0px; }
/* 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
* <h1>). 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.
*/
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-color: #E5ECF9;
border-top: 1px solid #36C;
margin-bottom: .5em;}
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%; }
div.sidebar {
float: left;
position: absolute;
left: 0px;
top: 0px;
width: 350px;
padding-left: 6px;
padding-right: 6px;
border-right: 3px solid #E5ECF9;
}
div.maincontent {
float: left;
position: absolute;
left: 362px;
top: 0px;
border-left: 3px solid #E5ECF9;
padding-left: 6px;
padding-top: 3px;
}
/* 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,
th.group-header { font-weight: bold;
text-align: left;
padding: 6px 12px;
border: 1px solid #BBB;
background-color: #E5ECF9 }
td.table-header table { color: #000000; }
td.table-header table a:link { color: #0000ff; }
td.table-header table a:visited { color: #204080; }
/* 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;
margin-bottom: 0.5em; }
td.summary { padding: 6px 12px;
border: 1px solid #BBB; }
.summary-type,.summary-sig { font-family: monospace;
color: #007000; }
/* 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;
border: 1px solid #BBB; }
table.details > tbody > tr > td { padding: 6px 12px; }
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-name { 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 { color: #555555; 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: 3px 0px 3px 0px;
padding: 1px 3px;
position: relative;
border-top: 1px solid #36C;
background-color: #E5ECF9; }
/* 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; display: none; }
.py-more { color: #005050; font-weight: bold; display: none; }
.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; }

View File

@ -1,54 +0,0 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include <Python.h>
#include <QApplication>
#include <QFile>
#include "playlist/playlistitem.h"
#include "scripting/scriptmanager.h"
#include "scripting/python/pythonengine.h"
int main(int argc, char** argv) {
QApplication a(argc, argv);
// Create the python engine
ScriptManager manager;
LanguageEngine* language_engine =
manager.EngineForLanguage(ScriptInfo::Language_Python);
PythonEngine* python_engine = qobject_cast<PythonEngine*>(language_engine);
// Initialise python
if (!python_engine->EnsureInitialised()) {
qFatal("Failed to initialise Python engine");
}
// Load the python script
QFile script(":/doc/python/generate_python_docs.py");
script.open(QIODevice::ReadOnly);
QByteArray script_data = script.readAll();
// Run it
PyEval_AcquireLock();
if (PyRun_SimpleString(script_data.constData()) != 0) {
qFatal("Could not execute generate_python_docs.py");
}
PyEval_ReleaseLock();
return 0;
}

View File

@ -1,190 +0,0 @@
import epydoc
import epydoc.apidoc
import epydoc.cli
import epydoc.docbuilder
import epydoc.docintrospecter
import epydoc.docwriter.html
import epydoc.markup.epytext
import inspect
import PyQt4.QtCore
import sys
import types
DOC_PAGES = [
("Hello world!", "doc-hello-world"),
]
OUTPUT_DIR = "output"
# SIP does some strange stuff with the __dict__ of wrapped C++ classes:
# someclass.__dict__["function"] != someclass.function
# These little hacks make epydoc generate documentation for the actual functions
# instead of their sip.methoddescriptor wrappers.
def is_pyqt_wrapper_class(thing):
return epydoc.docintrospecter.isclass(thing) and \
isinstance(thing, PyQt4.QtCore.pyqtWrapperType)
def introspect_pyqt_wrapper_class(thing, doc, module_name=None):
# Inspect the class as normal
doc = epydoc.docintrospecter.introspect_class(thing, doc, module_name=module_name)
# Re-inspect the actual member functions
for name in thing.__dict__.keys():
if name in doc.variables and hasattr(thing, name):
actual_var = getattr(thing, name)
val_doc = epydoc.docintrospecter.introspect_docs(
actual_var, context=doc, module_name=module_name)
var_doc = epydoc.docintrospecter.VariableDoc(
name=name, value=val_doc, container=doc, docs_extracted_by='introspecter')
doc.variables[name] = var_doc
return doc
epydoc.docintrospecter.register_introspecter(is_pyqt_wrapper_class, introspect_pyqt_wrapper_class)
# Monkey-patch some functions in the HTML docwriter to show a table of contents
# down the side of each page, instead of in a separate frame, and to do external
# API links.
original_write_header = epydoc.docwriter.html.HTMLWriter.write_header
def my_write_header(self, out, title):
original_write_header(self, out, title)
out('<div class="sidebar">')
# General doc pages
out('<h2 class="toc">%s</h2>\n' % "Documentation")
for (title, filename) in DOC_PAGES:
out('<a href="%s.html">%s</a><br/>' % (filename, title))
# Classes
self.write_toc_section(out, "Class reference", self.class_list)
# Functions
funcs = [d for d in self.routine_list
if not isinstance(self.docindex.container(d),
(epydoc.apidoc.ClassDoc, types.NoneType))]
self.write_toc_section(out, "Function reference", funcs)
# Variables
vars = []
for doc in self.module_list:
vars += doc.select_variables(value_type='other',
imported=False,
public=self._public_filter)
self.write_toc_section(out, "Variable reference", vars)
out('</div>')
out('<div class="maincontent">')
def my_write_footer(self, out, short=False):
out('</div></body></html>')
def my_write_navbar(self, out, context):
pass
original_write_toc_section = epydoc.docwriter.html.HTMLWriter.write_toc_section
def my_write_toc_section(self, out, name, docs, fullname=True):
docs = [x for x in docs if not str(x.canonical_name).startswith('PyQt4')]
original_write_toc_section(self, out, name, docs, fullname=fullname)
def qt_url(name):
if not isinstance(name, str) and \
not isinstance(name, epydoc.apidoc.DottedName) and \
not isinstance(name, unicode):
return None
parts = str(name).split('.')
if len(parts) >= 3 and parts[0] == "PyQt4":
parts = parts[2:]
if not parts or not parts[0].startswith("Q"):
return None
label = '.'.join(parts)
url = "http://www.riverbankcomputing.co.uk/static/Docs/PyQt4/html/"
url += "%s.html" % parts[0].lower()
if len(parts) >= 2:
url += "#%s" % parts[1].lower()
return url
original_translate_identifier_xref = epydoc.docwriter.html._HTMLDocstringLinker.translate_identifier_xref
def my_translate_identifier_xref(self, identifier, label=None):
url = qt_url(identifier)
if url:
label = '.'.join(identifier.split('.')[2:])
return '<a href="%s" class="link">%s</a>' % (url, label)
return original_translate_identifier_xref(self, identifier, label)
original_url = epydoc.docwriter.html.HTMLWriter.url
def my_url(self, obj):
if isinstance(obj, epydoc.apidoc.ValueDoc):
url = qt_url(obj.canonical_name)
if url:
return url
return original_url(self, obj)
original__write = epydoc.docwriter.html.HTMLWriter._write
def my__write(self, write_func, directory, filename, *args):
if filename.startswith("http://"):
return
original__write(self, write_func, directory, filename, *args)
epydoc.docwriter.html.HTMLWriter._write = my__write
epydoc.docwriter.html.HTMLWriter.write_header = my_write_header
epydoc.docwriter.html.HTMLWriter.write_footer = my_write_footer
epydoc.docwriter.html.HTMLWriter.write_navbar = my_write_navbar
epydoc.docwriter.html.HTMLWriter.write_toc_section = my_write_toc_section
epydoc.docwriter.html._HTMLDocstringLinker.translate_identifier_xref = my_translate_identifier_xref
epydoc.docwriter.html.HTMLWriter.url = my_url
sys.argv = [
"epydoc",
"--html",
"-o", OUTPUT_DIR,
"-v",
"--name", "clementine",
"--url", "http://www.clementine-player.org",
"--css", "epydoc.css",
"--no-sourcecode",
"--no-private",
"--no-frames",
"clementine",
]
print "Running '%s'" % ' '.join(sys.argv)
# Parse arguments
(options, names) = epydoc.cli.parse_arguments()
# Set up the logger
logger = epydoc.cli.ConsoleLogger(1, 'hide')
epydoc.log.register_logger(logger)
# Write the main docs - this is copied from cli()
epydoc.docstringparser.DEFAULT_DOCFORMAT = options.docformat
docindex = epydoc.docbuilder.build_doc_index(names,
options.introspect, options.parse,
add_submodules=True)
html_writer = epydoc.docwriter.html.HTMLWriter(docindex, **options.__dict__)
html_writer.write(options.target)
# Write extra pages
def write_extra_page(out, title, source):
handle = open(source, 'r')
parsed_docstring = epydoc.markup.epytext.parse_docstring(handle.read(), [])
html_writer.write_header(out, title)
out(html_writer.docstring_to_html(parsed_docstring))
html_writer.write_footer(out)
for (title, filename) in DOC_PAGES:
source = "%s.epydoc" % filename
html = "%s.html" % filename
print "Generating '%s' from '%s'..." % (html, source)
html_writer._write(write_extra_page, OUTPUT_DIR, html, title, source)

View File

@ -1,5 +0,0 @@
<RCC>
<qresource prefix="/doc/python">
<file>generate_python_docs.py</file>
</qresource>
</RCC>

View File

@ -1,20 +0,0 @@
function(install_script_files scriptname)
if(APPLE)
install(FILES ${ARGN} DESTINATION ${CMAKE_BINARY_DIR}/clementine.app/Contents/Resources/scripts/${scriptname}/)
else(APPLE)
install(FILES ${ARGN} DESTINATION ${CMAKE_INSTALL_PREFIX}/share/clementine/scripts/${scriptname}/)
endif(APPLE)
endfunction(install_script_files)
add_subdirectory(amazon-covers)
add_subdirectory(clipboard)
add_subdirectory(digitallyimported-radio)
add_subdirectory(google-covers)
add_subdirectory(invalidate-deleted)
add_subdirectory(nostalgia)
add_subdirectory(preview)
add_subdirectory(rainbowizer)
add_subdirectory(remove-duplicates)
add_subdirectory(setlistfm)
add_subdirectory(shutdown)
add_subdirectory(youtube)

View File

@ -1,5 +0,0 @@
install_script_files(amazon-covers
amazon_covers.py
icon.jpg
script.ini
)

View File

@ -1,154 +0,0 @@
import clementine
from PythonQt.QtCore import QUrl
from PythonQt.QtNetwork import QNetworkRequest
import base64
import hashlib
import hmac
import logging
import time
import urllib
import xml.etree.ElementTree
LOGGER = logging.getLogger("amazon_covers")
class AmazonCoverProvider(clementine.CoverProvider):
"""
Most of the Amazon API related code here comes from a plugin (which I wrote) for
an open source application called Cardapio.
"""
API_URL = 'http://ecs.amazonaws.com/onca/xml?{0}'
AWS_ACCESS_KEY = 'AKIAJ4QO3GQTSM3A43BQ'
AWS_SECRET_ACCESS_KEY = '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'
def __init__(self, parent=None):
clementine.CoverProvider.__init__(self, "Amazon", parent)
# basic API's arguments (search in all categories)
self.api_base_args = {
'Service' : 'AWSECommerceService',
'Version' : '2009-11-01',
'Operation' : 'ItemSearch',
'SearchIndex' : 'All',
'ResponseGroup' : 'Images',
'AWSAccessKeyId': self.AWS_ACCESS_KEY
}
self.network = clementine.NetworkAccessManager()
def StartSearch(self, artist, album, id):
query = self.PrepareAmazonRESTUrl(artist + " " + album)
url = QUrl.fromEncoded(self.API_URL.format(query))
LOGGER.debug("ID %d: Sending request to '%s'" % (id, url))
reply = self.network.get(QNetworkRequest(url))
def QueryFinished():
LOGGER.debug("ID %d: Finished" % id)
self.SearchFinished(id, self.ParseReply(reply))
reply.connect("finished()", QueryFinished)
return True
def ParseReply(self, reply):
parsed = []
# watch out for connection problems
try:
xml_body = str(reply.readAll())
# watch out for empty input
if len(xml_body) == 0:
return parsed
root = xml.etree.ElementTree.fromstring(xml_body)
# strip the namespaces from all of the parsed items
for el in root.getiterator():
ns_pos = el.tag.find('}')
if ns_pos != -1:
el.tag = el.tag[(ns_pos + 1):]
except Exception as ex:
LOGGER.exception(ex)
return parsed
used_urls = set()
# decode the result
try:
items = []
is_valid = root.find('Items/Request/IsValid')
total_results = root.find('Items/TotalResults')
# if we have a valid response with any results...
if is_valid is not None and is_valid != 'False' and \
total_results is not None and total_results != '0':
query = root.find('Items/Request/ItemSearchRequest/Keywords').text
# remember them all
for item in root.findall('Items/Item'):
final_url = None
current_url = item.find('LargeImage/URL')
if current_url is None:
current_url = item.find('MediumImage/URL')
if current_url is None or current_url.text in used_urls:
continue
used_urls.add(current_url.text)
current = clementine.CoverSearchResult()
current.description = str(query)
current.image_url = str(current_url.text)
parsed.append(current)
except KeyError as ex:
LOGGER.exception(ex)
return parsed
def PrepareAmazonRESTUrl(self, text):
"""
Prepares a RESTful URL according to Amazon's strict querying policies.
Deals with the variable part of the URL only (the one after the '?').
"""
# additional required API arguments
copy_args = self.api_base_args.copy()
copy_args['Keywords'] = str(text)
copy_args['Timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
# turn the argument map into a list of encoded request parameter strings
query_list = ["%s=%s" % (k, urllib.quote(v))
for k, v in copy_args.items()]
# sort the list (by parameter name)
query_list.sort()
# turn the list into a partial URL string
query_string = "&".join(query_list)
# prepare a string on which we will base the AWS signature
string_to_sign = """GET
{0}
/onca/xml
{1}""".format('ecs.amazonaws.com', query_string)
# create HMAC for the string (using SHA-256 and our secret API key)
hm = hmac.new(key = self.AWS_SECRET_ACCESS_KEY,
msg = string_to_sign,
digestmod = hashlib.sha256)
# final step... convert the HMAC to base64, then encode it
signature = urllib.quote(base64.b64encode(hm.digest()))
return query_string + '&Signature=' + signature
provider = AmazonCoverProvider()
clementine.cover_providers.AddProvider(provider)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,9 +0,0 @@
[Script]
name=Amazon cover provider
description=Thanks to this script Clementine will be able to download covers from Amazon for you.
author=Pawel Bara <keirangtp ( at ) gmail.com>
url=http://www.clementine-player.org
icon=icon.jpg
language=python
script_file=amazon_covers.py

View File

@ -1,4 +0,0 @@
install_script_files(clipboard
clipboard.py
script.ini
)

View File

@ -1,21 +0,0 @@
import clementine
from PythonQt.QtGui import QAction
from PythonQt.QtGui import QApplication
class Plugin:
def __init__(self):
self.clipboard = QApplication.clipboard()
self.action = QAction("Copy to clipboard", None)
clementine.ui.AddAction("song_menu", self.action)
self.action.connect("activated()", self.CopyToClipboard)
def CopyToClipboard(self):
selection = clementine.playlists.current_selection().indexes()
title = selection[clementine.Playlist.Column_Title].data()
artist = selection[clementine.Playlist.Column_Artist].data()
song = '%s - %s' % (title, artist)
self.clipboard.setText(song)
plugin = Plugin()

View File

@ -1,8 +0,0 @@
[Script]
name=Copy to Clipboard
description=Copies song details to the clipboard
author=John Maguire <john.maguire@gmail.com>
url=http://www.clementine-player.org
language=python
script_file=clipboard.py

View File

@ -1,12 +0,0 @@
install_script_files(digitallyimported-radio
diservice.py
icon.png
icon-sky.png
icon-small.png
main.py
script.ini
servicebase.py
settingsdialog.py
settingsdialog.ui
skyservice.py
)

View File

@ -1,67 +0,0 @@
import clementine
from servicebase import DigitallyImportedServiceBase
from PythonQt.QtCore import QSettings, QUrl
from PythonQt.QtNetwork import QNetworkCookie, QNetworkCookieJar, QNetworkRequest
import logging
LOGGER = logging.getLogger("di.service")
class DigitallyImportedService(DigitallyImportedServiceBase):
HOMEPAGE_URL = QUrl("http://www.di.fm/")
HOMEPAGE_NAME = "di.fm"
STREAM_LIST_URL = QUrl("http://listen.di.fm/")
ICON_FILENAME = "icon-small.png"
SERVICE_NAME = "DigitallyImported"
SERVICE_DESCRIPTION = "Digitally Imported"
URL_SCHEME = "digitallyimported"
# These have to be in the same order as in the settings dialog
PLAYLISTS = [
{"premium": False, "url": "http://listen.di.fm/public3/%s.pls"},
{"premium": True, "url": "http://www.di.fm/listen/%s/premium.pls"},
{"premium": False, "url": "http://listen.di.fm/public2/%s.pls"},
{"premium": True, "url": "http://www.di.fm/listen/%s/64k.pls"},
{"premium": True, "url": "http://www.di.fm/listen/%s/128k.pls"},
{"premium": False, "url": "http://listen.di.fm/public5/%s.asx"},
{"premium": True, "url": "http://www.di.fm/listen/%s/64k.asx"},
{"premium": True, "url": "http://www.di.fm/listen/%s/128k.asx"},
]
def __init__(self, model, settings_dialog_callback):
DigitallyImportedServiceBase.Init(self, model, settings_dialog_callback)
self.last_username_password = None
self.MaybeReloadCookies()
def MaybeReloadCookies(self):
if self.last_username_password == (self.username, self.password):
return
self.last_username_password = (self.username, self.password)
LOGGER.debug("Setting network cookies after config change")
# If a username and password were set by the user then set them in the
# cookies we pass to www.di.fm
self.network = clementine.NetworkAccessManager(self)
if len(self.username) and len(self.password):
cookie_jar = QNetworkCookieJar()
cookie_jar.setCookiesFromUrl([
QNetworkCookie("_amember_ru", self.username.encode("utf-8")),
QNetworkCookie("_amember_rp", self.password.encode("utf-8")),
], QUrl("http://www.di.fm/"))
self.network.setCookieJar(cookie_jar)
def LoadStation(self, key):
self.MaybeReloadCookies()
playlist_url = self.PLAYLISTS[self.audio_type]["url"] % key
LOGGER.info("Getting playlist URL '%s'" % playlist_url)
# Start fetching the playlist. Can't use a SongLoader to do this because
# we have to use the cookies we set in ReloadSettings()
self.load_station_reply = self.network.get(QNetworkRequest(QUrl(playlist_url)))
self.load_station_reply.connect("finished()", self.LoadPlaylistFinished)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -1,31 +0,0 @@
from diservice import DigitallyImportedService
from skyservice import SkyFmService
from settingsdialog import SettingsDialog
import clementine
class Plugin:
def __init__(self):
self.settings_dialog = None
cb = self.ShowSettings
# Register for when the user clicks the Settings button
__script__.connect("SettingsDialogRequested()", cb)
# Create the services and add them to the Internet tab
self.di_service = DigitallyImportedService(clementine.internet_model, cb)
self.sky_service = SkyFmService(clementine.internet_model, cb)
clementine.internet_model.AddService(self.di_service)
clementine.internet_model.AddService(self.sky_service)
def ShowSettings(self):
if not self.settings_dialog:
# Create the dialog the first time it's shown
self.settings_dialog = SettingsDialog()
self.settings_dialog.connect("accepted()", self.di_service.ReloadSettings)
self.settings_dialog.connect("accepted()", self.sky_service.ReloadSettings)
self.settings_dialog.show()
plugin = Plugin()

View File

@ -1,9 +0,0 @@
[Script]
name=Digitally Imported and SKY.fm
description=Digitally Imported and SKY.fm are sister sites offering a variety of internet radio stations. Installing this plugin will add Digitally Imported and SKY.fm radio stations to your Internet tab.
author=David Sansome <me@davidsansome.com>
url=http://www.di.fm
icon=icon.png
language=python
script_file=main.py

View File

@ -1,234 +0,0 @@
import clementine
import PythonQt
from PythonQt.QtCore import QSettings, QUrl
from PythonQt.QtGui import QAction, QDesktopServices, QIcon, QMenu, \
QStandardItem
from PythonQt.QtNetwork import QNetworkRequest
import json
import logging
import operator
import os.path
import weakref
LOGGER = logging.getLogger("di.servicebase")
class DigitallyImportedUrlHandler(clementine.UrlHandler):
def __init__(self, url_scheme, service):
clementine.UrlHandler.__init__(self, None)
# Avoid circular references
self.service_weakref = weakref.ref(service)
self.url_scheme = url_scheme
self.last_original_url = None
self.task_id = None
def scheme(self):
return self.url_scheme
def StartLoading(self, original_url):
if self.service_weakref() is None:
return
service = self.service_weakref()
result = clementine.UrlHandler_LoadResult()
if self.task_id is not None:
return result
if service.PLAYLISTS[service.audio_type]["premium"] and \
(len(service.username) == 0 or len(service.password) == 0):
service.StreamError(self.tr("You have selected a Premium-only audio type but do not have any account details entered"))
return result
key = original_url.host()
LOGGER.info("Loading station %s", key)
service.LoadStation(key)
# Save the original URL so we can emit it in the finished signal later
self.last_original_url = original_url
# Tell the user what's happening
self.task_id = clementine.task_manager.StartTask(self.tr("Loading stream"))
result.type_ = clementine.UrlHandler_LoadResult.WillLoadAsynchronously
result.original_url_ = original_url
return result
def LoadPlaylistFinished(self, reply):
if self.task_id is None:
return
if self.service_weakref() is None:
return
service = self.service_weakref()
# Stop the spinner in the status bar
clementine.task_manager.SetTaskFinished(self.task_id)
self.task_id = None
# Try to parse the playlist
parser = clementine.PlaylistParser(clementine.library)
songs = parser.LoadFromDevice(reply)
LOGGER.info("Loading station finished, got %d songs", len(songs))
# Failed to get the playlist?
if len(songs) == 0:
service.StreamError("Error loading playlist '%s'" % reply.url().toString())
return
result = clementine.UrlHandler_LoadResult()
result.original_url_ = self.last_original_url
# Take the first track in the playlist
result.type_ = clementine.UrlHandler_LoadResult.TrackAvailable
result.media_url_ = songs[0].url()
self.AsyncLoadComplete(result)
class DigitallyImportedServiceBase(clementine.InternetService):
# Set these in subclasses
HOMEPAGE_URL = None
HOMEPAGE_NAME = None
STREAM_LIST_URL = None
ICON_FILENAME = None
SERVICE_NAME = None
SERVICE_DESCRIPTION = None
PLAYLISTS = []
URL_SCHEME = None
SETTINGS_GROUP = "digitally_imported"
def Init(self, model, settings_dialog_callback):
clementine.InternetService.__init__(self, self.SERVICE_NAME, model)
# We must hold a weak reference to the callback or else it makes a circular
# reference between the services and Plugin from main.py.
self.settings_dialog_callback = weakref.ref(settings_dialog_callback)
self.url_handler = DigitallyImportedUrlHandler(self.URL_SCHEME, self)
clementine.player.RegisterUrlHandler(self.url_handler)
self.network = clementine.NetworkAccessManager(None)
self.path = os.path.dirname(__file__)
self.audio_type = 0
self.username = ""
self.password = ""
self.context_index = None
self.menu = None
self.root = None
self.task_id = None
self.refresh_streams_reply = None
self.load_station_reply = None
self.items = []
self.ReloadSettings()
def ReloadSettings(self):
settings = QSettings()
settings.beginGroup(self.SETTINGS_GROUP)
self.audio_type = int(settings.value("audio_type", 0))
self.username = unicode(settings.value("username", ""))
self.password = unicode(settings.value("password", ""))
def CreateRootItem(self):
self.root = QStandardItem(QIcon(os.path.join(self.path, self.ICON_FILENAME)),
self.SERVICE_DESCRIPTION)
self.root.setData(True, clementine.InternetModel.Role_CanLazyLoad)
return self.root
def LazyPopulate(self, parent):
if parent == self.root:
# Download the list of streams the first time the user expands the root
self.RefreshStreams()
def ShowContextMenu(self, index, global_pos):
if not self.menu:
self.menu = QMenu()
for action in self.GetPlaylistActions():
self.menu.addAction(action)
self.menu.addAction(clementine.IconLoader.Load("download"),
self.tr("Open " + self.HOMEPAGE_NAME + " in browser"), self.Homepage)
self.menu.addAction(clementine.IconLoader.Load("view-refresh"),
self.tr("Refresh streams"), self.RefreshStreams)
self.menu.addSeparator()
self.menu.addAction(clementine.IconLoader.Load("configure"),
self.tr("Configure..."), self.settings_dialog_callback())
self.context_index = index
self.menu.popup(global_pos)
def GetCurrentIndex(self):
return self.context_index
def Homepage(self):
QDesktopServices.openUrl(self.HOMEPAGE_URL)
def RefreshStreams(self):
if self.task_id is not None:
return
LOGGER.info("Getting stream list from '%s'", self.STREAM_LIST_URL)
# Request the list of stations
self.refresh_streams_reply = self.network.get(QNetworkRequest(self.STREAM_LIST_URL))
self.refresh_streams_reply.connect("finished()", self.RefreshStreamsFinished)
# Give the user some indication that we're doing something
self.task_id = clementine.task_manager.StartTask(self.tr("Getting streams"))
def RefreshStreamsFinished(self):
if self.refresh_streams_reply is None:
return
if self.task_id is None:
return
# Stop the spinner in the status bar
clementine.task_manager.SetTaskFinished(self.task_id)
self.task_id = None
# Read the data and parse the json object inside
json_data = self.refresh_streams_reply.readAll().data()
streams = json.loads(json_data)
# Sort by name
streams = sorted(streams, key=operator.itemgetter("name"))
LOGGER.info("Loaded %d streams", len(streams))
# Now we have the list of streams, so clear any existing items in the list
# and insert the new ones
if self.root.hasChildren():
self.root.removeRows(0, self.root.rowCount())
for stream in streams:
song = clementine.Song()
song.set_title(stream["name"])
song.set_artist(self.SERVICE_DESCRIPTION)
song.set_url(QUrl("%s://%s" % (self.URL_SCHEME, stream["key"])))
item = QStandardItem(QIcon(":last.fm/icon_radio.png"), stream["name"])
item.setData(stream["description"], PythonQt.QtCore.Qt.ToolTipRole)
item.setData(clementine.InternetModel.PlayBehaviour_SingleItem, clementine.InternetModel.Role_PlayBehaviour)
item.setData(song, clementine.InternetModel.Role_SongMetadata)
self.root.appendRow(item)
# Keep references to the items otherwise Python will delete them
self.items.append(item)
def playlistitem_options(self):
return clementine.PlaylistItem.Options(clementine.PlaylistItem.PauseDisabled)
def LoadStation(self, key):
raise NotImplementedError()
def LoadPlaylistFinished(self):
self.url_handler.LoadPlaylistFinished(self.load_station_reply)

View File

@ -1,39 +0,0 @@
from servicebase import DigitallyImportedServiceBase
from PythonQt.QtCore import QEvent, QFile, QSettings
from PythonQt.QtGui import QComboBox, QDialog, QIcon, QLineEdit
import uic
import os.path
class SettingsDialog(QDialog):
def __init__(self, parent=None):
QDialog.__init__(self, parent)
self.path = os.path.dirname(__file__)
# Set up the user interface
uic.loadUi(os.path.join(self.path, "settingsdialog.ui"), self)
# Set the window icon
self.setWindowIcon(QIcon(os.path.join(self.path, "icon-small.png")))
def showEvent(self, event):
# Load the settings
settings = QSettings()
settings.beginGroup(DigitallyImportedServiceBase.SETTINGS_GROUP)
self.type.setCurrentIndex(int(settings.value("audio_type", 0)))
self.username.setText(settings.value("username", ""))
self.password.setText(settings.value("password", ""))
#QDialog.showEvent(self, event)
def accept(self):
# Save the settings
settings = QSettings()
settings.beginGroup(DigitallyImportedServiceBase.SETTINGS_GROUP)
settings.setValue("audio_type", self.type.currentIndex)
settings.setValue("username", self.username.text)
settings.setValue("password", self.password.text)
QDialog.done(self, QDialog.Accepted)

View File

@ -1,205 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SettingsDialog</class>
<widget class="QDialog" name="SettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>610</width>
<height>331</height>
</rect>
</property>
<property name="windowTitle">
<string>Digitally Imported settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Account details (Premium)</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Digitally Imported username</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="username"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Digitally Imported password</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>You can &lt;b&gt;listen for free&lt;/b&gt; without an account, but Premium members can listen to &lt;b&gt;higher quality&lt;/b&gt; streams without advertisements.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QLabel" name="label_4">
<property name="text">
<string>&lt;a href=&quot;http://www.di.fm/premium/&quot;&gt;Upgrade to Premium now&lt;/a&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Preferences</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Audio type</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="type">
<item>
<property name="text">
<string>MP3 96k</string>
</property>
</item>
<item>
<property name="text">
<string>MP3 256k (Premium only)</string>
</property>
</item>
<item>
<property name="text">
<string>AAC 32k</string>
</property>
</item>
<item>
<property name="text">
<string>AAC 64k (Premium only)</string>
</property>
</item>
<item>
<property name="text">
<string>AAC 128k (Premium only)</string>
</property>
</item>
<item>
<property name="text">
<string>Windows Media 40k</string>
</property>
</item>
<item>
<property name="text">
<string>Windows Media 64k (Premium only)</string>
</property>
</item>
<item>
<property name="text">
<string>Windows Media 128k (Premium only)</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SettingsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SettingsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,87 +0,0 @@
import clementine
from servicebase import DigitallyImportedServiceBase
from PythonQt.QtCore import QSettings, QUrl
from PythonQt.QtNetwork import QNetworkCookie, QNetworkCookieJar, QNetworkRequest
import re
class SkyFmService(DigitallyImportedServiceBase):
HOMEPAGE_URL = QUrl("http://www.sky.fm/")
HOMEPAGE_NAME = "sky.fm"
STREAM_LIST_URL = QUrl("http://listen.sky.fm/")
ICON_FILENAME = "icon-sky.png"
SERVICE_NAME = "SKY.fm"
SERVICE_DESCRIPTION = "SKY.fm"
URL_SCHEME = "skyfm"
HASHKEY_RE = re.compile(r'hashKey\s*=\s*\'([0-9a-f]+)\'')
# These have to be in the same order as in the settings dialog
PLAYLISTS = [
{"premium": False, "url": "http://listen.sky.fm/public3/%s.pls"},
{"premium": True, "url": "http://listen.sky.fm/premium_high/%s.pls?hash=%s"},
{"premium": False, "url": "http://listen.sky.fm/public1/%s.pls"},
{"premium": True, "url": "http://listen.sky.fm/premium_medium/%s.pls?hash=%s"},
{"premium": True, "url": "http://listen.sky.fm/premium/%s.pls?hash=%s"},
{"premium": False, "url": "http://listen.sky.fm/public5/%s.asx"},
{"premium": True, "url": "http://listen.sky.fm/premium_wma_low/%s.asx?hash=%s"},
{"premium": True, "url": "http://listen.sky.fm/premium_wma/%s.asx?hash=%s"},
]
def __init__(self, model, settings_dialog_callback):
DigitallyImportedServiceBase.Init(self, model, settings_dialog_callback)
self.last_key = None
self.load_station_reply = None
def LoadStation(self, key):
# Non-premium streams can just start loading straight away
if not self.PLAYLISTS[self.audio_type]["premium"]:
self.LoadPlaylist(key)
return
# Otherwise we have to get the user's hashKey
request = QNetworkRequest(QUrl("http://www.sky.fm/configure_player.php"))
postdata = "amember_login=%s&amember_pass=%s" % (
QUrl.toPercentEncoding(self.username),
QUrl.toPercentEncoding(self.password))
self.load_station_reply = self.network.post(request, postdata)
self.load_station_reply.connect("finished()", self.LoadHashKeyFinished)
self.last_key = key
def LoadHashKeyFinished(self):
if self.load_station_reply is None:
return
# Parse the hashKey out of the reply
data = self.load_station_reply.readAll().data()
match = self.HASHKEY_RE.search(data)
self.load_station_reply = None
if match:
hash_key = match.group(1)
else:
clementine.task_manager.SetTaskFinished(self.task_id)
self.task_id = None
self.StreamError(self.tr("Invalid SKY.fm username or password"))
return
# Now we can load the playlist
self.LoadPlaylist(self.last_key, hash_key)
def LoadPlaylist(self, key, hash_key=None):
playlist_url = self.PLAYLISTS[self.audio_type]["url"]
if hash_key:
playlist_url = playlist_url % (key, hash_key)
else:
playlist_url = playlist_url % key
# Start fetching the playlist. Can't use a SongLoader to do this because
# we have to use the cookies we set in ReloadSettings()
self.load_station_reply = self.network.get(QNetworkRequest(QUrl(playlist_url)))
self.load_station_reply.connect("finished()", self.LoadPlaylistFinished)

View File

@ -1,5 +0,0 @@
install_script_files(google-covers
google_covers.py
icon.jpg
script.ini
)

View File

@ -1,72 +0,0 @@
import clementine
from PythonQt.QtCore import QUrl
from PythonQt.QtNetwork import QNetworkRequest
import json
import logging
import urllib
LOGGER = logging.getLogger("google_images")
class GoogleImagesCoverProvider(clementine.CoverProvider):
API_URL = 'https://ajax.googleapis.com/ajax/services/search/images?{0}'
def __init__(self, parent=None):
clementine.CoverProvider.__init__(self, "Google Images", parent)
self.api_args = {
'v' : '1.0',
# at most five results
'rsz' : '5',
# only larger sizes
'imgsz' : 'large|xlarge'
}
self.network = clementine.NetworkAccessManager()
def StartSearch(self, artist, album, id):
url = self.GetQueryURL(artist + " " + album)
LOGGER.info("Id %d - sending request to '%s'" % (id, url))
reply = self.network.get(QNetworkRequest(url))
def QueryFinished():
LOGGER.debug("Id %d - finished" % id)
self.SearchFinished(id, self.ParseReply(artist, album, reply))
reply.connect("finished()", QueryFinished)
return True
def ParseReply(self, artist, album, reply):
results = json.loads(str(reply.readAll()))
parsed = []
if "responseStatus" not in results or results["responseStatus"] != 200:
LOGGER.warning("Error parsing reply: %s", results["responseDetails"])
return parsed
query = "%s - %s" % (artist, album)
LOGGER.info("Parsing reply for query '%s'" % query)
for result in results['responseData']['results']:
current = clementine.CoverSearchResult()
current.description = query
current.image_url = result['url']
parsed.append(current)
return parsed
def GetQueryURL(self, query):
current_args = self.api_args.copy()
current_args['q'] = query
return QUrl.fromEncoded(self.API_URL.format(urllib.urlencode(current_args)))
provider = GoogleImagesCoverProvider()
clementine.cover_providers.AddProvider(provider)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -1,9 +0,0 @@
[Script]
name=Google Images cover provider
description=Thanks to this script Clementine will be able to download covers from Google Images for you.
author=Pawel Bara <keirangtp ( at ) gmail.com>
url=http://www.clementine-player.org
icon=icon.jpg
language=python
script_file=google_covers.py

View File

@ -1,5 +0,0 @@
install_script_files(invalidate-deleted
icon.png
invalidate_deleted.py
script.ini
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,35 +0,0 @@
import clementine
from PythonQt.QtCore import QObject
from PythonQt.QtGui import QAction
class InvalidateDeleted(QObject):
"""
TODO: actions which are defined here should be implemented here too instead of delegating
the responsibility to Playlist Manager. Unfortunately, it cannot be done at this moment
since using PlaylistItemPtrs in Python crashes Clementine.
"""
def __init__(self):
QObject.__init__(self)
self.invalidate = QAction("invalidate_deleted", self)
self.invalidate.setText("Grey out deleted songs")
self.invalidate.connect("activated()", self.grey_out_activated)
self.delete = QAction("remove_deleted", self)
self.delete.setText("Remove deleted songs")
self.delete.connect("activated()", self.delete_activated)
clementine.ui.AddAction('playlist_menu', self.invalidate)
clementine.ui.AddAction('playlist_menu', self.delete)
def grey_out_activated(self):
clementine.playlists.InvalidateDeletedSongs()
def delete_activated(self):
clementine.playlists.RemoveDeletedSongs()
script = InvalidateDeleted()

View File

@ -1,9 +0,0 @@
[Script]
name=Deleted songs invalidator
description=This script will add a menu item for playlist's main menu which will grey out all non existent songs in all of your playlists.
author=Pawel Bara <keirangtp ( at ) gmail.com>
url=http://www.clementine-player.org
icon=icon.png
language=python
script_file=invalidate_deleted.py

View File

@ -1,6 +0,0 @@
install_script_files(nostalgia
main.py
nostalgia.jpg
icon.png
script.ini
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,37 +0,0 @@
import clementine
from PythonQt.QtGui import QAction
from PythonQt.Qt import QImage
import os
class Plugin:
def __init__(self):
self.path = os.path.dirname(__file__)
clementine.player.connect("SongChangeRequestProcessed(QUrl, bool)", self.Nostalgia)
self.action = QAction("Nostalgia-ize!", None)
self.action.setCheckable(True)
self.action.connect("changed()", self.Nostalgia)
clementine.ui.AddAction("extras_menu", self.action)
self.title = "Never Gonna' Give You Up"
self.artist = "Rick Astley"
self.album = "Whenever You Need Somebody"
self.image = QImage(os.path.join(self.path, "nostalgia.jpg"))
def Nostalgia(self):
if self.action.isChecked():
for item in clementine.playlists.current().GetAllItems():
temp = clementine.Song(item.Metadata())
temp.set_title(self.title)
temp.set_artist(self.artist)
temp.set_album(self.album)
temp.set_image(self.image)
item.SetTemporaryMetadata(temp)
else:
for item in clementine.playlists.current().GetAllItems():
item.ClearTemporaryMetadata()
clementine.playlists.current().PlaylistChanged()
plugin = Plugin()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

View File

@ -1,9 +0,0 @@
[Script]
name=Nostalgia-izer
description=Brings back memories of the good ol\' times. Must be enabled in the \'Extras\' menu.
author=Tyler Rhodes <tyler.s.rhodes ( at ) gmail>
url=http://www.clementine-player.org
icon=icon.png
language=python
script_file=main.py

View File

@ -1,4 +0,0 @@
install_script_files(preview
main.py
script.ini
)

View File

@ -1,41 +0,0 @@
import clementine
from PyQt4.QtCore import QTimer
from PyQt4.QtGui import QAction
class Plugin:
def __init__(self):
self.timer = QTimer(None)
self.timer.setInterval(10000)
self.timer.timeout.connect(self.Timeout)
self.timer.setSingleShot(True)
self.action = QAction("Preview mode", None)
self.action.setCheckable(True)
self.action.triggered.connect(self.Enabled)
clementine.ui.AddAction("playlist_menu", self.action)
clementine.player.Playing.connect(self.Playing)
clementine.player.Paused.connect(self.Stopped)
clementine.player.Stopped.connect(self.Stopped)
self.enabled = False
def Enabled(self, enabled):
self.enabled = enabled
if enabled:
if clementine.player.GetState() == 2: # Playing
self.timer.start()
else:
self.timer.stop()
def Playing(self):
if self.enabled:
self.timer.start()
def Stopped(self):
self.timer.stop()
def Timeout(self):
if clementine.player.GetState() == 2:
clementine.player.Next()
plugin = Plugin()

View File

@ -1,8 +0,0 @@
[Script]
name=Preview
description=Adds an option to preview songs.
author=John Maguire <john.maguire@gmail.com>
url=http://www.clementine-player.org
language=python
script_file=main.py

View File

@ -1,5 +0,0 @@
install_script_files(rainbowizer
icon.jpg
rainbow.py
script.ini
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,37 +0,0 @@
from PythonQt.QtCore import QObject
from PythonQt.QtGui import QAction, QColor
import clementine
class RainbowizerScript(QObject):
PRIORITY = 1
def __init__(self):
QObject.__init__(self)
# Generate colors
self.colors = []
for hue in xrange(0, 255, 30):
self.colors.append(QColor.fromHsv(hue, 255, 255, 96))
self.action = QAction("rainbowize_playlist", self)
self.action.setText("Rainbowize!")
self.action.setCheckable(True)
self.action.connect("changed()", self.rainbowize)
clementine.ui.AddAction('playlist_menu', self.action)
def rainbowize(self):
for playlist in clementine.playlists.GetAllPlaylists():
if self.action.isChecked():
for i, item in enumerate(playlist.GetAllItems()):
item.SetBackgroundColor(self.PRIORITY, self.colors[i % len(self.colors)])
else:
# undo all rainbow colors
for item in playlist.GetAllItems():
item.RemoveBackgroundColor(self.PRIORITY)
script = RainbowizerScript()

View File

@ -1,9 +0,0 @@
[Script]
name=Rainbowizer
description=This is a sample plugin designed to show you how to use the color API of playlist items. It rainbowizes your playlist for better user experience!
author=Pawel Bara <keirangtp ( at ) gmail.com>
url=http://www.clementine-player.org
icon=icon.jpg
language=python
script_file=rainbow.py

View File

@ -1,5 +0,0 @@
install_script_files(remove-duplicates
icon.png
remove_duplicates.py
script.ini
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -1,46 +0,0 @@
import clementine
class RemoveDuplicatesListener(clementine.SongInsertVetoListener):
def __init__(self):
clementine.SongInsertVetoListener.__init__(self)
def init_listener(self):
for playlist in clementine.playlists.GetAllPlaylists():
playlist.AddSongInsertVetoListener(self)
clementine.playlists.connect("PlaylistAdded(int,QString)", self.playlist_added)
def remove_duplicates(self):
for playlist in clementine.playlists.GetAllPlaylists():
self.remove_duplicates_from(playlist)
def playlist_added(self, playlist_id, playlist_name):
playlist = clementine.playlists.playlist(playlist_id)
playlist.AddSongInsertVetoListener(self)
self.remove_duplicates_from(playlist)
def AboutToInsertSongs(self, old_songs, new_songs):
return [song for song in new_songs if song in old_songs]
def remove_duplicates_from(self, playlist):
duplicate_indices = []
uniques = []
songs = playlist.GetAllSongs()
for index in range(0, len(songs)):
song = songs[index]
if song not in uniques:
uniques.append(song)
else:
duplicate_indices.append(index)
if len(duplicate_indices) > 0:
playlist.RemoveItemsWithoutUndo(duplicate_indices)
script = RemoveDuplicatesListener()
script.init_listener()
script.remove_duplicates()

View File

@ -1,9 +0,0 @@
[Script]
name=Duplicate remover
description=This script will prevent duplicates being added to your playlists.
author=Pawel Bara <keirangtp ( at ) gmail.com>
url=http://www.clementine-player.org
icon=icon.png
language=python
script_file=remove_duplicates.py

View File

@ -1,5 +0,0 @@
install_script_files(setlistfm
icon.jpg
setlistfm.py
script.ini
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 838 B

View File

@ -1,9 +0,0 @@
[Script]
name=Setlist.fm
description=Helps you prepare for a concert by filling your playlist with the latest setlist of the chosen artist
author=Pawel Bara <keirangtp ( at ) gmail.com>
url=http://www.clementine-player.org
icon=icon.jpg
language=python
script_file=setlistfm.py

View File

@ -1,167 +0,0 @@
import json
import sys
from traceback import print_exc
from PythonQt.Qt import QObject
from PythonQt.Qt import QUrl
from PythonQt.QtGui import QAction
from PythonQt.QtNetwork import QNetworkRequest
import clementine
import logging
import urllib
LOGGER = logging.getLogger("setlistfm")
class SetlistFmScript(QObject):
def __init__(self):
QObject.__init__(self)
# maps QNetworkReply to artist of action which created it
# every thread knows what artist it's looking for
self.artist_map = {}
self.task_id = None
self.network = clementine.NetworkAccessManager(self)
self.action = QAction("fill_with_setlist", self)
self.action.setText("Load latest setlist")
self.action.connect("activated()", self.load_setlist_activated)
clementine.ui.AddAction('library_context_menu', self.action)
def load_setlist_activated(self):
# wait for the last call to finish
if self.task_id is not None:
return
# find the first artist
artist = ""
for song in clementine.library_view.GetSelectedSongs():
if len(str(song.artist())) > 0:
artist = str(song.artist())
break
# ignore the call if there's no artist in selection
if len(artist) == 0:
return
# start the progress spinner
self.task_id = clementine.task_manager.StartTask(self.tr("Getting setlist"))
# finally - request for the setlists of artist
reply = self.network.get(QNetworkRequest(self.get_setlist_fm_url(artist)))
self.artist_map[reply] = artist
reply.connect("finished()", lambda: self.load_setlist_activated_finalize(reply))
def load_setlist_activated_finalize(self, reply):
reply.deleteLater()
if self.task_id is None:
return
# stop the progress spinner
clementine.task_manager.SetTaskFinished(self.task_id)
self.task_id = None
artist = self.artist_map.pop(reply)
# get the titles of songs from the latest setlist available
# on setlist.fm for the artist
data = reply.readAll().data()
titles = self.parse_setlist_fm_reply(data)
if len(titles) == 0:
return
# we uppercase the titles to make the plugin case insensitive
titles = map(lambda title: title.upper(), titles)
# get song ids for titles
query = clementine.LibraryQuery()
query.SetColumnSpec('ROWID, title')
query.AddWhere('UPPER(artist)', artist.upper())
query.AddWhere('UPPER(title)', titles, 'IN')
# maps titles to ids; also removes possible title duplicates
# from the query
title_map = {}
if clementine.library.ExecQuery(query):
while query.Next():
# be super cautious and throw out the faulty ones
id = query.Value(0)
title = query.Value(1)
title_map[str(title).upper()] = id
if len(title_map) > 0:
# get complete song objects for ids
from_lib = clementine.library.GetSongsById(title_map.values())
# maps ids to song objects
lib_title_map = {}
for song in from_lib:
lib_title_map[song.id()] = song
unavailable = []
into_playlist = []
# iterate over titles to preserve ordering of songs
# in the setlist
for title in titles:
try:
# fill the list
into_playlist.append(lib_title_map[title_map[title]])
except KeyError:
# TODO: do something with songs not in library?
unavailable.append(title)
# finally - fill the playlist with songs!
current = clementine.playlists.current()
if current != None and len(into_playlist) > 0:
current.InsertLibraryItems(into_playlist)
def parse_setlist_fm_reply(self, response):
try:
response = json.loads(response)
count = response['setlists']['@total']
# only if we have at least one set
if count != None and count > 0:
# look for the first one with any information
for setlist in response['setlists']['setlist']:
result = []
sets = setlist['sets']
if len(sets) > 0:
# this may or may not be an array - make sure it always is!
final_sets = sets['set'] if len(sets['set']) > 1 else [sets['set']]
# from all the sets (like main + encore)
for set in final_sets:
# this may or may not be an array - make sure it always is!
final_songs = set['song'] if len(set['song']) > 1 else [set['song']]
for song in final_songs:
result.append(song['@name'])
if len(result) > 0:
return result
return []
except:
LOGGER.debug('No setlists found')
return []
def get_setlist_fm_url(self, artist):
url = QUrl('http://api.setlist.fm/rest/0.1/search/setlists.json')
url.addQueryItem('artistName', artist)
return url
script = SetlistFmScript()

View File

@ -1,4 +0,0 @@
install_script_files(shutdown
system-shutdown.png
main.py
script.ini)

View File

@ -1,30 +0,0 @@
import clementine
from PythonQt.QtGui import QAction
import logging
import sys
LOGGER = logging.getLogger("system-shutdown")
class Plugin:
def __init__(self):
self.enabled = False
clementine.player.connect("PlaylistFinished()", self.PlaylistFinished)
self.action = QAction("Shutdown at end", None)
self.action.setCheckable(True)
self.action.connect("triggered(bool)", self.Enabled)
clementine.ui.AddAction("playlist_menu", self.action)
def PlaylistFinished(self):
if self.enabled:
LOGGER.info("Reached the end of the playlist - shutting down")
sys.exit(0)
def Enabled(self, enabled):
LOGGER.info("Shutdown at end of playlist enabled: %s" % str(enabled))
self.enabled = enabled
plugin = Plugin()

View File

@ -1,9 +0,0 @@
[Script]
name=Shutdown At End
description=Adds an option to shutdown Clementine at the end of the current playlist.
author=John Maguire <john.maguire@gmail.com>
url=http://www.clementine-player.org
icon=system-shutdown.png
language=python
script_file=main.py

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,5 +0,0 @@
install_script_files(youtube
YouTube.png
youtube.py
script.ini
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

@ -1,9 +0,0 @@
[Script]
name=YouTube
description=Finds songs on YouTube
author=John Maguire <john.maguire@gmail.com>
url=http://www.clementine-player.org
icon=YouTube.png
language=python
script_file=youtube.py

View File

@ -1,41 +0,0 @@
import clementine
from PythonQt.Qt import QUrl
from PythonQt.QtGui import QAction
from PythonQt.QtGui import QDesktopServices
from PythonQt.QtNetwork import QNetworkRequest
import json
class Plugin:
def __init__(self):
self.action = QAction("Find on YouTube", None)
self.action.connect("activated()", self.SearchYoutube)
clementine.ui.AddAction("song_menu", self.action)
self.network = clementine.NetworkAccessManager()
def SearchYoutube(self):
selection = clementine.playlists.current_selection().indexes()
title = selection[clementine.Playlist.Column_Title].data()
artist = selection[clementine.Playlist.Column_Artist].data()
query = title + ' ' + artist
url = QUrl('https://gdata.youtube.com/feeds/api/videos')
url.addQueryItem('q', query)
url.addQueryItem('alt', 'json')
url.addQueryItem('max-results', 1)
reply = self.network.get(QNetworkRequest(url))
def SearchFinished():
data = json.loads(str(reply.readAll()))
feed = data['feed']
try:
print feed['entry'][0]['media$group']['media$player'][0]['url']
youtube_url = feed['entry'][0]['media$group']['media$player'][0]['url']
QDesktopServices.openUrl(QUrl.fromEncoded(str(youtube_url)))
except Exception, e:
print e
reply.connect("finished()", SearchFinished)
plugin = Plugin()