Compare commits

...
110 Commits
Author SHA1 Message Date
rembo10 130cf51cc6 Fixed .pep8 for travis build (v0.5.12) 2016-02-25 17:30:55 +00:00
rembo10 7e4a1e453b Revert "Took out E123 from .pep8"
This reverts commit 56de4c63a7.
2016-02-25 17:29:43 +00:00
rembo10 9c946b69e1 v0.5.12 2016-02-25 17:25:15 +00:00
rembo10 61ad94c0a0 Updated changelog for v0.5.12 2016-02-25 17:24:50 +00:00
rembo10 56de4c63a7 Took out E123 from .pep8 2016-02-25 17:23:41 +00:00
rembo10 1dfa31c275 Merge remote-tracking branch 'satreix/feat-pep-e302' into develop 2016-02-25 17:17:07 +00:00
rembo10 e16087eb54 Merge remote-tracking branch 'satreix/feat-pep-e111' into develop 2016-02-25 17:14:55 +00:00
rembo10 289293f23b Merge remote-tracking branch 'satreix/develop' into develop 2016-02-25 17:14:46 +00:00
rembo10 59112c1a83 Changed optional values for pathrender to use curly braces 2016-02-25 17:12:30 +00:00
satreix dd30ed460d pep: fix E111 2016-02-25 12:33:38 +01:00
satreix bc54b5abaf pep: fix E302 2016-02-25 12:24:06 +01:00
satreix 05d316615a pep: fix commenting 2016-02-25 11:15:24 +01:00
rembo10 0eb8702a8e Merge remote-tracking branch 'andrzejc/develop' into develop 2016-02-25 00:58:01 +00:00
rembo10 dfc6a6f93a Merge remote-tracking branch 'noam09/develop' into develop 2016-02-25 00:56:40 +00:00
Noam 1bb0b3c9bf No Need For Deluge Username
- Removed username configuration option, unneeded
2016-02-24 17:14:14 +02:00
Andrzej Ciarkowski 89d5649b4f config.html: Add explanation text for optional elements and single-quote escapes
This is follow up to the change introducing "pathrender.py" and extended path
formatting syntax.
2016-02-22 20:19:28 +01:00
rembo10 4b3b8d2795 v0.5.11 2016-02-20 21:19:03 +00:00
rembo10 eeff731858 Updated changelog for release 2016-02-20 21:18:54 +00:00
rembo10 63abd73942 Merge remote-tracking branch 'andrzejc/date-in-patterns' into develop 2016-02-20 21:12:06 +00:00
rembo10 c74bec2d29 Merge remote-tracking branch 'andrzejc/transmission-session-id' into develop 2016-02-20 21:10:55 +00:00
Noam bd74d21c83 Moved Around Torrent-Type Checking
- From local/data->URL>magnet to magnet->URL->local/data. I feel stupid.
2016-02-19 11:38:37 +02:00
Noam 41aee3d6bb Encode UTF-8 Before Base64ing
- I was using UTF-8 when I started out, don't know why I changed it back
then... Oh well, hopefully this solves the encoding problems (note to
self: you can't base64 unicode)
2016-02-18 19:35:48 +02:00
Noam 396e75ed2d Exception Debugging
- Add traceback output to log
2016-02-18 17:02:44 +02:00
Andrzej Ciarkowski 769e9c1730 postprocessor.py: Allow $Date variable in file/folder name patterns.
Also lowercase variant $date, both resolve to the same field ReleaseDate taken
from metadata.
2016-02-17 22:46:46 +01:00
Andrzej Ciarkowski 33d38f33e5 transmission.py: Don't request session id in subsequent requests.
If session id from previous request to Transmission is present, try using it and
retry the request with new session id only if the first try fails with status
code 409. This will decrease number of required requests by half.
2016-02-17 22:26:48 +01:00
Noam 876b6402c4 Better URL Checking and Logging
- Improved if/else so that URLs won't end up in the file/data opener
- Log torrent file content
2016-02-17 17:55:46 +02:00
Noam 0e334a8f92 Label Debugging
- Labels not set properly, need log
2016-02-17 17:28:19 +02:00
Noam 6218734783 Encoding Change and More String Formatting
- Try base64 encoding utf-8 if ascii fails
- More string formatting
2016-02-17 17:22:59 +02:00
Noam da5ac9757a Remove requests iter_content
- inter_content might have been the reason for repeated encoding issues
- let's try with good old r.text
2016-02-17 16:20:37 +02:00
rembo10 36107c11d5 Merge remote-tracking branch 'YipYup/WebMosher-patch-for-issue-2485' into develop 2016-02-17 12:15:27 +00:00
Noam 5adf684dbe More Logging
- Adding torrent by URL via Deluge still commented out because it
doesn't work with all links (probably because of user-agent)
- String formatting
- Removed debug bits
- Log exceptions
2016-02-17 10:58:29 +02:00
Get your own 'tots. Geez! 6bc3adbd33 Resolve image caused uncaught exception.
Implement Webmoshers ( https://github.com/webmosher ) fix for issue #2485 per https://github.com/rembo10/headphones/issues/2485#issuecomment-180047097
2016-02-16 14:14:41 -07:00
Noam c445a0732f Added Logging
For debugging
2016-02-16 22:06:49 +02:00
Noam 20baed87ff More debugging 2016-02-16 21:18:54 +02:00
Noam ef9a2015eb Added debugging for tests and fixed setTorrentPause 2016-02-16 21:04:56 +02:00
rembo10 583c71eca3 Merge remote-tracking branch 'maxkoryukov/fix/unittestcompat' into develop 2016-02-16 12:17:16 +00:00
maxkoryukov 1e3c3c8f6c Fix unittestcompat 2016-02-16 06:46:12 +05:00
rembo10 0a36166317 Merge remote-tracking branch 'andrzejc/develop' into develop 2016-02-15 23:57:13 +00:00
rembo10 661f01319c Revert "Update frontend libs"
This reverts commit 9cb8379c2d.
2016-02-15 23:38:51 +00:00
Andrzej Ciarkowski 6f8941d7be helpers.py: Fix replace_all() broken by commit not in sync 2016-02-16 00:23:27 +01:00
rembo10 f7116edc9a Merge remote-tracking branch 'maxkoryukov/feature/softchroot' into develop 2016-02-15 22:39:50 +00:00
rembo10 ea69d56ab4 Merge remote-tracking branch 'andrzejc/pathrender' into develop 2016-02-15 22:12:36 +00:00
rembo10 223e129477 Merge remote-tracking branch 'andrzejc/develop' into develop 2016-02-15 22:10:29 +00:00
rembo10 4a5674a679 Merge remote-tracking branch 'maxkoryukov/fix/css-select-beauty' into develop 2016-02-15 22:09:52 +00:00
rembo10 91c3a81cb1 Merge remote-tracking branch 'maxkoryukov/feature/test-config' into develop 2016-02-15 22:09:00 +00:00
rembo10 e6921730c0 Merge remote-tracking branch 'andrzejc/fix-spurious-tmp-dirs' into develop 2016-02-15 22:08:05 +00:00
rembo10 89ef0636a8 Merge remote-tracking branch 'maxkoryukov/feature/cache-travis' into develop 2016-02-15 21:43:35 +00:00
rembo10 769ef001ec Merge remote-tracking branch 'maxkoryukov/feature/six-1.10' into develop 2016-02-15 21:41:12 +00:00
rembo10 44b8ac79da Merge remote-tracking branch 'hamiltont/patch-4' into develop 2016-02-15 21:40:19 +00:00
rembo10 5ada65b2a0 Merge remote-tracking branch 'Uncreate/patch-1' into develop 2016-02-15 21:39:14 +00:00
rembo10 e433e7eda1 Merge remote-tracking branch 'JamieMagee/update-frontend-libs' into develop 2016-02-15 21:34:53 +00:00
Andrzej Ciarkowski 7de7dea1da webserve.py: Fix scanning artists with non-ASCII letters in folder names.
This patch prevents exceptions from os.path.isdir() called with unicode folder names.
2016-02-15 21:59:10 +01:00
Noam 86fb9bbd6c Update README.md
Deluge support
2016-02-14 18:56:10 +02:00
maxkoryukov 159474cf9b Improved CSS for 'select' tags. Now they looks similar to submit-buttons.
No more ugly rectangles!!
2016-02-14 20:01:55 +05:00
Noam a2e7d4bdd1 Base64 Encode Received Torrent Binary and Updated Credits
- Instead of base64-encoding the torrent binary data for each method
(data/file/url/link), _add_torrent_file receives binary data and base64
encodes it
- Today I found out SickRage split into two groups somewhere along the
way (https://github.com/SickRage/sickrage-issues/issues/3), and since I
used parts of the code from SickRage I thought the credits in the file
should reflect its true author
2016-02-14 15:08:56 +02:00
maxkoryukov b143a942ae Unittests for config.py 2016-02-14 16:56:59 +05:00
Noam c446dc263a Add Problematic URL to Debug 2016-02-14 11:31:23 +02:00
Noam 60f718c2ce Temporary Debugging
Print source of exception
2016-02-14 11:28:06 +02:00
Noam 05df81be2f Travis
Not sure why Travis failed, let's try again
2016-02-13 12:17:00 +02:00
Noam 92cd62f0e9 Removed b64encode Import
Already importing all of base64, silly me
2016-02-13 12:07:24 +02:00
Noam 602dfbabc2 Encoding issues
- UTF-8 encoding/decoding issues, removed conversion (for now...)
- Noticed I was accidentally base64-encoding already-based64-encoded
torrents
2016-02-13 00:41:15 +02:00
Noam c492a4122c Travis fixes
- Whitespace
- Old variable name
2016-02-12 15:59:47 +02:00
Noam fd55e84274 Try to Download and Parse Torrents if Receives URL
Deluge module will attempt to download any http(s) links it receives
2016-02-12 13:16:22 +02:00
Uncreate dba264cc5f Update init.ubuntu 2016-02-09 18:06:16 -06:00
Andrzej Ciarkowski 7ff27a42ca pathrender.py: Add module for extended path formatting
The pattern matching is loosely based on foobar2000 pattern syntax, i.e. the
notion of escaping characters with ' and optional elements enclosed in square
brackets [] is taken from there while the substitution variable names are
Perl-ish or sh-ish. The following syntax elements are supported:
 * escaped literal strings, that is everything that is enclosed within single
   quotes (like 'this');
 * substitution variables, which start with dollar sign ($) and extend until
   next non-alphanumeric+underscore character (like $This and $5_that).
 * optional elements enclosed in square brackets, which render nonempty value
   only if any variable or optional inside returned nonempty value, ignoring
   literals (like ['['$That']' ]).
2016-02-09 17:16:28 +01:00
Andrzej Ciarkowski 8e1aa18b8f postprocessor.py: Delete the proper temp dir after postprocessing
Fix for the bug leaving spurious temp dirs after each album processing, due to
headphones trying to rmtree the subdirectory instead of the temp dir itself.
2016-02-05 14:54:43 +01:00
maxkoryukov 99cab7304d ignore .coverage.yml 2016-02-04 22:50:37 +05:00
Noam b5a9d52592 Fixed Whitespace Again
- Oh Travis...
2016-02-04 16:17:27 +02:00
Noam ca36938a83 Removed Whitespace
- Travis stuff
2016-02-04 16:10:22 +02:00
Noam de9d77a8ef Renames Cleanups and Torrent Removal Function
**Changes:**
- Modified function names to adhere to policy
- Removed/commented unused elements
- Added torrent removal function
2016-02-04 15:48:53 +02:00
Noam 0c3ebfbbb3 Exception handling for torrent name regular expression
Fall back to the torrent file's name in case reular expression
extraction method from the torrent file doesn't work.
2016-02-04 14:05:12 +02:00
maxkoryukov c575b861ed Soft Chroot for Headphones 2016-02-04 04:13:27 +05:00
Noah Manning 8323649c10 Added Deluge (Downloader) and Telegram (Notifier) Support
**Features:**
- - Added the ability to use Deluge as a torrent downloader
(http://deluge-torrent.org/)
- - Added the ability to use Telegram messenger for download
notifications using Telegram's Bot API (https://core.telegram.org/bots)

**Issues:**
- - Post-processor doesn't look for the right directory when scanning
for completed downloads, it looks for the torrent's official name and
not the actually directory it was saved to
2016-02-03 17:32:50 +02:00
maxkoryukov 0dd84fad78 pep8 2016-02-03 18:37:50 +05:00
maxkoryukov 7dc25b3d33 improved testing crutches for python 2.6 2016-02-03 18:35:16 +05:00
maxkoryukov 5599e736ba improved testing crutches for python 2.6 2016-02-03 18:32:02 +05:00
maxkoryukov e4e9a6b493 bug in test 2016-02-03 18:25:57 +05:00
maxkoryukov c50ee144df pep8 2016-02-03 18:24:32 +05:00
maxkoryukov f5f32d902b SoftChroot module done 2016-02-03 18:19:35 +05:00
maxkoryukov f993d371a6 Improved testing approach (many testcases in decorators) 2016-02-03 18:19:18 +05:00
maxkoryukov b5fb769bff improved test compatability 2016-02-03 06:59:10 +05:00
maxkoryukov 28cbf6d803 pep8 and pyflakes 2016-02-03 06:52:35 +05:00
maxkoryukov 89ddd937b0 pep8-fy tests 2016-02-03 06:44:25 +05:00
maxkoryukov 5da950f61b * improved unitesting (compat level for python 2.6 and unittest)
* preparation for SoftChroot (stubs and mocks)
2016-02-03 06:30:09 +05:00
maxkoryukov 7bff1b6c3c fuckin python 2.6 2016-02-03 03:57:23 +05:00
maxkoryukov 2baadd19cf fixed travis matrix and tests 2016-02-03 03:53:49 +05:00
maxkoryukov 2852aadc71 Compability with 2.6 unittests.. looks like a dogs poops 2016-02-03 03:50:53 +05:00
maxkoryukov 32e1854e13 Fixed tests Part 2. Me vs. PyFlakes 2016-02-03 03:35:04 +05:00
maxkoryukov b31daaa657 Fixed tests 2016-02-03 03:29:02 +05:00
maxkoryukov 2401fc46ba Merge branch 'feature/softchroot' of https://github.com/maxkoryukov/headphones into feature/softchroot
Conflicts:
	.travis.yml
	headphones/config_test.py
2016-02-03 03:23:06 +05:00
maxkoryukov 0957d2d957 Fixed tests 2016-02-03 03:21:52 +05:00
maxkoryukov f8df9c502b travis: Coverall installing 2016-02-03 03:07:23 +05:00
maxkoryukov 52f071d8b0 Update config_test.py 2016-02-03 03:04:19 +05:00
maxkoryukov a175ec5b5c ignore coverage temporary files
removed "exclude-dirs" from setup.cfg. Travis still cant work with nose-plugins
2016-02-03 02:58:28 +05:00
maxkoryukov f32ee0a9be Merge branch 'feature/cache-travis' into feature/softchroot 2016-02-03 02:53:48 +05:00
maxkoryukov f93702b82c cache for pip on TravisCI 2016-02-03 02:44:29 +05:00
maxkoryukov 0d4b006537 setup.cfg without RedNose 2016-02-03 02:40:25 +05:00
maxkoryukov 3f8477a442 Pep8 styling 2016-02-03 02:36:26 +05:00
maxkoryukov bc9143b2a2 Travis sends coverage 2016-02-03 02:35:54 +05:00
maxkoryukov 16c553196c Default settings for nosetests (+colors +coverage) 2016-02-03 02:35:08 +05:00
maxkoryukov b063ba412f * Dummy test , example for albumart. Required for precise coverage 2016-02-03 02:34:32 +05:00
maxkoryukov aa406f191a Merge branch 'feature/six-1.10' into feature/softchroot 2016-02-02 17:01:58 +05:00
maxkoryukov f63b0cab81 six.py upgraded to 1.10.0 2016-02-02 16:56:34 +05:00
maxkoryukov beacc6ed1e Prototype for test file 2016-02-02 16:54:04 +05:00
maxkoryukov 028cfaa44c Class config.path has been added. All setting reviewed and tuned 2016-02-02 16:45:37 +05:00
maxkoryukov ac2949cf88 Merge pull request #3 from rembo10/develop
sync upstream
2016-02-01 15:38:56 +05:00
maxkoryukov a098fae0f8 Merge pull request #2 from rembo10/master
sync upstream
2016-02-01 15:37:45 +05:00
Hamilton Turner 960e1f473f Make travis builds faster
Headphones does not require a full virtual machine, it can run using 
Travis-CI's container infrastructure which has higher limits and is 
faster
2016-01-30 13:32:36 -05:00
rembo10 ffd72ed6f7 Fixed ubuntu init script as per #2446 2016-01-29 16:45:08 +00:00
Jamie Magee 9cb8379c2d Update frontend libs 2016-01-06 23:48:51 +01:00
30 changed files with 2317 additions and 189 deletions
+11 -5
View File
@@ -1,3 +1,13 @@
[Tt]est[Rr]esult*
/cache
/logs
.project
.pydevproject
# coverage generated:
/cover-html/
.coverage
.coveralls.yml
# Compiled source #
###################
@@ -61,8 +71,4 @@ Thumbs.db
obj/
[Rr]elease*/
_ReSharper*/
[Tt]est[Rr]esult*
/cache
/logs
.project
.pydevproject
.vscode
+2 -4
View File
@@ -1,5 +1,4 @@
[pep8]
# E111 indentation is not a multiple of four
# E121 continuation line under-indented for hanging indent
# E122 continuation line missing indentation or outdented
# E124 closing bracket does not match visual indentation
@@ -10,8 +9,7 @@
# E261 at least two spaces before inline comment
# E262 inline comment should start with '# '
# E265 block comment should start with '# '
# E302 expected 2 blank lines, found 1
# E501 line too long (312 > 160 characters)
# E502 the backslash is redundant between brackets
ignore = E111,E121,E122,E123,E124,E125,E126,E127,E128,E261,E262,E265,E302,E501,E502
max-line-length = 160
ignore = E121,E122,E123,E124,E125,E126,E127,E128,E261,E262,E265,E501,E502
max-line-length = 160
+23 -6
View File
@@ -2,19 +2,36 @@
# http://about.travis-ci.org/docs/
language: python
sudo: false
cache:
pip: true
directories:
- lib
# Available Python versions:
# http://about.travis-ci.org/docs/user/ci-environment/#Python-VM-images
python:
- "2.6"
- "2.7"
matrix:
include:
- python: "2.7"
env: SENDCOVERAGE=1
# pylint 1.4 does not run under python 2.6
install:
- pip install pyOpenSSL
- pip install pylint==1.3.1
- pip install pyflakes
- pip install pep8
- pip install pyOpenSSL
- pip install pylint==1.3.1
- pip install pyflakes
- pip install pep8
# coverage stuff:
- pip install coveralls
- pip install coverage
script:
- pep8 headphones
- pyflakes headphones
- nosetests headphones
- nosetests
after_success:
# coverage stuff:
- if [ $SENDCOVERAGE ]; then coveralls; fi
+25
View File
@@ -1,5 +1,30 @@
# Changelog
## v0.5.12
Released 25 February 2016
This is mostly a hotfix update
Highlights:
* Added: Experimental Deluge Support
* Fixed: Some pep8 stuff
* Improved: Use curly braces for pathrender optional variables
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.11...v0.5.12).
## v0.5.11
Released 20 February 2016
Highlights:
* Added: Soft chroot option
* Fixed: Post processing temporary directory fix (#2504)
* Fixed: Ubuntu init script (#2509)
* Fixed: Image cache uncaught exception (#2485)
* Improved: $Date/$date variable in folder renaming
* Improved: Reuse transmission session id
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.10...v0.5.11).
## v0.5.10
Released 29 January 2016
+4 -1
View File
@@ -152,7 +152,10 @@ def main():
headphones.DB_FILE = os.path.join(headphones.DATA_DIR, 'headphones.db')
# Read config and start logging
headphones.initialize(config_file)
try:
headphones.initialize(config_file)
except headphones.exceptions.SoftChrootError as e:
raise SystemExit('FATAL ERROR')
if headphones.DAEMON:
headphones.daemonize()
+1 -1
View File
@@ -3,7 +3,7 @@
**Master Branch:** [![Build Status](https://travis-ci.org/rembo10/headphones.svg?branch=master)](https://travis-ci.org/rembo10/headphones)
**Develop Branch:** [![Build Status](https://travis-ci.org/rembo10/headphones.svg?branch=develop)](https://travis-ci.org/rembo10/headphones)
Headphones is an automated music downloader for NZB and Torrent, written in Python. It supports SABnzbd, NZBget, Transmission, µTorrent and Blackhole.
Headphones is an automated music downloader for NZB and Torrent, written in Python. It supports SABnzbd, NZBget, Transmission, µTorrent, Deluge and Blackhole.
## Support & Discuss
You are free to join the Headphones support community on IRC where you can ask questions, hang around and discuss anything related to HP.
+87 -11
View File
@@ -311,6 +311,7 @@
<input type="radio" name="torrent_downloader" id="torrent_downloader_blackhole" value="0" ${config['torrent_downloader_blackhole']}> Black Hole
<input type="radio" name="torrent_downloader" id="torrent_downloader_transmission" value="1" ${config['torrent_downloader_transmission']}> Transmission
<input type="radio" name="torrent_downloader" id="torrent_downloader_utorrent" value="2" ${config['torrent_downloader_utorrent']}> uTorrent (Beta)
<input type="radio" name="torrent_downloader" id="torrent_downloader_deluge" value="3" ${config['torrent_downloader_deluge']}> Deluge (Beta)
</fieldset>
<fieldset id="torrent_blackhole_options">
<div class="row">
@@ -385,6 +386,35 @@
<input type="text" name="utorrent_label" value="${config['utorrent_label']}" size="30">
</div>
</fieldset>
<fieldset id="deluge_options">
<div class="row">
<label>Deluge WebUI Host and Port</label>
<input type="text" name="deluge_host" value="${config['deluge_host']}" size="30">
<small>Usually http://localhost:8112 (requires WebUI plugin)</small>
</div>
<div class="row">
<label>Deluge Password</label>
<input type="password" name="deluge_password" value="${config['deluge_password']}" size="30">
</div>
<div class="row">
<small>Note: With Deluge, you can specify a different download directory for downloads sent from Headphones.
Set it in the Music Download Directory below</small>
</div>
<div class="row">
<label>Deluge Label</label>
<input type="text" name="deluge_label" value="${config['deluge_label']}" size="30">
<small>Labels shouldn't contain spaces (requires Label plugin)</small>
</div>
<div class="row">
<label>Move When Completed</label>
<input type="text" name="deluge_done_directory" value="${config['deluge_done_directory']}" size="30">
<small>Directory where Deluge should move completed downloads</small>
</div>
<div class="row checkbox">
<label>Add Torrent Paused</label>
<input type="checkbox" name="deluge_paused" value="1" ${config['deluge_paused']}>
</div>
</fieldset>
<fieldset id="general_torrent_options">
<div class="row">
<label>Minimum seeders</label>
@@ -858,7 +888,7 @@
<div class="row">
as <input type="text" class="override-float" name="album_art_format" value="${config['album_art_format']}" size="10">.jpg
</div>
<small>Use $Artist/$artist, $Album/$album, $Year/$year</small>
<small>Use $Artist/$artist, $Album/$album, $Year/$year, put optional variables in square brackets, use single-quote marks to escape square brackets literally ('[', ']').</small>
</div>
<div class="row checkbox left clearfix nopad">
<label>
@@ -1095,7 +1125,7 @@
</div>
<div class="row">
<label>Plex Token</label><input type="text" name="plex_token" value="${config['plex_token']}" size="30">
<small>Plex Token (for use with Plex Home)</small>
<small>Plex Token (for use with Plex Home)</small>
</div>
<div class="checkbox row">
<input type="checkbox" name="plex_update" value="1" ${config['plex_update']} /><label>Update Plex Library</label>
@@ -1222,6 +1252,23 @@
</div>
</fieldset>
<fieldset>
<div class="row checkbox left">
<input type="checkbox" class="bigcheck" name="telegram_enabled" id="telegram" value="1" ${config['telegram_enabled']} /><label for="telegram"><span class="option">Telegram</span></label>
</div>
<div id="telegramoptions">
<div class="row">
<label>Bot Token</label><input type="text" name="telegram_token" value="${config['telegram_token']}" size="50"><small>Contact <a href="http://telegram.me/BotFather">@BotFather</a> to create a bot and get its token</small>
</div>
<div class="row">
<label>User ID</label><input type="text" name="telegram_userid" value="${config['telegram_userid']}" size="50"><small>Contact <a href="http://telegram.me/myidbot">@myidbot</a> to get your user ID</small>
</div>
<div class="row checkbox">
<input type="checkbox" name="telegram_onsnatch" value="1" ${config['telegram_onsnatch']} /><label>Notify on snatch?</label>
</div>
</div>
</fieldset>
</td>
</tr>
</table>
@@ -1236,14 +1283,13 @@
<div class="row">
<label>Folder Format</label>
<input type="text" name="folder_format" value="${config['folder_format']}" size="43">
<small>Use: $Artist/$artist, $SortArtist/$sortartist, $Album/$album, $Year/$year, $Type/$type (release type) and $First/$first (first letter in artist name), $OriginalFolder/$originalfolder (downloaded directory name)
E.g.: $Type/$First/$artist/$album [$year] = Album/G/girl talk/all day [2010]</small>
<small>Use: $Artist/$artist, $SortArtist/$sortartist, $Album/$album, $Year/$year, $Type/$type (release type) and $First/$first (first letter in artist name), $OriginalFolder/$originalfolder (downloaded directory name). Put optional variables in square brackets, use single-quote marks to escape square brackets literally ('[', ']').<br>E.g.: $Type/$First/$artist/$album '['$year']' = Album/G/girl talk/all day [2010]</small>
</div>
<div class="row">
<label>File Format</label>
<input type="text" name="file_format" value="${config['file_format']}" size="43">
<small>Use: $Disc/$disc (disc #), $Track/$track (track #), $Title/$title, $Artist/$artist, $Album/$album and $Year/$year</small>
<small>Use: $Disc/$disc (disc #), $Track/$track (track #), $Title/$title, $Artist/$artist, $Album/$album and $Year/$year. Put optional variables in square brackets, use single-quote marks to escape square brackets literally ('[', ']').</small>
</div>
<div class="checkbox row clearfix">
<input type="checkbox" name="file_underscores" id="file_underscores" value="1" ${config['file_underscores']}/><label>Use underscores instead of spaces</label>
@@ -2003,6 +2049,26 @@
}
});
if ($("#telegram").is(":checked"))
{
$("#telegramoptions").show();
}
else
{
$("#telegramoptions").hide();
}
$("#telegram").click(function(){
if ($("#telegram").is(":checked"))
{
$("#telegramoptions").slideDown();
}
else
{
$("#telegramoptions").slideUp();
}
});
if ($("#osx_notify").is(":checked"))
{
$("#osx_notify_options").show();
@@ -2159,19 +2225,25 @@
if ($("#torrent_downloader_blackhole").is(":checked"))
{
$("#transmission_options,#utorrent_options").hide();
$("#transmission_options,#utorrent_options,#deluge_options").hide();
$("#torrent_blackhole_options").show();
}
if ($("#torrent_downloader_transmission").is(":checked"))
{
$("#torrent_blackhole_options,#utorrent_options").hide();
$("#torrent_blackhole_options,#utorrent_options,#deluge_options").hide();
$("#transmission_options").show();
}
if ($("#torrent_downloader_utorrent").is(":checked"))
{
$("#torrent_blackhole_options,#transmission_options").hide();
$("#torrent_blackhole_options,#transmission_options,#deluge_options").hide();
$("#utorrent_options").show();
}
if ($("#torrent_downloader_deluge").is(":checked"))
{
$("#torrent_blackhole_options,#transmission_options,#utorrent_options").hide();
$("#deluge_options").show();
}
$('input[type=radio]').change(function(){
if ($("#preferred_bitrate").is(":checked"))
@@ -2208,15 +2280,19 @@
}
if ($("#torrent_downloader_blackhole").is(":checked"))
{
$("#transmission_options,#utorrent_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() });
$("#transmission_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() });
}
if ($("#torrent_downloader_transmission").is(":checked"))
{
$("#torrent_blackhole_options,#utorrent_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
$("#torrent_blackhole_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
}
if ($("#torrent_downloader_utorrent").is(":checked"))
{
$("#torrent_blackhole_options,#transmission_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
$("#torrent_blackhole_options,#transmission_options,#deluge_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
}
if ($("#torrent_downloader_deluge").is(":checked"))
{
$("#torrent_blackhole_options,#utorrent_options,#transmission_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() });
}
});
+11
View File
@@ -229,6 +229,17 @@ textarea,
button {
font: 99%;
}
select {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
background: #4F4F4F;
border: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
color: #fff;
padding: 3px 10px;
text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.25);
}
textarea {
overflow: auto;
}
+17 -1
View File
@@ -124,7 +124,23 @@ table {
// Forms
select, input, textarea, button { font: 99%;}
select, input, textarea, button
{
font: 99%;
}
select
{
.rounded(5px);
background: #4F4F4F;
border: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
color: #fff;
padding: 3px 10px;
text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.25);
}
textarea {overflow: auto;}
input { .rounded(3px);}
input:invalid, textarea:invalid {
+12 -1
View File
@@ -29,7 +29,8 @@ from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
from headphones import versioncheck, logger
import headphones.config
from headphones.softchroot import SoftChroot
import headphones.exceptions
# (append new extras to the end)
POSSIBLE_EXTRAS = [
@@ -74,6 +75,7 @@ started = False
DATA_DIR = None
CONFIG = None
SOFT_CHROOT = None
DB_FILE = None
@@ -97,6 +99,7 @@ def initialize(config_file):
with INIT_LOCK:
global CONFIG
global SOFT_CHROOT
global _INITIALIZED
global CURRENT_VERSION
global LATEST_VERSION
@@ -136,6 +139,14 @@ def initialize(config_file):
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR,
verbose=VERBOSE)
try:
SOFT_CHROOT = SoftChroot(str(CONFIG.SOFT_CHROOT))
if SOFT_CHROOT.isEnabled():
logger.info("Soft-chroot enabled for dir: %s", str(CONFIG.SOFT_CHROOT))
except exceptions.SoftChrootError as e:
logger.error("SoftChroot error: %s", e)
raise e
if not CONFIG.CACHE_DIR:
# Put the cache dir in the data dir for now
CONFIG.CACHE_DIR = os.path.join(DATA_DIR, 'cache')
+14
View File
@@ -0,0 +1,14 @@
#import unittest
#import mock
from headphones.unittestcompat import TestCase
import headphones.albumart
# no tests...
class AlbumArtTest(TestCase):
def test_nothing(self):
x = 100 - 2 * 50
if x:
headphones.albumart.getAlbumArt('asdf')
self.assertTrue(True)
+1 -1
View File
@@ -116,7 +116,7 @@ class Cache(object):
return None
for image in images:
if image['size'] == 'medium':
if image['size'] == 'medium' and '#text' in image:
thumb_url = image['#text']
break
+41 -17
View File
@@ -16,6 +16,20 @@ def bool_int(value):
return int(bool(value))
class path(str):
"""Internal 'marker' type for paths in config."""
@staticmethod
def __call__(val):
return path(val)
def __new__(cls, *args, **kw):
hstr = str.__new__(cls, *args, **kw)
return hstr
def __repr__(self):
return 'headphones.config.path(%s)' % self
_CONFIG_DEFINITIONS = {
'ADD_ALBUM_ART': (int, 'General', 0),
'ADVANCEDENCODER': (str, 'General', ''),
@@ -31,11 +45,11 @@ _CONFIG_DEFINITIONS = {
'AUTO_ADD_ARTISTS': (int, 'General', 1),
'BITRATE': (int, 'General', 192),
'BLACKHOLE': (int, 'General', 0),
'BLACKHOLE_DIR': (str, 'General', ''),
'BLACKHOLE_DIR': (path, 'General', ''),
'BOXCAR_ENABLED': (int, 'Boxcar', 0),
'BOXCAR_ONSNATCH': (int, 'Boxcar', 0),
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
'CACHE_DIR': (str, 'General', ''),
'CACHE_DIR': (path, 'General', ''),
'CACHE_SIZEMB': (int, 'Advanced', 32),
'CHECK_GITHUB': (int, 'General', 1),
'CHECK_GITHUB_INTERVAL': (int, 'General', 360),
@@ -44,8 +58,8 @@ _CONFIG_DEFINITIONS = {
'CONFIG_VERSION': (str, 'General', '0'),
'CORRECT_METADATA': (int, 'General', 0),
'CUE_SPLIT': (int, 'General', 1),
'CUE_SPLIT_FLAC_PATH': (str, 'General', ''),
'CUE_SPLIT_SHNTOOL_PATH': (str, 'General', ''),
'CUE_SPLIT_FLAC_PATH': (path, 'General', ''),
'CUE_SPLIT_SHNTOOL_PATH': (path, 'General', ''),
'CUSTOMAUTH': (int, 'General', 0),
'CUSTOMHOST': (str, 'General', 'localhost'),
'CUSTOMPASS': (str, 'General', ''),
@@ -53,12 +67,17 @@ _CONFIG_DEFINITIONS = {
'CUSTOMSLEEP': (int, 'General', 1),
'CUSTOMUSER': (str, 'General', ''),
'DELETE_LOSSLESS_FILES': (int, 'General', 1),
'DELUGE_HOST': (str, 'Deluge', ''),
'DELUGE_PASSWORD': (str, 'Deluge', ''),
'DELUGE_LABEL': (str, 'Deluge', ''),
'DELUGE_DONE_DIRECTORY': (str, 'Deluge', ''),
'DELUGE_PAUSED': (int, 'Deluge', 0),
'DESTINATION_DIR': (str, 'General', ''),
'DETECT_BITRATE': (int, 'General', 0),
'DO_NOT_PROCESS_UNMATCHED': (int, 'General', 0),
'DOWNLOAD_DIR': (str, 'General', ''),
'DOWNLOAD_DIR': (path, 'General', ''),
'DOWNLOAD_SCAN_INTERVAL': (int, 'General', 5),
'DOWNLOAD_TORRENT_DIR': (str, 'General', ''),
'DOWNLOAD_TORRENT_DIR': (path, 'General', ''),
'DO_NOT_OVERRIDE_GIT_BRANCH': (int, 'General', 0),
'EMAIL_ENABLED': (int, 'Email', 0),
'EMAIL_FROM': (str, 'Email', ''),
@@ -74,14 +93,14 @@ _CONFIG_DEFINITIONS = {
'EMBED_LYRICS': (int, 'General', 0),
'ENABLE_HTTPS': (int, 'General', 0),
'ENCODER': (str, 'General', 'ffmpeg'),
'ENCODERFOLDER': (str, 'General', ''),
'ENCODERFOLDER': (path, 'General', ''),
'ENCODERLOSSLESS': (int, 'General', 1),
'ENCODEROUTPUTFORMAT': (str, 'General', 'mp3'),
'ENCODERQUALITY': (int, 'General', 2),
'ENCODERVBRCBR': (str, 'General', 'cbr'),
'ENCODER_MULTICORE': (int, 'General', 0),
'ENCODER_MULTICORE_COUNT': (int, 'General', 0),
'ENCODER_PATH': (str, 'General', ''),
'ENCODER_PATH': (path, 'General', ''),
'EXTRAS': (str, 'General', ''),
'EXTRA_NEWZNABS': (list, 'Newznab', ''),
'EXTRA_TORZNABS': (list, 'Torznab', ''),
@@ -94,7 +113,7 @@ _CONFIG_DEFINITIONS = {
'FOLDER_PERMISSIONS': (str, 'General', '0755'),
'FREEZE_DB': (int, 'General', 0),
'GIT_BRANCH': (str, 'General', 'master'),
'GIT_PATH': (str, 'General', ''),
'GIT_PATH': (path, 'General', ''),
'GIT_USER': (str, 'General', 'rembo10'),
'GROWL_ENABLED': (int, 'Growl', 0),
'GROWL_HOST': (str, 'Growl', ''),
@@ -103,8 +122,8 @@ _CONFIG_DEFINITIONS = {
'HEADPHONES_INDEXER': (bool_int, 'General', False),
'HPPASS': (str, 'General', ''),
'HPUSER': (str, 'General', ''),
'HTTPS_CERT': (str, 'General', ''),
'HTTPS_KEY': (str, 'General', ''),
'HTTPS_CERT': (path, 'General', ''),
'HTTPS_KEY': (path, 'General', ''),
'HTTP_HOST': (str, 'General', 'localhost'),
'HTTP_PASSWORD': (str, 'General', ''),
'HTTP_PORT': (int, 'General', 8181),
@@ -114,8 +133,8 @@ _CONFIG_DEFINITIONS = {
'IDTAG': (int, 'Beets', 0),
'IGNORE_CLEAN_RELEASES': (int, 'General', 0),
'IGNORED_WORDS': (str, 'General', ''),
'IGNORED_FOLDERS': (list, 'Advanced', []),
'IGNORED_FILES': (list, 'Advanced', []),
'IGNORED_FOLDERS': (list, 'Advanced', []), # path
'IGNORED_FILES': (list, 'Advanced', []), # path
'INCLUDE_EXTRAS': (int, 'General', 0),
'INTERFACE': (str, 'General', 'default'),
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
@@ -130,17 +149,17 @@ _CONFIG_DEFINITIONS = {
'LIBRARYSCAN_INTERVAL': (int, 'General', 300),
'LMS_ENABLED': (int, 'LMS', 0),
'LMS_HOST': (str, 'LMS', ''),
'LOG_DIR': (str, 'General', ''),
'LOG_DIR': (path, 'General', ''),
'LOSSLESS_BITRATE_FROM': (int, 'General', 0),
'LOSSLESS_BITRATE_TO': (int, 'General', 0),
'LOSSLESS_DESTINATION_DIR': (str, 'General', ''),
'LOSSLESS_DESTINATION_DIR': (path, 'General', ''),
'MB_IGNORE_AGE': (int, 'General', 365),
'MININOVA': (int, 'Mininova', 0),
'MININOVA_RATIO': (str, 'Mininova', ''),
'MIRROR': (str, 'General', 'musicbrainz.org'),
'MOVE_FILES': (int, 'General', 0),
'MPC_ENABLED': (bool_int, 'MPC', False),
'MUSIC_DIR': (str, 'General', ''),
'MUSIC_DIR': (path, 'General', ''),
'MUSIC_ENCODER': (int, 'General', 0),
'NEWZNAB': (int, 'Newznab', 0),
'NEWZNAB_APIKEY': (str, 'Newznab', ''),
@@ -223,6 +242,7 @@ _CONFIG_DEFINITIONS = {
'SAB_USERNAME': (str, 'SABnzbd', ''),
'SAMPLINGFREQUENCY': (int, 'General', 44100),
'SEARCH_INTERVAL': (int, 'General', 1440),
'SOFT_CHROOT': (path, 'General', ''),
'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'),
'SONGKICK_ENABLED': (int, 'Songkick', 1),
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
@@ -234,6 +254,10 @@ _CONFIG_DEFINITIONS = {
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
'SUBSONIC_USERNAME': (str, 'Subsonic', ''),
'SYNOINDEX_ENABLED': (int, 'Synoindex', 0),
'TELEGRAM_TOKEN': (str, 'Telegram', ''),
'TELEGRAM_USERID': (str, 'Telegram', ''),
'TELEGRAM_ENABLED': (int, 'Telegram', 0),
'TELEGRAM_ONSNATCH': (int, 'Telegram', 0),
'TORRENTBLACKHOLE_DIR': (str, 'General', ''),
'TORRENT_DOWNLOADER': (int, 'General', 0),
'TORRENT_REMOVAL_INTERVAL': (int, 'General', 720),
@@ -295,7 +319,7 @@ class Config(object):
definition = _CONFIG_DEFINITIONS[key]
if len(definition) == 3:
definition_type, section, default = definition
else:
elif len(definition) == 4:
definition_type, section, _, default = definition
return key, definition_type, section, ini_key, default
+440
View File
@@ -0,0 +1,440 @@
import mock
from mock import MagicMock
import headphones.config
import re
import unittestcompat
from unittestcompat import TestCase, TestArgs
class ConfigApiTest(TestCase):
""" Common tests for headphones.Config
Common tests for headphones.Config This test suite guarantees, that external
API of the Config class conforms all expectations of other modules.
"""
def _setUpConfigMock(self, mock, sections):
# every constructor `xx = ConfigObj()` in headphones.config will return
# this mock:
self.config_mock = self.config_module_mock.return_value = mock
if sections:
mock.__contains__.side_effect = sections.__contains__
mock.__getitem__.side_effect = sections.__getitem__
mock.__setitem__.side_effect = sections.__setitem__
mock.items.side_effect = sections.items
return mock
def setUp(self):
# patch for low-level ConfigObj for entire test class
# result - each test_* method will get one additional
# argument during testing
self.config_module_mock_patcher = mock.patch('headphones.config.ConfigObj', name='ConfigObjModuleMock')
self.config_module_mock = self.config_module_mock_patcher.start()
existing_sections = {'General': {}, 'Email': {}}
# every constructor `xx = ConfigObj()` in headphones.config will return
# this mock:
self._setUpConfigMock(MagicMock(), existing_sections)
def tearDown(self):
self.config_module_mock_patcher.stop()
def test_constructor(self):
""" Config : creating """
cf = headphones.config.Config('/tmp/notexist')
self.assertIsInstance(cf, headphones.config.Config)
@TestArgs(
# this sections are explicitly added in the test body:
('General', False),
('Email', False),
# this sections will not be created nor in the test, either in the
# Config module
('some_new_section_never_defined', True),
('another_new_section_never_defined', True),
)
def test_check_section(self, section_name, expected_return):
""" Config : check_section """
path = '/tmp/notexist'
# call methods
c = headphones.config.Config(path)
res = c.check_section(section_name)
res2 = c.check_section(section_name)
# assertions:
self.assertEqual(res, expected_return)
self.assertFalse(res2)
@TestArgs(
('api_enabled', 0, int),
('Api_Key', '', str),
)
def test_check_setting(self, setting_name, expected_return, expected_instance):
""" Config: check_setting , basic cases """
path = '/tmp/notexist'
# call methods
c = headphones.config.Config(path)
res = c.check_setting(setting_name)
res2 = c.check_setting(setting_name)
# assertions:
self.assertIsInstance(res, expected_instance)
self.assertEqual(res, expected_return)
self.assertEqual(res, res2)
@TestArgs(
(''),
('This_IsNew_Name'),
)
def test_check_setting_raise_on_unknown_settings(self, setting_name):
""" Config: check_setting should raise on unknown """
path = '/tmp/notexist'
exc_regex = re.compile(setting_name, re.IGNORECASE)
# call methods
c = headphones.config.Config(path)
# assertions:
with self.assertRaisesRegexp(KeyError, exc_regex):
c.check_setting(setting_name)
pass
@TestArgs(
(None)
)
def test_check_setting_raise_on_none(self, setting_name):
""" Config: check_setting shoud raise on None name """
path = '/tmp/notexist'
# call methods
c = headphones.config.Config(path)
# assertions:
with self.assertRaises(AttributeError):
c.check_setting(setting_name)
pass
def test_write(self):
""" Config : write """
path = '/tmp/notexist'
# overload mocks, defined in setUp:
old_conf_mock = self._setUpConfigMock(MagicMock(), {'a': {}})
option_name_not_from_definitions = 'some_invalid_option_with_super_uniq1_name'
option_name_not_from_definitions_value = 1
old_conf_mock['asdf'] = {option_name_not_from_definitions: option_name_not_from_definitions_value}
# call methods
cf = headphones.config.Config(path)
# overload mock-patching for NEW CONFIG
new_patcher = mock.patch('headphones.config.ConfigObj', name='NEW_ConfigObjModuleMock_FOR_WRITE')
new_conf_module_mock = new_patcher.start()
new_conf_mock = \
new_conf_module_mock.return_value = \
MagicMock()
cf.write()
new_patcher.stop()
# assertions:
self.assertFalse(old_conf_mock.write.called, 'write not called for old config')
self.assertTrue(new_conf_mock.write.called, 'write called for new config')
self.assertEqual(new_conf_mock.filename, path)
new_conf_mock['General'].__setitem__.assert_any_call('download_dir', '')
# from 3.5... new_conf_mock['asdf'].__setitem__.assert_not_called('download_dir', '')
new_conf_mock['asdf'].__setitem__.assert_any_call(option_name_not_from_definitions, option_name_not_from_definitions_value)
@unittestcompat.skip("process_kwargs should be removed")
def test_process_kwargs(self):
self.assertTrue(True)
# ===========================================================
# GET ATTR
# ===========================================================
@TestArgs(
('ADD_ALBUM_ART', True),
('ALBUM_ART_FORMAT', 'shmolder'),
('API_ENABLED', 1),
('API_KEY', 'Hello'),
)
def test__getattr__ConfValues(self, name, value):
""" Config: __getattr__ with setting value explicit """
path = '/tmp/notexist'
self.config_mock["General"] = {name.lower(): value}
# call methods
c = headphones.config.Config(path)
act = c.__getattr__(name)
# assertions:
self.assertEqual(act, value)
@TestArgs(
('ADD_ALBUM_ART', 0),
('ALBUM_ART_FORMAT', 'folder'),
('API_ENABLED', 0),
('API_KEY', ''),
)
def test__getattr__ConfValuesDefault(self, name, value):
""" Config: __getattr__ from config(by braces), default values """
path = '/tmp/notexist'
# call methods
c = headphones.config.Config(path)
res = c.__getattr__(name)
# assertions:
self.assertEqual(res, value)
def test__getattr__ConfValuesDefaultUsingDotNotation(self):
""" Config: __getattr__ from config (by dot), default values """
path = '/tmp/notexist'
# call methods
c = headphones.config.Config(path)
# assertions:
self.assertEqual(c.ALBUM_ART_FORMAT, 'folder')
self.assertEqual(c.API_ENABLED, 0)
self.assertEqual(c.API_KEY, '')
def test__getattr__OwnAttributes(self):
""" Config: __getattr__ access own attrs """
path = '/tmp/notexist'
# call methods
c = headphones.config.Config(path)
# assertions:
self.assertIsNotNone(c)
self.assertIn('<headphones.config.Config', c.__str__())
# ===========================================================
# SET ATTR
# ===========================================================
@TestArgs(
('ADD_ALBUM_ART', True),
('ALBUM_ART_FORMAT', 'shmolder'),
('API_ENABLED', 1),
('API_KEY', 'Hello'),
)
def test__setattr__ConfValuesDefault(self, name, value):
""" Config: __setattr__ with setting value explicit """
path = '/tmp/notexist'
# call methods
c = headphones.config.Config(path)
act = c.__setattr__(name, value)
# assertions:
self.assertEqual(self.config_mock["General"][name.lower()], value)
self.assertEqual(act, value)
def test__setattr__ExplicitSetUsingDotNotation(self):
""" Config: __setattr__ with setting values using dot notation """
path = '/tmp/notexist'
# call methods
c = headphones.config.Config(path)
act1 = c.ALBUM_ART_FORMAT = 'Apple'
act2 = c.API_ENABLED = True
act3 = c.API_KEY = 123
# assertions:
self.assertEqual(self.config_mock["General"]['album_art_format'], 'Apple')
self.assertEqual(self.config_mock["General"]['api_enabled'], 1)
self.assertEqual(self.config_mock["General"]['api_key'], '123')
self.assertEqual(act1, 'Apple')
self.assertEqual(act2, 1)
# TODO : check this trange behaviour. I have expected to see here '123', not 123.
self.assertEqual(act3, 123)
# ===========================================================
# NEWZNABS
#
@TestArgs(
('', []),
('ABCDEF', [('A', 'B', 'C'), ('D', 'E', 'F')]),
(['ABC', 'DEF'], []),
([1], []),
([1, 2], []),
([1, 2, 3], [(1, 2, 3)]),
([1, 2, 3, 'Aaa'], [(1, 2, 3)]),
([1, 2, 3, 'Aaa', 'Bbba'], [(1, 2, 3)]),
([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc', 'Ddddda'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
)
def test_get_extra_newznabs(self, conf_value, expected):
""" Config: get_extra_newznabs """
path = '/tmp/notexist'
#itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
# set up mocks:
# 'EXTRA_NEWZNABS': (list, 'Newznab', ''),
# 'EXTRA_TORZNABS': (list, 'Torznab', ''),
self.config_mock["Newznab"] = {"extra_newznabs": conf_value}
# call methods
c = headphones.config.Config(path)
res = c.get_extra_newznabs()
# assertions:
self.assertEqual(res, expected)
def test_clear_extra_newznabs(self):
""" Config: clear_extra_newznabs """
path = '/tmp/notexist'
random_value = 1827746
self.config_mock["Newznab"] = {"extra_newznabs": [1, 2, 3]}
self.config_mock["Newznab"] = {"do_not_touch": random_value}
# call methods
c = headphones.config.Config(path)
res = c.clear_extra_newznabs()
# assertions:
self.assertIsNone(res)
self.assertEqual(self.config_mock["Newznab"]["extra_newznabs"], [])
self.assertEqual(self.config_mock["Newznab"]["do_not_touch"], random_value)
@TestArgs(
([], [''], ['']),
([], 'ABCDEF', ['A', 'B', 'C', 'D', 'E', 'F']),
([1, 2, [False, True]], ['3', [0, 0]], [1, 2, [False, True], '3', [0, 0]]),
)
def test_add_extra_newznab(self, initial, added, expected):
""" Config: add_extra_newznab """
path = '/tmp/notexist'
self.config_mock["Newznab"] = {"extra_newznabs": initial}
# call methods
c = headphones.config.Config(path)
c.add_extra_newznab(added)
act = self.config_mock["Newznab"]["extra_newznabs"]
# assertions:
self.assertEqual(act, expected)
@TestArgs(
(None),
([]),
([1, 2, 3]),
([True]),
)
def test_add_extra_newznab_raise_on_none(self, initial):
""" Config: add_extra_newznab should raise on None adding"""
path = '/tmp/notexist'
self.config_mock["Newznab"] = {"extra_newznabs": initial}
# call methods
c = headphones.config.Config(path)
with self.assertRaises(TypeError):
c.add_extra_newznab(None)
pass
# ===========================================================
# TORZNABS
# TODO : here is copypaste from of NEZNABS tests. Make tests better, plz refactor them
#
@TestArgs(
('', []),
('ABCDEF', [('A', 'B', 'C'), ('D', 'E', 'F')]),
(['ABC', 'DEF'], []),
([1], []),
([1, 2], []),
([1, 2, 3], [(1, 2, 3)]),
([1, 2, 3, 'Aaa'], [(1, 2, 3)]),
([1, 2, 3, 'Aaa', 'Bbba'], [(1, 2, 3)]),
([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc', 'Ddddda'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
)
def test_get_extra_torznabs(self, conf_value, expected):
""" Config: get_extra_torznabs """
path = '/tmp/notexist'
#itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
# set up mocks:
# 'EXTRA_TORZNABS': (list, '', ''),
self.config_mock["Torznab"] = {"extra_torznabs": conf_value}
# call methods
c = headphones.config.Config(path)
res = c.get_extra_torznabs()
# assertions:
self.assertEqual(res, expected)
def test_clear_extra_torznabs(self):
""" Config: clear_extra_torznabs """
path = '/tmp/notexist'
random_value = -1292721
self.config_mock["Torznab"] = {"extra_torznabs": [1, 2, 3]}
self.config_mock["Torznab"] = {"do_not_touch": random_value}
# call methods
c = headphones.config.Config(path)
res = c.clear_extra_torznabs()
# assertions:
self.assertIsNone(res)
self.assertEqual(self.config_mock["Torznab"]["extra_torznabs"], [])
self.assertEqual(self.config_mock["Torznab"]["do_not_touch"], random_value)
@TestArgs(
([], [''], ['']),
([], 'ABCDEF', ['A', 'B', 'C', 'D', 'E', 'F']),
([1, 2, [False, True]], ['3', [0, 0]], [1, 2, [False, True], '3', [0, 0]]),
)
def test_add_extra_torznab(self, initial, added, expected):
""" Config: add_extra_torznab """
path = '/tmp/notexist'
self.config_mock["Torznab"] = {"extra_torznabs": initial}
# call methods
c = headphones.config.Config(path)
c.add_extra_torznab(added)
act = self.config_mock["Torznab"]["extra_torznabs"]
# assertions:
self.assertEqual(act, expected)
@TestArgs(
(None),
([]),
([1, 2, 3]),
([True]),
)
def test_add_extra_torznab_raise_on_none(self, initial):
""" Config: add_extra_torznab should raise on None adding"""
path = '/tmp/notexist'
self.config_mock["Torznab"] = {"extra_torznabs": initial}
# call methods
c = headphones.config.Config(path)
with self.assertRaises(TypeError):
c.add_extra_torznab(None)
pass
+460
View File
@@ -0,0 +1,460 @@
# This file is part of Headphones.
#
# Headphones is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Headphones is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
# Parts of this file are a part of SickRage.
# Author: Mr_Orange <mr_orange@hotmail.it>
# URL: http://code.google.com/p/sickbeard/
# Adapted for Headphones by <noamgit@gmail.com>
# URL: https://github.com/noam09
#
# SickRage is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SickRage is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SickRage. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from headphones import logger
#from headphones import request
import time
import re
import os
import json
import headphones
import requests
from base64 import b64encode
import traceback
delugeweb_auth = {}
delugeweb_url = ''
def addTorrent(link, data=None):
try:
result = {}
retid = False
if link.startswith('magnet:'):
logger.debug('Deluge: Got a magnet link: %s' % link)
result = {'type': 'magnet',
'url': link}
retid = _add_torrent_magnet(result)
elif link.startswith('http://') or link.startswith('https://'):
logger.debug('Deluge: Got a URL: %s' % link)
user_agent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
headers = {'User-Agent': user_agent}
torrentfile = ''
logger.debug('Deluge: Trying to download (GET)')
try:
r = requests.get(link, headers=headers)
if r.status_code == 200:
logger.debug('Deluge: 200 OK')
torrentfile = r.text
#for chunk in r.iter_content(chunk_size=1024):
# if chunk: # filter out keep-alive new chunks
# torrentfile = torrentfile + chunk
else:
logger.debug('Deluge: Trying to GET %s returned status %d' % (link, r.status_code))
return False
except Exception as e:
logger.debug('Deluge: Download failed: %s' % str(e))
if 'announce' not in torrentfile[:40]:
logger.debug('Deluge: Contents of %s doesn\'t look like a torrent file' % link)
return False
# Extract torrent name from .torrent
try:
logger.debug('Deluge: Getting torrent name length')
name_length = int(re.findall('name([0-9]*)\:.*?\:', torrentfile)[0])
logger.debug('Deluge: Getting torrent name')
name = re.findall('name[0-9]*\:(.*?)\:', torrentfile)[0][:name_length]
except Exception as e:
logger.debug('Deluge: Could not get torrent name, getting file name')
# get last part of link/path (name only)
name = link.split('\\')[-1].split('/')[-1]
# remove '.torrent' suffix
if name[-len('.torrent'):] == '.torrent':
name = name[:-len('.torrent')]
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, torrentfile[:40]))
result = {'type': 'torrent',
'name': name,
'content': torrentfile}
retid = _add_torrent_file(result)
# elif link.endswith('.torrent') or data:
elif not (link.startswith('http://') or link.startswith('https://')):
if data:
logger.debug('Deluge: Getting .torrent data')
torrentfile = data
else:
logger.debug('Deluge: Getting .torrent file')
with open(link, 'rb') as f:
torrentfile = f.read()
# Extract torrent name from .torrent
try:
logger.debug('Deluge: Getting torrent name length')
name_length = int(re.findall('name([0-9]*)\:.*?\:', torrentfile)[0])
logger.debug('Deluge: Getting torrent name')
name = re.findall('name[0-9]*\:(.*?)\:', torrentfile)[0][:name_length]
except Exception as e:
logger.debug('Deluge: Could not get torrent name, getting file name')
# get last part of link/path (name only)
name = link.split('\\')[-1].split('/')[-1]
# remove '.torrent' suffix
if name[-len('.torrent'):] == '.torrent':
name = name[:-len('.torrent')]
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, torrentfile[:40]))
result = {'type': 'torrent',
'name': name,
'content': torrentfile}
retid = _add_torrent_file(result)
else:
logger.error('Deluge: Unknown file type: %s' % link)
if retid:
logger.info('Deluge: Torrent sent to Deluge successfully (%s)' % retid)
return retid
else:
logger.info('Deluge returned status %s' % retid)
return False
except Exception as e:
logger.error(str(e))
formatted_lines = traceback.format_exc().splitlines()
logger.error('; '.join(formatted_lines))
def getTorrentFolder(result):
logger.debug('Deluge: Get torrent folder name')
if not any(delugeweb_auth):
_get_auth()
try:
post_data = json.dumps({"method": "web.get_torrent_status",
"params": [
result['hash'],
["total_done"]
],
"id": 22})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
result['total_done'] = json.loads(response.text)['result']['total_done']
tries = 0
while result['total_done'] == 0 and tries < 10:
tries += 1
time.sleep(5)
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
result['total_done'] = json.loads(response.text)['result']['total_done']
post_data = json.dumps({"method": "web.get_torrent_status",
"params": [
result['hash'],
[
"name",
"save_path",
"total_size",
"num_files",
"message",
"tracker",
"comment"
]
],
"id": 23})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
result['save_path'] = json.loads(response.text)['result']['save_path']
result['name'] = json.loads(response.text)['result']['name']
return json.loads(response.text)['result']['name']
except Exception as e:
logger.debug('Deluge: Could not get torrent folder name: %s' % str(e))
def removeTorrent(torrentid, remove_data=False):
if not any(delugeweb_auth):
_get_auth()
result = False
post_data = json.dumps({"method": "core.remove_torrent",
"params": [
torrentid,
remove_data
],
"id": 25})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
result = json.loads(response.text)['result']
return result
def _get_auth():
logger.debug('Deluge: Authenticating...')
global delugeweb_auth, delugeweb_url
delugeweb_auth = {}
delugeweb_host = headphones.CONFIG.DELUGE_HOST
delugeweb_password = headphones.CONFIG.DELUGE_PASSWORD
if not delugeweb_host.startswith('http'):
delugeweb_host = 'http://%s' % delugeweb_host
if delugeweb_host.endswith('/'):
delugeweb_host = delugeweb_host[:-1]
delugeweb_url = delugeweb_host + '/json'
post_data = json.dumps({"method": "auth.login",
"params": [delugeweb_password],
"id": 1})
try:
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
# , verify=TORRENT_VERIFY_CERT)
except Exception:
return None
auth = json.loads(response.text)["result"]
delugeweb_auth = response.cookies
post_data = json.dumps({"method": "web.connected",
"params": [],
"id": 10})
try:
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
# , verify=TORRENT_VERIFY_CERT)
except Exception:
return None
connected = json.loads(response.text)['result']
if not connected:
post_data = json.dumps({"method": "web.get_hosts",
"params": [],
"id": 11})
try:
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
# , verify=TORRENT_VERIFY_CERT)
except Exception:
return None
delugeweb_hosts = json.loads(response.text)['result']
if len(delugeweb_hosts) == 0:
logger.error('Deluge: WebUI does not contain daemons')
return None
post_data = json.dumps({"method": "web.connect",
"params": [delugeweb_hosts[0][0]],
"id": 11})
try:
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
# , verify=TORRENT_VERIFY_CERT)
except Exception:
return None
post_data = json.dumps({"method": "web.connected",
"params": [],
"id": 10})
try:
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
# , verify=TORRENT_VERIFY_CERT)
except Exception:
return None
connected = json.loads(response.text)['result']
if not connected:
logger.error('Deluge: WebUI could not connect to daemon')
return None
return auth
def _add_torrent_magnet(result):
logger.debug('Deluge: Adding magnet')
if not any(delugeweb_auth):
_get_auth()
try:
post_data = json.dumps({"method": "core.add_torrent_magnet",
"params": [result['url'], {}],
"id": 2})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
result['hash'] = json.loads(response.text)['result']
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)['result']))
return json.loads(response.text)['result']
except Exception as e:
logger.error('Deluge: Adding torrent magnet failed: %s' % str(e))
'''
def _add_torrent_url(result):
logger.debug('Deluge: Adding URL')
if not any(delugeweb_auth):
_get_auth()
try:
post_data = json.dumps({"method": "web.download_torrent_from_url",
"params": [result['url'], {}],
"id": 2})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
result['hash'] = json.loads(response.text)['result']
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)['result']))
return json.loads(response.text)['result']
except Exception as e:
logger.error('Deluge: Adding torrent URL failed: %s' % str(e))
'''
def _add_torrent_file(result):
logger.debug('Deluge: Adding file')
if not any(delugeweb_auth):
_get_auth()
try:
# content is torrent file contents that needs to be encoded to base64
post_data = json.dumps({"method": "core.add_torrent_file",
"params": [result['name'] + '.torrent', b64encode(result['content'].encode('utf8')), {}],
"id": 2})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
result['hash'] = json.loads(response.text)['result']
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)['result']))
return json.loads(response.text)['result']
except Exception as e:
logger.error('Deluge: Adding torrent file failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines()
logger.error('; '.join(formatted_lines))
def setTorrentLabel(result):
logger.debug('Deluge: Setting label')
label = headphones.CONFIG.DELUGE_LABEL
if not any(delugeweb_auth):
_get_auth()
if ' ' in label:
logger.error('Deluge: Invalid label. Label can\'t contain spaces - replacing with underscores')
label = label.replace(' ', '_')
if label:
# check if label already exists and create it if not
post_data = json.dumps({"method": 'label.get_labels',
"params": [],
"id": 3})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
labels = json.loads(response.text)['result']
if labels is not None:
if label not in labels:
try:
logger.debug('Deluge: %s label doesn\'t exist in Deluge, let\'s add it' % label)
post_data = json.dumps({"method": 'label.add',
"params": [label],
"id": 4})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
logger.debug('Deluge: %s label added to Deluge' % label)
except Exception as e:
logger.error('Deluge: Setting label failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines()
logger.error('; '.join(formatted_lines))
# add label to torrent
post_data = json.dumps({"method": 'label.set_torrent',
"params": [result['hash'], label],
"id": 5})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
logger.debug('Deluge: %s label added to torrent' % label)
else:
logger.debug('Deluge: Label plugin not detected')
return False
return not json.loads(response.text)['error']
def setSeedRatio(result):
logger.debug('Deluge: Setting seed ratio')
if not any(delugeweb_auth):
_get_auth()
ratio = None
if result['ratio']:
ratio = result['ratio']
if ratio:
post_data = json.dumps({"method": "core.set_torrent_stop_at_ratio",
"params": [result['hash'], True],
"id": 5})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
post_data = json.dumps({"method": "core.set_torrent_stop_ratio",
"params": [result['hash'], float(ratio)],
"id": 6})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
return not json.loads(response.text)['error']
return True
def setTorrentPath(result):
logger.debug('Deluge: Setting download path')
if not any(delugeweb_auth):
_get_auth()
if headphones.CONFIG.DELUGE_DONE_DIRECTORY or headphones.CONFIG.DOWNLOAD_TORRENT_DIR:
post_data = json.dumps({"method": "core.set_torrent_move_completed",
"params": [result['hash'], True],
"id": 7})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY
else:
move_to = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
if not os.path.exists(move_to):
logger.debug('Deluge: %s directory doesn\'t exist, let\'s create it' % move_to)
os.makedirs(move_to)
post_data = json.dumps({"method": "core.set_torrent_move_completed_path",
"params": [result['hash'], move_to],
"id": 8})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
return not json.loads(response.text)['error']
return True
def setTorrentPause(result):
logger.debug('Deluge: Pausing torrent')
if not any(delugeweb_auth):
_get_auth()
if headphones.CONFIG.DELUGE_PAUSED:
post_data = json.dumps({"method": "core.pause_torrent",
"params": [[result['hash']]],
"id": 9})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
return not json.loads(response.text)['error']
return True
+7
View File
@@ -24,3 +24,10 @@ class NewzbinAPIThrottled(HeadphonesException):
"""
Newzbin has throttled us, deal with it
"""
class SoftChrootError(HeadphonesException):
"""
Fatal errors in SoftChroot module
"""
pass
+7 -4
View File
@@ -191,11 +191,13 @@ def piratesize(size):
def replace_all(text, dic, normalize=False):
from headphones import pathrender
if not text:
return ''
for i, j in dic.iteritems():
if normalize:
if normalize:
new_dic = {}
for i, j in dic.iteritems():
try:
if sys.platform == 'darwin':
j = unicodedata.normalize('NFD', j)
@@ -203,8 +205,9 @@ def replace_all(text, dic, normalize=False):
j = unicodedata.normalize('NFC', j)
except TypeError:
j = unicodedata.normalize('NFC', j.decode(headphones.SYS_ENCODING, 'replace'))
text = text.replace(i, j)
return text
new_dic[i] = j
dic = new_dic
return pathrender.render(text, dic)[0]
def replace_illegal_chars(string, type="file"):
+33
View File
@@ -856,3 +856,36 @@ class Email(object):
except Exception, e:
logger.warn('Error sending Email: %s' % e)
return False
class TELEGRAM(object):
def notify(self, message, status):
if not headphones.CONFIG.TELEGRAM_ENABLED:
return
import requests
TELEGRAM_API = "https://api.telegram.org/bot%s/%s"
# Get configuration data
token = headphones.CONFIG.TELEGRAM_TOKEN
userid = headphones.CONFIG.TELEGRAM_USERID
# Construct message
payload = {'chat_id': userid, 'text': status + ': ' + message}
# Send message to user using Telegram's Bot API
try:
response = requests.post(TELEGRAM_API % (token, "sendMessage"), data=payload)
except Exception, e:
logger.info(u'Telegram notify failed: ' + str(e))
# Error logging
sent_successfuly = True
if not response.status_code == 200:
logger.info(u'Could not send notification to TelegramBot (token=%s). Response: [%s]', (token, response.text))
sent_successfuly = False
logger.info(u"Telegram notifications sent.")
return sent_successfuly
+237
View File
@@ -0,0 +1,237 @@
# encoding=utf8
# This file is part of Headphones.
#
# Headphones is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Headphones is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
'''Path pattern substitution module, see details below for syntax.
The pattern matching is loosely based on foobar2000 pattern syntax,
i.e. the notion of escaping characters with \' and optional elements
enclosed in square brackets [] is taken from there while the
substitution variable names are Perl-ish or sh-ish. The following
syntax elements are supported:
* escaped literal strings, that is everything that is enclosed
within single quotes (like \'this\');
* substitution variables, which start with dollar sign ($) and
extend until next non-alphanumeric+underscore character
(like $This and $5_that).
* optional elements enclosed in curly braces, which render
nonempty value only if any variable or optional inside returned
nonempty value, ignoring literals (like {\'[\'$That\']\'}).
'''
from __future__ import print_function
from enum import Enum
__author__ = "Andrzej Ciarkowski <andrzej.ciarkowski@gmail.com>"
class _PatternElement(object):
'''ABC for hierarchy of path name renderer pattern elements.'''
def render(self, replacement):
# type: (Mapping[str,str]) -> str
'''Format this _PatternElement into string using provided substitution dictionary.'''
raise NotImplementedError()
class _Generator(_PatternElement):
# pylint: disable=abstract-method
'''Tagging interface for "content-generating" elements like replacement or optional block.'''
pass
class _Replacement(_Generator):
'''Replacement variable, eg. $title.'''
def __init__(self, pattern):
# type: (str)
self._pattern = pattern
def render(self, replacement):
# type: (Mapping[str,str]) -> str
return replacement.get(self._pattern, self._pattern)
def __str__(self):
return self._pattern
class _LiteralText(_PatternElement):
'''Just a plain piece of text to be rendered "as is".'''
def __init__(self, text):
# type: (str)
self._text = text
def render(self, replacement):
# type: (Mapping[str,str]) -> str
return self._text
def __str__(self):
return self._text
class _OptionalBlock(_Generator):
'''Optional block will render its contents only if any _Generator in its scope did return non-empty result.'''
def __init__(self, scope):
# type: ([_PatternElement])
self._scope = scope
def render(self, replacement):
# type: (Mapping[str,str]) -> str
res = [(isinstance(x, _Generator), x.render(replacement)) for x in self._scope]
if any((t[0] and len(t[1]) != 0) for t in res):
return u"".join(t[1] for t in res)
else:
return u""
_OPTIONAL_START = u'{'
_OPTIONAL_END = u'}'
_ESCAPE_CHAR = u'\''
_REPLACEMENT_START = u'$'
def _is_replacement_valid(c):
# type: (str) -> bool
return c.isalnum() or c == u'_'
class _State(Enum):
LITERAL = 0
ESCAPE = 1
REPLACEMENT = 2
def _append_literal(scope, text):
# type: ([_PatternElement], str) -> None
'''Append literal text to the scope BUT ONLY if it's not an empty string.'''
if len(text) == 0:
return
scope.append(_LiteralText(text))
class Warnings(Enum):
'''Pattern parsing warnings, as stored withing warnings property of Pattern object after parsing.'''
UNCLOSED_ESCAPE = 'Warnings.UNCLOSED_ESCAPE'
UNCLOSED_OPTIONAL = 'Warnings.UNCLOSED_OPTIONAL'
def _parse_pattern(pattern, warnings):
# type: (str,MutableSet[Warnings]) -> [_PatternElement]
'''Parse path pattern text into list of _PatternElements, put warnings into the provided set.'''
start = 0 # index of current state start char
root_scope = [] # here our _PatternElements will reside
scope_stack = [root_scope] # stack so that we can return to the outer scope
scope = root_scope # pointer to the current list for _OptionalBlock
inside_optional = 0 # nesting level of _OptionalBlocks
state = _State.LITERAL # current state
for i, c in enumerate(pattern):
if state is _State.ESCAPE:
if c != _ESCAPE_CHAR:
# only escape char can get us out of _State.ESCAPE
continue
_append_literal(scope, pattern[start + 1:i])
state = _State.LITERAL
start = i + 1
# after exiting _State.ESCAPE on escape char no more processing of c
continue
if state is _State.REPLACEMENT:
if _is_replacement_valid(c):
# only replacement invalid can get us out _State.REPLACEMENT
continue
scope.append(_Replacement(pattern[start:i]))
state = _State.LITERAL
start = i
# intentional fall-through to _State.LITERAL
assert state is _State.LITERAL
if c == _ESCAPE_CHAR:
_append_literal(scope, pattern[start:i])
state = _State.ESCAPE
start = i
# no more processing to escape char c
continue
if c == _REPLACEMENT_START:
_append_literal(scope, pattern[start:i])
state = _State.REPLACEMENT
start = i
# no more processing to replacement char c
continue
if c == _OPTIONAL_START:
_append_literal(scope, pattern[start:i])
inside_optional += 1
new_scope = []
scope_stack.append(new_scope)
scope = new_scope
start = i + 1
continue
if c == _OPTIONAL_END:
if inside_optional == 0:
# no optional block to end, just treat as literal text
continue
inside_optional -= 1
_append_literal(scope, pattern[start:i])
scope_stack.pop()
prev_scope = scope_stack[-1]
prev_scope.append(_OptionalBlock(scope))
scope = prev_scope
start = i + 1
# fi
# done
if state is _State.ESCAPE:
warnings.add(Warnings.UNCLOSED_ESCAPE)
if inside_optional != 0:
warnings.add(Warnings.UNCLOSED_OPTIONAL)
if state is _State.REPLACEMENT:
root_scope.append(_Replacement(pattern[start:]))
else:
# don't care about unclosed elements :P
_append_literal(root_scope, pattern[start:])
return root_scope
class Pattern(object):
'''Stores preparsed rename pattern for repeated use.
If using the same pattern repeatedly it is much more effective
to parse the pattern into Pattern object and use it instead of
parsing the textual pattern on each substitution. To use Pattern
object for substitution simply call it as it was function
providing dictionary as an argument (see __call__()).'''
def __init__(self, pattern):
# type: (str)
self._warnings = set()
self._pattern = _parse_pattern(pattern, self._warnings)
def __call__(self, replacement):
# type: (Mapping[str,str]) -> str
'''Execute path rendering/substitution based on replacement dictionary.'''
return u"".join(p.render(replacement) for p in self._pattern)
def _get_warnings(self):
# type: () -> str
'''Getter for warnings property.'''
return self._warnings
warnings = property(_get_warnings, doc="Access warnings raised during pattern parsing")
def render(pattern, replacement):
# type: (str, Mapping[str,str]) -> (str, AbstractSet[Warnings])
'''Render path name based on replacement pattern and dictionary.'''
p = Pattern(pattern)
return p(replacement), p.warnings
if __name__ == "__main__":
# primitive test ;)
p = Pattern(u"[$Disc.]$Track - $Artist - $Title[ '['$Year']'")
d = {'$Disc': '', '$Track': '05', '$Artist': u'Grzegżółka', '$Title': u'Błona kapłona', '$Year': '2019'}
print(p(d).encode('utf8'), p.warnings)
+29 -12
View File
@@ -27,7 +27,7 @@ from beets import autotag
from beets import config as beetsconfig
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
from beetsplug import lyrics as beetslyrics
from headphones import notifiers, utorrent, transmission
from headphones import notifiers, utorrent, transmission, deluge
from headphones import db, albumart, librarysync
from headphones import logger, helpers, request, mb, music_encoder
@@ -46,7 +46,10 @@ def checkFolder():
if album['Kind'] == 'nzb':
download_dir = headphones.CONFIG.DOWNLOAD_DIR
else:
download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
download_dir = headphones.CONFIG.DELUGE_DONE_DIRECTORY
else:
download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
album_path = os.path.join(download_dir, album['FolderName']).encode(
headphones.SYS_ENCODING, 'replace')
@@ -314,12 +317,13 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
new_folder = None
# Check to see if we're preserving the torrent dir
if (headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent" and 'headphones-modified' not in albumpath) or headphones.CONFIG.KEEP_ORIGINAL_FOLDER or keep_original_folder:
new_folder = os.path.join(tempfile.mkdtemp(prefix="headphones_"), "headphones")
logger.info("Copying files to " + new_folder.decode(headphones.SYS_ENCODING, 'replace') + " subfolder to preserve downloaded files for seeding")
new_folder = tempfile.mkdtemp(prefix="headphones_")
subdir = os.path.join(new_folder, "headphones")
logger.info("Copying files to " + subdir.decode(headphones.SYS_ENCODING, 'replace') + " subfolder to preserve downloaded files for seeding")
try:
shutil.copytree(albumpath, new_folder)
shutil.copytree(albumpath, subdir)
# Update the album path with the new location
albumpath = new_folder
albumpath = subdir
except Exception as e:
logger.warn("Cannot copy/move files to temp folder: " + new_folder.decode(headphones.SYS_ENCODING, 'replace') + ". Not continuing. Error: " + str(e))
shutil.rmtree(new_folder)
@@ -453,6 +457,8 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
release['ArtistName'], release['AlbumTitle']))
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
torrent_removed = transmission.removeTorrent(hash, True)
elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
torrent_removed = deluge.removeTorrent(hash, True)
else:
torrent_removed = utorrent.removeTorrent(hash, True)
@@ -532,6 +538,11 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
pushbullet = notifiers.PUSHBULLET()
pushbullet.notify(pushmessage, statusmessage)
if headphones.CONFIG.TELEGRAM_ENABLED:
logger.info(u"Telegram request")
telegram = notifiers.TELEGRAM()
telegram.notify(pushmessage, statusmessage)
if headphones.CONFIG.TWITTER_ENABLED:
logger.info(u"Sending Twitter notification")
twitter = notifiers.TwitterNotifier()
@@ -665,10 +676,10 @@ def renameNFO(albumpath):
def moveFiles(albumpath, release, tracks):
logger.info("Moving files: %s" % albumpath)
try:
year = release['ReleaseDate'][:4]
date = release['ReleaseDate']
except TypeError:
year = u''
date = u''
year = date[:4]
artist = release['ArtistName'].replace('/', '_')
album = release['AlbumTitle'].replace('/', '_')
if headphones.CONFIG.FILE_UNDERSCORES:
@@ -698,6 +709,7 @@ def moveFiles(albumpath, release, tracks):
'$SortArtist': sortname,
'$Album': album,
'$Year': year,
'$Date': date,
'$Type': releasetype,
'$OriginalFolder': origfolder,
'$First': firstchar.upper(),
@@ -705,6 +717,7 @@ def moveFiles(albumpath, release, tracks):
'$sortartist': sortname.lower(),
'$album': album.lower(),
'$year': year,
'$date': date,
'$type': releasetype.lower(),
'$first': firstchar.lower(),
'$originalfolder': origfolder.lower()
@@ -1056,9 +1069,11 @@ def embedLyrics(downloaded_track_list):
def renameFiles(albumpath, downloaded_track_list, release):
logger.info('Renaming files')
try:
year = release['ReleaseDate'][:4]
date = release['ReleaseDate']
except TypeError:
year = ''
date = u''
year = date[:4]
# Until tagging works better I'm going to rely on the already provided metadata
for downloaded_track in downloaded_track_list:
@@ -1107,13 +1122,15 @@ def renameFiles(albumpath, downloaded_track_list, release):
'$SortArtist': sortname,
'$Album': release['AlbumTitle'],
'$Year': year,
'$Date': date,
'$disc': discnumber,
'$track': tracknumber,
'$title': title.lower(),
'$artist': artistname.lower(),
'$sortartist': sortname.lower(),
'$album': release['AlbumTitle'].lower(),
'$year': year
'$year': year,
'$date': date
}
ext = os.path.splitext(downloaded_track)[1]
+48 -2
View File
@@ -33,7 +33,7 @@ from pygazelle import format as gazelleformat
import headphones
from headphones.common import USER_AGENT
from headphones import logger, db, helpers, classes, sab, nzbget, request
from headphones import utorrent, transmission, notifiers, rutracker
from headphones import utorrent, transmission, notifiers, rutracker, deluge
from bencode import bencode, bdecode
@@ -851,7 +851,7 @@ def send_to_downloader(data, bestqual, album):
else:
logger.error("Cannot save magnet link in blackhole. " \
"Please switch your torrent downloader to " \
"Transmission or uTorrent, or allow Headphones " \
"Transmission, uTorrent or Deluge, or allow Headphones " \
"to open or convert magnet links")
return
else:
@@ -889,6 +889,48 @@ def send_to_downloader(data, bestqual, album):
if seed_ratio is not None:
transmission.setSeedRatio(torrentid, seed_ratio)
elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
logger.info("Sending torrent to Deluge")
try:
# Add torrent
if bestqual[3] == 'rutracker.org':
torrentid = deluge.addTorrent('', data)
else:
torrentid = deluge.addTorrent(bestqual[2])
if not torrentid:
logger.error("Error sending torrent to Deluge. Are you sure it's running? Maybe the torrent already exists?")
return
# This pauses the torrent right after it is added
if headphones.CONFIG.DELUGE_PAUSED:
deluge.setTorrentPause({'hash': torrentid})
# Set Label
if headphones.CONFIG.DELUGE_LABEL:
deluge.setTorrentLabel({'hash': torrentid})
# Set Seed Ratio
seed_ratio = get_seed_ratio(bestqual[3])
if seed_ratio is not None:
deluge.setSeedRatio({'hash': torrentid, 'ratio': seed_ratio})
# Set move-to directory
if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
deluge.setTorrentPath({'hash': torrentid})
# I only just realized this function is useless...
folder_name = deluge.getTorrentFolder({'hash': torrentid})
if folder_name:
logger.info('Torrent folder name: %s' % folder_name)
else:
logger.error('Torrent folder name could not be determined')
return
except Exception as e:
logger.error('Error sending torrent to Deluge: %s' % str(e))
else: # if headphones.CONFIG.TORRENT_DOWNLOADER == 2:
logger.info("Sending torrent to uTorrent")
@@ -960,6 +1002,10 @@ def send_to_downloader(data, bestqual, album):
logger.info(u"Sending PushBullet notification")
pushbullet = notifiers.PUSHBULLET()
pushbullet.notify(name, "Download started")
if headphones.CONFIG.TELEGRAM_ENABLED and headphones.CONFIG.TELEGRAM_ONSNATCH:
logger.info(u"Sending Telegram notification")
telegram = notifiers.TELEGRAM()
telegram.notify(name, "Download started")
if headphones.CONFIG.TWITTER_ENABLED and headphones.CONFIG.TWITTER_ONSNATCH:
logger.info(u"Sending Twitter notification")
twitter = notifiers.TwitterNotifier()
+71
View File
@@ -0,0 +1,71 @@
import os
from headphones.exceptions import SoftChrootError
class SoftChroot(object):
""" SoftChroot provides SOFT chrooting for UI
IMPORTANT: call methods of this class just in modules, which generates data for client UI. Try to avoid unnecessary usage.
"""
enabled = False
chroot = None
def __init__(self, path):
if not path:
#disabled
return
path = path.strip()
if not path:
return
if (not os.path.exists(path) or
not os.path.isdir(path)):
raise SoftChrootError('No such directory: %s' % path)
path = path.rstrip(os.path.sep) + os.path.sep
self.enabled = True
self.chroot = path
def isEnabled(self):
return self.enabled
def getRoot(self):
return self.chroot
def apply(self, path):
if not self.enabled:
return path
if not path:
return path
p = path.strip()
if not p:
return path
if path.startswith(self.chroot):
p = os.path.sep + path[len(self.chroot):]
else:
p = os.path.sep
return p
def revoke(self, path):
if not self.enabled:
return path
if not path:
return path
p = path.strip()
if not p:
return path
if os.path.sep == p[0]:
p = p[1:]
p = self.chroot + p
return p
+122
View File
@@ -0,0 +1,122 @@
import os
import mock
from headphones.unittestcompat import TestCase, TestArgs
#from mock import MagicMock
from headphones.softchroot import SoftChroot
from headphones.exceptions import SoftChrootError
class SoftChrootTest(TestCase):
def test_create(self):
""" create headphones.SoftChroot """
cf = SoftChroot('/tmp/')
self.assertIsInstance(cf, SoftChroot)
self.assertTrue(cf.isEnabled())
self.assertEqual(cf.getRoot(), '/tmp/')
@TestArgs(
(None),
(''),
(' '),
)
def test_create_disabled(self, empty_path):
""" create DISABLED SoftChroot """
cf = SoftChroot(empty_path)
self.assertIsInstance(cf, SoftChroot)
self.assertFalse(cf.isEnabled())
self.assertIsNone(cf.getRoot())
def test_create_on_not_exists_dir(self):
""" create SoftChroot on non existent dir """
path = os.path.join('/tmp', 'notexist', 'asdf', '11', '12', 'np', 'itsssss')
cf = None
with self.assertRaises(SoftChrootError) as exc:
cf = SoftChroot(path)
self.assertIsNone(cf)
self.assertRegexpMatches(str(exc.exception), r'No such directory')
self.assertRegexpMatches(str(exc.exception), path)
@mock.patch('headphones.softchroot.os', wrap=os, name='OsMock')
def test_create_on_file(self, os_mock):
""" create SoftChroot on file, not a directory """
path = os.path.join('/tmp', 'notexist', 'asdf', '11', '12', 'np', 'itsssss')
os_mock.path.sep = os.path.sep
os_mock.path.isdir.side_effect = lambda x: x != path
cf = None
with self.assertRaises(SoftChrootError) as exc:
cf = SoftChroot(path)
self.assertIsNone(cf)
self.assertTrue(os_mock.path.isdir.called)
self.assertRegexpMatches(str(exc.exception), r'No such directory')
self.assertRegexpMatches(str(exc.exception), path)
@TestArgs(
(None, None),
('', ''),
(' ', ' '),
('/tmp/', '/'),
('/tmp/asdf', '/asdf'),
)
def test_apply(self, p, e):
""" apply SoftChroot """
sc = SoftChroot('/tmp/')
a = sc.apply(p)
self.assertEqual(a, e)
@TestArgs(
('/'),
('/nonch/path/asdf'),
('tmp/asdf'),
)
def test_apply_out_of_root(self, p):
""" apply SoftChroot to paths outside of the chroot """
sc = SoftChroot('/tmp/')
a = sc.apply(p)
self.assertEqual(a, '/')
@TestArgs(
(None, None),
('', ''),
(' ', ' '),
('/', '/tmp/'),
('/asdf', '/tmp/asdf'),
('/asdf/', '/tmp/asdf/'),
('localdir/adf', '/tmp/localdir/adf'),
('localdir/adf/', '/tmp/localdir/adf/'),
)
def test_revoke(self, p, e):
""" revoke SoftChroot """
sc = SoftChroot('/tmp/')
a = sc.revoke(p)
self.assertEqual(a, e)
@TestArgs(
(None),
(''),
(' '),
('/tmp'),
('/tmp/'),
('/tmp/asdf'),
('/tmp/localdir/adf'),
('localdir/adf'),
('localdir/adf/'),
)
def test_actions_on_disabled(self, p):
""" disabled SoftChroot should not change args on apply and revoke """
sc = SoftChroot(None)
a = sc.apply(p)
self.assertEqual(a, p)
r = sc.revoke(p)
self.assertEqual(r, p)
+33 -40
View File
@@ -25,8 +25,9 @@ import headphones
# This is just a simple script to send torrents to transmission. The
# intention is to turn this into a class where we can check the state
# of the download, set the download dir, etc.
# TODO: Store the session id so we don't need to make 2 calls
# Store torrent id so we can check up on it
# TODO: Store torrent id so we can check up on it
_session_id = None
def addTorrent(link, data=None):
@@ -127,6 +128,7 @@ def removeTorrent(torrentid, remove_data=False):
def torrentAction(method, arguments):
global _session_id
host = headphones.CONFIG.TRANSMISSION_HOST
username = headphones.CONFIG.TRANSMISSION_USERNAME
password = headphones.CONFIG.TRANSMISSION_PASSWORD
@@ -148,43 +150,34 @@ def torrentAction(method, arguments):
parts[2] += "/transmission/rpc"
host = urlparse.urlunparse(parts)
# Retrieve session id
auth = (username, password) if username and password else None
response = request.request_response(host, auth=auth,
whitelist_status_code=[401, 409])
if response is None:
logger.error("Error gettings Transmission session ID")
return
# Parse response
if response.status_code == 401:
if auth:
logger.error("Username and/or password not accepted by " \
"Transmission")
else:
logger.error("Transmission authorization required")
return
elif response.status_code == 409:
session_id = response.headers['x-transmission-session-id']
if not session_id:
logger.error("Expected a Session ID from Transmission")
return
# Prepare next request
headers = {'x-transmission-session-id': session_id}
data = {'method': method, 'arguments': arguments}
data_json = json.dumps(data)
auth = (username, password) if username and password else None
for retry in range(2):
if _session_id is not None:
headers = {'x-transmission-session-id': _session_id}
response = request.request_response(host, method="POST",
data=data_json, headers=headers, auth=auth,
whitelist_status_code=[200, 401, 409])
else:
response = request.request_response(host, auth=auth,
whitelist_status_code=[401, 409])
if response.status_code == 401:
if auth:
logger.error("Username and/or password not accepted by " \
"Transmission")
else:
logger.error("Transmission authorization required")
return
elif response.status_code == 409:
_session_id = response.headers['x-transmission-session-id']
if _session_id is None:
logger.error("Expected a Session ID from Transmission, got None")
return
# retry request with new session id
logger.debug("Retrying Transmission request with new session id")
continue
response = request.request_json(host, method="POST", data=json.dumps(data),
headers=headers, auth=auth)
print response
if not response:
logger.error("Error sending torrent to Transmission")
return
return response
resp_json = response.json()
print resp_json
return resp_json
+115
View File
@@ -0,0 +1,115 @@
import sys
if sys.version_info < (2, 7):
import unittest2 as unittest
from unittest2 import TestCase as TC
else:
import unittest
from unittest import TestCase as TC
skip = unittest.skip
_dummy = False
# less than 2.6 ...
if sys.version_info[0] == 2 and sys.version_info[1] <= 6:
_dummy = True
def _d(f):
def decorate(self, *args, **kw):
if not _dummy:
return f(self, *args, **kw)
return self.assertTrue(True)
return decorate
class TestCase(TC):
"""
Wrapper for python 2.6 stubs
"""
def assertIsInstance(self, obj, cls, msg=None):
if not _dummy:
return super(TestCase, self).assertIsInstance(obj, cls, msg)
tst = isinstance(obj, cls)
return self.assertTrue(tst, msg)
@_d
def assertNotIsInstance(self, *args, **kw):
return super(TestCase, self).assertNotIsInstance(*args, **kw)
@_d
def assertIn(self, *args, **kw):
return super(TestCase, self).assertIn(*args, **kw)
@_d
def assertRegexpMatches(self, *args, **kw):
return super(TestCase, self).assertRegexpMatches(*args, **kw)
# -----------------------------------------------------------
# NOT DUMMY ASSERTIONS
# -----------------------------------------------------------
def assertIsNone(self, val, msg=None):
if not _dummy:
return super(TestCase, self).assertIsNone(val, msg)
tst = val is None
return super(TestCase, self).assertTrue(tst, msg)
def assertIsNotNone(self, val, msg=None):
if not _dummy:
return super(TestCase, self).assertIsNotNone(val, msg)
tst = val is not None
return super(TestCase, self).assertTrue(tst, msg)
def assertRaises(self, exc, msg=None):
if not _dummy:
return super(TestCase, self).assertRaises(exc, msg)
return TestCase._TestCaseRaiseStub(self, exc, msg=msg)
def assertRaisesRegexp(self, exc, regex, msg=None):
if not _dummy:
return super(TestCase, self).assertRaises(exc, msg)
return TestCase._TestCaseRaiseStub(self, exc, regex=regex, msg=msg)
class _TestCaseRaiseStub:
""" Internal stuff for stubbing `assertRaises*` """
def __init__(self, test_case, exc, regex=None, msg=None):
self.exc = exc
self.test_case = test_case
self.regex = regex
self.msg = msg
def __enter__(self):
return self
def __exit__(self, tp, value, traceback):
tst = tp is self.exc
self.test_case.assertTrue(tst, msg=self.msg)
self.exception = value
# TODO: implement self.regex checking
# True indicates, that exception is handled
return True
def TestArgs(*parameters):
def tuplify(x):
if not isinstance(x, tuple):
return (x,)
return x
def decorator(method, parameters=parameters):
for parameter in (tuplify(x) for x in parameters):
def method_for_parameter(self, method=method, parameter=parameter):
method(self, *parameter)
args_for_parameter = ",".join(repr(v) for v in parameter)
name_for_parameter = method.__name__ + "(" + args_for_parameter + ")"
frame = sys._getframe(1) # pylint: disable-msg=W0212
frame.f_locals[name_for_parameter] = method_for_parameter
frame.f_locals[name_for_parameter].__doc__ = method.__doc__ + '(' + args_for_parameter + ')'
method_for_parameter.__name__ = name_for_parameter + '(' + args_for_parameter + ')'
return None
return decorator
+38 -2
View File
@@ -343,7 +343,7 @@ class WebInterface(object):
for dir in dirs:
artistfolder = os.path.join(dir, folder)
if not os.path.isdir(artistfolder):
if not os.path.isdir(artistfolder.encode(headphones.SYS_ENCODING)):
logger.debug("Cannot find directory: " + artistfolder)
continue
threading.Thread(target=librarysync.libraryScan,
@@ -1156,6 +1156,11 @@ class WebInterface(object):
"transmission_host": headphones.CONFIG.TRANSMISSION_HOST,
"transmission_username": headphones.CONFIG.TRANSMISSION_USERNAME,
"transmission_password": headphones.CONFIG.TRANSMISSION_PASSWORD,
"deluge_host": headphones.CONFIG.DELUGE_HOST,
"deluge_password": headphones.CONFIG.DELUGE_PASSWORD,
"deluge_label": headphones.CONFIG.DELUGE_LABEL,
"deluge_done_directory": headphones.CONFIG.DELUGE_DONE_DIRECTORY,
"deluge_paused": checked(headphones.CONFIG.DELUGE_PAUSED),
"utorrent_host": headphones.CONFIG.UTORRENT_HOST,
"utorrent_username": headphones.CONFIG.UTORRENT_USERNAME,
"utorrent_password": headphones.CONFIG.UTORRENT_PASSWORD,
@@ -1166,6 +1171,7 @@ class WebInterface(object):
"torrent_downloader_blackhole": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 0),
"torrent_downloader_transmission": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 1),
"torrent_downloader_utorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 2),
"torrent_downloader_deluge": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 3),
"download_dir": headphones.CONFIG.DOWNLOAD_DIR,
"use_blackhole": checked(headphones.CONFIG.BLACKHOLE),
"blackhole_dir": headphones.CONFIG.BLACKHOLE_DIR,
@@ -1324,6 +1330,10 @@ class WebInterface(object):
"pushbullet_onsnatch": checked(headphones.CONFIG.PUSHBULLET_ONSNATCH),
"pushbullet_apikey": headphones.CONFIG.PUSHBULLET_APIKEY,
"pushbullet_deviceid": headphones.CONFIG.PUSHBULLET_DEVICEID,
"telegram_enabled": checked(headphones.CONFIG.TELEGRAM_ENABLED),
"telegram_onsnatch": checked(headphones.CONFIG.TELEGRAM_ONSNATCH),
"telegram_token": headphones.CONFIG.TELEGRAM_TOKEN,
"telegram_userid": headphones.CONFIG.TELEGRAM_USERID,
"subsonic_enabled": checked(headphones.CONFIG.SUBSONIC_ENABLED),
"subsonic_host": headphones.CONFIG.SUBSONIC_HOST,
"subsonic_username": headphones.CONFIG.SUBSONIC_USERNAME,
@@ -1367,8 +1377,16 @@ class WebInterface(object):
"idtag": checked(headphones.CONFIG.IDTAG)
}
for k, v in config.iteritems():
if isinstance(v, headphones.config.path):
# need to apply SoftChroot to paths:
nv = headphones.SOFT_CHROOT.apply(v)
if v != nv:
config[k] = headphones.config.path(nv)
# Need to convert EXTRAS to a dictionary we can pass to the config:
# it'll come in as a string like 2,5,6,8
extra_munges = {
"dj-mix": "dj_mix",
"mixtape/street": "mixtape_street"
@@ -1420,10 +1438,11 @@ class WebInterface(object):
"synoindex_enabled", "pushover_enabled",
"pushover_onsnatch", "pushbullet_enabled", "pushbullet_onsnatch", "subsonic_enabled",
"twitter_enabled", "twitter_onsnatch",
"telegram_enabled", "telegram_onsnatch",
"osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch",
"songkick_enabled", "songkick_filter_enabled",
"mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch",
"customauth", "idtag"
"customauth", "idtag", "deluge_paused"
]
for checked_config in checked_configs:
if checked_config not in kwargs:
@@ -1435,6 +1454,17 @@ class WebInterface(object):
kwargs[plain_config] = kwargs[use_config]
del kwargs[use_config]
for k, v in kwargs.iteritems():
# TODO : HUGE crutch. It is all because there is no way to deal with options...
_conf = headphones.CONFIG._define(k)
conftype = _conf[1]
#print '===>', conftype
if conftype is headphones.config.path:
nv = headphones.SOFT_CHROOT.revoke(v)
if nv != v:
kwargs[k] = nv
# Check if encoderoutputformat is set multiple times
if len(kwargs['encoderoutputformat'][-1]) > 1:
kwargs['encoderoutputformat'] = kwargs['encoderoutputformat'][-1]
@@ -1673,6 +1703,12 @@ class WebInterface(object):
pushbullet = notifiers.PUSHBULLET()
pushbullet.notify("it works!", "Test message")
@cherrypy.expose
def testTelegram(self):
logger.info("Testing Telegram notifications")
telegram = notifiers.TELEGRAM()
telegram.notify("it works!", "lazers pew pew")
class Artwork(object):
@cherrypy.expose
+1 -1
View File
@@ -164,7 +164,7 @@ start_headphones () {
handle_updates
if ! is_running; then
log_daemon_msg "Starting $DESC"
start-stop-daemon -o -d "$APP_PATH" -c "$RUN_AS" --start "$EXTRA_SSD"_OPTS --pidfile "$PID_FILE" --exec "$DAEMON" -- "$DAEMON_OPTS"
start-stop-daemon -o -d $APP_PATH -c $RUN_AS --start $EXTRA_SSD_OPTS --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS
check_retval
else
log_success_msg "$DESC: already running (pid $PID)"
+302 -80
View File
@@ -1,6 +1,6 @@
"""Utilities for writing code that runs on Python 2 and 3"""
# Copyright (c) 2010-2014 Benjamin Peterson
# Copyright (c) 2010-2015 Benjamin Peterson
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -20,17 +20,22 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import absolute_import
import functools
import itertools
import operator
import sys
import types
__author__ = "Benjamin Peterson <benjamin@python.org>"
__version__ = "1.6.1"
__version__ = "1.10.0"
# Useful for very coarse version differentiation.
PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3
PY34 = sys.version_info[0:2] >= (3, 4)
if PY3:
string_types = str,
@@ -53,6 +58,7 @@ else:
else:
# It's possible to have sizeof(long) != sizeof(Py_ssize_t).
class X(object):
def __len__(self):
return 1 << 31
try:
@@ -83,14 +89,14 @@ class _LazyDescr(object):
self.name = name
def __get__(self, obj, tp):
result = self._resolve()
setattr(obj, self.name, result) # Invokes __set__.
try:
result = self._resolve()
except ImportError:
# See the nice big comment in MovedModule.__getattr__.
raise AttributeError("%s could not be imported " % self.name)
setattr(obj, self.name, result) # Invokes __set__.
# This is a bit ugly, but it avoids running this again.
delattr(obj.__class__, self.name)
# This is a bit ugly, but it avoids running this again by
# removing this descriptor.
delattr(obj.__class__, self.name)
except AttributeError:
pass
return result
@@ -109,22 +115,7 @@ class MovedModule(_LazyDescr):
return _import_module(self.mod)
def __getattr__(self, attr):
# It turns out many Python frameworks like to traverse sys.modules and
# try to load various attributes. This causes problems if this is a
# platform-specific module on the wrong platform, like _winreg on
# Unixes. Therefore, we silently pretend unimportable modules do not
# have any attributes. See issues #51, #53, #56, and #63 for the full
# tales of woe.
#
# First, if possible, avoid loading the module just to look at __file__,
# __name__, or __path__.
if (attr in ("__file__", "__name__", "__path__") and
self.mod not in sys.modules):
raise AttributeError(attr)
try:
_module = self._resolve()
except ImportError:
raise AttributeError(attr)
_module = self._resolve()
value = getattr(_module, attr)
setattr(self, attr, value)
return value
@@ -170,9 +161,75 @@ class MovedAttribute(_LazyDescr):
return getattr(module, self.attr)
class _SixMetaPathImporter(object):
"""
A meta path importer to import six.moves and its submodules.
This class implements a PEP302 finder and loader. It should be compatible
with Python 2.5 and all existing versions of Python3
"""
def __init__(self, six_module_name):
self.name = six_module_name
self.known_modules = {}
def _add_module(self, mod, *fullnames):
for fullname in fullnames:
self.known_modules[self.name + "." + fullname] = mod
def _get_module(self, fullname):
return self.known_modules[self.name + "." + fullname]
def find_module(self, fullname, path=None):
if fullname in self.known_modules:
return self
return None
def __get_module(self, fullname):
try:
return self.known_modules[fullname]
except KeyError:
raise ImportError("This loader does not know module " + fullname)
def load_module(self, fullname):
try:
# in case of a reload
return sys.modules[fullname]
except KeyError:
pass
mod = self.__get_module(fullname)
if isinstance(mod, MovedModule):
mod = mod._resolve()
else:
mod.__loader__ = self
sys.modules[fullname] = mod
return mod
def is_package(self, fullname):
"""
Return true, if the named module is a package.
We need this method to get correct spec objects with
Python 3.4 (see PEP451)
"""
return hasattr(self.__get_module(fullname), "__path__")
def get_code(self, fullname):
"""Return None
Required, if is_package is implemented"""
self.__get_module(fullname) # eventually raises ImportError
return None
get_source = get_code # same as get_code
_importer = _SixMetaPathImporter(__name__)
class _MovedItems(_LazyModule):
"""Lazy loading of moved objects"""
__path__ = [] # mark as package
_moved_attributes = [
@@ -180,26 +237,33 @@ _moved_attributes = [
MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
MovedAttribute("intern", "__builtin__", "sys"),
MovedAttribute("map", "itertools", "builtins", "imap", "map"),
MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"),
MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"),
MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"),
MovedAttribute("reduce", "__builtin__", "functools"),
MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
MovedAttribute("StringIO", "StringIO", "io"),
MovedAttribute("UserDict", "UserDict", "collections"),
MovedAttribute("UserList", "UserList", "collections"),
MovedAttribute("UserString", "UserString", "collections"),
MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
MovedModule("builtins", "__builtin__"),
MovedModule("configparser", "ConfigParser"),
MovedModule("copyreg", "copy_reg"),
MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"),
MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
MovedModule("http_cookies", "Cookie", "http.cookies"),
MovedModule("html_entities", "htmlentitydefs", "html.entities"),
MovedModule("html_parser", "HTMLParser", "html.parser"),
MovedModule("http_client", "httplib", "http.client"),
MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
@@ -233,21 +297,28 @@ _moved_attributes = [
MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
MovedModule("xmlrpc_server", "xmlrpclib", "xmlrpc.server"),
MovedModule("winreg", "_winreg"),
MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
]
# Add windows specific modules.
if sys.platform == "win32":
_moved_attributes += [
MovedModule("winreg", "_winreg"),
]
for attr in _moved_attributes:
setattr(_MovedItems, attr.name, attr)
if isinstance(attr, MovedModule):
sys.modules[__name__ + ".moves." + attr.name] = attr
_importer._add_module(attr, "moves." + attr.name)
del attr
_MovedItems._moved_attributes = _moved_attributes
moves = sys.modules[__name__ + ".moves"] = _MovedItems(__name__ + ".moves")
moves = _MovedItems(__name__ + ".moves")
_importer._add_module(moves, "moves")
class Module_six_moves_urllib_parse(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_parse"""
@@ -268,6 +339,13 @@ _urllib_parse_moved_attributes = [
MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
MovedAttribute("urlencode", "urllib", "urllib.parse"),
MovedAttribute("splitquery", "urllib", "urllib.parse"),
MovedAttribute("splittag", "urllib", "urllib.parse"),
MovedAttribute("splituser", "urllib", "urllib.parse"),
MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
MovedAttribute("uses_params", "urlparse", "urllib.parse"),
MovedAttribute("uses_query", "urlparse", "urllib.parse"),
MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
]
for attr in _urllib_parse_moved_attributes:
setattr(Module_six_moves_urllib_parse, attr.name, attr)
@@ -275,10 +353,12 @@ del attr
Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
sys.modules[__name__ + ".moves.urllib_parse"] = sys.modules[__name__ + ".moves.urllib.parse"] = Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse")
_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
"moves.urllib_parse", "moves.urllib.parse")
class Module_six_moves_urllib_error(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_error"""
@@ -293,10 +373,12 @@ del attr
Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
sys.modules[__name__ + ".moves.urllib_error"] = sys.modules[__name__ + ".moves.urllib.error"] = Module_six_moves_urllib_error(__name__ + ".moves.urllib.error")
_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
"moves.urllib_error", "moves.urllib.error")
class Module_six_moves_urllib_request(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_request"""
@@ -341,10 +423,12 @@ del attr
Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
sys.modules[__name__ + ".moves.urllib_request"] = sys.modules[__name__ + ".moves.urllib.request"] = Module_six_moves_urllib_request(__name__ + ".moves.urllib.request")
_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
"moves.urllib_request", "moves.urllib.request")
class Module_six_moves_urllib_response(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_response"""
@@ -360,10 +444,12 @@ del attr
Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
sys.modules[__name__ + ".moves.urllib_response"] = sys.modules[__name__ + ".moves.urllib.response"] = Module_six_moves_urllib_response(__name__ + ".moves.urllib.response")
_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
"moves.urllib_response", "moves.urllib.response")
class Module_six_moves_urllib_robotparser(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_robotparser"""
@@ -376,22 +462,25 @@ del attr
Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
sys.modules[__name__ + ".moves.urllib_robotparser"] = sys.modules[__name__ + ".moves.urllib.robotparser"] = Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser")
_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
"moves.urllib_robotparser", "moves.urllib.robotparser")
class Module_six_moves_urllib(types.ModuleType):
"""Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
parse = sys.modules[__name__ + ".moves.urllib_parse"]
error = sys.modules[__name__ + ".moves.urllib_error"]
request = sys.modules[__name__ + ".moves.urllib_request"]
response = sys.modules[__name__ + ".moves.urllib_response"]
robotparser = sys.modules[__name__ + ".moves.urllib_robotparser"]
__path__ = [] # mark as package
parse = _importer._get_module("moves.urllib_parse")
error = _importer._get_module("moves.urllib_error")
request = _importer._get_module("moves.urllib_request")
response = _importer._get_module("moves.urllib_response")
robotparser = _importer._get_module("moves.urllib_robotparser")
def __dir__(self):
return ['parse', 'error', 'request', 'response', 'robotparser']
sys.modules[__name__ + ".moves.urllib"] = Module_six_moves_urllib(__name__ + ".moves.urllib")
_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
"moves.urllib")
def add_move(move):
@@ -418,11 +507,6 @@ if PY3:
_func_code = "__code__"
_func_defaults = "__defaults__"
_func_globals = "__globals__"
_iterkeys = "keys"
_itervalues = "values"
_iteritems = "items"
_iterlists = "lists"
else:
_meth_func = "im_func"
_meth_self = "im_self"
@@ -432,11 +516,6 @@ else:
_func_defaults = "func_defaults"
_func_globals = "func_globals"
_iterkeys = "iterkeys"
_itervalues = "itervalues"
_iteritems = "iteritems"
_iterlists = "iterlists"
try:
advance_iterator = next
@@ -459,6 +538,9 @@ if PY3:
create_bound_method = types.MethodType
def create_unbound_method(func, cls):
return func
Iterator = object
else:
def get_unbound_function(unbound):
@@ -467,6 +549,9 @@ else:
def create_bound_method(func, obj):
return types.MethodType(func, obj, obj.__class__)
def create_unbound_method(func, cls):
return types.MethodType(func, None, cls)
class Iterator(object):
def next(self):
@@ -485,66 +570,117 @@ get_function_defaults = operator.attrgetter(_func_defaults)
get_function_globals = operator.attrgetter(_func_globals)
def iterkeys(d, **kw):
"""Return an iterator over the keys of a dictionary."""
return iter(getattr(d, _iterkeys)(**kw))
if PY3:
def iterkeys(d, **kw):
return iter(d.keys(**kw))
def itervalues(d, **kw):
"""Return an iterator over the values of a dictionary."""
return iter(getattr(d, _itervalues)(**kw))
def itervalues(d, **kw):
return iter(d.values(**kw))
def iteritems(d, **kw):
"""Return an iterator over the (key, value) pairs of a dictionary."""
return iter(getattr(d, _iteritems)(**kw))
def iteritems(d, **kw):
return iter(d.items(**kw))
def iterlists(d, **kw):
"""Return an iterator over the (key, [values]) pairs of a dictionary."""
return iter(getattr(d, _iterlists)(**kw))
def iterlists(d, **kw):
return iter(d.lists(**kw))
viewkeys = operator.methodcaller("keys")
viewvalues = operator.methodcaller("values")
viewitems = operator.methodcaller("items")
else:
def iterkeys(d, **kw):
return d.iterkeys(**kw)
def itervalues(d, **kw):
return d.itervalues(**kw)
def iteritems(d, **kw):
return d.iteritems(**kw)
def iterlists(d, **kw):
return d.iterlists(**kw)
viewkeys = operator.methodcaller("viewkeys")
viewvalues = operator.methodcaller("viewvalues")
viewitems = operator.methodcaller("viewitems")
_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
_add_doc(iteritems,
"Return an iterator over the (key, value) pairs of a dictionary.")
_add_doc(iterlists,
"Return an iterator over the (key, [values]) pairs of a dictionary.")
if PY3:
def b(s):
return s.encode("latin-1")
def u(s):
return s
unichr = chr
if sys.version_info[1] <= 1:
def int2byte(i):
return bytes((i,))
else:
# This is about 2x faster than the implementation above on 3.2+
int2byte = operator.methodcaller("to_bytes", 1, "big")
import struct
int2byte = struct.Struct(">B").pack
del struct
byte2int = operator.itemgetter(0)
indexbytes = operator.getitem
iterbytes = iter
import io
StringIO = io.StringIO
BytesIO = io.BytesIO
_assertCountEqual = "assertCountEqual"
if sys.version_info[1] <= 1:
_assertRaisesRegex = "assertRaisesRegexp"
_assertRegex = "assertRegexpMatches"
else:
_assertRaisesRegex = "assertRaisesRegex"
_assertRegex = "assertRegex"
else:
def b(s):
return s
# Workaround for standalone backslash
def u(s):
return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
unichr = unichr
int2byte = chr
def byte2int(bs):
return ord(bs[0])
def indexbytes(buf, i):
return ord(buf[i])
def iterbytes(buf):
return (ord(byte) for byte in buf)
iterbytes = functools.partial(itertools.imap, ord)
import StringIO
StringIO = BytesIO = StringIO.StringIO
_assertCountEqual = "assertItemsEqual"
_assertRaisesRegex = "assertRaisesRegexp"
_assertRegex = "assertRegexpMatches"
_add_doc(b, """Byte literal""")
_add_doc(u, """Text literal""")
def assertCountEqual(self, *args, **kwargs):
return getattr(self, _assertCountEqual)(*args, **kwargs)
def assertRaisesRegex(self, *args, **kwargs):
return getattr(self, _assertRaisesRegex)(*args, **kwargs)
def assertRegex(self, *args, **kwargs):
return getattr(self, _assertRegex)(*args, **kwargs)
if PY3:
exec_ = getattr(moves.builtins, "exec")
def reraise(tp, value, tb=None):
if value is None:
value = tp()
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
@@ -562,12 +698,26 @@ else:
_locs_ = _globs_
exec("""exec _code_ in _globs_, _locs_""")
exec_("""def reraise(tp, value, tb=None):
raise tp, value, tb
""")
if sys.version_info[:2] == (3, 2):
exec_("""def raise_from(value, from_value):
if from_value is None:
raise value
raise value from from_value
""")
elif sys.version_info[:2] > (3, 2):
exec_("""def raise_from(value, from_value):
raise value from from_value
""")
else:
def raise_from(value, from_value):
raise value
print_ = getattr(moves.builtins, "print", None)
if print_ is None:
def print_(*args, **kwargs):
@@ -575,13 +725,14 @@ if print_ is None:
fp = kwargs.pop("file", sys.stdout)
if fp is None:
return
def write(data):
if not isinstance(data, basestring):
data = str(data)
# If the file has an encoding, encode unicode with it.
if (isinstance(fp, file) and
isinstance(data, unicode) and
fp.encoding is not None):
isinstance(data, unicode) and
fp.encoding is not None):
errors = getattr(fp, "errors", None)
if errors is None:
errors = "strict"
@@ -622,25 +773,96 @@ if print_ is None:
write(sep)
write(arg)
write(end)
if sys.version_info[:2] < (3, 3):
_print = print_
def print_(*args, **kwargs):
fp = kwargs.get("file", sys.stdout)
flush = kwargs.pop("flush", False)
_print(*args, **kwargs)
if flush and fp is not None:
fp.flush()
_add_doc(reraise, """Reraise an exception.""")
if sys.version_info[0:2] < (3, 4):
def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
updated=functools.WRAPPER_UPDATES):
def wrapper(f):
f = functools.wraps(wrapped, assigned, updated)(f)
f.__wrapped__ = wrapped
return f
return wrapper
else:
wraps = functools.wraps
def with_metaclass(meta, *bases):
"""Create a base class with a metaclass."""
return meta("NewBase", bases, {})
# This requires a bit of explanation: the basic idea is to make a dummy
# metaclass for one level of class instantiation that replaces itself with
# the actual metaclass.
class metaclass(meta):
def __new__(cls, name, this_bases, d):
return meta(name, bases, d)
return type.__new__(metaclass, 'temporary_class', (), {})
def add_metaclass(metaclass):
"""Class decorator for creating a class with a metaclass."""
def wrapper(cls):
orig_vars = cls.__dict__.copy()
orig_vars.pop('__dict__', None)
orig_vars.pop('__weakref__', None)
slots = orig_vars.get('__slots__')
if slots is not None:
if isinstance(slots, str):
slots = [slots]
for slots_var in slots:
orig_vars.pop(slots_var)
orig_vars.pop('__dict__', None)
orig_vars.pop('__weakref__', None)
return metaclass(cls.__name__, cls.__bases__, orig_vars)
return wrapper
def python_2_unicode_compatible(klass):
"""
A decorator that defines __unicode__ and __str__ methods under Python 2.
Under Python 3 it does nothing.
To support Python 2 and 3 with a single code base, define a __str__ method
returning text and apply this decorator to the class.
"""
if PY2:
if '__str__' not in klass.__dict__:
raise ValueError("@python_2_unicode_compatible cannot be applied "
"to %s because it doesn't define __str__()." %
klass.__name__)
klass.__unicode__ = klass.__str__
klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
return klass
# Complete the moves implementation.
# This code is at the end of this module to speed up module loading.
# Turn this module into a package.
__path__ = [] # required for PEP 302 and PEP 451
__package__ = __name__ # see PEP 366 @ReservedAssignment
if globals().get("__spec__") is not None:
__spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
# Remove other six meta path importers, since they cause problems. This can
# happen if six is removed from sys.modules and then reloaded. (Setuptools does
# this for some reason.)
if sys.meta_path:
for i, importer in enumerate(sys.meta_path):
# Here's some real nastiness: Another "instance" of the six module might
# be floating around. Therefore, we can't use isinstance() to check for
# the six meta path importer, since the other six instance will have
# inserted an importer with different class.
if (type(importer).__name__ == "_SixMetaPathImporter" and
importer.name == __name__):
del sys.meta_path[i]
break
del i, importer
# Finally, add the importer to the meta path import hook.
sys.meta_path.append(_importer)
+113
View File
@@ -0,0 +1,113 @@
import sys
if sys.version_info < (2, 7):
import unittest2 as unittest
from unittest2 import TestCase as TC
else:
import unittest
from unittest import TestCase as TC
skip = unittest.skip
_dummy = False
# less than 2.6 ...
if sys.version_info[0] == 2 and sys.version_info[1] <= 6:
_dummy = True
def _d(f):
def decorate(self, *args, **kw):
if not _dummy:
return f(self, *args, **kw)
return self.assertTrue(True)
return decorate
class TestCase(TC):
"""
Wrapper for python 2.6 stubs
"""
def assertIsInstance(self, obj, cls, msg=None):
if not _dummy:
return super(TestCase, self).assertIsInstance(obj, cls, msg)
tst = isinstance(obj, cls)
return self.assertTrue(tst, msg)
@_d
def assertNotIsInstance(self, *args, **kw):
return super(TestCase, self).assertNotIsInstance(*args, **kw)
@_d
def assertIn(self, *args, **kw):
return super(TestCase, self).assertIn(*args, **kw)
@_d
def assertRegexpMatches(self, *args, **kw):
return super(TestCase, self).assertRegexpMatches(*args, **kw)
# -----------------------------------------------------------
# NOT DUMMY ASSERTIONS
# -----------------------------------------------------------
def assertIsNone(self, val, msg=None):
if not _dummy:
return super(TestCase, self).assertIsNone(val, msg)
tst = val is None
return super(TestCase, self).assertTrue(tst, msg)
def assertIsNotNone(self, val, msg=None):
if not _dummy:
return super(TestCase, self).assertIsNotNone(val, msg)
tst = val is not None
return super(TestCase, self).assertTrue(tst, msg)
def assertRaises(self, exc, msg=None):
if not _dummy:
return super(TestCase, self).assertRaises(exc, msg)
return TestCase._TestCaseRaiseStub(self, exc, msg=msg)
def assertRaisesRegexp(self, exc, regex, msg=None):
if not _dummy:
return super(TestCase, self).assertRaises(exc, msg)
return TestCase._TestCaseRaiseStub(self, exc, regex=regex, msg=msg)
class _TestCaseRaiseStub:
""" Internal stuff for stubbing `assertRaises*` """
def __init__(self, test_case, exc, regex=None, msg=None):
self.exc = exc
self.test_case = test_case
self.regex = regex
self.msg = msg
def __enter__(self):
return self
def __exit__(self, tp, value, traceback):
tst = tp is self.exc
self.test_case.assertTrue(tst, msg=self.msg)
self.exception = value
# TODO: implement self.regex checking
# True indicates, that exception is handled
return True
def TestArgs(*parameters):
def tuplify(x):
if not isinstance(x, tuple):
return (x,)
return x
def decorator(method, parameters=parameters):
for parameter in (tuplify(x) for x in parameters):
def method_for_parameter(self, method=method, parameter=parameter):
method(self, *parameter)
args_for_parameter = ",".join(repr(v) for v in parameter)
name_for_parameter = method.__name__ + "(" + args_for_parameter + ")"
frame = sys._getframe(1) # pylint: disable-msg=W0212
frame.f_locals[name_for_parameter] = method_for_parameter
frame.f_locals[name_for_parameter].__doc__ = method.__doc__ + '(' + args_for_parameter + ')'
method_for_parameter.__name__ = name_for_parameter + '(' + args_for_parameter + ')'
return None
return decorator
+12
View File
@@ -0,0 +1,12 @@
[nosetests]
verbosity=2
tests=headphones
#rednose=1
#exclude-dir=lib
with-coverage=1
cover-branches=1
cover-html=1
cover-html-dir=cover-html
cover-package=headphones