├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── libsonic
├── __init__.py
├── connection.py
└── errors.py
├── requirements.txt
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | test.py
3 | MANIFEST
4 | dist
5 | build
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.7.0
2 |
3 | * py-sonic should now python 2/3 compatible. Unfortunately, this adds the requirement of the "six" module for this compatibility. Please file a bug if you run into issues with either python 2 or 3.
4 | * Cut over to setuptools from disttools for pip requirement support
5 |
6 | ## 0.6.2
7 |
8 | * Added an option to use GET requests, instead of the default POST requests
9 |
10 | ## 0.6.1
11 |
12 | * Added `legacyAuth` option for pre-1.13.0 support
13 |
14 | ## 0.6.0
15 |
16 | * Added API 1.14.0 support
17 |
18 | ## 0.5.1
19 |
20 | * Added the ability to use a netrc file for credentials
21 |
22 | ## 0.5.0
23 |
24 | * Added support for using credentials via a netrc file
25 |
26 | ## 0.4.1
27 |
28 | * Fixed SSL handling issues
29 |
30 | ## 0.4.0
31 |
32 | * Added missing 1.12.0 API items
33 | * Added 1.13.0 API items
34 | * All timestamps both passed in, and returned, should now be in **proper** unix time, which is seconds since the epoch, **not** milliseconds since the epoch
35 |
36 | ## 0.3.5
37 |
38 | * allow for self-signed certs
39 |
40 | ## 0.3.4
41 |
42 | * Add missing parameters to getAlbumList2 (thanks to basilfx)
43 | * Remove trailing whitespace (thanks to basilfx)
44 |
45 | ## 0.3.3
46 |
47 | * Added support for API version 1.11.0
48 | * Added a couple of additions from API version 1.10.x that were previously
49 | missed
50 |
51 | ## 0.3.1
52 |
53 | * Incorporated unofficial API calls (beallio)
54 |
55 | ## 0.2.1
56 |
57 | * Added a patch to force SSLv3 as some users were apparently having issues
58 | with the 4.7 release of Subsonic and SSL. (thanks to orangepeelbeef)
59 |
60 | ## 0.2.0
61 |
62 | * Added support for API version 1.8.0 (Subsonic verion 4.7)
63 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include CHANGELOG.md
3 | include README.md
4 | include requirements.txt
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # py-sonic #
2 | ## INSTALL ##
3 |
4 | Installation is fairly simple. Just do the standard install as root:
5 |
6 | tar -xvzf py-sonic-*.tar.gz
7 | cd py-sonic-*
8 | python setup.py install
9 |
10 | You can also install directly using *pip* or *easy_install*
11 |
12 | pip install py-sonic
13 |
14 | ## USAGE ##
15 |
16 | This library follows the REST API almost exactly (for now). If you follow the
17 | documentation on http://www.subsonic.org/pages/api.jsp or you do a:
18 |
19 | pydoc libsonic.connection
20 |
21 | I have also added documentation at http://stuffivelearned.org/doku.php?id=programming:python:py-sonic
22 |
23 | ## BASIC TUTORIAL ##
24 |
25 | This is about as basic as it gets. We are just going to set up the connection
26 | and then get a couple of random songs.
27 |
28 | ```python
29 | #!/usr/bin/env python
30 |
31 | from pprint import pprint
32 | import libsonic
33 |
34 | # We pass in the base url, the username, password, and port number
35 | # Be sure to use https:// if this is an ssl connection!
36 | conn = libsonic.Connection('https://music.example.com' , 'myuser' ,
37 | 'secretpass' , port=443)
38 | # Let's get 2 completely random songs
39 | songs = conn.getRandomSongs(size=2)
40 | # We'll just pretty print the results we got to the terminal
41 | pprint(songs)
42 | ```
43 |
44 | As you can see, it's really pretty simple. If you use the documentation
45 | provided in the library:
46 |
47 | pydoc libsonic.connection
48 |
49 | or the api docs on subsonic.org (listed above), you should be able to make use
50 | of your server without too much trouble.
51 |
52 | Right now, only plain old dictionary structures are returned. The plan
53 | for a later release includes the following:
54 |
55 | * Proper object representations for Artist, Album, Song, etc.
56 | * Lazy access of members (the song objects aren't created until you want to
57 | do something with them)
58 |
--------------------------------------------------------------------------------
/libsonic/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | This file is part of py-sonic.
3 |
4 | py-sonic is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | py-sonic is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU General Public License for more details.
13 |
14 | You should have received a copy of the GNU General Public License
15 | along with py-sonic. If not, see
16 |
17 | For information on method calls, see 'pydoc libsonic.connection'
18 |
19 | ----------
20 | Basic example:
21 | ----------
22 |
23 | import libsonic
24 |
25 | conn = libsonic.Connection('http://localhost' , 'admin' , 'password')
26 | print conn.ping()
27 |
28 | """
29 |
30 | from .connection import *
31 |
32 | __version__ = '1.0.3'
33 |
--------------------------------------------------------------------------------
/libsonic/connection.py:
--------------------------------------------------------------------------------
1 | """
2 | This file is part of py-sonic.
3 |
4 | py-sonic is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | py-sonic is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU General Public License for more details.
13 |
14 | You should have received a copy of the GNU General Public License
15 | along with py-sonic. If not, see
16 | """
17 |
18 | from libsonic.errors import *
19 | from netrc import netrc
20 | from hashlib import md5
21 | import urllib.request
22 | import urllib.error
23 | from http import client as http_client
24 | from urllib.parse import urlencode
25 | from io import StringIO
26 |
27 | import json
28 | import logging
29 | import socket
30 | import ssl
31 | import sys
32 | import os
33 |
34 | API_VERSION = '1.16.1'
35 |
36 | logger = logging.getLogger(__name__)
37 |
38 | class Connection(object):
39 | def __init__(self, baseUrl, username=None, password=None, port=4040,
40 | serverPath='/rest', appName='py-sonic', apiVersion=API_VERSION,
41 | insecure=False, useNetrc=None, legacyAuth=False, useGET=False,
42 | salt=None, token=None, userAgent=None):
43 | """
44 | This will create a connection to your subsonic server
45 |
46 | baseUrl:str The base url for your server. Be sure to use
47 | "https" for SSL connections. If you are using
48 | a port other than the default 4040, be sure to
49 | specify that with the port argument. Do *not*
50 | append it here.
51 |
52 | ex: http://subsonic.example.com
53 |
54 | If you are running subsonic under a different
55 | path, specify that with the "serverPath" arg,
56 | *not* here. For example, if your subsonic
57 | lives at:
58 |
59 | https://mydomain.com:8080/path/to/subsonic/rest
60 |
61 | You would set the following:
62 |
63 | baseUrl = "https://mydomain.com"
64 | port = 8080
65 | serverPath = "/path/to/subsonic/rest"
66 | username:str The username to use for the connection. This
67 | can be None if `useNetrc' is True (and you
68 | have a valid entry in your netrc file)
69 | password:str The password to use for the connection. This
70 | can be None if `useNetrc' is True (and you
71 | have a valid entry in your netrc file)
72 | salt:str Instead of providing a password, the caller can
73 | provide both token and salt arguments for
74 | authenticaion, reducing the impact of plaintext
75 | passwords
76 | token:str Must be provided if the salt is provided.
77 | port:int The port number to connect on. The default for
78 | unencrypted subsonic connections is 4040
79 | serverPath:str The base resource path for the subsonic views.
80 | This is useful if you have your subsonic server
81 | behind a proxy and the path that you are proxying
82 | is different from the default of '/rest'.
83 | Ex:
84 | serverPath='/path/to/subs'
85 |
86 | The full url that would be built then would be
87 | (assuming defaults and using "example.com" and
88 | you are using the "ping" view):
89 |
90 | http://example.com:4040/path/to/subs/ping.view
91 | appName:str The name of your application.
92 | apiVersion:str The API version you wish to use for your
93 | application. Subsonic will throw an error if you
94 | try to use/send an api version higher than what
95 | the server supports. See the Subsonic API docs
96 | to find the Subsonic version -> API version table.
97 | This is useful if you are connecting to an older
98 | version of Subsonic.
99 | insecure:bool This will allow you to use self signed
100 | certificates when connecting if set to True.
101 | useNetrc:str|bool You can either specify a specific netrc
102 | formatted file or True to use your default
103 | netrc file ($HOME/.netrc).
104 | legacyAuth:bool Use pre-1.13.0 API version authentication
105 | useGET:bool Use a GET request instead of the default POST
106 | request. This is not recommended as request
107 | URLs can get very long with some API calls
108 | userAgent:str If specified, use this User-Agent string in
109 | the request header. If None, the default Python
110 | urllib UA will be used.
111 | """
112 | self._baseUrl = baseUrl
113 | self._hostname = baseUrl.split('://')[1].strip()
114 | self._username = username
115 | self._rawPass = password
116 | self._salt = salt
117 | self._token = token
118 | self._legacyAuth = legacyAuth
119 | self._useGET = useGET
120 | self._userAgent = userAgent
121 |
122 | self._netrc = None
123 | if useNetrc is not None:
124 | self._process_netrc(useNetrc)
125 | elif username is None or (password is None and (salt is None or token is None)):
126 | raise CredentialError('You must specify either a username/password '
127 | 'combination or salt/token combination or "useNetrc" must be either True or a string '
128 | 'representing a path to a netrc file')
129 |
130 | self._port = int(port)
131 | self._apiVersion = apiVersion
132 | self._appName = appName
133 | self._serverPath = serverPath.strip('/')
134 | self._insecure = insecure
135 | self._opener = self._getOpener(self._username, self._rawPass)
136 |
137 | # Properties
138 | def setBaseUrl(self, url):
139 | self._baseUrl = url
140 | self._opener = self._getOpener(self._username, self._rawPass)
141 | baseUrl = property(lambda s: s._baseUrl, setBaseUrl)
142 |
143 | def setPort(self, port):
144 | self._port = int(port)
145 | port = property(lambda s: s._port, setPort)
146 |
147 | def setUsername(self, username):
148 | self._username = username
149 | self._opener = self._getOpener(self._username, self._rawPass)
150 | username = property(lambda s: s._username, setUsername)
151 |
152 | def setPassword(self, password):
153 | self._rawPass = password
154 | # Redo the opener with the new creds
155 | self._opener = self._getOpener(self._username, self._rawPass)
156 | password = property(lambda s: s._rawPass, setPassword)
157 |
158 | apiVersion = property(lambda s: s._apiVersion)
159 |
160 | def setAppName(self, appName):
161 | self._appName = appName
162 | appName = property(lambda s: s._appName, setAppName)
163 |
164 | def setServerPath(self, path):
165 | self._serverPath = path.strip('/')
166 | serverPath = property(lambda s: s._serverPath, setServerPath)
167 |
168 | def setInsecure(self, insecure):
169 | self._insecure = insecure
170 | insecure = property(lambda s: s._insecure, setInsecure)
171 |
172 | def setLegacyAuth(self, lauth):
173 | self._legacyAuth = lauth
174 | legacyAuth = property(lambda s: s._legacyAuth, setLegacyAuth)
175 |
176 | def setGET(self, get):
177 | self._useGET = get
178 | useGET = property(lambda s: s._useGET, setGET)
179 |
180 | # API methods
181 | def ping(self):
182 | """
183 | since: 1.0.0
184 |
185 | Returns a boolean True if the server is alive, False otherwise
186 | """
187 | methodName = 'ping'
188 | viewName = '%s.view' % methodName
189 |
190 | req = self._getRequest(viewName)
191 | try:
192 | res = self._doInfoReq(req)
193 | except:
194 | return False
195 | if res['status'] == 'ok':
196 | return True
197 | elif res['status'] == 'failed':
198 | exc = getExcByCode(res['error']['code'])
199 | raise exc(res['error']['message'])
200 | return False
201 |
202 | def getLicense(self):
203 | """
204 | since: 1.0.0
205 |
206 | Gets details related to the software license
207 |
208 | Returns a dict like the following:
209 |
210 | {u'license': {u'date': u'2010-05-21T11:14:39',
211 | u'email': u'email@example.com',
212 | u'key': u'12345678901234567890123456789012',
213 | u'valid': True},
214 | u'status': u'ok',
215 | u'version': u'1.5.0',
216 | u'xmlns': u'http://subsonic.org/restapi'}
217 | """
218 | methodName = 'getLicense'
219 | viewName = '%s.view' % methodName
220 |
221 | req = self._getRequest(viewName)
222 | res = self._doInfoReq(req)
223 | self._checkStatus(res)
224 | return res
225 |
226 | def getScanStatus(self):
227 | """
228 | since: 1.15.0
229 |
230 | returns the current status for media library scanning.
231 | takes no extra parameters.
232 |
233 | returns a dict like the following:
234 |
235 | {'status': 'ok', 'version': '1.15.0',
236 | 'scanstatus': {'scanning': true, 'count': 4680}}
237 |
238 | 'count' is the total number of items to be scanned
239 | """
240 | methodName = 'getScanStatus'
241 | viewName = '%s.view' % methodName
242 |
243 | req = self._getRequest(viewName)
244 | res = self._doInfoReq(req)
245 | self._checkStatus(res)
246 | return res
247 |
248 | def startScan(self):
249 | """
250 | since: 1.15.0
251 |
252 | Initiates a rescan of the media libraries.
253 | Takes no extra parameters.
254 |
255 | returns a dict like the following:
256 |
257 | {'status': 'ok', 'version': '1.15.0',
258 | 'scanstatus': {'scanning': true, 'count': 0}}
259 |
260 | 'scanning' changes to false when a scan is complete
261 | 'count' starts a 0 and ends at the total number of items scanned
262 |
263 | """
264 | methodName = 'startScan'
265 | viewName = '%s.view' % methodName
266 |
267 | req = self._getRequest(viewName)
268 | res = self._doInfoReq(req)
269 | self._checkStatus(res)
270 | return res
271 |
272 | def getMusicFolders(self):
273 | """
274 | since: 1.0.0
275 |
276 | Returns all configured music folders
277 |
278 | Returns a dict like the following:
279 |
280 | {u'musicFolders': {u'musicFolder': [{u'id': 0, u'name': u'folder1'},
281 | {u'id': 1, u'name': u'folder2'},
282 | {u'id': 2, u'name': u'folder3'}]},
283 | u'status': u'ok',
284 | u'version': u'1.5.0',
285 | u'xmlns': u'http://subsonic.org/restapi'}
286 | """
287 | methodName = 'getMusicFolders'
288 | viewName = '%s.view' % methodName
289 |
290 | req = self._getRequest(viewName)
291 | res = self._doInfoReq(req)
292 | self._checkStatus(res)
293 | return res
294 |
295 | def getNowPlaying(self):
296 | """
297 | since: 1.0.0
298 |
299 | Returns what is currently being played by all users
300 |
301 | Returns a dict like the following:
302 |
303 | {u'nowPlaying': {u'entry': {u'album': u"Jazz 'Round Midnight 12",
304 | u'artist': u'Astrud Gilberto',
305 | u'bitRate': 172,
306 | u'contentType': u'audio/mpeg',
307 | u'coverArt': u'98349284',
308 | u'duration': 325,
309 | u'genre': u'Jazz',
310 | u'id': u'2424324',
311 | u'isDir': False,
312 | u'isVideo': False,
313 | u'minutesAgo': 0,
314 | u'parent': u'542352',
315 | u'path': u"Astrud Gilberto/Jazz 'Round Midnight 12/01 - The Girl From Ipanema.mp3",
316 | u'playerId': 1,
317 | u'size': 7004089,
318 | u'suffix': u'mp3',
319 | u'title': u'The Girl From Ipanema',
320 | u'track': 1,
321 | u'username': u'user1',
322 | u'year': 1996}},
323 | u'status': u'ok',
324 | u'version': u'1.5.0',
325 | u'xmlns': u'http://subsonic.org/restapi'}
326 | """
327 | methodName = 'getNowPlaying'
328 | viewName = '%s.view' % methodName
329 |
330 | req = self._getRequest(viewName)
331 | res = self._doInfoReq(req)
332 | self._checkStatus(res)
333 | return res
334 |
335 | def getIndexes(self, musicFolderId=None, ifModifiedSince=0):
336 | """
337 | since: 1.0.0
338 |
339 | Returns an indexed structure of all artists
340 |
341 | musicFolderId:int If this is specified, it will only return
342 | artists for the given folder ID from
343 | the getMusicFolders call
344 | ifModifiedSince:int If specified, return a result if the artist
345 | collection has changed since the given
346 | unix timestamp
347 |
348 | Returns a dict like the following:
349 |
350 | {u'indexes': {u'index': [{u'artist': [{u'id': u'29834728934',
351 | u'name': u'A Perfect Circle'},
352 | {u'id': u'238472893',
353 | u'name': u'A Small Good Thing'},
354 | {u'id': u'9327842983',
355 | u'name': u'A Tribe Called Quest'},
356 | {u'id': u'29348729874',
357 | u'name': u'A-Teens, The'},
358 | {u'id': u'298472938',
359 | u'name': u'ABA STRUCTURE'}],
360 | u'lastModified': 1303318347000L},
361 | u'status': u'ok',
362 | u'version': u'1.5.0',
363 | u'xmlns': u'http://subsonic.org/restapi'}
364 | """
365 | methodName = 'getIndexes'
366 | viewName = '%s.view' % methodName
367 |
368 | q = self._getQueryDict({'musicFolderId': musicFolderId,
369 | 'ifModifiedSince': self._ts2milli(ifModifiedSince)})
370 |
371 | req = self._getRequest(viewName, q)
372 | res = self._doInfoReq(req)
373 | self._checkStatus(res)
374 | self._fixLastModified(res)
375 | return res
376 |
377 | def getMusicDirectory(self, mid):
378 | """
379 | since: 1.0.0
380 |
381 | Returns a listing of all files in a music directory. Typically used
382 | to get a list of albums for an artist or list of songs for an album.
383 |
384 | mid:str The string ID value which uniquely identifies the
385 | folder. Obtained via calls to getIndexes or
386 | getMusicDirectory. REQUIRED
387 |
388 | Returns a dict like the following:
389 |
390 | {u'directory': {u'child': [{u'artist': u'A Tribe Called Quest',
391 | u'coverArt': u'223484',
392 | u'id': u'329084',
393 | u'isDir': True,
394 | u'parent': u'234823940',
395 | u'title': u'Beats, Rhymes And Life'},
396 | {u'artist': u'A Tribe Called Quest',
397 | u'coverArt': u'234823794',
398 | u'id': u'238472893',
399 | u'isDir': True,
400 | u'parent': u'2308472938',
401 | u'title': u'Midnight Marauders'},
402 | {u'artist': u'A Tribe Called Quest',
403 | u'coverArt': u'39284792374',
404 | u'id': u'983274892',
405 | u'isDir': True,
406 | u'parent': u'9823749',
407 | u'title': u"People's Instinctive Travels And The Paths Of Rhythm"},
408 | {u'artist': u'A Tribe Called Quest',
409 | u'coverArt': u'289347293',
410 | u'id': u'3894723934',
411 | u'isDir': True,
412 | u'parent': u'9832942',
413 | u'title': u'The Anthology'},
414 | {u'artist': u'A Tribe Called Quest',
415 | u'coverArt': u'923847923',
416 | u'id': u'29834729',
417 | u'isDir': True,
418 | u'parent': u'2934872893',
419 | u'title': u'The Love Movement'},
420 | {u'artist': u'A Tribe Called Quest',
421 | u'coverArt': u'9238742893',
422 | u'id': u'238947293',
423 | u'isDir': True,
424 | u'parent': u'9432878492',
425 | u'title': u'The Low End Theory'}],
426 | u'id': u'329847293',
427 | u'name': u'A Tribe Called Quest'},
428 | u'status': u'ok',
429 | u'version': u'1.5.0',
430 | u'xmlns': u'http://subsonic.org/restapi'}
431 | """
432 | methodName = 'getMusicDirectory'
433 | viewName = '%s.view' % methodName
434 |
435 | req = self._getRequest(viewName, {'id': mid})
436 | res = self._doInfoReq(req)
437 | self._checkStatus(res)
438 | return res
439 |
440 | def search(self, artist=None, album=None, title=None, any=None,
441 | count=20, offset=0, newerThan=None):
442 | """
443 | since: 1.0.0
444 |
445 | DEPRECATED SINCE API 1.4.0! USE search2() INSTEAD!
446 |
447 | Returns a listing of files matching the given search criteria.
448 | Supports paging with offset
449 |
450 | artist:str Search for artist
451 | album:str Search for album
452 | title:str Search for title of song
453 | any:str Search all fields
454 | count:int Max number of results to return [default: 20]
455 | offset:int Search result offset. For paging [default: 0]
456 | newerThan:int Return matches newer than this timestamp
457 | """
458 | if artist == album == title == any == None:
459 | raise ArgumentError('Invalid search. You must supply search '
460 | 'criteria')
461 | methodName = 'search'
462 | viewName = '%s.view' % methodName
463 |
464 | q = self._getQueryDict({'artist': artist, 'album': album,
465 | 'title': title, 'any': any, 'count': count, 'offset': offset,
466 | 'newerThan': self._ts2milli(newerThan)})
467 |
468 | req = self._getRequest(viewName, q)
469 | res = self._doInfoReq(req)
470 | self._checkStatus(res)
471 | return res
472 |
473 | def search2(self, query, artistCount=20, artistOffset=0, albumCount=20,
474 | albumOffset=0, songCount=20, songOffset=0, musicFolderId=None):
475 | """
476 | since: 1.4.0
477 |
478 | Returns albums, artists and songs matching the given search criteria.
479 | Supports paging through the result.
480 |
481 | query:str The search query
482 | artistCount:int Max number of artists to return [default: 20]
483 | artistOffset:int Search offset for artists (for paging) [default: 0]
484 | albumCount:int Max number of albums to return [default: 20]
485 | albumOffset:int Search offset for albums (for paging) [default: 0]
486 | songCount:int Max number of songs to return [default: 20]
487 | songOffset:int Search offset for songs (for paging) [default: 0]
488 | musicFolderId:int Only return results from the music folder
489 | with the given ID. See getMusicFolders
490 |
491 | Returns a dict like the following:
492 |
493 | {u'searchResult2': {u'album': [{u'artist': u'A Tribe Called Quest',
494 | u'coverArt': u'289347',
495 | u'id': u'32487298',
496 | u'isDir': True,
497 | u'parent': u'98374289',
498 | u'title': u'The Love Movement'}],
499 | u'artist': [{u'id': u'2947839',
500 | u'name': u'A Tribe Called Quest'},
501 | {u'id': u'239847239',
502 | u'name': u'Tribe'}],
503 | u'song': [{u'album': u'Beats, Rhymes And Life',
504 | u'artist': u'A Tribe Called Quest',
505 | u'bitRate': 224,
506 | u'contentType': u'audio/mpeg',
507 | u'coverArt': u'329847',
508 | u'duration': 148,
509 | u'genre': u'default',
510 | u'id': u'3928472893',
511 | u'isDir': False,
512 | u'isVideo': False,
513 | u'parent': u'23984728394',
514 | u'path': u'A Tribe Called Quest/Beats, Rhymes And Life/A Tribe Called Quest - Beats, Rhymes And Life - 03 - Motivators.mp3',
515 | u'size': 4171913,
516 | u'suffix': u'mp3',
517 | u'title': u'Motivators',
518 | u'track': 3}]},
519 | u'status': u'ok',
520 | u'version': u'1.5.0',
521 | u'xmlns': u'http://subsonic.org/restapi'}
522 | """
523 | methodName = 'search2'
524 | viewName = '%s.view' % methodName
525 |
526 | q = self._getQueryDict({'query': query, 'artistCount': artistCount,
527 | 'artistOffset': artistOffset, 'albumCount': albumCount,
528 | 'albumOffset': albumOffset, 'songCount': songCount,
529 | 'songOffset': songOffset, 'musicFolderId': musicFolderId})
530 |
531 | req = self._getRequest(viewName, q)
532 | res = self._doInfoReq(req)
533 | self._checkStatus(res)
534 | return res
535 |
536 | def search3(self, query, artistCount=20, artistOffset=0, albumCount=20,
537 | albumOffset=0, songCount=20, songOffset=0, musicFolderId=None):
538 | """
539 | since: 1.8.0
540 |
541 | Works the same way as search2, but uses ID3 tags for
542 | organization
543 |
544 | query:str The search query
545 | artistCount:int Max number of artists to return [default: 20]
546 | artistOffset:int Search offset for artists (for paging) [default: 0]
547 | albumCount:int Max number of albums to return [default: 20]
548 | albumOffset:int Search offset for albums (for paging) [default: 0]
549 | songCount:int Max number of songs to return [default: 20]
550 | songOffset:int Search offset for songs (for paging) [default: 0]
551 | musicFolderId:int Only return results from the music folder
552 | with the given ID. See getMusicFolders
553 |
554 | Returns a dict like the following (search for "Tune Yards":
555 | {u'searchResult3': {u'album': [{u'artist': u'Tune-Yards',
556 | u'artistId': 1,
557 | u'coverArt': u'al-7',
558 | u'created': u'2012-01-30T12:35:33',
559 | u'duration': 3229,
560 | u'id': 7,
561 | u'name': u'Bird-Brains',
562 | u'songCount': 13},
563 | {u'artist': u'Tune-Yards',
564 | u'artistId': 1,
565 | u'coverArt': u'al-8',
566 | u'created': u'2011-03-22T15:08:00',
567 | u'duration': 2531,
568 | u'id': 8,
569 | u'name': u'W H O K I L L',
570 | u'songCount': 10}],
571 | u'artist': {u'albumCount': 2,
572 | u'coverArt': u'ar-1',
573 | u'id': 1,
574 | u'name': u'Tune-Yards'},
575 | u'song': [{u'album': u'Bird-Brains',
576 | u'albumId': 7,
577 | u'artist': u'Tune-Yards',
578 | u'artistId': 1,
579 | u'bitRate': 160,
580 | u'contentType': u'audio/mpeg',
581 | u'coverArt': 105,
582 | u'created': u'2012-01-30T12:35:33',
583 | u'duration': 328,
584 | u'genre': u'Lo-Fi',
585 | u'id': 107,
586 | u'isDir': False,
587 | u'isVideo': False,
588 | u'parent': 105,
589 | u'path': u'Tune Yards/Bird-Brains/10-tune-yards-fiya.mp3',
590 | u'size': 6588498,
591 | u'suffix': u'mp3',
592 | u'title': u'Fiya',
593 | u'track': 10,
594 | u'type': u'music',
595 | u'year': 2009}]},
596 |
597 | u'status': u'ok',
598 | u'version': u'1.5.0',
599 | u'xmlns': u'http://subsonic.org/restapi'}
600 | """
601 | methodName = 'search3'
602 | viewName = '%s.view' % methodName
603 |
604 | q = self._getQueryDict({'query': query, 'artistCount': artistCount,
605 | 'artistOffset': artistOffset, 'albumCount': albumCount,
606 | 'albumOffset': albumOffset, 'songCount': songCount,
607 | 'songOffset': songOffset, 'musicFolderId': musicFolderId})
608 |
609 | req = self._getRequest(viewName, q)
610 | res = self._doInfoReq(req)
611 | self._checkStatus(res)
612 | return res
613 |
614 | def getPlaylists(self, username=None):
615 | """
616 | since: 1.0.0
617 |
618 | Returns the ID and name of all saved playlists
619 | The "username" option was added in 1.8.0.
620 |
621 | username:str If specified, return playlists for this user
622 | rather than for the authenticated user. The
623 | authenticated user must have admin role
624 | if this parameter is used
625 |
626 | Returns a dict like the following:
627 |
628 | {u'playlists': {u'playlist': [{u'id': u'62656174732e6d3375',
629 | u'name': u'beats'},
630 | {u'id': u'766172696574792e6d3375',
631 | u'name': u'variety'}]},
632 | u'status': u'ok',
633 | u'version': u'1.5.0',
634 | u'xmlns': u'http://subsonic.org/restapi'}
635 | """
636 | methodName = 'getPlaylists'
637 | viewName = '%s.view' % methodName
638 |
639 | q = self._getQueryDict({'username': username})
640 |
641 | req = self._getRequest(viewName, q)
642 | res = self._doInfoReq(req)
643 | self._checkStatus(res)
644 | return res
645 |
646 | def getPlaylist(self, pid):
647 | """
648 | since: 1.0.0
649 |
650 | Returns a listing of files in a saved playlist
651 |
652 | id:str The ID of the playlist as returned in getPlaylists()
653 |
654 | Returns a dict like the following:
655 |
656 | {u'playlist': {u'entry': {u'album': u'The Essential Bob Dylan',
657 | u'artist': u'Bob Dylan',
658 | u'bitRate': 32,
659 | u'contentType': u'audio/mpeg',
660 | u'coverArt': u'2983478293',
661 | u'duration': 984,
662 | u'genre': u'Classic Rock',
663 | u'id': u'982739428',
664 | u'isDir': False,
665 | u'isVideo': False,
666 | u'parent': u'98327428974',
667 | u'path': u"Bob Dylan/Essential Bob Dylan Disc 1/Bob Dylan - The Essential Bob Dylan - 03 - The Times They Are A-Changin'.mp3",
668 | u'size': 3921899,
669 | u'suffix': u'mp3',
670 | u'title': u"The Times They Are A-Changin'",
671 | u'track': 3},
672 | u'id': u'44796c616e2e6d3375',
673 | u'name': u'Dylan'},
674 | u'status': u'ok',
675 | u'version': u'1.5.0',
676 | u'xmlns': u'http://subsonic.org/restapi'}
677 | """
678 | methodName = 'getPlaylist'
679 | viewName = '%s.view' % methodName
680 |
681 | req = self._getRequest(viewName, {'id': pid})
682 | res = self._doInfoReq(req)
683 | self._checkStatus(res)
684 | return res
685 |
686 | def createPlaylist(self, playlistId=None, name=None, songIds=[]):
687 | """
688 | since: 1.2.0
689 |
690 | Creates OR updates a playlist. If updating the list, the
691 | playlistId is required. If creating a list, the name is required.
692 |
693 | playlistId:str The ID of the playlist to UPDATE
694 | name:str The name of the playlist to CREATE
695 | songIds:list The list of songIds to populate the list with in
696 | either create or update mode. Note that this
697 | list will replace the existing list if updating
698 |
699 | Returns a dict like the following:
700 |
701 | {u'status': u'ok',
702 | u'version': u'1.5.0',
703 | u'xmlns': u'http://subsonic.org/restapi'}
704 | """
705 | methodName = 'createPlaylist'
706 | viewName = '%s.view' % methodName
707 |
708 | if playlistId == name == None:
709 | raise ArgumentError('You must supply either a playlistId or a name')
710 | if playlistId is not None and name is not None:
711 | raise ArgumentError('You can only supply either a playlistId '
712 | 'OR a name, not both')
713 |
714 | q = self._getQueryDict({'playlistId': playlistId, 'name': name})
715 |
716 | req = self._getRequestWithList(viewName, 'songId', songIds, q)
717 | res = self._doInfoReq(req)
718 | self._checkStatus(res)
719 | return res
720 |
721 | def deletePlaylist(self, pid):
722 | """
723 | since: 1.2.0
724 |
725 | Deletes a saved playlist
726 |
727 | pid:str ID of the playlist to delete, as obtained by getPlaylists
728 |
729 | Returns a dict like the following:
730 |
731 | """
732 | methodName = 'deletePlaylist'
733 | viewName = '%s.view' % methodName
734 |
735 | req = self._getRequest(viewName, {'id': pid})
736 | res = self._doInfoReq(req)
737 | self._checkStatus(res)
738 | return res
739 |
740 | def download(self, sid):
741 | """
742 | since: 1.0.0
743 |
744 | Downloads a given music file.
745 |
746 | sid:str The ID of the music file to download.
747 |
748 | Returns the file-like object for reading or raises an exception
749 | on error
750 | """
751 | methodName = 'download'
752 | viewName = '%s.view' % methodName
753 |
754 | req = self._getRequest(viewName, {'id': sid})
755 | res = self._doBinReq(req)
756 | if isinstance(res, dict):
757 | self._checkStatus(res)
758 | return res
759 |
760 | def stream(self, sid, maxBitRate=0, tformat=None, timeOffset=None,
761 | size=None, estimateContentLength=False, converted=False):
762 | """
763 | since: 1.0.0
764 |
765 | Downloads a given music file.
766 |
767 | sid:str The ID of the music file to download.
768 | maxBitRate:int (since: 1.2.0) If specified, the server will
769 | attempt to limit the bitrate to this value, in
770 | kilobits per second. If set to zero (default), no limit
771 | is imposed. Legal values are: 0, 32, 40, 48, 56, 64,
772 | 80, 96, 112, 128, 160, 192, 224, 256 and 320.
773 | tformat:str (since: 1.6.0) Specifies the target format
774 | (e.g. "mp3" or "flv") in case there are multiple
775 | applicable transcodings (since: 1.9.0) You can use
776 | the special value "raw" to disable transcoding
777 | timeOffset:int (since: 1.6.0) Only applicable to video
778 | streaming. Start the stream at the given
779 | offset (in seconds) into the video
780 | size:str (since: 1.6.0) The requested video size in
781 | WxH, for instance 640x480
782 | estimateContentLength:bool (since: 1.8.0) If set to True,
783 | the HTTP Content-Length header
784 | will be set to an estimated
785 | value for trancoded media
786 | converted:bool (since: 1.14.0) Only applicable to video streaming.
787 | Subsonic can optimize videos for streaming by
788 | converting them to MP4. If a conversion exists for
789 | the video in question, then setting this parameter
790 | to "true" will cause the converted video to be
791 | returned instead of the original.
792 |
793 | Returns the file-like object for reading or raises an exception
794 | on error
795 | """
796 | methodName = 'stream'
797 | viewName = '%s.view' % methodName
798 |
799 | q = self._getQueryDict({'id': sid, 'maxBitRate': maxBitRate,
800 | 'format': tformat, 'timeOffset': timeOffset, 'size': size,
801 | 'estimateContentLength': estimateContentLength,
802 | 'converted': converted})
803 |
804 | req = self._getRequest(viewName, q)
805 | res = self._doBinReq(req)
806 | if isinstance(res, dict):
807 | self._checkStatus(res)
808 | return res
809 |
810 | def getCoverArt(self, aid, size=None):
811 | """
812 | since: 1.0.0
813 |
814 | Returns a cover art image
815 |
816 | aid:str ID string for the cover art image to download
817 | size:int If specified, scale image to this size
818 |
819 | Returns the file-like object for reading or raises an exception
820 | on error
821 | """
822 | methodName = 'getCoverArt'
823 | viewName = '%s.view' % methodName
824 |
825 | q = self._getQueryDict({'id': aid, 'size': size})
826 |
827 | req = self._getRequest(viewName, q)
828 | res = self._doBinReq(req)
829 | if isinstance(res, dict):
830 | self._checkStatus(res)
831 | return res
832 |
833 | def scrobble(self, sid, submission=True, listenTime=None):
834 | """
835 | since: 1.5.0
836 |
837 | "Scrobbles" a given music file on last.fm. Requires that the user
838 | has set this up.
839 |
840 | Since 1.8.0 you may specify multiple id (and optionally time)
841 | parameters to scrobble multiple files.
842 |
843 | Since 1.11.0 this method will also update the play count and
844 | last played timestamp for the song and album. It will also make
845 | the song appear in the "Now playing" page in the web app, and
846 | appear in the list of songs returned by getNowPlaying
847 |
848 | sid:str The ID of the file to scrobble
849 | submission:bool Whether this is a "submission" or a "now playing"
850 | notification
851 | listenTime:int (Since 1.8.0) The time (unix timestamp) at
852 | which the song was listened to.
853 |
854 | Returns a dict like the following:
855 |
856 | {u'status': u'ok',
857 | u'version': u'1.5.0',
858 | u'xmlns': u'http://subsonic.org/restapi'}
859 | """
860 | methodName = 'scrobble'
861 | viewName = '%s.view' % methodName
862 |
863 | q = self._getQueryDict({'id': sid, 'submission': submission,
864 | 'time': self._ts2milli(listenTime)})
865 |
866 | req = self._getRequest(viewName, q)
867 | res = self._doInfoReq(req)
868 | self._checkStatus(res)
869 | return res
870 |
871 | def changePassword(self, username, password):
872 | """
873 | since: 1.1.0
874 |
875 | Changes the password of an existing Subsonic user. Note that the
876 | user performing this must have admin privileges
877 |
878 | username:str The username whose password is being changed
879 | password:str The new password of the user
880 |
881 | Returns a dict like the following:
882 |
883 | {u'status': u'ok',
884 | u'version': u'1.5.0',
885 | u'xmlns': u'http://subsonic.org/restapi'}
886 | """
887 | methodName = 'changePassword'
888 | viewName = '%s.view' % methodName
889 | hexPass = 'enc:%s' % self._hexEnc(password)
890 |
891 | # There seems to be an issue with some subsonic implementations
892 | # not recognizing the "enc:" precursor to the encoded password and
893 | # encodes the whole "enc:" as the password. Weird.
894 | #q = {'username': username, 'password': hexPass.lower()}
895 | q = {'username': username, 'password': password}
896 |
897 | req = self._getRequest(viewName, q)
898 | res = self._doInfoReq(req)
899 | self._checkStatus(res)
900 | return res
901 |
902 | def getUser(self, username):
903 | """
904 | since: 1.3.0
905 |
906 | Get details about a given user, including which auth roles it has.
907 | Can be used to enable/disable certain features in the client, such
908 | as jukebox control
909 |
910 | username:str The username to retrieve. You can only retrieve
911 | your own user unless you have admin privs.
912 |
913 | Returns a dict like the following:
914 |
915 | {u'status': u'ok',
916 | u'user': {u'adminRole': False,
917 | u'commentRole': False,
918 | u'coverArtRole': False,
919 | u'downloadRole': True,
920 | u'jukeboxRole': False,
921 | u'playlistRole': True,
922 | u'podcastRole': False,
923 | u'settingsRole': True,
924 | u'streamRole': True,
925 | u'uploadRole': True,
926 | u'username': u'test'},
927 | u'version': u'1.5.0',
928 | u'xmlns': u'http://subsonic.org/restapi'}
929 | """
930 | methodName = 'getUser'
931 | viewName = '%s.view' % methodName
932 |
933 | q = {'username': username}
934 |
935 | req = self._getRequest(viewName, q)
936 | res = self._doInfoReq(req)
937 | self._checkStatus(res)
938 | return res
939 |
940 | def getUsers(self):
941 | """
942 | since 1.8.0
943 |
944 | Gets a list of users
945 |
946 | returns a dict like the following
947 |
948 | {u'status': u'ok',
949 | u'users': {u'user': [{u'adminRole': True,
950 | u'commentRole': True,
951 | u'coverArtRole': True,
952 | u'downloadRole': True,
953 | u'jukeboxRole': True,
954 | u'playlistRole': True,
955 | u'podcastRole': True,
956 | u'scrobblingEnabled': True,
957 | u'settingsRole': True,
958 | u'shareRole': True,
959 | u'streamRole': True,
960 | u'uploadRole': True,
961 | u'username': u'user1'},
962 | ...
963 | ...
964 | ]},
965 | u'version': u'1.10.2',
966 | u'xmlns': u'http://subsonic.org/restapi'}
967 | """
968 | methodName = 'getUsers'
969 | viewName = '%s.view' % methodName
970 |
971 | req = self._getRequest(viewName)
972 | res = self._doInfoReq(req)
973 | self._checkStatus(res)
974 | return res
975 |
976 | def createUser(self, username, password, email,
977 | ldapAuthenticated=False, adminRole=False, settingsRole=True,
978 | streamRole=True, jukeboxRole=False, downloadRole=False,
979 | uploadRole=False, playlistRole=False, coverArtRole=False,
980 | commentRole=False, podcastRole=False, shareRole=False,
981 | videoConversionRole=False, musicFolderId=None):
982 | """
983 | since: 1.1.0
984 |
985 | Creates a new subsonic user, using the parameters defined. See the
986 | documentation at http://subsonic.org for more info on all the roles.
987 |
988 | username:str The username of the new user
989 | password:str The password for the new user
990 | email:str The email of the new user
991 |
992 | musicFolderId:int These are the only folders the user has access to
993 |
994 | Returns a dict like the following:
995 |
996 | {u'status': u'ok',
997 | u'version': u'1.5.0',
998 | u'xmlns': u'http://subsonic.org/restapi'}
999 | """
1000 | methodName = 'createUser'
1001 | viewName = '%s.view' % methodName
1002 | hexPass = 'enc:%s' % self._hexEnc(password)
1003 |
1004 | q = self._getQueryDict({
1005 | 'username': username, 'password': hexPass, 'email': email,
1006 | 'ldapAuthenticated': ldapAuthenticated, 'adminRole': adminRole,
1007 | 'settingsRole': settingsRole, 'streamRole': streamRole,
1008 | 'jukeboxRole': jukeboxRole, 'downloadRole': downloadRole,
1009 | 'uploadRole': uploadRole, 'playlistRole': playlistRole,
1010 | 'coverArtRole': coverArtRole, 'commentRole': commentRole,
1011 | 'podcastRole': podcastRole, 'shareRole': shareRole,
1012 | 'videoConversionRole': videoConversionRole,
1013 | 'musicFolderId': musicFolderId
1014 | })
1015 |
1016 | req = self._getRequest(viewName, q)
1017 | res = self._doInfoReq(req)
1018 | self._checkStatus(res)
1019 | return res
1020 |
1021 | def updateUser(self, username, password=None, email=None,
1022 | ldapAuthenticated=False, adminRole=False, settingsRole=True,
1023 | streamRole=True, jukeboxRole=False, downloadRole=False,
1024 | uploadRole=False, playlistRole=False, coverArtRole=False,
1025 | commentRole=False, podcastRole=False, shareRole=False,
1026 | videoConversionRole=False, musicFolderId=None, maxBitRate=0):
1027 | """
1028 | since 1.10.1
1029 |
1030 | Modifies an existing Subsonic user.
1031 |
1032 | username:str The username of the user to update.
1033 | musicFolderId:int Only return results from the music folder
1034 | with the given ID. See getMusicFolders
1035 | maxBitRate:int The max bitrate for the user. 0 is unlimited
1036 |
1037 | All other args are the same as create user and you can update
1038 | whatever item you wish to update for the given username.
1039 |
1040 | Returns a dict like the following:
1041 |
1042 | {u'status': u'ok',
1043 | u'version': u'1.5.0',
1044 | u'xmlns': u'http://subsonic.org/restapi'}
1045 | """
1046 | methodName = 'updateUser'
1047 | viewName = '%s.view' % methodName
1048 | if password is not None:
1049 | password = 'enc:%s' % self._hexEnc(password)
1050 | q = self._getQueryDict({'username': username, 'password': password,
1051 | 'email': email, 'ldapAuthenticated': ldapAuthenticated,
1052 | 'adminRole': adminRole,
1053 | 'settingsRole': settingsRole, 'streamRole': streamRole,
1054 | 'jukeboxRole': jukeboxRole, 'downloadRole': downloadRole,
1055 | 'uploadRole': uploadRole, 'playlistRole': playlistRole,
1056 | 'coverArtRole': coverArtRole, 'commentRole': commentRole,
1057 | 'podcastRole': podcastRole, 'shareRole': shareRole,
1058 | 'videoConversionRole': videoConversionRole,
1059 | 'musicFolderId': musicFolderId, 'maxBitRate': maxBitRate
1060 | })
1061 | req = self._getRequest(viewName, q)
1062 | res = self._doInfoReq(req)
1063 | self._checkStatus(res)
1064 | return res
1065 |
1066 | def deleteUser(self, username):
1067 | """
1068 | since: 1.3.0
1069 |
1070 | Deletes an existing Subsonic user. Of course, you must have admin
1071 | rights for this.
1072 |
1073 | username:str The username of the user to delete
1074 |
1075 | Returns a dict like the following:
1076 |
1077 | {u'status': u'ok',
1078 | u'version': u'1.5.0',
1079 | u'xmlns': u'http://subsonic.org/restapi'}
1080 | """
1081 | methodName = 'deleteUser'
1082 | viewName = '%s.view' % methodName
1083 |
1084 | q = {'username': username}
1085 |
1086 | req = self._getRequest(viewName, q)
1087 | res = self._doInfoReq(req)
1088 | self._checkStatus(res)
1089 | return res
1090 |
1091 | def getChatMessages(self, since=1):
1092 | """
1093 | since: 1.2.0
1094 |
1095 | Returns the current visible (non-expired) chat messages.
1096 |
1097 | since:int Only return messages newer than this timestamp
1098 |
1099 | NOTE: All times returned are in MILLISECONDS since the Epoch, not
1100 | seconds!
1101 |
1102 | Returns a dict like the following:
1103 | {u'chatMessages': {u'chatMessage': {u'message': u'testing 123',
1104 | u'time': 1303411919872L,
1105 | u'username': u'admin'}},
1106 | u'status': u'ok',
1107 | u'version': u'1.5.0',
1108 | u'xmlns': u'http://subsonic.org/restapi'}
1109 | """
1110 | methodName = 'getChatMessages'
1111 | viewName = '%s.view' % methodName
1112 |
1113 | q = {'since': self._ts2milli(since)}
1114 |
1115 | req = self._getRequest(viewName, q)
1116 | res = self._doInfoReq(req)
1117 | self._checkStatus(res)
1118 | return res
1119 |
1120 | def addChatMessage(self, message):
1121 | """
1122 | since: 1.2.0
1123 |
1124 | Adds a message to the chat log
1125 |
1126 | message:str The message to add
1127 |
1128 | Returns a dict like the following:
1129 |
1130 | {u'status': u'ok',
1131 | u'version': u'1.5.0',
1132 | u'xmlns': u'http://subsonic.org/restapi'}
1133 | """
1134 | methodName = 'addChatMessage'
1135 | viewName = '%s.view' % methodName
1136 |
1137 | q = {'message': message}
1138 |
1139 | req = self._getRequest(viewName, q)
1140 | res = self._doInfoReq(req)
1141 | self._checkStatus(res)
1142 | return res
1143 |
1144 | def getAlbumList(self, ltype, size=10, offset=0, fromYear=None,
1145 | toYear=None, genre=None, musicFolderId=None):
1146 | """
1147 | since: 1.2.0
1148 |
1149 | Returns a list of random, newest, highest rated etc. albums.
1150 | Similar to the album lists on the home page of the Subsonic
1151 | web interface
1152 |
1153 | ltype:str The list type. Must be one of the following: random,
1154 | newest, highest, frequent, recent,
1155 | (since 1.8.0 -> )starred, alphabeticalByName,
1156 | alphabeticalByArtist
1157 | Since 1.10.1 you can use byYear and byGenre to
1158 | list albums in a given year range or genre.
1159 | size:int The number of albums to return. Max 500
1160 | offset:int The list offset. Use for paging. Max 5000
1161 | fromYear:int If you specify the ltype as "byYear", you *must*
1162 | specify fromYear
1163 | toYear:int If you specify the ltype as "byYear", you *must*
1164 | specify toYear
1165 | genre:str The name of the genre e.g. "Rock". You must specify
1166 | genre if you set the ltype to "byGenre"
1167 | musicFolderId:str Only return albums in the music folder with
1168 | the given ID. See getMusicFolders()
1169 |
1170 | Returns a dict like the following:
1171 |
1172 | {u'albumList': {u'album': [{u'artist': u'Hank Williams',
1173 | u'id': u'3264928374',
1174 | u'isDir': True,
1175 | u'parent': u'9238479283',
1176 | u'title': u'The Original Singles Collection...Plus'},
1177 | {u'artist': u'Freundeskreis',
1178 | u'coverArt': u'9823749823',
1179 | u'id': u'23492834',
1180 | u'isDir': True,
1181 | u'parent': u'9827492374',
1182 | u'title': u'Quadratur des Kreises'}]},
1183 | u'status': u'ok',
1184 | u'version': u'1.5.0',
1185 | u'xmlns': u'http://subsonic.org/restapi'}
1186 | """
1187 | methodName = 'getAlbumList'
1188 | viewName = '%s.view' % methodName
1189 |
1190 | q = self._getQueryDict({'type': ltype, 'size': size,
1191 | 'offset': offset, 'fromYear': fromYear, 'toYear': toYear,
1192 | 'genre': genre, 'musicFolderId': musicFolderId})
1193 |
1194 | req = self._getRequest(viewName, q)
1195 | res = self._doInfoReq(req)
1196 | self._checkStatus(res)
1197 | return res
1198 |
1199 | def getAlbumList2(self, ltype, size=10, offset=0, fromYear=None,
1200 | toYear=None, genre=None):
1201 | """
1202 | since 1.8.0
1203 |
1204 | Returns a list of random, newest, highest rated etc. albums.
1205 | This is similar to getAlbumList, but uses ID3 tags for
1206 | organization
1207 |
1208 | ltype:str The list type. Must be one of the following: random,
1209 | newest, highest, frequent, recent,
1210 | (since 1.8.0 -> )starred, alphabeticalByName,
1211 | alphabeticalByArtist
1212 | Since 1.10.1 you can use byYear and byGenre to
1213 | list albums in a given year range or genre.
1214 | size:int The number of albums to return. Max 500
1215 | offset:int The list offset. Use for paging. Max 5000
1216 | fromYear:int If you specify the ltype as "byYear", you *must*
1217 | specify fromYear
1218 | toYear:int If you specify the ltype as "byYear", you *must*
1219 | specify toYear
1220 | genre:str The name of the genre e.g. "Rock". You must specify
1221 | genre if you set the ltype to "byGenre"
1222 |
1223 | Returns a dict like the following:
1224 | {u'albumList2': {u'album': [{u'artist': u'Massive Attack',
1225 | u'artistId': 0,
1226 | u'coverArt': u'al-0',
1227 | u'created': u'2009-08-28T10:00:44',
1228 | u'duration': 3762,
1229 | u'id': 0,
1230 | u'name': u'100th Window',
1231 | u'songCount': 9},
1232 | {u'artist': u'Massive Attack',
1233 | u'artistId': 0,
1234 | u'coverArt': u'al-5',
1235 | u'created': u'2003-11-03T22:00:00',
1236 | u'duration': 2715,
1237 | u'id': 5,
1238 | u'name': u'Blue Lines',
1239 | u'songCount': 9}]},
1240 | u'status': u'ok',
1241 | u'version': u'1.8.0',
1242 | u'xmlns': u'http://subsonic.org/restapi'}
1243 | """
1244 | methodName = 'getAlbumList2'
1245 | viewName = '%s.view' % methodName
1246 |
1247 | q = self._getQueryDict({'type': ltype, 'size': size,
1248 | 'offset': offset, 'fromYear': fromYear, 'toYear': toYear,
1249 | 'genre': genre})
1250 |
1251 | req = self._getRequest(viewName, q)
1252 | res = self._doInfoReq(req)
1253 | self._checkStatus(res)
1254 | return res
1255 |
1256 | def getRandomSongs(self, size=10, genre=None, fromYear=None,
1257 | toYear=None, musicFolderId=None):
1258 | """
1259 | since 1.2.0
1260 |
1261 | Returns random songs matching the given criteria
1262 |
1263 | size:int The max number of songs to return. Max 500
1264 | genre:str Only return songs from this genre
1265 | fromYear:int Only return songs after or in this year
1266 | toYear:int Only return songs before or in this year
1267 | musicFolderId:str Only return songs in the music folder with the
1268 | given ID. See getMusicFolders
1269 |
1270 | Returns a dict like the following:
1271 |
1272 | {u'randomSongs': {u'song': [{u'album': u'1998 EP - Airbag (How Am I Driving)',
1273 | u'artist': u'Radiohead',
1274 | u'bitRate': 320,
1275 | u'contentType': u'audio/mpeg',
1276 | u'duration': 129,
1277 | u'id': u'9284728934',
1278 | u'isDir': False,
1279 | u'isVideo': False,
1280 | u'parent': u'983249823',
1281 | u'path': u'Radiohead/1998 EP - Airbag (How Am I Driving)/06 - Melatonin.mp3',
1282 | u'size': 5177469,
1283 | u'suffix': u'mp3',
1284 | u'title': u'Melatonin'},
1285 | {u'album': u'Mezmerize',
1286 | u'artist': u'System Of A Down',
1287 | u'bitRate': 214,
1288 | u'contentType': u'audio/mpeg',
1289 | u'coverArt': u'23849372894',
1290 | u'duration': 176,
1291 | u'id': u'28937492834',
1292 | u'isDir': False,
1293 | u'isVideo': False,
1294 | u'parent': u'92837492837',
1295 | u'path': u'System Of A Down/Mesmerize/10 - System Of A Down - Old School Hollywood.mp3',
1296 | u'size': 4751360,
1297 | u'suffix': u'mp3',
1298 | u'title': u'Old School Hollywood',
1299 | u'track': 10}]},
1300 | u'status': u'ok',
1301 | u'version': u'1.5.0',
1302 | u'xmlns': u'http://subsonic.org/restapi'}
1303 | """
1304 | methodName = 'getRandomSongs'
1305 | viewName = '%s.view' % methodName
1306 |
1307 | q = self._getQueryDict({'size': size, 'genre': genre,
1308 | 'fromYear': fromYear, 'toYear': toYear,
1309 | 'musicFolderId': musicFolderId})
1310 |
1311 | req = self._getRequest(viewName, q)
1312 | res = self._doInfoReq(req)
1313 | self._checkStatus(res)
1314 | return res
1315 |
1316 | def getLyrics(self, artist=None, title=None):
1317 | """
1318 | since: 1.2.0
1319 |
1320 | Searches for and returns lyrics for a given song
1321 |
1322 | artist:str The artist name
1323 | title:str The song title
1324 |
1325 | Returns a dict like the following for
1326 | getLyrics('Bob Dylan', 'Blowin in the wind'):
1327 |
1328 | {u'lyrics': {u'artist': u'Bob Dylan',
1329 | u'content': u"How many roads must a man walk down",
1330 | u'title': u"Blowin' in the Wind"},
1331 | u'status': u'ok',
1332 | u'version': u'1.5.0',
1333 | u'xmlns': u'http://subsonic.org/restapi'}
1334 | """
1335 | methodName = 'getLyrics'
1336 | viewName = '%s.view' % methodName
1337 |
1338 | q = self._getQueryDict({'artist': artist, 'title': title})
1339 |
1340 | req = self._getRequest(viewName, q)
1341 | res = self._doInfoReq(req)
1342 | self._checkStatus(res)
1343 | return res
1344 |
1345 | def jukeboxControl(self, action, index=None, sids=[], gain=None,
1346 | offset=None):
1347 | """
1348 | since: 1.2.0
1349 |
1350 | NOTE: Some options were added as of API version 1.7.0
1351 |
1352 | Controls the jukebox, i.e., playback directly on the server's
1353 | audio hardware. Note: The user must be authorized to control
1354 | the jukebox
1355 |
1356 | action:str The operation to perform. Must be one of: get,
1357 | start, stop, skip, add, clear, remove, shuffle,
1358 | setGain, status (added in API 1.7.0),
1359 | set (added in API 1.7.0)
1360 | index:int Used by skip and remove. Zero-based index of the
1361 | song to skip to or remove.
1362 | sids:str Used by "add" and "set". ID of song to add to the
1363 | jukebox playlist. Use multiple id parameters to
1364 | add many songs in the same request. Whether you
1365 | are passing one song or many into this, this
1366 | parameter MUST be a list
1367 | gain:float Used by setGain to control the playback volume.
1368 | A float value between 0.0 and 1.0
1369 | offset:int (added in API 1.7.0) Used by "skip". Start playing
1370 | this many seconds into the track.
1371 | """
1372 | methodName = 'jukeboxControl'
1373 | viewName = '%s.view' % methodName
1374 |
1375 | q = self._getQueryDict({'action': action, 'index': index,
1376 | 'gain': gain, 'offset': offset})
1377 |
1378 | req = None
1379 | if action == 'add':
1380 | # We have to deal with the sids
1381 | if not (isinstance(sids, list) or isinstance(sids, tuple)):
1382 | raise ArgumentError('If you are adding songs, "sids" must '
1383 | 'be a list or tuple!')
1384 | req = self._getRequestWithList(viewName, 'id', sids, q)
1385 | else:
1386 | req = self._getRequest(viewName, q)
1387 | res = self._doInfoReq(req)
1388 | self._checkStatus(res)
1389 | return res
1390 |
1391 | def getPodcasts(self, incEpisodes=True, pid=None):
1392 | """
1393 | since: 1.6.0
1394 |
1395 | Returns all podcast channels the server subscribes to and their
1396 | episodes.
1397 |
1398 | incEpisodes:bool (since: 1.9.0) Whether to include Podcast
1399 | episodes in the returned result.
1400 | pid:str (since: 1.9.0) If specified, only return
1401 | the Podcast channel with this ID.
1402 |
1403 | Returns a dict like the following:
1404 | {u'status': u'ok',
1405 | u'version': u'1.6.0',
1406 | u'xmlns': u'http://subsonic.org/restapi',
1407 | u'podcasts': {u'channel': {u'description': u"Dr Chris Smith...",
1408 | u'episode': [{u'album': u'Dr Karl and the Naked Scientist',
1409 | u'artist': u'BBC Radio 5 live',
1410 | u'bitRate': 64,
1411 | u'contentType': u'audio/mpeg',
1412 | u'coverArt': u'2f6f7074',
1413 | u'description': u'Dr Karl answers all your science related questions.',
1414 | u'duration': 2902,
1415 | u'genre': u'Podcast',
1416 | u'id': 0,
1417 | u'isDir': False,
1418 | u'isVideo': False,
1419 | u'parent': u'2f6f70742f737562736f6e69632f706f6463617374732f4472204b61726c20616e6420746865204e616b656420536369656e74697374',
1420 | u'publishDate': u'2011-08-17 22:06:00.0',
1421 | u'size': 23313059,
1422 | u'status': u'completed',
1423 | u'streamId': u'2f6f70742f737562736f6e69632f706f6463617374732f4472204b61726c20616e6420746865204e616b656420536369656e746973742f64726b61726c5f32303131303831382d30343036612e6d7033',
1424 | u'suffix': u'mp3',
1425 | u'title': u'DrKarl: Peppermints, Chillies & Receptors',
1426 | u'year': 2011},
1427 | {u'description': u'which is warmer, a bath with bubbles in it or one without? Just one of the stranger science stories tackled this week by Dr Chris Smith and the Naked Scientists!',
1428 | u'id': 1,
1429 | u'publishDate': u'2011-08-14 21:05:00.0',
1430 | u'status': u'skipped',
1431 | u'title': u'DrKarl: how many bubbles in your bath? 15 AUG 11'},
1432 | ...
1433 | {u'description': u'Dr Karl joins Rhod to answer all your science questions',
1434 | u'id': 9,
1435 | u'publishDate': u'2011-07-06 22:12:00.0',
1436 | u'status': u'skipped',
1437 | u'title': u'DrKarl: 8 Jul 11 The Strange Sound of the MRI Scanner'}],
1438 | u'id': 0,
1439 | u'status': u'completed',
1440 | u'title': u'Dr Karl and the Naked Scientist',
1441 | u'url': u'http://downloads.bbc.co.uk/podcasts/fivelive/drkarl/rss.xml'}}
1442 | }
1443 |
1444 | See also: http://subsonic.svn.sourceforge.net/viewvc/subsonic/trunk/subsonic-main/src/main/webapp/xsd/podcasts_example_1.xml?view=markup
1445 | """
1446 | methodName = 'getPodcasts'
1447 | viewName = '%s.view' % methodName
1448 |
1449 | q = self._getQueryDict({'includeEpisodes': incEpisodes,
1450 | 'id': pid})
1451 | req = self._getRequest(viewName, q)
1452 | res = self._doInfoReq(req)
1453 | self._checkStatus(res)
1454 | return res
1455 |
1456 | def getShares(self):
1457 | """
1458 | since: 1.6.0
1459 |
1460 | Returns information about shared media this user is allowed to manage
1461 |
1462 | Note that entry can be either a single dict or a list of dicts
1463 |
1464 | Returns a dict like the following:
1465 |
1466 | {u'status': u'ok',
1467 | u'version': u'1.6.0',
1468 | u'xmlns': u'http://subsonic.org/restapi',
1469 | u'shares': {u'share': [
1470 | {u'created': u'2011-08-18T10:01:35',
1471 | u'entry': {u'artist': u'Alice In Chains',
1472 | u'coverArt': u'2f66696c65732f6d7033732f412d4d2f416c69636520496e20436861696e732f416c69636520496e20436861696e732f636f7665722e6a7067',
1473 | u'id': u'2f66696c65732f6d7033732f412d4d2f416c69636520496e20436861696e732f416c69636520496e20436861696e73',
1474 | u'isDir': True,
1475 | u'parent': u'2f66696c65732f6d7033732f412d4d2f416c69636520496e20436861696e73',
1476 | u'title': u'Alice In Chains'},
1477 | u'expires': u'2012-08-18T10:01:35',
1478 | u'id': 0,
1479 | u'url': u'http://crustymonkey.subsonic.org/share/BuLbF',
1480 | u'username': u'admin',
1481 | u'visitCount': 0
1482 | }]}
1483 | }
1484 | """
1485 | methodName = 'getShares'
1486 | viewName = '%s.view' % methodName
1487 |
1488 | req = self._getRequest(viewName)
1489 | res = self._doInfoReq(req)
1490 | self._checkStatus(res)
1491 | return res
1492 |
1493 | def createShare(self, shids=[], description=None, expires=None):
1494 | """
1495 | since: 1.6.0
1496 |
1497 | Creates a public URL that can be used by anyone to stream music
1498 | or video from the Subsonic server. The URL is short and suitable
1499 | for posting on Facebook, Twitter etc. Note: The user must be
1500 | authorized to share (see Settings > Users > User is allowed to
1501 | share files with anyone).
1502 |
1503 | shids:list[str] A list of ids of songs, albums or videos
1504 | to share.
1505 | description:str A description that will be displayed to
1506 | people visiting the shared media
1507 | (optional).
1508 | expires:float A timestamp pertaining to the time at
1509 | which this should expire (optional)
1510 |
1511 | This returns a structure like you would get back from getShares()
1512 | containing just your new share.
1513 | """
1514 | methodName = 'createShare'
1515 | viewName = '%s.view' % methodName
1516 |
1517 | q = self._getQueryDict({'description': description,
1518 | 'expires': self._ts2milli(expires)})
1519 | req = self._getRequestWithList(viewName, 'id', shids, q)
1520 | res = self._doInfoReq(req)
1521 | self._checkStatus(res)
1522 | return res
1523 |
1524 | def updateShare(self, shid, description=None, expires=None):
1525 | """
1526 | since: 1.6.0
1527 |
1528 | Updates the description and/or expiration date for an existing share
1529 |
1530 | shid:str The id of the share to update
1531 | description:str The new description for the share (optional).
1532 | expires:float The new timestamp for the expiration time of this
1533 | share (optional).
1534 | """
1535 | methodName = 'updateShare'
1536 | viewName = '%s.view' % methodName
1537 |
1538 | q = self._getQueryDict({'id': shid, 'description': description,
1539 | expires: self._ts2milli(expires)})
1540 |
1541 | req = self._getRequest(viewName, q)
1542 | res = self._doInfoReq(req)
1543 | self._checkStatus(res)
1544 | return res
1545 |
1546 | def deleteShare(self, shid):
1547 | """
1548 | since: 1.6.0
1549 |
1550 | Deletes an existing share
1551 |
1552 | shid:str The id of the share to delete
1553 |
1554 | Returns a standard response dict
1555 | """
1556 | methodName = 'deleteShare'
1557 | viewName = '%s.view' % methodName
1558 |
1559 | q = self._getQueryDict({'id': shid})
1560 |
1561 | req = self._getRequest(viewName, q)
1562 | res = self._doInfoReq(req)
1563 | self._checkStatus(res)
1564 | return res
1565 |
1566 | def setRating(self, id, rating):
1567 | """
1568 | since: 1.6.0
1569 |
1570 | Sets the rating for a music file
1571 |
1572 | id:str The id of the item (song/artist/album) to rate
1573 | rating:int The rating between 1 and 5 (inclusive), or 0 to remove
1574 | the rating
1575 |
1576 | Returns a standard response dict
1577 | """
1578 | methodName = 'setRating'
1579 | viewName = '%s.view' % methodName
1580 |
1581 | try:
1582 | rating = int(rating)
1583 | except:
1584 | raise ArgumentError('Rating must be an integer between 0 and 5: '
1585 | '%r' % rating)
1586 | if rating < 0 or rating > 5:
1587 | raise ArgumentError('Rating must be an integer between 0 and 5: '
1588 | '%r' % rating)
1589 |
1590 | q = self._getQueryDict({'id': id, 'rating': rating})
1591 |
1592 | req = self._getRequest(viewName, q)
1593 | res = self._doInfoReq(req)
1594 | self._checkStatus(res)
1595 | return res
1596 |
1597 | def getArtists(self):
1598 | """
1599 | since 1.8.0
1600 |
1601 | Similar to getIndexes(), but this method uses the ID3 tags to
1602 | determine the artist
1603 |
1604 | Returns a dict like the following:
1605 | {u'artists': {u'index': [{u'artist': {u'albumCount': 7,
1606 | u'coverArt': u'ar-0',
1607 | u'id': 0,
1608 | u'name': u'Massive Attack'},
1609 | u'name': u'M'},
1610 | {u'artist': {u'albumCount': 2,
1611 | u'coverArt': u'ar-1',
1612 | u'id': 1,
1613 | u'name': u'Tune-Yards'},
1614 | u'name': u'T'}]},
1615 | u'status': u'ok',
1616 | u'version': u'1.8.0',
1617 | u'xmlns': u'http://subsonic.org/restapi'}
1618 | """
1619 | methodName = 'getArtists'
1620 | viewName = '%s.view' % methodName
1621 |
1622 | req = self._getRequest(viewName)
1623 | res = self._doInfoReq(req)
1624 | self._checkStatus(res)
1625 | return res
1626 |
1627 | def getArtist(self, id):
1628 | """
1629 | since 1.8.0
1630 |
1631 | Returns the info (albums) for an artist. This method uses
1632 | the ID3 tags for organization
1633 |
1634 | id:str The artist ID
1635 |
1636 | Returns a dict like the following:
1637 |
1638 | {u'artist': {u'album': [{u'artist': u'Tune-Yards',
1639 | u'artistId': 1,
1640 | u'coverArt': u'al-7',
1641 | u'created': u'2012-01-30T12:35:33',
1642 | u'duration': 3229,
1643 | u'id': 7,
1644 | u'name': u'Bird-Brains',
1645 | u'songCount': 13},
1646 | {u'artist': u'Tune-Yards',
1647 | u'artistId': 1,
1648 | u'coverArt': u'al-8',
1649 | u'created': u'2011-03-22T15:08:00',
1650 | u'duration': 2531,
1651 | u'id': 8,
1652 | u'name': u'W H O K I L L',
1653 | u'songCount': 10}],
1654 | u'albumCount': 2,
1655 | u'coverArt': u'ar-1',
1656 | u'id': 1,
1657 | u'name': u'Tune-Yards'},
1658 | u'status': u'ok',
1659 | u'version': u'1.8.0',
1660 | u'xmlns': u'http://subsonic.org/restapi'}
1661 | """
1662 | methodName = 'getArtist'
1663 | viewName = '%s.view' % methodName
1664 |
1665 | q = self._getQueryDict({'id': id})
1666 |
1667 | req = self._getRequest(viewName, q)
1668 | res = self._doInfoReq(req)
1669 | self._checkStatus(res)
1670 | return res
1671 |
1672 | def getAlbum(self, id):
1673 | """
1674 | since 1.8.0
1675 |
1676 | Returns the info and songs for an album. This method uses
1677 | the ID3 tags for organization
1678 |
1679 | id:str The album ID
1680 |
1681 | Returns a dict like the following:
1682 |
1683 | {u'album': {u'artist': u'Massive Attack',
1684 | u'artistId': 0,
1685 | u'coverArt': u'al-0',
1686 | u'created': u'2009-08-28T10:00:44',
1687 | u'duration': 3762,
1688 | u'id': 0,
1689 | u'name': u'100th Window',
1690 | u'song': [{u'album': u'100th Window',
1691 | u'albumId': 0,
1692 | u'artist': u'Massive Attack',
1693 | u'artistId': 0,
1694 | u'bitRate': 192,
1695 | u'contentType': u'audio/mpeg',
1696 | u'coverArt': 2,
1697 | u'created': u'2009-08-28T10:00:57',
1698 | u'duration': 341,
1699 | u'genre': u'Rock',
1700 | u'id': 14,
1701 | u'isDir': False,
1702 | u'isVideo': False,
1703 | u'parent': 2,
1704 | u'path': u'Massive Attack/100th Window/01 - Future Proof.mp3',
1705 | u'size': 8184445,
1706 | u'suffix': u'mp3',
1707 | u'title': u'Future Proof',
1708 | u'track': 1,
1709 | u'type': u'music',
1710 | u'year': 2003}],
1711 | u'songCount': 9},
1712 | u'status': u'ok',
1713 | u'version': u'1.8.0',
1714 | u'xmlns': u'http://subsonic.org/restapi'}
1715 | """
1716 | methodName = 'getAlbum'
1717 | viewName = '%s.view' % methodName
1718 |
1719 | q = self._getQueryDict({'id': id})
1720 |
1721 | req = self._getRequest(viewName, q)
1722 | res = self._doInfoReq(req)
1723 | self._checkStatus(res)
1724 | return res
1725 |
1726 | def getSong(self, id):
1727 | """
1728 | since 1.8.0
1729 |
1730 | Returns the info for a song. This method uses the ID3
1731 | tags for organization
1732 |
1733 | id:str The song ID
1734 |
1735 | Returns a dict like the following:
1736 | {u'song': {u'album': u'W H O K I L L',
1737 | u'albumId': 8,
1738 | u'artist': u'Tune-Yards',
1739 | u'artistId': 1,
1740 | u'bitRate': 320,
1741 | u'contentType': u'audio/mpeg',
1742 | u'coverArt': 106,
1743 | u'created': u'2011-03-22T15:08:00',
1744 | u'discNumber': 1,
1745 | u'duration': 192,
1746 | u'genre': u'Indie Rock',
1747 | u'id': 120,
1748 | u'isDir': False,
1749 | u'isVideo': False,
1750 | u'parent': 106,
1751 | u'path': u'Tune Yards/Who Kill/10 Killa.mp3',
1752 | u'size': 7692656,
1753 | u'suffix': u'mp3',
1754 | u'title': u'Killa',
1755 | u'track': 10,
1756 | u'type': u'music',
1757 | u'year': 2011},
1758 | u'status': u'ok',
1759 | u'version': u'1.8.0',
1760 | u'xmlns': u'http://subsonic.org/restapi'}
1761 | """
1762 | methodName = 'getSong'
1763 | viewName = '%s.view' % methodName
1764 |
1765 | q = self._getQueryDict({'id': id})
1766 |
1767 | req = self._getRequest(viewName, q)
1768 | res = self._doInfoReq(req)
1769 | self._checkStatus(res)
1770 | return res
1771 |
1772 | def getVideos(self):
1773 | """
1774 | since 1.8.0
1775 |
1776 | Returns all video files
1777 |
1778 | Returns a dict like the following:
1779 | {u'status': u'ok',
1780 | u'version': u'1.8.0',
1781 | u'videos': {u'video': {u'bitRate': 384,
1782 | u'contentType': u'video/x-matroska',
1783 | u'created': u'2012-08-26T13:36:44',
1784 | u'duration': 1301,
1785 | u'id': 130,
1786 | u'isDir': False,
1787 | u'isVideo': True,
1788 | u'path': u'South Park - 16x07 - Cartman Finds Love.mkv',
1789 | u'size': 287309613,
1790 | u'suffix': u'mkv',
1791 | u'title': u'South Park - 16x07 - Cartman Finds Love',
1792 | u'transcodedContentType': u'video/x-flv',
1793 | u'transcodedSuffix': u'flv'}},
1794 | u'xmlns': u'http://subsonic.org/restapi'}
1795 | """
1796 | methodName = 'getVideos'
1797 | viewName = '%s.view' % methodName
1798 |
1799 | req = self._getRequest(viewName)
1800 | res = self._doInfoReq(req)
1801 | self._checkStatus(res)
1802 | return res
1803 |
1804 | def getStarred(self, musicFolderId=None):
1805 | """
1806 | since 1.8.0
1807 |
1808 | musicFolderId:int Only return results from the music folder
1809 | with the given ID. See getMusicFolders
1810 |
1811 | Returns starred songs, albums and artists
1812 |
1813 | Returns a dict like the following:
1814 | {u'starred': {u'album': {u'album': u'Bird-Brains',
1815 | u'artist': u'Tune-Yards',
1816 | u'coverArt': 105,
1817 | u'created': u'2012-01-30T13:16:58',
1818 | u'id': 105,
1819 | u'isDir': True,
1820 | u'parent': 104,
1821 | u'starred': u'2012-08-26T13:18:34',
1822 | u'title': u'Bird-Brains'},
1823 | u'song': [{u'album': u'Mezzanine',
1824 | u'albumId': 4,
1825 | u'artist': u'Massive Attack',
1826 | u'artistId': 0,
1827 | u'bitRate': 256,
1828 | u'contentType': u'audio/mpeg',
1829 | u'coverArt': 6,
1830 | u'created': u'2009-06-15T07:48:28',
1831 | u'duration': 298,
1832 | u'genre': u'Dub',
1833 | u'id': 72,
1834 | u'isDir': False,
1835 | u'isVideo': False,
1836 | u'parent': 6,
1837 | u'path': u'Massive Attack/Mezzanine/Massive Attack_02_mezzanine.mp3',
1838 | u'size': 9564160,
1839 | u'starred': u'2012-08-26T13:19:26',
1840 | u'suffix': u'mp3',
1841 | u'title': u'Risingson',
1842 | u'track': 2,
1843 | u'type': u'music'},
1844 | {u'album': u'Mezzanine',
1845 | u'albumId': 4,
1846 | u'artist': u'Massive Attack',
1847 | u'artistId': 0,
1848 | u'bitRate': 256,
1849 | u'contentType': u'audio/mpeg',
1850 | u'coverArt': 6,
1851 | u'created': u'2009-06-15T07:48:25',
1852 | u'duration': 380,
1853 | u'genre': u'Dub',
1854 | u'id': 71,
1855 | u'isDir': False,
1856 | u'isVideo': False,
1857 | u'parent': 6,
1858 | u'path': u'Massive Attack/Mezzanine/Massive Attack_01_mezzanine.mp3',
1859 | u'size': 12179456,
1860 | u'starred': u'2012-08-26T13:19:03',
1861 | u'suffix': u'mp3',
1862 | u'title': u'Angel',
1863 | u'track': 1,
1864 | u'type': u'music'}]},
1865 | u'status': u'ok',
1866 | u'version': u'1.8.0',
1867 | u'xmlns': u'http://subsonic.org/restapi'}
1868 | """
1869 | methodName = 'getStarred'
1870 | viewName = '%s.view' % methodName
1871 |
1872 | q = {}
1873 | if musicFolderId:
1874 | q['musicFolderId'] = musicFolderId
1875 |
1876 | req = self._getRequest(viewName, q)
1877 | res = self._doInfoReq(req)
1878 | self._checkStatus(res)
1879 | return res
1880 |
1881 | def getStarred2(self, musicFolderId=None):
1882 | """
1883 | since 1.8.0
1884 |
1885 | musicFolderId:int Only return results from the music folder
1886 | with the given ID. See getMusicFolders
1887 |
1888 | Returns starred songs, albums and artists like getStarred(),
1889 | but this uses ID3 tags for organization
1890 |
1891 | Returns a dict like the following:
1892 |
1893 | **See the output from getStarred()**
1894 | """
1895 | methodName = 'getStarred2'
1896 | viewName = '%s.view' % methodName
1897 |
1898 | q = {}
1899 | if musicFolderId:
1900 | q['musicFolderId'] = musicFolderId
1901 |
1902 | req = self._getRequest(viewName, q)
1903 | res = self._doInfoReq(req)
1904 | self._checkStatus(res)
1905 | return res
1906 |
1907 | def updatePlaylist(self, lid, name=None, comment=None, songIdsToAdd=[],
1908 | songIndexesToRemove=[]):
1909 | """
1910 | since 1.8.0
1911 |
1912 | Updates a playlist. Only the owner of a playlist is allowed to
1913 | update it.
1914 |
1915 | lid:str The playlist id
1916 | name:str The human readable name of the playlist
1917 | comment:str The playlist comment
1918 | songIdsToAdd:list A list of song IDs to add to the playlist
1919 | songIndexesToRemove:list Remove the songs at the
1920 | 0 BASED INDEXED POSITIONS in the
1921 | playlist, NOT the song ids. Note that
1922 | this is always a list.
1923 |
1924 | Returns a normal status response dict
1925 | """
1926 | methodName = 'updatePlaylist'
1927 | viewName = '%s.view' % methodName
1928 |
1929 | q = self._getQueryDict({'playlistId': lid, 'name': name,
1930 | 'comment': comment})
1931 | if not isinstance(songIdsToAdd, list) or isinstance(songIdsToAdd,
1932 | tuple):
1933 | songIdsToAdd = [songIdsToAdd]
1934 | if not isinstance(songIndexesToRemove, list) or isinstance(
1935 | songIndexesToRemove, tuple):
1936 | songIndexesToRemove = [songIndexesToRemove]
1937 | listMap = {'songIdToAdd': songIdsToAdd,
1938 | 'songIndexToRemove': songIndexesToRemove}
1939 | req = self._getRequestWithLists(viewName, listMap, q)
1940 | res = self._doInfoReq(req)
1941 | self._checkStatus(res)
1942 | return res
1943 |
1944 | def getAvatar(self, username):
1945 | """
1946 | since 1.8.0
1947 |
1948 | Returns the avatar for a user or None if the avatar does not exist
1949 |
1950 | username:str The user to retrieve the avatar for
1951 |
1952 | Returns the file-like object for reading or raises an exception
1953 | on error
1954 | """
1955 | methodName = 'getAvatar'
1956 | viewName = '%s.view' % methodName
1957 |
1958 | q = {'username': username}
1959 |
1960 | req = self._getRequest(viewName, q)
1961 | try:
1962 | res = self._doBinReq(req)
1963 | except urllib.error.HTTPError:
1964 | # Avatar is not set/does not exist, return None
1965 | return None
1966 | if isinstance(res, dict):
1967 | self._checkStatus(res)
1968 | return res
1969 |
1970 | def star(self, sids=[], albumIds=[], artistIds=[]):
1971 | """
1972 | since 1.8.0
1973 |
1974 | Attaches a star to songs, albums or artists
1975 |
1976 | sids:list A list of song IDs to star
1977 | albumIds:list A list of album IDs to star. Use this rather than
1978 | "sids" if the client access the media collection
1979 | according to ID3 tags rather than file
1980 | structure
1981 | artistIds:list The ID of an artist to star. Use this rather
1982 | than sids if the client access the media
1983 | collection according to ID3 tags rather
1984 | than file structure
1985 |
1986 | Returns a normal status response dict
1987 | """
1988 | methodName = 'star'
1989 | viewName = '%s.view' % methodName
1990 |
1991 | if not isinstance(sids, list) or isinstance(sids, tuple):
1992 | sids = [sids]
1993 | if not isinstance(albumIds, list) or isinstance(albumIds, tuple):
1994 | albumIds = [albumIds]
1995 | if not isinstance(artistIds, list) or isinstance(artistIds, tuple):
1996 | artistIds = [artistIds]
1997 | listMap = {'id': sids,
1998 | 'albumId': albumIds,
1999 | 'artistId': artistIds}
2000 | req = self._getRequestWithLists(viewName, listMap)
2001 | res = self._doInfoReq(req)
2002 | self._checkStatus(res)
2003 | return res
2004 |
2005 | def unstar(self, sids=[], albumIds=[], artistIds=[]):
2006 | """
2007 | since 1.8.0
2008 |
2009 | Removes a star to songs, albums or artists. Basically, the
2010 | same as star in reverse
2011 |
2012 | sids:list A list of song IDs to star
2013 | albumIds:list A list of album IDs to star. Use this rather than
2014 | "sids" if the client access the media collection
2015 | according to ID3 tags rather than file
2016 | structure
2017 | artistIds:list The ID of an artist to star. Use this rather
2018 | than sids if the client access the media
2019 | collection according to ID3 tags rather
2020 | than file structure
2021 |
2022 | Returns a normal status response dict
2023 | """
2024 | methodName = 'unstar'
2025 | viewName = '%s.view' % methodName
2026 |
2027 | if not isinstance(sids, list) or isinstance(sids, tuple):
2028 | sids = [sids]
2029 | if not isinstance(albumIds, list) or isinstance(albumIds, tuple):
2030 | albumIds = [albumIds]
2031 | if not isinstance(artistIds, list) or isinstance(artistIds, tuple):
2032 | artistIds = [artistIds]
2033 | listMap = {'id': sids,
2034 | 'albumId': albumIds,
2035 | 'artistId': artistIds}
2036 | req = self._getRequestWithLists(viewName, listMap)
2037 | res = self._doInfoReq(req)
2038 | self._checkStatus(res)
2039 | return res
2040 |
2041 | def getGenres(self):
2042 | """
2043 | since 1.9.0
2044 |
2045 | Returns all genres
2046 | """
2047 | methodName = 'getGenres'
2048 | viewName = '%s.view' % methodName
2049 |
2050 | req = self._getRequest(viewName)
2051 | res = self._doInfoReq(req)
2052 | self._checkStatus(res)
2053 | return res
2054 |
2055 | def getSongsByGenre(self, genre, count=10, offset=0, musicFolderId=None):
2056 | """
2057 | since 1.9.0
2058 |
2059 | Returns songs in a given genre
2060 |
2061 | genre:str The genre, as returned by getGenres()
2062 | count:int The maximum number of songs to return. Max is 500
2063 | default: 10
2064 | offset:int The offset if you are paging. default: 0
2065 | musicFolderId:int Only return results from the music folder
2066 | with the given ID. See getMusicFolders
2067 | """
2068 | methodName = 'getSongsByGenre'
2069 | viewName = '%s.view' % methodName
2070 |
2071 | q = self._getQueryDict({'genre': genre,
2072 | 'count': count,
2073 | 'offset': offset,
2074 | 'musicFolderId': musicFolderId,
2075 | })
2076 |
2077 | req = self._getRequest(viewName, q)
2078 | res = self._doInfoReq(req)
2079 | self._checkStatus(res)
2080 | return res
2081 |
2082 | def hls (self, mid, bitrate=None):
2083 | """
2084 | since 1.8.0
2085 |
2086 | Creates an HTTP live streaming playlist for streaming video or
2087 | audio HLS is a streaming protocol implemented by Apple and
2088 | works by breaking the overall stream into a sequence of small
2089 | HTTP-based file downloads. It's supported by iOS and newer
2090 | versions of Android. This method also supports adaptive
2091 | bitrate streaming, see the bitRate parameter.
2092 |
2093 | mid:str The ID of the media to stream
2094 | bitrate:str If specified, the server will attempt to limit the
2095 | bitrate to this value, in kilobits per second. If
2096 | this parameter is specified more than once, the
2097 | server will create a variant playlist, suitable
2098 | for adaptive bitrate streaming. The playlist will
2099 | support streaming at all the specified bitrates.
2100 | The server will automatically choose video dimensions
2101 | that are suitable for the given bitrates.
2102 | (since: 1.9.0) you may explicitly request a certain
2103 | width (480) and height (360) like so:
2104 | bitRate=1000@480x360
2105 |
2106 | Returns the raw m3u8 file as a string
2107 | """
2108 | methodName = 'hls'
2109 | viewName = '%s.view' % methodName
2110 |
2111 | q = self._getQueryDict({'id': mid, 'bitrate': bitrate})
2112 | req = self._getRequest(viewName, q)
2113 | try:
2114 | res = self._doBinReq(req)
2115 | except urllib.error.HTTPError:
2116 | # Avatar is not set/does not exist, return None
2117 | return None
2118 | if isinstance(res, dict):
2119 | self._checkStatus(res)
2120 | return res.read()
2121 |
2122 | def refreshPodcasts(self):
2123 | """
2124 | since: 1.9.0
2125 |
2126 | Tells the server to check for new Podcast episodes. Note: The user
2127 | must be authorized for Podcast administration
2128 | """
2129 | methodName = 'refreshPodcasts'
2130 | viewName = '%s.view' % methodName
2131 |
2132 | req = self._getRequest(viewName)
2133 | res = self._doInfoReq(req)
2134 | self._checkStatus(res)
2135 | return res
2136 |
2137 | def createPodcastChannel(self, url):
2138 | """
2139 | since: 1.9.0
2140 |
2141 | Adds a new Podcast channel. Note: The user must be authorized
2142 | for Podcast administration
2143 |
2144 | url:str The URL of the Podcast to add
2145 | """
2146 | methodName = 'createPodcastChannel'
2147 | viewName = '%s.view' % methodName
2148 |
2149 | q = {'url': url}
2150 |
2151 | req = self._getRequest(viewName, q)
2152 | res = self._doInfoReq(req)
2153 | self._checkStatus(res)
2154 | return res
2155 |
2156 | def deletePodcastChannel(self, pid):
2157 | """
2158 | since: 1.9.0
2159 |
2160 | Deletes a Podcast channel. Note: The user must be authorized
2161 | for Podcast administration
2162 |
2163 | pid:str The ID of the Podcast channel to delete
2164 | """
2165 | methodName = 'deletePodcastChannel'
2166 | viewName = '%s.view' % methodName
2167 |
2168 | q = {'id': pid}
2169 |
2170 | req = self._getRequest(viewName, q)
2171 | res = self._doInfoReq(req)
2172 | self._checkStatus(res)
2173 | return res
2174 |
2175 | def deletePodcastEpisode(self, pid):
2176 | """
2177 | since: 1.9.0
2178 |
2179 | Deletes a Podcast episode. Note: The user must be authorized
2180 | for Podcast administration
2181 |
2182 | pid:str The ID of the Podcast episode to delete
2183 | """
2184 | methodName = 'deletePodcastEpisode'
2185 | viewName = '%s.view' % methodName
2186 |
2187 | q = {'id': pid}
2188 |
2189 | req = self._getRequest(viewName, q)
2190 | res = self._doInfoReq(req)
2191 | self._checkStatus(res)
2192 | return res
2193 |
2194 | def downloadPodcastEpisode(self, pid):
2195 | """
2196 | since: 1.9.0
2197 |
2198 | Tells the server to start downloading a given Podcast episode.
2199 | Note: The user must be authorized for Podcast administration
2200 |
2201 | pid:str The ID of the Podcast episode to download
2202 | """
2203 | methodName = 'downloadPodcastEpisode'
2204 | viewName = '%s.view' % methodName
2205 |
2206 | q = {'id': pid}
2207 |
2208 | req = self._getRequest(viewName, q)
2209 | res = self._doInfoReq(req)
2210 | self._checkStatus(res)
2211 | return res
2212 |
2213 | def getInternetRadioStations(self):
2214 | """
2215 | since: 1.9.0
2216 |
2217 | Returns all internet radio stations
2218 | """
2219 | methodName = 'getInternetRadioStations'
2220 | viewName = '%s.view' % methodName
2221 |
2222 | req = self._getRequest(viewName)
2223 | res = self._doInfoReq(req)
2224 | self._checkStatus(res)
2225 | return res
2226 |
2227 | def createInternetRadioStation(self, streamUrl, name, homepageUrl=None):
2228 | """
2229 | since 1.16.0
2230 |
2231 | Create an internet radio station
2232 |
2233 | streamUrl:str The stream URL for the station
2234 | name:str The user-defined name for the station
2235 | homepageUrl:str The homepage URL for the station
2236 | """
2237 | methodName = 'createInternetRadioStation'
2238 | viewName = '{}.view'.format(methodName)
2239 |
2240 | q = self._getQueryDict({
2241 | 'streamUrl': streamUrl, 'name': name, 'homepageUrl': homepageUrl})
2242 |
2243 | req = self._getRequest(viewName, q)
2244 | res = self._doInfoReq(req)
2245 | self._checkStatus(res)
2246 | return res
2247 |
2248 | def updateInternetRadioStation(self, iid, streamUrl, name,
2249 | homepageUrl=None):
2250 | """
2251 | since 1.16.0
2252 |
2253 | Create an internet radio station
2254 |
2255 | iid:str The ID for the station
2256 | streamUrl:str The stream URL for the station
2257 | name:str The user-defined name for the station
2258 | homepageUrl:str The homepage URL for the station
2259 | """
2260 | methodName = 'updateInternetRadioStation'
2261 | viewName = '{}.view'.format(methodName)
2262 |
2263 | q = self._getQueryDict({
2264 | 'id': iid, 'streamUrl': streamUrl, 'name': name,
2265 | 'homepageUrl': homepageUrl,
2266 | })
2267 |
2268 | req = self._getRequest(viewName, q)
2269 | res = self._doInfoReq(req)
2270 | self._checkStatus(res)
2271 | return res
2272 |
2273 | def deleteInternetRadioStation(self, iid):
2274 | """
2275 | since 1.16.0
2276 |
2277 | Create an internet radio station
2278 |
2279 | iid:str The ID for the station
2280 | """
2281 | methodName = 'deleteInternetRadioStation'
2282 | viewName = '{}.view'.format(methodName)
2283 |
2284 | q = {'id': iid}
2285 |
2286 | req = self._getRequest(viewName, q)
2287 | res = self._doInfoReq(req)
2288 | self._checkStatus(res)
2289 | return res
2290 |
2291 | def getBookmarks(self):
2292 | """
2293 | since: 1.9.0
2294 |
2295 | Returns all bookmarks for this user. A bookmark is a position
2296 | within a media file
2297 | """
2298 | methodName = 'getBookmarks'
2299 | viewName = '%s.view' % methodName
2300 |
2301 | req = self._getRequest(viewName)
2302 | res = self._doInfoReq(req)
2303 | self._checkStatus(res)
2304 | return res
2305 |
2306 | def createBookmark(self, mid, position, comment=None):
2307 | """
2308 | since: 1.9.0
2309 |
2310 | Creates or updates a bookmark (position within a media file).
2311 | Bookmarks are personal and not visible to other users
2312 |
2313 | mid:str The ID of the media file to bookmark. If a bookmark
2314 | already exists for this file, it will be overwritten
2315 | position:int The position (in milliseconds) within the media file
2316 | comment:str A user-defined comment
2317 | """
2318 | methodName = 'createBookmark'
2319 | viewName = '%s.view' % methodName
2320 |
2321 | q = self._getQueryDict({'id': mid, 'position': position,
2322 | 'comment': comment})
2323 |
2324 | req = self._getRequest(viewName, q)
2325 | res = self._doInfoReq(req)
2326 | self._checkStatus(res)
2327 | return res
2328 |
2329 | def deleteBookmark(self, mid):
2330 | """
2331 | since: 1.9.0
2332 |
2333 | Deletes the bookmark for a given file
2334 |
2335 | mid:str The ID of the media file to delete the bookmark from.
2336 | Other users' bookmarks are not affected
2337 | """
2338 | methodName = 'deleteBookmark'
2339 | viewName = '%s.view' % methodName
2340 |
2341 | q = {'id': mid}
2342 |
2343 | req = self._getRequest(viewName, q)
2344 | res = self._doInfoReq(req)
2345 | self._checkStatus(res)
2346 | return res
2347 |
2348 | def getArtistInfo(self, aid, count=20, includeNotPresent=False):
2349 | """
2350 | since: 1.11.0
2351 |
2352 | Returns artist info with biography, image URLS and similar artists
2353 | using data from last.fm
2354 |
2355 | aid:str The ID of the artist, album or song
2356 | count:int The max number of similar artists to return
2357 | includeNotPresent:bool Whether to return artists that are not
2358 | present in the media library
2359 | """
2360 | methodName = 'getArtistInfo'
2361 | viewName = '%s.view' % methodName
2362 |
2363 | q = {'id': aid, 'count': count,
2364 | 'includeNotPresent': includeNotPresent}
2365 |
2366 | req = self._getRequest(viewName, q)
2367 | res = self._doInfoReq(req)
2368 | self._checkStatus(res)
2369 | return res
2370 |
2371 | def getArtistInfo2(self, aid, count=20, includeNotPresent=False):
2372 | """
2373 | since: 1.11.0
2374 |
2375 | Similar to getArtistInfo(), but organizes music according to ID3 tags
2376 |
2377 | aid:str The ID of the artist, album or song
2378 | count:int The max number of similar artists to return
2379 | includeNotPresent:bool Whether to return artists that are not
2380 | present in the media library
2381 | """
2382 | methodName = 'getArtistInfo2'
2383 | viewName = '%s.view' % methodName
2384 |
2385 | q = {'id': aid, 'count': count,
2386 | 'includeNotPresent': includeNotPresent}
2387 |
2388 | req = self._getRequest(viewName, q)
2389 | res = self._doInfoReq(req)
2390 | self._checkStatus(res)
2391 | return res
2392 |
2393 | def getSimilarSongs(self, iid, count=50):
2394 | """
2395 | since 1.11.0
2396 |
2397 | Returns a random collection of songs from the given artist and
2398 | similar artists, using data from last.fm. Typically used for
2399 | artist radio features.
2400 |
2401 | iid:str The artist, album, or song ID
2402 | count:int Max number of songs to return
2403 | """
2404 | methodName = 'getSimilarSongs'
2405 | viewName = '%s.view' % methodName
2406 |
2407 | q = {'id': iid, 'count': count}
2408 |
2409 | req = self._getRequest(viewName, q)
2410 | res = self._doInfoReq(req)
2411 | self._checkStatus(res)
2412 | return res
2413 |
2414 | def getSimilarSongs2(self, iid, count=50):
2415 | """
2416 | since 1.11.0
2417 |
2418 | Similar to getSimilarSongs(), but organizes music according to
2419 | ID3 tags
2420 |
2421 | iid:str The artist, album, or song ID
2422 | count:int Max number of songs to return
2423 | """
2424 | methodName = 'getSimilarSongs2'
2425 | viewName = '%s.view' % methodName
2426 |
2427 | q = {'id': iid, 'count': count}
2428 |
2429 | req = self._getRequest(viewName, q)
2430 | res = self._doInfoReq(req)
2431 | self._checkStatus(res)
2432 | return res
2433 |
2434 | def savePlayQueue(self, qids, current=None, position=None):
2435 | """
2436 | since 1.12.0
2437 |
2438 | qid:list[int] The list of song ids in the play queue
2439 | current:int The id of the current playing song
2440 | position:int The position, in milliseconds, within the current
2441 | playing song
2442 |
2443 | Saves the state of the play queue for this user. This includes
2444 | the tracks in the play queue, the currently playing track, and
2445 | the position within this track. Typically used to allow a user to
2446 | move between different clients/apps while retaining the same play
2447 | queue (for instance when listening to an audio book).
2448 | """
2449 | methodName = 'savePlayQueue'
2450 | viewName = '%s.view' % methodName
2451 |
2452 | if not isinstance(qids, (tuple, list)):
2453 | qids = [qids]
2454 |
2455 | q = self._getQueryDict({'current': current, 'position': position})
2456 |
2457 | req = self._getRequestWithLists(viewName, {'id': qids}, q)
2458 | res = self._doInfoReq(req)
2459 | self._checkStatus(res)
2460 | return res
2461 |
2462 | def getPlayQueue(self):
2463 | """
2464 | since 1.12.0
2465 |
2466 | Returns the state of the play queue for this user (as set by
2467 | savePlayQueue). This includes the tracks in the play queue,
2468 | the currently playing track, and the position within this track.
2469 | Typically used to allow a user to move between different
2470 | clients/apps while retaining the same play queue (for instance
2471 | when listening to an audio book).
2472 | """
2473 | methodName = 'getPlayQueue'
2474 | viewName = '%s.view' % methodName
2475 |
2476 | req = self._getRequest(viewName)
2477 | res = self._doInfoReq(req)
2478 | self._checkStatus(res)
2479 | return res
2480 |
2481 | def getTopSongs(self, artist, count=50):
2482 | """
2483 | since 1.13.0
2484 |
2485 | Returns the top songs for a given artist
2486 |
2487 | artist:str The artist to get songs for
2488 | count:int The number of songs to return
2489 | """
2490 | methodName = 'getTopSongs'
2491 | viewName = '%s.view' % methodName
2492 |
2493 | q = {'artist': artist, 'count': count}
2494 |
2495 | req = self._getRequest(viewName, q)
2496 | res = self._doInfoReq(req)
2497 | self._checkStatus(res)
2498 | return res
2499 |
2500 | def getNewestPodcasts(self, count=20):
2501 | """
2502 | since 1.13.0
2503 |
2504 | Returns the most recently published Podcast episodes
2505 |
2506 | count:int The number of episodes to return
2507 | """
2508 | methodName = 'getNewestPodcasts'
2509 | viewName = '%s.view' % methodName
2510 |
2511 | q = {'count': count}
2512 |
2513 | req = self._getRequest(viewName, q)
2514 | res = self._doInfoReq(req)
2515 | self._checkStatus(res)
2516 | return res
2517 |
2518 | def scanMediaFolders(self):
2519 | """
2520 | This is not an officially supported method of the API
2521 |
2522 | Same as selecting 'Settings' > 'Scan media folders now' with
2523 | Subsonic web GUI
2524 |
2525 | Returns True if refresh successful, False otherwise
2526 | """
2527 | viewName = 'scanNow'
2528 | return self._unsupportedAPIFunction(methodName)
2529 |
2530 | def cleanupDatabase(self):
2531 | """
2532 | This is not an officially supported method of the API
2533 |
2534 | Same as selecting 'Settings' > 'Clean-up Database' with Subsonic
2535 | web GUI
2536 |
2537 | Returns True if cleanup initiated successfully, False otherwise
2538 |
2539 | Subsonic stores information about all media files ever encountered.
2540 | By cleaning up the database, information about files that are
2541 | no longer in your media collection is permanently removed.
2542 | """
2543 | viewName = 'expunge'
2544 | return self._unsupportedAPIFunction(methodName)
2545 |
2546 | def getVideoInfo(self, vid):
2547 | """
2548 | since 1.14.0
2549 |
2550 | Returns details for a video, including information about available
2551 | audio tracks, subtitles (captions) and conversions.
2552 |
2553 | vid:int The video ID
2554 | """
2555 | methodName = 'getVideoInfo'
2556 | viewName = '%s.view' % methodName
2557 |
2558 | q = {'id': int(vid)}
2559 | req = self._getRequest(viewName, q)
2560 | res = self._doInfoReq(req)
2561 | self._checkStatus(res)
2562 | return res
2563 |
2564 | def getAlbumInfo(self, aid):
2565 | """
2566 | since 1.14.0
2567 |
2568 | Returns the album notes, image URLs, etc., using data from last.fm
2569 |
2570 | aid:int The album ID
2571 | """
2572 | methodName = 'getAlbumInfo'
2573 | viewName = '%s.view' % methodName
2574 |
2575 | q = {'id': aid}
2576 | req = self._getRequest(viewName, q)
2577 | res = self._doInfoReq(req)
2578 | self._checkStatus(res)
2579 | return res
2580 |
2581 | def getAlbumInfo2(self, aid):
2582 | """
2583 | since 1.14.0
2584 |
2585 | Same as getAlbumInfo, but uses ID3 tags
2586 |
2587 | aid:int The album ID
2588 | """
2589 | methodName = 'getAlbumInfo2'
2590 | viewName = '%s.view' % methodName
2591 |
2592 | q = {'id': aid}
2593 | req = self._getRequest(viewName, q)
2594 | res = self._doInfoReq(req)
2595 | self._checkStatus(res)
2596 | return res
2597 |
2598 | def getCaptions(self, vid, fmt=None):
2599 | """
2600 | since 1.14.0
2601 |
2602 | Returns captions (subtitles) for a video. Use getVideoInfo for a list
2603 | of captions.
2604 |
2605 | vid:int The ID of the video
2606 | fmt:str Preferred captions format ("srt" or "vtt")
2607 | """
2608 | methodName = 'getCaptions'
2609 | viewName = '%s.view' % methodName
2610 |
2611 | q = self._getQueryDict({'id': int(vid), 'format': fmt})
2612 | req = self._getRequest(viewName, q)
2613 | res = self._doInfoReq(req)
2614 | self._checkStatus(res)
2615 | return res
2616 |
2617 | def _unsupportedAPIFunction(self, methodName):
2618 | """
2619 | base function to call unsupported API methods
2620 |
2621 | Returns True if refresh successful, False otherwise
2622 | :rtype : boolean
2623 | """
2624 | baseMethod = 'musicFolderSettings'
2625 | viewName = '%s.view' % baseMethod
2626 |
2627 | url = '%s:%d/%s/%s?%s' % (self._baseUrl, self._port,
2628 | self._separateServerPath(), viewName, methodName)
2629 | req = urllib.request.Request(url)
2630 | res = self._opener.open(req)
2631 | res_msg = res.msg.lower()
2632 | return res_msg == 'ok'
2633 |
2634 | #
2635 | # Private internal methods
2636 | #
2637 | def _getOpener(self, username, passwd):
2638 | return urllib.request.build_opener()
2639 |
2640 | def _getQueryDict(self, d):
2641 | """
2642 | Given a dictionary, it cleans out all the values set to None
2643 | """
2644 | for k, v in list(d.items()):
2645 | if v is None:
2646 | del d[k]
2647 | return d
2648 |
2649 | def _getBaseQdict(self):
2650 | qdict = {
2651 | 'f': 'json',
2652 | 'v': self._apiVersion,
2653 | 'c': self._appName,
2654 | 'u': self._username,
2655 | }
2656 |
2657 | if self._legacyAuth:
2658 | qdict['p'] = 'enc:%s' % self._hexEnc(self._rawPass)
2659 | else:
2660 | if self._rawPass:
2661 | salt = self._getSalt()
2662 | token = md5((self._rawPass + salt).encode('utf-8')).hexdigest()
2663 | else:
2664 | salt = self._salt
2665 | token = self._token
2666 | qdict.update({
2667 | 's': salt,
2668 | 't': token,
2669 | })
2670 |
2671 | return qdict
2672 |
2673 | def _getRequest(self, viewName, query={}):
2674 | qdict = self._getBaseQdict()
2675 | qdict.update(query)
2676 | url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
2677 | viewName)
2678 | req = urllib.request.Request(url, urlencode(qdict).encode('utf-8'))
2679 |
2680 | if self._useGET:
2681 | url += '?%s' % urlencode(qdict)
2682 | req = urllib.request.Request(url)
2683 |
2684 | if self._userAgent:
2685 | req.add_header('User-Agent', self._userAgent)
2686 |
2687 | return req
2688 |
2689 | def _getRequestWithList(self, viewName, listName, alist, query={}):
2690 | """
2691 | Like _getRequest, but allows appending a number of items with the
2692 | same key (listName). This bypasses the limitation of urlencode()
2693 | """
2694 | qdict = self._getBaseQdict()
2695 | qdict.update(query)
2696 | url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
2697 | viewName)
2698 | data = StringIO()
2699 | data.write(urlencode(qdict))
2700 | for i in alist:
2701 | data.write('&%s' % urlencode({listName: i}))
2702 | req = urllib.request.Request(url, data.getvalue().encode('utf-8'))
2703 |
2704 | if self._useGET:
2705 | url += '?%s' % data.getvalue()
2706 | req = urllib2.Request(url)
2707 |
2708 | if self._userAgent:
2709 | req.add_header('User-Agent', self._userAgent)
2710 |
2711 | return req
2712 |
2713 | def _getRequestWithLists(self, viewName, listMap, query={}):
2714 | """
2715 | Like _getRequestWithList(), but you must pass a dictionary
2716 | that maps the listName to the list. This allows for multiple
2717 | list parameters to be used, like in updatePlaylist()
2718 |
2719 | viewName:str The name of the view
2720 | listMap:dict A mapping of listName to a list of entries
2721 | query:dict The normal query dict
2722 | """
2723 | qdict = self._getBaseQdict()
2724 | qdict.update(query)
2725 | url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
2726 | viewName)
2727 | data = StringIO()
2728 | data.write(urlencode(qdict))
2729 | for k, l in listMap.items():
2730 | for i in l:
2731 | data.write('&%s' % urlencode({k: i}))
2732 | req = urllib.request.Request(url, data.getvalue().encode('utf-8'))
2733 |
2734 | if self._useGET:
2735 | url += '?%s' % data.getvalue()
2736 | req = urllib2.Request(url)
2737 |
2738 | if self._userAgent:
2739 | req.add_header('User-Agent', self._userAgent)
2740 |
2741 | return req
2742 |
2743 | def _doInfoReq(self, req):
2744 | # Returns a parsed dictionary version of the result
2745 | res = self._opener.open(req)
2746 | dres = json.loads(res.read().decode('utf-8'))
2747 | return dres['subsonic-response']
2748 |
2749 | def _doBinReq(self, req):
2750 | res = self._opener.open(req)
2751 | info = res.info()
2752 | if hasattr(info, 'getheader'):
2753 | contType = info.getheader('Content-Type')
2754 | else:
2755 | contType = info.get('Content-Type')
2756 |
2757 | if contType:
2758 | if contType.startswith('text/html') or \
2759 | contType.startswith('application/json'):
2760 | dres = json.loads(res.read())
2761 | return dres['subsonic-response']
2762 | return res
2763 |
2764 | def _checkStatus(self, result):
2765 | if result['status'] == 'ok':
2766 | return True
2767 | elif result['status'] == 'failed':
2768 | exc = getExcByCode(result['error']['code'])
2769 | raise exc(result['error']['message'])
2770 |
2771 | def _hexEnc(self, raw):
2772 | """
2773 | Returns a "hex encoded" string per the Subsonic api docs
2774 |
2775 | raw:str The string to hex encode
2776 | """
2777 | ret = ''
2778 | for c in raw:
2779 | ret += '%02X' % ord(c)
2780 | return ret
2781 |
2782 | def _ts2milli(self, ts):
2783 | """
2784 | For whatever reason, Subsonic uses timestamps in milliseconds since
2785 | the unix epoch. I have no idea what need there is of this precision,
2786 | but this will just multiply the timestamp times 1000 and return the int
2787 | """
2788 | if ts is None:
2789 | return None
2790 | return int(ts * 1000)
2791 |
2792 | def _separateServerPath(self):
2793 | """
2794 | separate REST portion of URL from base server path.
2795 | """
2796 | return urllib.parse.splithost(self._serverPath)[1].split('/')[0]
2797 |
2798 | def _fixLastModified(self, data):
2799 | """
2800 | This will recursively walk through a data structure and look for
2801 | a dict key/value pair where the key is "lastModified" and change
2802 | the shitty java millisecond timestamp to a real unix timestamp
2803 | of SECONDS since the unix epoch. JAVA SUCKS!
2804 | """
2805 | if isinstance(data, dict):
2806 | for k, v in list(data.items()):
2807 | if k == 'lastModified':
2808 | data[k] = int(v) / 1000.0
2809 | return
2810 | elif isinstance(v, (tuple, list, dict)):
2811 | return self._fixLastModified(v)
2812 | elif isinstance(data, (list, tuple)):
2813 | for item in data:
2814 | if isinstance(item, (list, tuple, dict)):
2815 | return self._fixLastModified(item)
2816 |
2817 | def _process_netrc(self, use_netrc):
2818 | """
2819 | The use_netrc var is either a boolean, which means we should use
2820 | the user's default netrc, or a string specifying a path to a
2821 | netrc formatted file
2822 |
2823 | use_netrc:bool|str Either set to True to use the user's default
2824 | netrc file or a string specifying a specific
2825 | netrc file to use
2826 | """
2827 | if not use_netrc:
2828 | raise CredentialError('useNetrc must be either a boolean "True" '
2829 | 'or a string representing a path to a netrc file, '
2830 | 'not {0}'.format(repr(use_netrc)))
2831 | if isinstance(use_netrc, bool) and use_netrc:
2832 | self._netrc = netrc()
2833 | else:
2834 | # This should be a string specifying a path to a netrc file
2835 | self._netrc = netrc(os.path.expanduser(use_netrc))
2836 | auth = self._netrc.authenticators(self._hostname)
2837 | if not auth:
2838 | raise CredentialError('No machine entry found for {0} in '
2839 | 'your netrc file'.format(self._hostname))
2840 |
2841 | # If we get here, we have credentials
2842 | self._username = auth[0]
2843 | self._rawPass = auth[2]
2844 |
2845 | def _getSalt(self, length=12):
2846 | salt = md5(os.urandom(100)).hexdigest()
2847 | return salt[:length]
2848 |
--------------------------------------------------------------------------------
/libsonic/errors.py:
--------------------------------------------------------------------------------
1 | """
2 | This file is part of py-sonic.
3 |
4 | py-sonic is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | py-sonic is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU General Public License for more details.
13 |
14 | You should have received a copy of the GNU General Public License
15 | along with py-sonic. If not, see
16 | """
17 |
18 | class SonicError(Exception):
19 | pass
20 |
21 | class ParameterError(SonicError):
22 | pass
23 |
24 | class VersionError(SonicError):
25 | pass
26 |
27 | class CredentialError(SonicError):
28 | pass
29 |
30 | class AuthError(SonicError):
31 | pass
32 |
33 | class LicenseError(SonicError):
34 | pass
35 |
36 | class DataNotFoundError(SonicError):
37 | pass
38 |
39 | class ArgumentError(SonicError):
40 | pass
41 |
42 | # This maps the error code numbers from the Subsonic server to their
43 | # appropriate Exceptions
44 | ERR_CODE_MAP = {
45 | 0: SonicError ,
46 | 10: ParameterError ,
47 | 20: VersionError ,
48 | 30: VersionError ,
49 | 40: CredentialError ,
50 | 50: AuthError ,
51 | 60: LicenseError ,
52 | 70: DataNotFoundError ,
53 | }
54 |
55 | def getExcByCode(code):
56 | code = int(code)
57 | if code in ERR_CODE_MAP:
58 | return ERR_CODE_MAP[code]
59 | return SonicError
60 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crustymonkey/py-sonic/e8f12f8c088668dc39a1b4ee98a068f3efa6b001/requirements.txt
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | """
4 | This file is part of py-sonic.
5 |
6 | py-sonic is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | py-sonic is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with py-sonic. If not, see
18 | """
19 |
20 | from setuptools import setup
21 | from libsonic import __version__ as version
22 | import os
23 |
24 | req_file = os.path.join(os.path.dirname(__file__), 'requirements.txt')
25 | requirements = [line for line in open(req_file) if line]
26 |
27 | setup(name='py-sonic',
28 | version=version,
29 | author='Jay Deiman',
30 | author_email='admin@splitstreams.com',
31 | url='http://stuffivelearned.org',
32 | description='A python wrapper library for the Subsonic REST API. '
33 | 'http://subsonic.org',
34 | long_description='This is a basic wrapper library for the Subsonic '
35 | 'REST API. This will allow you to connect to your server and retrieve '
36 | 'information and have it returned in basic Python types.',
37 | packages=['libsonic'],
38 | package_dir={'libsonic': 'libsonic'},
39 | install_requires=requirements,
40 | python_requires='>=3',
41 | classifiers=[
42 | 'Development Status :: 4 - Beta',
43 | 'Intended Audience :: System Administrators',
44 | 'Intended Audience :: Information Technology',
45 | 'License :: OSI Approved :: GNU General Public License (GPL)',
46 | 'Natural Language :: English',
47 | 'Operating System :: POSIX',
48 | 'Programming Language :: Python',
49 | 'Topic :: System :: Systems Administration',
50 | 'Topic :: Internet :: WWW/HTTP',
51 | 'Topic :: Software Development :: Libraries :: Python Modules',
52 | 'Topic :: Software Development :: Libraries',
53 | 'Topic :: System',
54 | ]
55 | )
56 |
--------------------------------------------------------------------------------