Remove obsolete Python scripts and doc generator.
|
@ -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}
|
||||
)
|
|
@ -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.
|
|
@ -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; }
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
|
@ -1,5 +0,0 @@
|
|||
<RCC>
|
||||
<qresource prefix="/doc/python">
|
||||
<file>generate_python_docs.py</file>
|
||||
</qresource>
|
||||
</RCC>
|
|
@ -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)
|
|
@ -1,5 +0,0 @@
|
|||
install_script_files(amazon-covers
|
||||
amazon_covers.py
|
||||
icon.jpg
|
||||
script.ini
|
||||
)
|
|
@ -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 = 'KBlHVSNEvJrebNB/BBmGIh4a38z4cedfFvlDJ5fE'
|
||||
|
||||
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)
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
install_script_files(clipboard
|
||||
clipboard.py
|
||||
script.ini
|
||||
)
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
||||
)
|
|
@ -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)
|
Before Width: | Height: | Size: 864 B |
Before Width: | Height: | Size: 211 B |
Before Width: | Height: | Size: 6.3 KiB |
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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 <b>listen for free</b> without an account, but Premium members can listen to <b>higher quality</b> 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><a href="http://www.di.fm/premium/">Upgrade to Premium now</a></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>
|
|
@ -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)
|
|
@ -1,5 +0,0 @@
|
|||
install_script_files(google-covers
|
||||
google_covers.py
|
||||
icon.jpg
|
||||
script.ini
|
||||
)
|
|
@ -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)
|
Before Width: | Height: | Size: 6.0 KiB |
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
install_script_files(invalidate-deleted
|
||||
icon.png
|
||||
invalidate_deleted.py
|
||||
script.ini
|
||||
)
|
Before Width: | Height: | Size: 4.4 KiB |
|
@ -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()
|
|
@ -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
|
|
@ -1,6 +0,0 @@
|
|||
install_script_files(nostalgia
|
||||
main.py
|
||||
nostalgia.jpg
|
||||
icon.png
|
||||
script.ini
|
||||
)
|
Before Width: | Height: | Size: 2.2 KiB |
|
@ -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()
|
Before Width: | Height: | Size: 57 KiB |
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
install_script_files(preview
|
||||
main.py
|
||||
script.ini
|
||||
)
|
|
@ -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()
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
install_script_files(rainbowizer
|
||||
icon.jpg
|
||||
rainbow.py
|
||||
script.ini
|
||||
)
|
Before Width: | Height: | Size: 2.4 KiB |
|
@ -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()
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
install_script_files(remove-duplicates
|
||||
icon.png
|
||||
remove_duplicates.py
|
||||
script.ini
|
||||
)
|
Before Width: | Height: | Size: 6.2 KiB |
|
@ -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()
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
install_script_files(setlistfm
|
||||
icon.jpg
|
||||
setlistfm.py
|
||||
script.ini
|
||||
)
|
Before Width: | Height: | Size: 838 B |
|
@ -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
|
|
@ -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()
|
|
@ -1,4 +0,0 @@
|
|||
install_script_files(shutdown
|
||||
system-shutdown.png
|
||||
main.py
|
||||
script.ini)
|
|
@ -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()
|
|
@ -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
|
Before Width: | Height: | Size: 4.2 KiB |
|
@ -1,5 +0,0 @@
|
|||
install_script_files(youtube
|
||||
YouTube.png
|
||||
youtube.py
|
||||
script.ini
|
||||
)
|
Before Width: | Height: | Size: 71 KiB |
|
@ -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
|
|
@ -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()
|