Compare commits

...
226 Commits
Author SHA1 Message Date
rembo10 3b48f22bd9 v0.5.19 2018-03-27 10:14:08 -05:00
rembo10 79d508fb07 Merge branch 'develop' 2018-03-27 10:13:51 -05:00
rembo10 ae8fadd587 Updated changelog for release 2018-03-27 10:13:16 -05:00
Ade 5d7fdc7a82 Speed up Choose Alternate Release
Possible fix for #3078
2018-03-06 16:48:19 +13:00
AdeHub 1ded166765 Merge pull request #3082 from YipYup/master
Add Feature: Ignore Releases Missing a Release Date
2018-03-03 22:07:57 +01:00
Ade 22eb21a4ef Jackett seeders
Use Soup to get seeders returned from Jackett

Fixes #3081
2018-03-03 20:02:14 +13:00
Get your own 'tots. Geez! 4283b381b6 Fix indentation. 2018-02-22 19:17:23 -07:00
Get your own 'tots. Geez! 2cdc6669da Merge branch 'develop' into master 2018-02-22 19:02:29 -07:00
AdeHub de8fac347d Merge pull request #3037 from Lartza/fix-win-encoding
Fix SYS_ENCODING selection on Windows
2017-10-22 09:49:48 +13:00
Ade da9287d7cf Merge branch 'develop' 2017-10-22 09:45:15 +13:00
Ade f9d73da31b Remove unidecode for windows file names 2017-10-22 09:43:55 +13:00
Lartza 3cc280666e Fix encoding selection on Windows 2017-10-18 22:44:37 +03:00
Ade 0828643c09 Replace windows file/folder problem characters 2017-09-26 11:06:34 +13:00
AdeHub 9b3d7f3bbb Merge pull request #3023 from noam09/develop
Deluge encoding issues with unicode torrent names
2017-09-22 08:54:58 +12:00
Noam 68c551c828 Try DEcoding torrent name utf-8 in params 2017-09-17 02:40:09 +03:00
Noam b7af246340 Try encoding torrent name utf-8 in params 2017-09-17 02:10:30 +03:00
Noam 4c3b60be68 UnicodeDecodeError printing torrent name containing unicode 2017-09-17 01:40:04 +03:00
AdeHub 539db608e2 Merge pull request #3018 from noam09/develop
Added UTF-8 header for encoding issues
2017-09-14 20:01:28 +12:00
Noam 0b6f263985 Added UTF-8 header for encoding issues 2017-09-11 19:38:01 +03:00
AdeHub a10ec8dce4 Merge pull request #3016 from noam09/develop
Deluge WebUI daemons - fix #3010
2017-09-10 12:08:22 +12:00
Noam 867f75a681 Check _if_ WebUI daemons exist before checking _how many_ exist, fix #3010 2017-09-10 02:15:36 +03:00
Ade c8057676db pep 2017-09-07 22:48:54 +12:00
Ade 9197f27a50 Jackett provider formatting 2017-09-07 22:26:30 +12:00
Ade e7062a6710 Jackett/Torznab v2 api
Fixes #2999
2017-09-02 19:14:37 +12:00
Ade 9c2bfcf7d8 Scan Artist exception handling 2017-08-28 20:54:19 +12:00
Ade 4d575787cd last.fm import fix
Fixes #2753
2017-08-24 20:48:15 +12:00
Ade bdf55f8662 replace torcache 2017-08-19 08:42:04 +12:00
Ade cd57668b92 allow .aif 2017-08-15 17:25:37 +12:00
Ade f20520541d Merge branch 'develop' 2017-08-09 12:52:43 +12:00
Ade 48dd832d4e pep 2017-08-09 12:52:11 +12:00
Ade af10088dc8 Merge branch 'develop' 2017-08-09 12:20:53 +12:00
AdeHub cf9737e804 Merge pull request #2958 from Fish2/master
lossless compression of images saved 93KB
2017-07-28 19:05:56 +12:00
AdeHub 4f118eaaef Merge pull request #2957 from permster/develop
Join notification support
2017-07-27 20:41:30 +12:00
AdeHub 79277ddc33 Merge pull request #2969 from doucheymcdoucherson/develop
Make apollo.rip searches specific to the album type
2017-07-27 20:38:27 +12:00
AdeHub 3ee2598f4c Merge pull request #2989 from djl/sort-wanted
Sorted wanted albums by release date.
2017-07-27 20:07:32 +12:00
AdeHub 1e1b8a9351 Merge pull request #2992 from djl/fix-typeerror
Fix TypeError in dbcheck()
2017-07-27 20:06:38 +12:00
David Logie 0cc2dd7b31 Fix TypeError in dbcheck().
Fixes #2987
2017-07-26 12:17:31 +01:00
David Logie 80fe81e345 Sorted wanted albums by release date.
Without this SQLite will order the results by whichever column happens
to be the fastest for it. This will usually result in the rows being
sorted by the primary key (or rowid).

Sorting by release date also matches the sort order of the "upcoming"
albums list.
2017-07-24 12:54:42 +01:00
Ade 0c98085018 Merge branch 'develop' 2017-07-03 11:44:45 +12:00
Ade e96a6fc7c8 Transmission magnet issue
Fixes #2972
2017-06-24 09:27:51 +12:00
Ade ee7254ddbe Version check logging 2017-06-24 09:12:17 +12:00
Ade 3e44baaded Boxcar icon url 2017-06-17 21:36:17 +12:00
doucheymcdoucherson 914ab65577 Make apollo.rip searches specific to the album type 2017-06-16 22:52:47 -07:00
Ade 4d674689b1 rutracker search term fix 2017-06-16 13:02:13 +12:00
Fish2 ae328a15aa lossless compression of images saved 93KB 2017-06-04 11:41:07 +01:00
Travis Golliher c612a228b6 Fix pep8 error 2017-05-31 21:59:48 -07:00
Travis Golliher 334493d81e Join notification support
Fix pep8 errors.
2017-05-31 21:40:01 -07:00
Travis Golliher 50de38ce82 Join notification support
Initial Join by Joaoapps API Notification Support.  See issue #2712.
2017-05-31 21:04:30 -07:00
AdeHub 36434b57ee Merge pull request #2952 from noam09/develop
rutracker optional session cookie
2017-05-27 18:49:45 +12:00
Noam 3110659633 travis space 2017-05-18 18:28:01 +03:00
Noam ecadf8cdab log cookie attempt 2017-05-18 18:26:16 +03:00
Noam 6c0b451882 rutracker optional session cookie 2017-05-18 17:02:53 +03:00
AdeHub 755944f15c Merge pull request #2938 from Kallys/develop
Remove Deezloader integration
2017-05-03 17:34:14 +12:00
Kallys 0acb5a413a Revert "Integration of deezloader provider by ParadoxalManiak under CC BY-NC-SA 4.0"
This reverts commit 04e8767ea9.
2017-05-02 13:41:51 +02:00
Kallys e3c7790039 Revert "Fix #2930"
This reverts commit cd2860f4e3.
2017-05-02 13:41:29 +02:00
AdeHub d802948a35 Merge pull request #2934 from pratstercs/develop
Bug (#2933) - Can't remove artist with name "null"
2017-05-02 18:38:17 +12:00
AdeHub b2f5919b26 Merge pull request #2932 from Kallys/master
Aria2 & deezloader bugfixes
2017-05-02 18:37:26 +12:00
pratstercs 74d5333bdf Fixing tabs/spaces 2017-05-01 00:35:28 +01:00
pratstercs 54edacdfe7 Fixed bug (#2933 at least) where an artist will null name errors when trying to remove from library as logger is trying to concat artist name (NoneType) with string 2017-05-01 00:29:39 +01:00
Kallys cd2860f4e3 Fix #2930
Fix #2931
2017-04-30 14:00:10 +02:00
Ade bc0ce99adf Set clean name to lower 2017-04-29 18:05:10 +12:00
AdeHub 3bfdc9f069 Merge pull request #2925 from Kallys/master
Add deezloader provider and aria2 downloader (part 2)
2017-04-25 21:01:15 +12:00
Ade 39e589bc08 Use Release Id for beets matching if supplied 2017-04-25 18:16:33 +12:00
Ade cef1563b4b Trap errors for Scan 2017-04-25 17:03:59 +12:00
Kallys 04e8767ea9 Integration of deezloader provider by ParadoxalManiak under CC BY-NC-SA 4.0
Integration of aria2 downloader by Xyne under GPLv2

I'm not responsible of any kind for the usage of this programs by other people.
These integrations come with no warranty.
Please refer to your local country laws and respect Deezer's terms of service.
2017-04-24 14:32:51 +02:00
Ade 2df13ad823 bit more beets logging 2017-04-24 14:16:08 +12:00
Ade 18d85d12dd pep 2017-04-23 20:26:04 +12:00
Ade 1e3cbb64ca beets recommendation logging 2017-04-23 20:08:28 +12:00
Ade d6e1a16286 Pass full mb unicode names to beets
Possibly fixes https://github.com/rembo10/headphones/issues/2919
2017-04-22 11:54:18 +12:00
AdeHub 979b06ae55 Merge pull request #2920 from rembo10/revert-2918-master
Revert "Add deezloader provider and aria2 downloader"
2017-04-21 19:46:23 +12:00
AdeHub 7fec54289b Revert "Add deezloader provider and aria2 downloader" 2017-04-21 19:45:37 +12:00
AdeHub 49d632a156 Merge pull request #2918 from Kallys/master
Add deezloader provider and aria2 downloader
2017-04-21 19:35:26 +12:00
Kallys a2a14e185a Merge remote-tracking branch 'upstream/develop' 2017-04-20 14:32:27 +02:00
Ade 50851a4953 pep8 2017-04-20 20:42:04 +12:00
Ade 61a3abbf20 last.fm series info 2017-04-20 20:10:34 +12:00
Kallys e8f436a5db Fix travis errors bis 2017-04-19 20:01:45 +02:00
Kallys 34b8a11145 Fix travis errors 2017-04-19 19:55:41 +02:00
Kallys f0fe23899a Add deezloader provider
Add aria2 downloader
2017-04-19 18:59:27 +02:00
Ade 4eabc1c614 opus format
fixes https://github.com/rembo10/headphones/issues/2909
2017-04-14 08:47:09 +12:00
Ade b9d41ad779 Merge branch 'master' into develop 2017-04-14 08:40:15 +12:00
Ade 8e1cd3155f redacted config fix
Fixes https://github.com/rembo10/headphones/issues/2910
2017-04-14 08:40:04 +12:00
Ade 624a3d6cc1 Merge branch 'master' into develop 2017-04-14 08:39:06 +12:00
Ade a042fd44a2 Merge branch 'develop' 2017-04-14 08:24:10 +12:00
Ade 2e5ce520eb pp fix up
Fixes https://github.com/rembo10/headphones/issues/2911
2017-04-14 08:23:11 +12:00
Ade e9407f5dbc Merge branch 'develop' 2017-04-08 12:01:20 +12:00
Ade 8eb79701a6 t411, pb url change 2017-04-08 11:58:14 +12:00
Ade 4c95f99d80 Transmission pp folder name
Get folder name from hash
2017-04-08 11:51:40 +12:00
Ade e1e361a46e Merge branch 'develop' 2017-04-06 10:11:01 +12:00
Ade 437d33e7a8 Qbittorrent folder name 2017-04-05 21:38:59 +12:00
Ade 2832166842 pep 2017-04-04 22:34:46 +12:00
Ade 2841f36d8d artwork fix
fixes #2898
2017-04-04 22:28:26 +12:00
Ade de15f6b632 Qbittorrent
- Postprocessing get folder from hash
- Postprocessing allow for single file
- Rutracker fix up
2017-04-04 20:24:26 +12:00
AdeHub 5bb6ec04a9 Merge pull request #2892 from DJ-666/develop
Rename PTH to Redacted and fix invalid User-Agent (Again)
2017-04-01 16:22:11 +13:00
DJ-666 1da1f5124a Rename PTH to Redacted and fix invalid User-Agent (Again)
This is a duplicate of albshin's PR as I got impatient waiting for him
to submit it to the develop branch with the addition of removing the
previous user agents and replacing them with Headphones as the agent.
Redacted considers falsifying the agent as a bannable offence.

From albshin's PR;
"Changes all instances of PassTheHeadphones to Redacted. Removed the URL
option for Redacted as it's unnecessary(?).

Fixed a bug in the pyGazelle library in which the User-Agent was invalid
due to a missing space. Should work now for Redacted."
2017-03-31 23:36:34 +10:30
Ade f8b82337a2 error msg tidy up 2017-03-27 12:51:47 +13:00
AdeHub 3efd3931f5 Merge pull request #2887 from noam09/develop
Fix title markdown
2017-03-27 09:35:29 +13:00
Noam 95402144d1 Merge branch 'develop' of github.com:noam09/headphones into develop 2017-03-26 12:36:20 +03:00
Noam 220a8931d1 Fix title markdown
Missing space after ##
2017-03-26 12:34:00 +03:00
Ade a96b186825 Add deluge, qbittorrent to scheduled torrent removal 2017-03-26 11:36:38 +13:00
Ade de3b73dfa5 Move to 2.7 2017-03-25 23:17:41 +13:00
Ade 263564f15f pep8 2017-03-25 23:05:37 +13:00
Ade a5ca14e6d6 Waffles is back!
Ignore 0 seeders for now

Fixes https://github.com/rembo10/headphones/issues/2884
2017-03-25 14:38:23 +13:00
Ade 861747c235 qbittorrent folder name
Fixes #2869
2017-03-25 13:22:24 +13:00
Ade 4095bf213e mutagen, musicbrainzngs, beets lib updates 2017-03-25 11:47:04 +13:00
Ade f3020207c0 Artwork pp
Post processing Min/Max artwork size
2017-03-20 10:18:09 +13:00
Ade 1cee19be74 Merge branch 'develop' of https://github.com/rembo10/headphones into develop 2017-03-16 07:55:47 +13:00
Ade f877caff06 Latest artwork changes not working so well
Revisit later
2017-03-16 07:55:27 +13:00
AdeHub 9c0297326e Merge pull request #2875 from noam09/develop
More exception handling for Deluge torrent removal
2017-03-15 07:14:27 +13:00
Noam 1ed12770c2 Merge branch 'develop' of https://github.com/rembo10/headphones into develop 2017-03-14 15:02:32 +02:00
Ade 519fdc3015 Merge branch 'develop' of https://github.com/rembo10/headphones into develop 2017-03-14 17:39:36 +13:00
Noam 91ed781acb More exception handling for torrent removal 2017-03-13 22:10:57 +02:00
AdeHub bf5d07efec Merge pull request #2871 from noam09/develop
Fix Qbittorrent config appearing while Deluge is selected + Deluge check if torrent seeding before removal
2017-03-14 07:11:29 +13:00
Noam 33fdb3d7bd Wrong variable name 2017-03-13 18:46:04 +02:00
Noam 3f67396a55 Deluge: check if torrent finished seeding before deleting 2017-03-13 18:32:36 +02:00
Noam 3680c2b349 Fix typo causing Qbittorrent config to show while Deluge is selected 2017-03-13 10:41:46 +02:00
Ade ff7ff79f85 Merge branch 'master' into develop 2017-03-13 11:11:20 +13:00
Ade debc65db5d Temp processing fixup
Fixes
2017-03-13 11:01:21 +13:00
Ade a02cc4d7b9 pep8 2017-03-12 17:19:21 +13:00
Ade 578ae2c2d8 More post processing artwork changes
- Added min/max pixel width config options
- Artwork < min width is ignored
- Artwork > max width is downsized to max width using a proxy web
service
2017-03-12 12:47:12 +13:00
AdeHub 85749c4e94 Merge pull request #2870 from noam09/develop
Added JSON-related HTTP headers
2017-03-12 09:13:39 +13:00
Noam e415de7698 Added JSON-related HTTP headers 2017-03-11 15:38:12 +02:00
Ade a9bbd34a52 more pep8 2017-03-11 12:44:13 +13:00
Ade 7797cd0681 pep8 2017-03-11 12:37:13 +13:00
Ade 9ccb0f85dc Use CAA artwork for post processing
Preference is:
CAA
Amazon
last.fm
2017-03-11 12:19:53 +13:00
Ade 50185ab3d3 Merge branch 'develop' 2017-03-11 09:04:37 +13:00
Ade 697e695f7d More temp dir stuff 2017-03-11 08:38:42 +13:00
Ade 36ad37f005 Don't check temp pp dir if forced 2017-03-10 11:47:36 +13:00
Ade a45894b802 Merge branch 'develop' 2017-03-09 11:51:47 +13:00
Ade dd1e4bfe3b Label change 2017-03-09 11:35:06 +13:00
Ade 88366443fd Post processing temp dir fixup
Hopefully fixes #2859
2017-03-09 11:10:06 +13:00
widewing 6a1aca66b1 change ajax call method to POST 2017-03-04 11:24:42 +13:00
MrClayPole ef6a203cda Update init.freebsd
This path is incorrect for freebsd the path to python bin is /usr/local/bin/python rather than /usr/bin/python
2017-03-04 11:24:42 +13:00
hypsometric 83c50ccd4f HTML escape pth_password 2017-03-04 11:24:42 +13:00
Fritsbenik 3e4c5cb5e2 update init.freebsd
Allow to set variables in /etc/rc.conf
2017-03-04 11:24:42 +13:00
Ade 8c1621f604 Merge branch 'master' of https://github.com/rembo10/headphones 2017-03-04 11:17:18 +13:00
Ade bc465edd91 Merge branch 'master' into develop 2017-03-04 11:08:06 +13:00
AdeHub 3d1a9e8947 Merge pull request #2851 from MrClayPole/patch-1
Update init.freebsd
2017-03-04 11:02:56 +13:00
AdeHub 9d6a7d1d36 Merge pull request #2855 from widewing/patch-1
change ajax call method to POST
2017-03-04 11:02:32 +13:00
widewing 3cfc434e8a change ajax call method to POST 2017-02-24 16:32:54 +08:00
MrClayPole e7017305d8 Update init.freebsd
This path is incorrect for freebsd the path to python bin is /usr/local/bin/python rather than /usr/bin/python
2017-02-21 08:54:24 +00:00
AdeHub a0fa658956 Merge pull request #2848 from hypsometric/patch-1
HTML escape pth_password
2017-02-17 19:33:34 +13:00
hypsometric 709073358b HTML escape pth_password 2017-02-14 15:37:59 +01:00
Ade e6b74be1f6 Replace Piratebay apostrophe
Fixes #2839
2017-02-02 19:46:31 +13:00
Ade c29dda3af6 Torznab
- Use 3050 for music/other
- Does not return size for Torrentech, hack our own
2017-01-22 10:22:19 +13:00
Ade 335586893c Keep original folder
- Added config option for directory to use when keeping folder for
seeding
- cue processing fixup
2017-01-22 10:17:24 +13:00
Ade 44bb1bea08 rutracker links and cookie changes 2017-01-21 19:45:00 +13:00
Ade 9f378ec79c kat domain change 2017-01-21 09:55:38 +13:00
Ade 5501d84611 omgwtfnzbs domain change
Fixes #2820
2017-01-21 09:52:12 +13:00
AdeHub 34746400e6 Merge pull request #2818 from Fritsbenik/patch-1
update init.freebsd
2017-01-21 09:38:39 +13:00
AdeHub cf502aa880 Merge pull request #2806 from Darkitty/develop
Add Slack notification
2017-01-21 09:38:03 +13:00
AdeHub 2ca498d74f Merge pull request #2811 from Gadzy/develop
T411 specific download bugfix
2017-01-21 09:37:12 +13:00
AdeHub 432018a2a4 Merge pull request #2822 from dsm1212/qbittorrent
Add qbittorrent downloader support
2017-01-21 09:35:36 +13:00
dsm1212 784ec2927e more pep8 fixes and a typo 2017-01-14 01:20:42 -05:00
root fa1cb27e04 fix pep8 errors 2017-01-14 01:13:04 -05:00
root 7e915c5902 Add qbittorrent downloader support 2017-01-14 00:49:55 -05:00
Fritsbenik 7f48b021ec update init.freebsd
Allow to set variables in /etc/rc.conf
2017-01-04 19:08:48 +01:00
Gadzy e8009c00d2 T411 specific dowload bugfix
Fixed a bug where T411 was not checked when choosing specific download
2016-12-22 21:43:06 +01:00
Nicolas Le Gall 772fb524c7 Fix for pep8 2016-12-18 10:39:26 +01:00
Nicolas Le Gall b538e5b321 Add Slack notification 2016-12-18 10:32:39 +01:00
Ade decc548086 PEP8 2016-12-11 19:23:38 +13:00
Ade 82695311c4 PEP8 2016-12-11 19:12:29 +13:00
AdeHub 6d40178ce2 Merge pull request #2790 from noam09/develop
Apollo replaces WCD, PTH object fix, Deluge logging tweak
2016-12-11 17:17:38 +13:00
Ade 4d1107ee54 cue sheet + temp dir fix ups
- cue not processing correctly if using temp dir
-  allow temp dir to be specified from new config option
- attempt to stop multiple temp dirs created for the same albumpath
2016-12-11 17:11:42 +13:00
Noam 8fd9958d70 Separated use of Gazelle objects for Apollo and PTH so that objects don't collide, renamed objects to make this clear 2016-12-02 14:20:28 +02:00
Noam f8900795da Replaced all instances of WCD with Apollo 2016-12-02 13:08:03 +02:00
Noam b8e8e752f0 RIP WCD 2016-12-02 12:13:07 +02:00
Noam d04cdcd8fa Merge branch 'develop' of github.com:noam09/headphones into develop 2016-12-02 12:07:04 +02:00
Noam edc243a245 Changed some logging to better understand issue #2734 2016-12-02 12:06:13 +02:00
rembo10 b71c0d8b93 v0.5.18 2016-12-01 15:59:35 +00:00
rembo10 0ecfe499fc Updated changelog for v0.5.18 2016-12-01 15:59:11 +00:00
rembo10 68d85841ea Merge remote-tracking branch 'DenzoNL/master' into develop 2016-12-01 15:52:46 +00:00
rembo10 1ffcc177a4 Merge remote-tracking branch 'MIKAd3v/develop' into develop 2016-12-01 15:51:01 +00:00
rembo10 2230c5bc0c Merge remote-tracking branch 'wfriesen/develop' into develop 2016-12-01 15:49:58 +00:00
Denzo 352b009e91 # Removed slash from ajax.php 2016-11-29 20:54:20 +01:00
Denzo b6b33e1b1e + Added support for PassTheHeadphones.me
% Removed hardcoding for What.CD in pygazelle API, making it easy to add other Gazelle-based trackers

+ Added URL parameter for What.CD and PTH, in case they come back under a new domain name
2016-11-29 20:49:52 +01:00
massiliattak dff1ac2e41 Update T411 new domain ( t411.li ) 2016-11-20 20:15:37 +01:00
Noam 36f75f44e9 Changed some logging to better understand issue #2734 2016-11-19 22:58:57 +02:00
William Friesen 92601e0eb8 Use HTML escaping for password fields
Fixes rembo10/headphones#2474
2016-11-19 12:15:22 +11:00
rembo10 6e82423947 Merge remote-tracking branch 'Betree/search_with_partial_dates' into develop 2016-11-10 21:07:04 +00:00
rembo10 0a805a01f3 v0.5.17 2016-11-10 20:58:00 +00:00
rembo10 579b5a2f2f Updated changelog for v0.5.17 2016-11-10 20:56:51 +00:00
Benjamin Piouffle c77f43be5e Accept partial release date for search 2016-10-25 19:21:16 +11:00
AdeHub f10d1834dd Merge pull request #2749 from likeitneverwentaway/t411
Fixed lossless only for t411
2016-10-20 22:51:42 +13:00
AdeHub b4c7ed2227 Merge pull request #2756 from Betree/fix_rutracker_login
Fix Rutracker loggin by looking for the cookie in request history
2016-10-20 22:51:23 +13:00
Benjamin Piouffle 0086980b1b Fix Rutracker loggin by looking for the cookie in request history 2016-10-19 12:41:46 +11:00
likeitneverwentaway 89e1793978 Fixed lossless only for t411 2016-10-15 14:39:55 +02:00
rembo10 5a598fe611 Merge remote-tracking branch 'etomm/develop' into develop 2016-10-15 10:13:20 +01:00
etomm b58f670694 Handling Deluge empty password 2016-10-15 10:08:51 +02:00
AdeHub 5103436d17 Merge pull request #2745 from likeitneverwentaway/t411
New provider : add t411 support
2016-10-15 18:58:41 +13:00
Ade c9a1796e56 Allow hyphen in mb search
Fixes #2499
2016-10-15 09:59:11 +13:00
Ade 7428b56dd5 Remove mb apostrophe
Fixes #2737
2016-10-09 15:56:25 +13:00
likeitneverwentaway 42294d261d Fix typo, import loads from json 2016-10-08 01:55:55 +02:00
likeitneverwentaway 31a99a908a Added credit 2016-10-08 01:08:54 +02:00
likeitneverwentaway aa503f0fe8 Update config.py to add t411 support 2016-10-08 01:01:39 +02:00
likeitneverwentaway d3d48d27df Update webserve.py to add t411 support 2016-10-08 01:00:49 +02:00
likeitneverwentaway bd8eb74662 Update config.html to add t411 support 2016-10-08 00:50:48 +02:00
likeitneverwentaway 99680e9a0f Update searcher.py to add t411 support 2016-10-08 00:46:12 +02:00
rembo10 0cc58bdb46 Merge remote-tracking branch 'clowninasack/freebsd-init-script' into develop 2016-09-28 22:37:17 +01:00
rembo10 09435b8786 Merge remote-tracking branch 'kainz/add_crier' into develop 2016-09-28 22:37:14 +01:00
rembo10 5d2753a34e Merge remote-tracking branch 'VoidVolker/develop' into develop 2016-09-28 22:37:01 +01:00
Ade 8eb3ece2f9 rutracker login fix
stop redirecting to the same login page
2016-09-17 20:27:01 +12:00
Get your own 'tots. Geez! ed923a2a94 Update config.html 2016-08-03 11:09:05 -06:00
Get your own 'tots. Geez! 95cc0670e2 Update config.html 2016-08-03 11:03:28 -06:00
Get your own 'tots. Geez! a62b55c40f Update importer.py 2016-08-03 10:52:39 -06:00
Get your own 'tots. Geez! 7440b77377 Update importer.py 2016-08-03 10:22:55 -06:00
Get your own 'tots. Geez! 372c6ffd3b Update config.html 2016-08-03 10:07:01 -06:00
Get your own 'tots. Geez! 14d1524971 Update config.html 2016-08-03 09:53:53 -06:00
Get your own 'tots. Geez! 9cddcabad4 Update webserve.py 2016-08-03 09:31:17 -06:00
Get your own 'tots. Geez! 656e7161cb Update config.py 2016-08-03 09:29:09 -06:00
Get your own 'tots. Geez! 7c12761066 Update config.py 2016-08-03 09:27:44 -06:00
clowninasack d3584a5ae9 Fixed FreeBSD init script. 2016-08-01 10:30:10 -07:00
Bryon Roche c0c64621e6 Add a thread-dumping handler
Add a 'crier' handler to dump the thread stacks of running threads to
the log, for easier debugging.
2016-07-14 05:34:36 -07:00
Bryon Roche 323d364572 Added missing support method for current FauxSocket for pypy 2016-07-14 05:27:07 -07:00
VoidVolker 9a681a9422 [=] HP_HOST description 2016-07-08 14:44:01 +03:00
VoidVolker a7a709df51 [+] HP_HOST option 2016-07-08 14:42:19 +03:00
VoidVolker 3f23b5553a [+] --host command line option 2016-07-08 14:39:36 +03:00
rembo10 5a2082ede5 v0.5.16 2016-06-10 00:23:22 +01:00
rembo10 e29f3a0a9a Updated changelog for v0.5.16 2016-06-10 00:22:59 +01:00
rembo10 f4bebf6a41 Merge remote-tracking branch 'SlashingWolf/patch-1' into develop 2016-06-10 00:20:43 +01:00
Gabriel 564df8136c Update to searcher.py to work with waffles.ch
Changed the url used for searching waffles from https://www.waffles.ch/browse.php to https://waffles.ch/browse.php.
2016-06-09 16:19:49 -05:00
rembo10 e85560952f v0.5.15 2016-06-07 10:53:10 +01:00
rembo10 5d6a4adae8 Updated changelog for v0.5.15 2016-06-07 10:50:13 +01:00
rembo10 ecfd3204ff Updated vip server url 2016-06-07 10:49:59 +01:00
rembo10 f0e6554309 Merge branch 'develop' of https://github.com/rembo10/headphones into develop 2016-06-07 10:48:50 +01:00
AdeHub 995294e605 Merge pull request #2666 from noam09/develop
Waffles move from .fm to .ch
2016-06-06 11:59:58 +12:00
Noam 0525493c99 Waffles move from .fm to .ch 2016-06-05 14:39:19 +03:00
208 changed files with 19106 additions and 6874 deletions
-1
View File
@@ -11,7 +11,6 @@ cache:
- lib - lib
python: python:
- "2.6"
- "2.7" - "2.7"
install: install:
+54
View File
@@ -1,5 +1,59 @@
# Changelog # Changelog
## v0.5.19
Released 27 March 2018
Highlights:
* Improved: Windows encoding fixes
* Improved: Performance enhancements
* Improved: Many more since the last release. Check the list
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.18...v0.5.19).
## v0.5.18
Released 01 December 2016
Highlights:
* Added: PassTheHeadphones support
* Fixed: Special characters in password fields breaking on config page
* Improved: Updated t411 url
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.17...v0.5.18).
## v0.5.17
Released 10 November 2016
Highlights:
* Added: t411 support
* Fixed: Rutracker login
* Fixed: Deluge empty password
* Fixed: FreeBSD init script
* Improved: Musicbrainz searching
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.16...v0.5.17).
## v0.5.16
Released 10 June 2016
Hotfix update
Highlights:
* Fixed: Waffles url
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.15...v0.5.16).
## v0.5.15
Released 07 June 2016
Hotfix update
Highlights:
* Fixed: Update vip mirror url to point to new server
* Fixed: Update waffles url to .ch
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.14...v0.5.15).
## v0.5.14 ## v0.5.14
Released 02 June 2016 Released 02 June 2016
+14 -2
View File
@@ -54,7 +54,10 @@ def main():
try: try:
locale.setlocale(locale.LC_ALL, "") locale.setlocale(locale.LC_ALL, "")
headphones.SYS_ENCODING = locale.getpreferredencoding() if headphones.SYS_PLATFORM == 'win32':
headphones.SYS_ENCODING = sys.getdefaultencoding().upper()
else:
headphones.SYS_ENCODING = locale.getpreferredencoding()
except (locale.Error, IOError): except (locale.Error, IOError):
pass pass
@@ -81,6 +84,8 @@ def main():
help='Prevent browser from launching on startup') help='Prevent browser from launching on startup')
parser.add_argument( parser.add_argument(
'--pidfile', help='Create a pid file (only relevant when running as a daemon)') '--pidfile', help='Create a pid file (only relevant when running as a daemon)')
parser.add_argument(
'--host', help='Specify a host (default - localhost)')
args = parser.parse_args() args = parser.parse_args()
@@ -170,6 +175,13 @@ def main():
else: else:
http_port = int(headphones.CONFIG.HTTP_PORT) http_port = int(headphones.CONFIG.HTTP_PORT)
# Force the http host if neccessary
if args.host:
http_host = args.host
logger.info('Using forced web server host: %s', http_host)
else:
http_host = headphones.CONFIG.HTTP_HOST
# Check if pyOpenSSL is installed. It is required for certificate generation # Check if pyOpenSSL is installed. It is required for certificate generation
# and for CherryPy. # and for CherryPy.
if headphones.CONFIG.ENABLE_HTTPS: if headphones.CONFIG.ENABLE_HTTPS:
@@ -183,7 +195,7 @@ def main():
# Try to start the server. Will exit here is address is already in use. # Try to start the server. Will exit here is address is already in use.
web_config = { web_config = {
'http_port': http_port, 'http_port': http_port,
'http_host': headphones.CONFIG.HTTP_HOST, 'http_host': http_host,
'http_root': headphones.CONFIG.HTTP_ROOT, 'http_root': headphones.CONFIG.HTTP_ROOT,
'http_proxy': headphones.CONFIG.HTTP_PROXY, 'http_proxy': headphones.CONFIG.HTTP_PROXY,
'enable_https': headphones.CONFIG.ENABLE_HTTPS, 'enable_https': headphones.CONFIG.ENABLE_HTTPS,
+1 -1
View File
@@ -1,4 +1,4 @@
##![Headphones Logo](https://github.com/rembo10/headphones/raw/master/data/images/headphoneslogo.png) Headphones ## ![Headphones Logo](https://github.com/rembo10/headphones/raw/master/data/images/headphoneslogo.png) Headphones
[![Build Status](https://travis-ci.org/rembo10/headphones.svg?branch=master)](https://travis-ci.org/rembo10/headphones) [![Build Status](https://travis-ci.org/rembo10/headphones.svg?branch=master)](https://travis-ci.org/rembo10/headphones)
[![Build Status](https://img.shields.io/travis/rembo10/headphones/develop.svg?label=develop%20branch%20build)](https://travis-ci.org/rembo10/headphones) [![Build Status](https://img.shields.io/travis/rembo10/headphones/develop.svg?label=develop%20branch%20build)](https://travis-ci.org/rembo10/headphones)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 B

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 635 B

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 B

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 B

After

Width:  |  Height:  |  Size: 79 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 B

After

Width:  |  Height:  |  Size: 86 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 B

After

Width:  |  Height:  |  Size: 86 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 B

After

Width:  |  Height:  |  Size: 86 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 B

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 B

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

+253 -45
View File
@@ -56,7 +56,7 @@
<label title="Password for web server authentication. Leave empty to disable."> <label title="Password for web server authentication. Leave empty to disable.">
HTTP Password HTTP Password
</label> </label>
<input type="password" name="http_password" value="${config['http_password']}" size="30"> <input type="password" name="http_password" value="${config['http_password'] | h}" size="30">
</div> </div>
<div class="row checkbox"> <div class="row checkbox">
<input type="checkbox" name="launch_browser" value="1" ${config['launch_browser']} /> <input type="checkbox" name="launch_browser" value="1" ${config['launch_browser']} />
@@ -133,6 +133,12 @@
</label> </label>
<input type="text" name="mb_ignore_age" value="${config['mb_ignore_age']}" size="4">days <input type="text" name="mb_ignore_age" value="${config['mb_ignore_age']}" size="4">days
</div> </div>
<div class="row checkbox">
<label title="Ignore MusicBrainz album updates missing a release date.">
Ignore Undated Releases
</label>
<input type="checkbox" name="mb_ignore_age_missing" id="mb_ignore_age_missing" value="${config['mb_ignore_age_missing']}">
</div>
</fieldset> </fieldset>
</td> </td>
</tr> </tr>
@@ -173,7 +179,7 @@
<label title="SABnzbd password. Leave empty if not applicable."> <label title="SABnzbd password. Leave empty if not applicable.">
SABnzbd Password SABnzbd Password
</label> </label>
<input type="password" name="sab_password" value="${config['sab_password']}" size="20"> <input type="password" name="sab_password" value="${config['sab_password'] | h}" size="20">
</div> </div>
<div class="row"> <div class="row">
<label title="SABnzbd API key. Can be found in SABnzbd settings."> <label title="SABnzbd API key. Can be found in SABnzbd settings.">
@@ -207,7 +213,7 @@
<label title="NZBGet password. Leave empty if not applicable"> <label title="NZBGet password. Leave empty if not applicable">
NZBget Password NZBget Password
</label> </label>
<input type="password" name="nzbget_password" value="${config['nzbget_password']}" size="20"> <input type="password" name="nzbget_password" value="${config['nzbget_password'] | h}" size="20">
</div> </div>
<div class="row"> <div class="row">
<label title="Name of NZBget category to add downloads to."> <label title="Name of NZBget category to add downloads to.">
@@ -312,6 +318,7 @@
<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_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_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) <input type="radio" name="torrent_downloader" id="torrent_downloader_deluge" value="3" ${config['torrent_downloader_deluge']}> Deluge (Beta)
<input type="radio" name="torrent_downloader" id="torrent_downloader_qbittorrent" value="4" ${config['torrent_downloader_qbittorrent']}> QBitTorrent
</fieldset> </fieldset>
<fieldset id="torrent_blackhole_options"> <fieldset id="torrent_blackhole_options">
<div class="row"> <div class="row">
@@ -359,7 +366,7 @@
</div> </div>
<div class="row"> <div class="row">
<label>Transmission Password</label> <label>Transmission Password</label>
<input type="password" name="transmission_password" value="${config['transmission_password']}" size="30"> <input type="password" name="transmission_password" value="${config['transmission_password'] | h}" size="30">
</div> </div>
<div class="row"> <div class="row">
<small>Note: With Transmission, you can specify a different download directory for downloads sent from Headphones. <small>Note: With Transmission, you can specify a different download directory for downloads sent from Headphones.
@@ -379,13 +386,33 @@
</div> </div>
<div class="row"> <div class="row">
<label>uTorrent Password</label> <label>uTorrent Password</label>
<input type="password" name="utorrent_password" value="${config['utorrent_password']}" size="30"> <input type="password" name="utorrent_password" value="${config['utorrent_password'] | h}" size="30">
</div> </div>
<div class="row"> <div class="row">
<label>uTorrent Label</label> <label>uTorrent Label</label>
<input type="text" name="utorrent_label" value="${config['utorrent_label']}" size="30"> <input type="text" name="utorrent_label" value="${config['utorrent_label']}" size="30">
</div> </div>
</fieldset> </fieldset>
<fieldset id="qbittorrent_options">
<small class="heading"><i class="fa fa-info-circle"></i> Note: Works with WebAPI Rev 11 and later (QBitTorrent 3.4.0 and later) </small>
<div class="row">
<label>QBitTorrent Host</label>
<input type="text" name="qbittorrent_host" value="${config['qbittorrent_host']}" size="30">
<small>usually http://localhost:8081</small>
</div>
<div class="row">
<label>QBitTorrent Username</label>
<input type="text" name="qbittorrent_username" value="${config['qbittorrent_username']}" size="30">
</div>
<div class="row">
<label>QBitTorrent Password</label>
<input type="password" name="qbittorrent_password" value="${config['qbittorrent_password']}" size="30">
</div>
<div class="row">
<label>QBitTorrent Label</label>
<input type="text" name="qbittorrent_label" value="${config['qbittorrent_label']}" size="30">
</div>
</fieldset>
<fieldset id="deluge_options"> <fieldset id="deluge_options">
<div class="row"> <div class="row">
<label>Deluge WebUI Host and Port</label> <label>Deluge WebUI Host and Port</label>
@@ -400,7 +427,7 @@
</div> </div>
<div class="row"> <div class="row">
<label>Deluge Password</label> <label>Deluge Password</label>
<input type="password" name="deluge_password" value="${config['deluge_password']}" size="30"> <input type="password" name="deluge_password" value="${config['deluge_password'] | h}" size="30">
</div> </div>
<div class="row"> <div class="row">
<small>Note: With Deluge, you can specify a different download directory for downloads sent from Headphones. <small>Note: With Deluge, you can specify a different download directory for downloads sent from Headphones.
@@ -466,7 +493,7 @@
</div> </div>
<div class="row"> <div class="row">
<label>Password</label> <label>Password</label>
<input class="hppass" type="password" value="${config['hppass']}" size="20"> <input class="hppass" type="password" value="${config['hppass'] | h}" size="20">
</div> </div>
<div class="row"> <div class="row">
<a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Don't have an account? Sign up!</a> <a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Don't have an account? Sign up!</a>
@@ -593,7 +620,7 @@
<fieldset> <fieldset>
<div class="row checkbox left"> <div class="row checkbox left">
<input id="use_waffles" type="checkbox" class="bigcheck" name="use_waffles" value="1" ${config['use_waffles']} /><label for="use_waffles"><span class="option">Waffles.fm</span></label> <input id="use_waffles" type="checkbox" class="bigcheck" name="use_waffles" value="1" ${config['use_waffles']} /><label for="use_waffles"><span class="option">Waffles.ch</span></label>
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
@@ -622,31 +649,59 @@
</div> </div>
<div class="row"> <div class="row">
<label>Password</label> <label>Password</label>
<input type="password" name="rutracker_password" value="${config['rutracker_password']}" size="36"> <input type="password" name="rutracker_password" value="${config['rutracker_password'] | h}" size="36">
</div> </div>
<div class="row"> <div class="row">
<label>Seed Ratio</label> <label>Seed Ratio</label>
<input type="text" class="override-float" name="rutracker_ratio" value="${config['rutracker_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding"> <input type="text" class="override-float" name="rutracker_ratio" value="${config['rutracker_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
</div> </div>
<div class="row">
<label>Session Cookie (Optional - Advanced)</label>
<input type="text" class="override-float" name="rutracker_cookie" value="${config['rutracker_cookie']}" size="10" title="bb_session cookie (Advanced)">
</div>
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<div class="row checkbox left"> <div class="row checkbox left">
<input id="use_whatcd" type="checkbox" class="bigcheck" name="use_whatcd" value="1" ${config['use_whatcd']} /><label for="use_whatcd"><span class="option">What.cd</span></label> <input id="use_apollo" type="checkbox" class="bigcheck" name="use_apollo" value="1" ${config['use_apollo']} /><label for="use_apollo"><span class="option">Apollo.rip</span></label>
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
<label>Username</label> <label>Username</label>
<input type="text" name="whatcd_username" value="${config['whatcd_username']}" size="36"> <input type="text" name="apollo_username" value="${config['apollo_username']}" size="36">
</div> </div>
<div class="row"> <div class="row">
<label>Password</label> <label>Password</label>
<input type="password" name="whatcd_password" value="${config['whatcd_password']}" size="36"> <input type="password" name="apollo_password" value="${config['apollo_password'] | h}" size="36">
</div>
<div class="row">
<label>URL</label>
<input type="text" name="apollo_url" value="${config['apollo_url']}" size="36">
</div> </div>
<div class="row"> <div class="row">
<label>Seed Ratio</label> <label>Seed Ratio</label>
<input type="text" class="override-float" name="whatcd_ratio" value="${config['whatcd_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding"> <input type="text" class="override-float" name="apollo_ratio" value="${config['apollo_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
</div>
</div>
</fieldset>
<fieldset>
<div class="row checkbox left">
<input id="use_redacted" type="checkbox" class="bigcheck" name="use_redacted" value="1" ${config['use_redacted']} /><label for="use_redacted"><span class="option">Redacted</span></label>
</div>
<div class="config">
<div class="row">
<label>Username</label>
<input type="text" name="redacted_username" value="${config['redacted_username']}" size="36">
</div>
<div class="row">
<label>Password</label>
<input type="password" name="redacted_password" value="${config['redacted_password'] | h}" size="36">
</div>
<div class="row">
<label>Seed Ratio</label>
<input type="text" class="override-float" name="redacted_ratio" value="${config['redacted_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
</div> </div>
</div> </div>
</fieldset> </fieldset>
@@ -671,8 +726,8 @@
<div class="config" id="torznab1"> <div class="config" id="torznab1">
<div class="row"> <div class="row">
<label>Torznab Host</label> <label>Torznab Host</label>
<input type="text" name="torznab_host" value="${config['torznab_host']}" size="30"> <input type="text" name="torznab_host" value="${config['torznab_host']}" size="36">
<small>e.g. http://localhost:9117/torznab/iptorrents</small> <small>e.g. http://localhost:9117/api/v2.0/indexers/demonoid/results/torznab/</small>
</div> </div>
<div class="row"> <div class="row">
<label>Torznab API</label> <label>Torznab API</label>
@@ -695,7 +750,7 @@
<div class="config" id="torznab${torznab_number}"> <div class="config" id="torznab${torznab_number}">
<div class="row"> <div class="row">
<label>Torznab Host</label> <label>Torznab Host</label>
<input type="text" name="torznab_host${torznab_number}" value="${torznab[0]}" size="30"> <input type="text" name="torznab_host${torznab_number}" value="${torznab[0]}" size="36">
</div> </div>
<div class="row"> <div class="row">
<label>Torznab API</label> <label>Torznab API</label>
@@ -743,6 +798,22 @@
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset>
<div class="row checkbox left">
<input id="use_tquattrecentonze" type="checkbox" class="bigcheck" name="use_tquattrecentonze" value="1" ${config['use_tquattrecentonze']} /><label for="use_tquattrecentonze"><span class="option">t411</span></label>
</div>
<div class="config">
<div class="row">
<label>Username</label>
<input type="text" name="tquattrecentonze_user" value="${config['tquattrecentonze_user']}" size="36">
</div>
<div class="row">
<label>Password</label>
<input type="password" name="tquattrecentonze_password" value="${config['tquattrecentonze_password'] | h}" size="36">
</div>
</div>
</fieldset>
</fieldset> </fieldset>
</td> </td>
@@ -884,30 +955,42 @@
<input type="checkbox" name="keep_nfo" value="1" ${config['keep_nfo']} /> <input type="checkbox" name="keep_nfo" value="1" ${config['keep_nfo']} />
</label> </label>
</div> </div>
<div class="row checkbox left clearfix nopad">
<label>
Embed lyrics
<input type="checkbox" name="embed_lyrics" value="1" ${config['embed_lyrics']}>
</label>
</div>
<div class="row checkbox left clearfix nopad">
<label>
Embed album art in each file
<input type="checkbox" name="embed_album_art" id="embed_album_art" value="1" ${config['embed_album_art']}>
</label>
</div>
<div class="row checkbox left clearfix nopad"> <div class="row checkbox left clearfix nopad">
<label> <label>
Add album art jpeg to album folder Add album art jpeg to album folder
<input type="checkbox" name="add_album_art" id="add_album_art" value="1" ${config['add_album_art']}> <input type="checkbox" name="add_album_art" id="add_album_art" value="1" ${config['add_album_art']}>
</label> </label>
</div> </div>
<div id="album_art_options" style="padding-left: 20px"> <div id="album_art_options" style="padding-left: 20px">
<div class="row"> <div class="row">
as <input type="text" class="override-float" name="album_art_format" value="${config['album_art_format']}" size="10">.jpg as <input type="text" class="override-float" name="album_art_format" value="${config['album_art_format']}" size="10">.jpg
</div> </div>
<small>Use $Artist/$artist, $Album/$album, $Year/$year, put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}').</small> <small>Use $Artist/$artist, $Album/$album, $Year/$year, put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}').</small>
</div> </div>
<div class="row checkbox left clearfix nopad">
<label> <div id="album_art_size_options" style="padding-left: 20px">
Embed album art in each file <div class="row">
<input type="checkbox" name="embed_album_art" value="1" ${config['embed_album_art']}> Album art min width <input type="text" class="override-float" name="album_art_min_width" value="${config['album_art_min_width']}" size="4" title="Only images with a pixel width greater or equal are considered as valid album art candidates. Default: 0.">\
</label> Album art max width <input type="text" class="override-float" name="album_art_max_width" value="${config['album_art_max_width']}" size="4" title="A maximum image width to downscale fetched images if they are too big.">
</div> </div>
<div class="row checkbox left clearfix nopad">
<label>
Embed lyrics
<input type="checkbox" name="embed_lyrics" value="1" ${config['embed_lyrics']}>
</label>
</div> </div>
<div class="row"> <div class="row">
<label> <label>
Destination Directory: Destination Directory:
@@ -964,7 +1047,7 @@
<label>SMTP User</label><input type="text" name="email_smtp_user" value="${config['email_smtp_user']}" size="254"> <label>SMTP User</label><input type="text" name="email_smtp_user" value="${config['email_smtp_user']}" size="254">
</div> </div>
<div class="row"> <div class="row">
<label>SMTP Password</label><input type="password" name="email_smtp_password" value="${config['email_smtp_password']}" size="50"> <label>SMTP Password</label><input type="password" name="email_smtp_password" value="${config['email_smtp_password'] | h}" size="50">
</div> </div>
<div class="row checkbox"> <div class="row checkbox">
<input type="text" class="override-float" name="email_smtp_port" value="${config['email_smtp_port']}" size="4"><label>SMTP Port</label> <input type="text" class="override-float" name="email_smtp_port" value="${config['email_smtp_port']}" size="4"><label>SMTP Port</label>
@@ -990,7 +1073,7 @@
<label>Growl Host:Port</label><input type="text" name="growl_host" value="${config['growl_host']}" size="30"> <label>Growl Host:Port</label><input type="text" name="growl_host" value="${config['growl_host']}" size="30">
</div> </div>
<div class="row"> <div class="row">
<label>Growl Password</label><input type="password" name="growl_password" value="${config['growl_password']}" size="30"> <label>Growl Password</label><input type="password" name="growl_password" value="${config['growl_password'] | h}" size="30">
</div> </div>
<div class="row checkbox"> <div class="row checkbox">
<input type="checkbox" name="growl_onsnatch" value="1" ${config['growl_onsnatch']} /><label>Notify on snatch?</label> <input type="checkbox" name="growl_onsnatch" value="1" ${config['growl_onsnatch']} /><label>Notify on snatch?</label>
@@ -1012,7 +1095,7 @@
<label>Username</label><input type="text" name="xbmc_username" value="${config['xbmc_username']}" size="30"> <label>Username</label><input type="text" name="xbmc_username" value="${config['xbmc_username']}" size="30">
</div> </div>
<div class="row"> <div class="row">
<label>Password</label><input type="password" name="xbmc_password" value="${config['xbmc_password']}" size="30"> <label>Password</label><input type="password" name="xbmc_password" value="${config['xbmc_password'] | h}" size="30">
</div> </div>
<div class="checkbox row"> <div class="checkbox row">
<input type="checkbox" name="xbmc_update" value="1" ${config['xbmc_update']} /><label>Update Library</label> <input type="checkbox" name="xbmc_update" value="1" ${config['xbmc_update']} /><label>Update Library</label>
@@ -1126,7 +1209,7 @@
<small>Username of your Plex client API (blank for none)</small> <small>Username of your Plex client API (blank for none)</small>
</div> </div>
<div class="row"> <div class="row">
<label>Plex Password</label><input type="password" name="plex_password" value="${config['plex_password']}" size="30"> <label>Plex Password</label><input type="password" name="plex_password" value="${config['plex_password'] | h}" size="30">
<small>Password of your Plex client API (blank for none)</small> <small>Password of your Plex client API (blank for none)</small>
</div> </div>
<div class="row"> <div class="row">
@@ -1226,7 +1309,7 @@
<label>Subsonic Username</label><input type="text" name="subsonic_username" value="${config['subsonic_username']}" size="30"> <label>Subsonic Username</label><input type="text" name="subsonic_username" value="${config['subsonic_username']}" size="30">
</div> </div>
<div class="row"> <div class="row">
<label>Subsonic Password</label><input type="password" name="subsonic_password" value="${config['subsonic_password']}" size="30"> <label>Subsonic Password</label><input type="password" name="subsonic_password" value="${config['subsonic_password'] | h}" size="30">
</div> </div>
</div> </div>
</fieldset> </fieldset>
@@ -1258,6 +1341,26 @@
</div> </div>
</fieldset> </fieldset>
<fieldset>
<div class="row checkbox left">
<input type="checkbox" class="bigcheck" name="slack_enabled" id="slack" value="1" ${config['slack_enabled']} /><label for="slack"><span class="option">Slack</span></label>
</div>
<div id="slackoptions">
<div class="row">
<label>Slack Webhook</label><input type="text" name="slack_url" value="${config['slack_url']}" size="50">
</div>
<div class="row">
<label>Channel</label><input type="text" name="slack_channel" value="${config['slack_channel']}" size="50">
</div>
<div class="row checkbox">
<label>Emoji</label><input type="text" name="slack_emoji" value="${config['slack_emoji']}" size="50">
</div>
<div class="row checkbox">
<input type="checkbox" name="slack_onsnatch" value="1" ${config['slack_onsnatch']} /><label>Notify on snatch?</label>
</div>
</div>
</fieldset>
<fieldset> <fieldset>
<div class="row checkbox left"> <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> <input type="checkbox" class="bigcheck" name="telegram_enabled" id="telegram" value="1" ${config['telegram_enabled']} /><label for="telegram"><span class="option">Telegram</span></label>
@@ -1275,6 +1378,23 @@
</div> </div>
</fieldset> </fieldset>
<fieldset>
<div class="row checkbox left">
<input type="checkbox" class="bigcheck" name="join_enabled" id="join" value="1" ${config['join_enabled']} /><label for="join"><span class="option">Join</span></label>
</div>
<div id="joinoptions">
<div class="row">
<label>Join API Key</label><input type="text" name="join_apikey" value="${config['join_apikey']}" size="50">
</div>
<div class="row">
<label>Device ID(s)</label><input type="text" name="join_deviceid" value="${config['join_deviceid']}" size="50"><small>Comma separated list. Leave blank to send to all devices</small>
</div>
<div class="row checkbox">
<input type="checkbox" name="join_onsnatch" value="1" ${config['join_onsnatch']} /><label>Notify on snatch?</label>
</div>
</div>
</fieldset>
</td> </td>
</tr> </tr>
</table> </table>
@@ -1570,6 +1690,10 @@
<label>Cache Directory</label> <label>Cache Directory</label>
<input type="text" name="cache_dir" value="${config['cache_dir']}" size="50"> <input type="text" name="cache_dir" value="${config['cache_dir']}" size="50">
</div> </div>
<div class="row">
<label>Post Processing Temporary Directory</label>
<input type="text" name="keep_torrent_files_dir" value="${config['keep_torrent_files_dir']}" size="50" title="Enter the directory to preserve files for seeding, default is system temp directory">
</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
@@ -1626,7 +1750,7 @@
<label>Username</label><input type="text" class="customuser" name="customuser" value="${config['customuser']}" size="20"> <label>Username</label><input type="text" class="customuser" name="customuser" value="${config['customuser']}" size="20">
</div> </div>
<div class="row"> <div class="row">
<label>Password</label><input type="password" class="custompass" name="custompass" value="${config['custompass']}" size="15"><br> <label>Password</label><input type="password" class="custompass" name="custompass" value="${config['custompass'] | h}" size="15"><br>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@@ -1639,7 +1763,7 @@
<label>Username</label><input type="text" class="hpuser" name="hpuser" value="${config['hpuser']}" size="20"> <label>Username</label><input type="text" class="hpuser" name="hpuser" value="${config['hpuser']}" size="20">
</div> </div>
<div class="row"> <div class="row">
<label>Password</label><input type="password" class="hppass" name="hppass" value="${config['hppass']}" size="20"><br> <label>Password</label><input type="password" class="hppass" name="hppass" value="${config['hppass'] | h}" size="20"><br>
<a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Get an Account!</a> <a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Get an Account!</a>
</div> </div>
</div> </div>
@@ -1814,9 +1938,36 @@
} }
}); });
if ($("#embed_album_art").is(":checked"))
{
$("#album_art_size_options").show();
}
else
{
if (!$("#add_album_art").is(":checked"))
{
$("#album_art_size_options").hide();
}
}
$("#embed_album_art").click(function(){
if ($("#embed_album_art").is(":checked"))
{
$("#album_art_size_options").slideDown();
}
else
{
if (!$("#add_album_art").is(":checked"))
{
$("#album_art_size_options").slideUp();
}
}
});
if ($("#add_album_art").is(":checked")) if ($("#add_album_art").is(":checked"))
{ {
$("#album_art_options").show(); $("#album_art_options").show();
$("#album_art_size_options").show();
} }
else else
{ {
@@ -1827,10 +1978,15 @@
if ($("#add_album_art").is(":checked")) if ($("#add_album_art").is(":checked"))
{ {
$("#album_art_options").slideDown(); $("#album_art_options").slideDown();
$("#album_art_size_options").slideDown();
} }
else else
{ {
$("#album_art_options").slideUp(); $("#album_art_options").slideUp();
if (!$("#embed_album_art").is(":checked"))
{
$("#album_art_size_options").slideUp();
}
} }
}); });
@@ -2035,6 +2191,27 @@
} }
}); });
if ($("#join").is(":checked"))
{
$("#joinoptions").show();
}
else
{
$("#joinoptions").hide();
}
$("#join").click(function(){
if ($("#join").is(":checked"))
{
$("#joinoptions").slideDown();
}
else
{
$("#joinoptions").slideUp();
}
});
if ($("#twitter").is(":checked")) if ($("#twitter").is(":checked"))
{ {
$("#twitteroptions").show(); $("#twitteroptions").show();
@@ -2055,6 +2232,27 @@
} }
}); });
if ($("#slack").is(":checked"))
{
$("#slackoptions").show();
}
else
{
$("#slackoptions").hide();
}
$("#slack").click(function(){
if ($("#slack").is(":checked"))
{
$("#slackoptions").slideDown();
}
else
{
$("#slackoptions").slideUp();
}
});
if ($("#telegram").is(":checked")) if ($("#telegram").is(":checked"))
{ {
$("#telegramoptions").show(); $("#telegramoptions").show();
@@ -2231,26 +2429,30 @@
if ($("#torrent_downloader_blackhole").is(":checked")) if ($("#torrent_downloader_blackhole").is(":checked"))
{ {
$("#transmission_options,#utorrent_options,#deluge_options").hide(); $("#transmission_options,#utorrent_options,#deluge_options,#qbittorrent_options").hide();
$("#torrent_blackhole_options").show(); $("#torrent_blackhole_options").show();
} }
if ($("#torrent_downloader_transmission").is(":checked")) if ($("#torrent_downloader_transmission").is(":checked"))
{ {
$("#torrent_blackhole_options,#utorrent_options,#deluge_options").hide(); $("#torrent_blackhole_options,#utorrent_options,#deluge_options,#qbittorrent_options").hide();
$("#transmission_options").show(); $("#transmission_options").show();
} }
if ($("#torrent_downloader_utorrent").is(":checked")) if ($("#torrent_downloader_utorrent").is(":checked"))
{ {
$("#torrent_blackhole_options,#transmission_options,#deluge_options").hide(); $("#torrent_blackhole_options,#transmission_options,#deluge_options,#qbittorrent_options").hide();
$("#utorrent_options").show(); $("#utorrent_options").show();
} }
if ($("#torrent_downloader_qbittorrent").is(":checked"))
{
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#deluge_options").hide();
$("#qbittorrent_options").show();
}
if ($("#torrent_downloader_deluge").is(":checked")) if ($("#torrent_downloader_deluge").is(":checked"))
{ {
$("#torrent_blackhole_options,#transmission_options,#utorrent_options").hide(); $("#torrent_blackhole_options,#transmission_options,#utorrent_options,#qbittorrent_options").hide();
$("#deluge_options").show(); $("#deluge_options").show();
} }
$('input[type=radio]').change(function(){ $('input[type=radio]').change(function(){
if ($("#preferred_bitrate").is(":checked")) if ($("#preferred_bitrate").is(":checked"))
{ {
@@ -2286,19 +2488,23 @@
} }
if ($("#torrent_downloader_blackhole").is(":checked")) if ($("#torrent_downloader_blackhole").is(":checked"))
{ {
$("#transmission_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() }); $("#transmission_options,#utorrent_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() });
} }
if ($("#torrent_downloader_transmission").is(":checked")) if ($("#torrent_downloader_transmission").is(":checked"))
{ {
$("#torrent_blackhole_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() }); $("#torrent_blackhole_options,#utorrent_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
} }
if ($("#torrent_downloader_utorrent").is(":checked")) if ($("#torrent_downloader_utorrent").is(":checked"))
{ {
$("#torrent_blackhole_options,#transmission_options,#deluge_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() }); $("#torrent_blackhole_options,#transmission_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
} }
if ($("#torrent_downloader_qbittorrent").is(":checked"))
{
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#qbittorrent_options").fadeIn() });
}
if ($("#torrent_downloader_deluge").is(":checked")) if ($("#torrent_downloader_deluge").is(":checked"))
{ {
$("#torrent_blackhole_options,#utorrent_options,#transmission_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() }); $("#torrent_blackhole_options,#utorrent_options,#transmission_options,#qbittorrent_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() });
} }
}); });
@@ -2395,12 +2601,14 @@
initConfigCheckbox("#use_mininova"); initConfigCheckbox("#use_mininova");
initConfigCheckbox("#use_waffles"); initConfigCheckbox("#use_waffles");
initConfigCheckbox("#use_rutracker"); initConfigCheckbox("#use_rutracker");
initConfigCheckbox("#use_whatcd"); initConfigCheckbox("#use_apollo");
initConfigCheckbox("#use_redacted");
initConfigCheckbox("#use_strike"); initConfigCheckbox("#use_strike");
initConfigCheckbox("#api_enabled"); initConfigCheckbox("#api_enabled");
initConfigCheckbox("#enable_https"); initConfigCheckbox("#enable_https");
initConfigCheckbox("#customauth"); initConfigCheckbox("#customauth");
initConfigCheckbox("#mb_ignore_age_missing");
initConfigCheckbox("#use_tquattrecentonze");
$('#twitterStep1').click(function () { $('#twitterStep1').click(function () {
$.get("/twitterStep1", function (data) {window.open(data); }) $.get("/twitterStep1", function (data) {window.open(data); })
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 B

After

Width:  |  Height:  |  Size: 89 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 93 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 B

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 99 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 485 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 B

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 688 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 688 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 968 B

After

Width:  |  Height:  |  Size: 87 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 B

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 B

After

Width:  |  Height:  |  Size: 83 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 838 B

After

Width:  |  Height:  |  Size: 56 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1003 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1010 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 B

After

Width:  |  Height:  |  Size: 106 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 B

After

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 B

After

Width:  |  Height:  |  Size: 107 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 B

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 B

After

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 B

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 B

After

Width:  |  Height:  |  Size: 158 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 B

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+1
View File
@@ -312,6 +312,7 @@ function doAjaxCall(url,elem,reload,form) {
$.ajax({ $.ajax({
url: url, url: url,
data: dataString, data: dataString,
type: 'POST',
beforeSend: function(jqXHR, settings) { beforeSend: function(jqXHR, settings) {
// Start loader etc. // Start loader etc.
feedback.prepend(loader); feedback.prepend(loader);
+23 -6
View File
@@ -86,8 +86,8 @@ CURRENT_VERSION = None
LATEST_VERSION = None LATEST_VERSION = None
COMMITS_BEHIND = None COMMITS_BEHIND = None
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma"] LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma", "opus"]
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff"] LOSSLESS_MEDIA_FORMATS = ["flac", "aiff", "aif"]
MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS
MIRRORLIST = ["musicbrainz.org", "headphones", "custom"] MIRRORLIST = ["musicbrainz.org", "headphones", "custom"]
@@ -306,9 +306,10 @@ def initialize_scheduler():
minutes=minutes) minutes=minutes)
# Remove Torrent + data if Post Processed and finished Seeding # Remove Torrent + data if Post Processed and finished Seeding
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL if headphones.CONFIG.TORRENT_DOWNLOADER != 0:
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0, minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
minutes=minutes) schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
minutes=minutes)
# Start scheduler # Start scheduler
if start_jobs and len(SCHED.get_jobs()): if start_jobs and len(SCHED.get_jobs()):
@@ -376,7 +377,7 @@ def dbcheck():
c.execute( c.execute(
'CREATE TABLE IF NOT EXISTS alltracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER, Location TEXT, BitRate INTEGER, CleanName TEXT, Format TEXT, ReleaseID TEXT)') 'CREATE TABLE IF NOT EXISTS alltracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER, Location TEXT, BitRate INTEGER, CleanName TEXT, Format TEXT, ReleaseID TEXT)')
c.execute( c.execute(
'CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT, Kind TEXT)') 'CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT, Kind TEXT, TorrentHash TEXT)')
# Matched is a temporary value used to see if there was a match found in # Matched is a temporary value used to see if there was a match found in
# alltracks # alltracks
c.execute( c.execute(
@@ -416,6 +417,8 @@ def dbcheck():
'CREATE INDEX IF NOT EXISTS tracks_Location ON tracks(Location ASC)') 'CREATE INDEX IF NOT EXISTS tracks_Location ON tracks(Location ASC)')
c.execute( c.execute(
'CREATE INDEX IF NOT EXISTS alltracks_Location ON alltracks(Location ASC)') 'CREATE INDEX IF NOT EXISTS alltracks_Location ON alltracks(Location ASC)')
c.execute(
'CREATE INDEX IF NOT EXISTS tracks_artistid ON tracks(ArtistID ASC)')
try: try:
c.execute('SELECT IncludeExtras from artists') c.execute('SELECT IncludeExtras from artists')
@@ -613,6 +616,20 @@ def dbcheck():
except sqlite3.OperationalError: except sqlite3.OperationalError:
c.execute('ALTER TABLE artists ADD COLUMN MetaCritic TEXT DEFAULT NULL') c.execute('ALTER TABLE artists ADD COLUMN MetaCritic TEXT DEFAULT NULL')
try:
c.execute('SELECT TorrentHash from snatched')
except sqlite3.OperationalError:
c.execute('ALTER TABLE snatched ADD COLUMN TorrentHash TEXT')
c.execute('UPDATE snatched SET TorrentHash = FolderName WHERE Status LIKE "Seed_%"')
# One off script to set CleanName to lower case
clean_name_mixed = c.execute('SELECT CleanName FROM have ORDER BY Date Desc').fetchone()
if clean_name_mixed and clean_name_mixed[0] != clean_name_mixed[0].lower():
logger.info("Updating track clean name, this could take some time...")
c.execute('UPDATE tracks SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
c.execute('UPDATE alltracks SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
c.execute('UPDATE have SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
conn.commit() conn.commit()
c.close() c.close()
+154 -6
View File
@@ -13,16 +13,164 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from headphones import request, db, logger import struct
from six.moves.urllib.parse import urlencode
from io import BytesIO
import headphones
from headphones import db, request, logger
def getAlbumArt(albumid): def getAlbumArt(albumid):
myDB = db.DBConnection()
asin = myDB.action(
'SELECT AlbumASIN from albums WHERE AlbumID=?', [albumid]).fetchone()[0]
if asin: artwork_path = None
return 'http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % asin artwork = None
# CAA
logger.info("Searching for artwork at CAA")
artwork_path = 'http://coverartarchive.org/release-group/%s/front' % albumid
artwork = getartwork(artwork_path)
if artwork:
logger.info("Artwork found at CAA")
return artwork_path, artwork
# Amazon
logger.info("Searching for artwork at Amazon")
myDB = db.DBConnection()
dbalbum = myDB.action(
'SELECT ArtistName, AlbumTitle, ReleaseID, AlbumASIN FROM albums WHERE AlbumID=?',
[albumid]).fetchone()
if dbalbum['AlbumASIN']:
artwork_path = 'http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % dbalbum['AlbumASIN']
artwork = getartwork(artwork_path)
if artwork:
logger.info("Artwork found at Amazon")
return artwork_path, artwork
# last.fm
from headphones import lastfm
logger.info("Searching for artwork at last.fm")
if dbalbum['ReleaseID'] != albumid:
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'])
if not data:
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
album=dbalbum['AlbumTitle'])
else:
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
album=dbalbum['AlbumTitle'])
if data:
try:
images = data['album']['image']
for image in images:
if image['size'] == 'extralarge':
artwork_path = image['#text']
elif image['size'] == 'mega':
artwork_path = image['#text']
break
except KeyError:
artwork_path = None
if artwork_path:
artwork = getartwork(artwork_path)
if artwork:
logger.info("Artwork found at last.fm")
return artwork_path, artwork
logger.info("No suitable album art found.")
return None, None
def jpeg(bites):
fhandle = BytesIO(bites)
try:
fhandle.seek(0)
size = 2
ftype = 0
while not 0xc0 <= ftype <= 0xcf:
fhandle.seek(size, 1)
byte = fhandle.read(1)
while ord(byte) == 0xff:
byte = fhandle.read(1)
ftype = ord(byte)
size = struct.unpack('>H', fhandle.read(2))[0] - 2
fhandle.seek(1, 1)
height, width = struct.unpack('>HH', fhandle.read(4))
return width, height
except struct.error:
return None, None
except TypeError:
return None, None
def png(bites):
try:
check = struct.unpack('>i', bites[4:8])[0]
if check != 0x0d0a1a0a:
return None, None
return struct.unpack('>ii', bites[16:24])
except struct.error:
return None, None
def get_image_data(bites):
type = None
width = None
height = None
if len(bites) < 24:
return None, None, None
peek = bites[0:2]
if peek == b'\xff\xd8':
width, height = jpeg(bites)
type = 'jpg'
elif peek == b'\x89P':
width, height = png(bites)
type = 'png'
return type, width, height
def getartwork(artwork_path):
artwork = bytes()
minwidth = 0
maxwidth = 0
if headphones.CONFIG.ALBUM_ART_MIN_WIDTH:
minwidth = int(headphones.CONFIG.ALBUM_ART_MIN_WIDTH)
if headphones.CONFIG.ALBUM_ART_MAX_WIDTH:
maxwidth = int(headphones.CONFIG.ALBUM_ART_MAX_WIDTH)
resp = request.request_response(artwork_path, timeout=20, stream=True, whitelist_status_code=404)
if resp:
img_width = None
for chunk in resp.iter_content(chunk_size=1024):
artwork += chunk
if not img_width and (minwidth or maxwidth):
img_type, img_width, img_height = get_image_data(artwork)
# Check min/max
if img_width and (minwidth or maxwidth):
if minwidth and img_width < minwidth:
logger.info("Artwork is too small. Type: %s. Width: %s. Height: %s",
img_type, img_width, img_height)
artwork = None
break
elif maxwidth and img_width > maxwidth:
# Downsize using proxy service to max width
artwork_path = '{0}?{1}'.format('http://images.weserv.nl/', urlencode({
'url': artwork_path.replace('http://', ''),
'w': maxwidth,
}))
artwork = bytes()
r = request.request_response(artwork_path, timeout=20, stream=True, whitelist_status_code=404)
if r:
for chunk in r.iter_content(chunk_size=1024):
artwork += chunk
r.close()
logger.info("Artwork is greater than the maximum width, downsized using proxy service")
break
resp.close()
return artwork
def getCachedArt(albumid): def getCachedArt(albumid):
+2 -2
View File
@@ -85,8 +85,8 @@ def switch(AlbumID, ReleaseID):
# Update have track counts on index # Update have track counts on index
totaltracks = len(myDB.select( totaltracks = len(myDB.select(
'SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', 'SELECT TrackTitle from tracks AS tr INNER JOIN albums AS al ON al.AlbumID = tr.AlbumID WHERE al.ArtistID=? '
[newalbumdata['ArtistID']])) 'AND al.Status != "Ignored"', [newalbumdata['ArtistID']]))
havetracks = len(myDB.select( havetracks = len(myDB.select(
'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', 'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
[newalbumdata['ArtistID']])) [newalbumdata['ArtistID']]))
+27 -6
View File
@@ -16,7 +16,7 @@
import os import os
import headphones import headphones
from headphones import db, helpers, logger, lastfm, request from headphones import db, helpers, logger, lastfm, request, mb
LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4" LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
@@ -290,6 +290,14 @@ class Cache(object):
data = lastfm.request_lastfm("artist.getinfo", mbid=self.id, api_key=LASTFM_API_KEY) data = lastfm.request_lastfm("artist.getinfo", mbid=self.id, api_key=LASTFM_API_KEY)
# Try with name if not found
if not data:
dbartist = myDB.action('SELECT ArtistName, Type FROM artists WHERE ArtistID=?', [self.id]).fetchone()
if dbartist:
data = lastfm.request_lastfm("artist.getinfo",
artist=helpers.clean_musicbrainz_name(dbartist['ArtistName']),
api_key=LASTFM_API_KEY)
if not data: if not data:
return return
@@ -315,18 +323,31 @@ class Cache(object):
else: else:
dbalbum = myDB.action( dbalbum = myDB.action(
'SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?', 'SELECT ArtistName, AlbumTitle, ReleaseID, Type FROM albums WHERE AlbumID=?',
[self.id]).fetchone() [self.id]).fetchone()
if dbalbum['ReleaseID'] != self.id: if dbalbum['ReleaseID'] != self.id:
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'], data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'],
api_key=LASTFM_API_KEY) api_key=LASTFM_API_KEY)
if not data: if not data:
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'], data = lastfm.request_lastfm("album.getinfo",
album=dbalbum['AlbumTitle'], artist=helpers.clean_musicbrainz_name(dbalbum['ArtistName']),
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
api_key=LASTFM_API_KEY) api_key=LASTFM_API_KEY)
else: else:
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'], if dbalbum['Type'] != "part of":
album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY) data = lastfm.request_lastfm("album.getinfo",
artist=helpers.clean_musicbrainz_name(dbalbum['ArtistName']),
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
api_key=LASTFM_API_KEY)
else:
# Series, use actual artist for the release-group
artist = mb.getArtistForReleaseGroup(self.id)
if artist:
data = lastfm.request_lastfm("album.getinfo",
artist=helpers.clean_musicbrainz_name(artist),
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
api_key=LASTFM_API_KEY)
if not data: if not data:
return return
+30 -4
View File
@@ -34,11 +34,18 @@ _CONFIG_DEFINITIONS = {
'ADD_ALBUM_ART': (int, 'General', 0), 'ADD_ALBUM_ART': (int, 'General', 0),
'ADVANCEDENCODER': (str, 'General', ''), 'ADVANCEDENCODER': (str, 'General', ''),
'ALBUM_ART_FORMAT': (str, 'General', 'folder'), 'ALBUM_ART_FORMAT': (str, 'General', 'folder'),
'ALBUM_ART_MIN_WIDTH': (str, 'General', ''),
'ALBUM_ART_MAX_WIDTH': (str, 'General', ''),
# This is used in importer.py to determine how complete an album needs to # This is used in importer.py to determine how complete an album needs to
# be - to be considered "downloaded". Percentage from 0-100 # be - to be considered "downloaded". Percentage from 0-100
'ALBUM_COMPLETION_PCT': (int, 'Advanced', 80), 'ALBUM_COMPLETION_PCT': (int, 'Advanced', 80),
'API_ENABLED': (int, 'General', 0), 'API_ENABLED': (int, 'General', 0),
'API_KEY': (str, 'General', ''), 'API_KEY': (str, 'General', ''),
'APOLLO': (int, 'Apollo.rip', 0),
'APOLLO_PASSWORD': (str, 'Apollo.rip', ''),
'APOLLO_RATIO': (str, 'Apollo.rip', ''),
'APOLLO_USERNAME': (str, 'Apollo.rip', ''),
'APOLLO_URL': (str, 'Apollo.rip', 'https://apollo.rip'),
'AUTOWANT_ALL': (int, 'General', 0), 'AUTOWANT_ALL': (int, 'General', 0),
'AUTOWANT_MANUALLY_ADDED': (int, 'General', 1), 'AUTOWANT_MANUALLY_ADDED': (int, 'General', 1),
'AUTOWANT_UPCOMING': (int, 'General', 1), 'AUTOWANT_UPCOMING': (int, 'General', 1),
@@ -138,12 +145,17 @@ _CONFIG_DEFINITIONS = {
'IGNORED_FILES': (list, 'Advanced', []), # path 'IGNORED_FILES': (list, 'Advanced', []), # path
'INCLUDE_EXTRAS': (int, 'General', 0), 'INCLUDE_EXTRAS': (int, 'General', 0),
'INTERFACE': (str, 'General', 'default'), 'INTERFACE': (str, 'General', 'default'),
'JOIN_APIKEY': (str, 'Join', ''),
'JOIN_DEVICEID': (str, 'Join', ''),
'JOIN_ENABLED': (int, 'Join', 0),
'JOIN_ONSNATCH': (int, 'Join', 0),
'JOURNAL_MODE': (str, 'Advanced', 'wal'), 'JOURNAL_MODE': (str, 'Advanced', 'wal'),
'KAT': (int, 'Kat', 0), 'KAT': (int, 'Kat', 0),
'KAT_PROXY_URL': (str, 'Kat', ''), 'KAT_PROXY_URL': (str, 'Kat', ''),
'KAT_RATIO': (str, 'Kat', ''), 'KAT_RATIO': (str, 'Kat', ''),
'KEEP_NFO': (int, 'General', 0), 'KEEP_NFO': (int, 'General', 0),
'KEEP_TORRENT_FILES': (int, 'General', 0), 'KEEP_TORRENT_FILES': (int, 'General', 0),
'KEEP_TORRENT_FILES_DIR': (path, 'General', ''),
'LASTFM_USERNAME': (str, 'General', ''), 'LASTFM_USERNAME': (str, 'General', ''),
'LAUNCH_BROWSER': (int, 'General', 1), 'LAUNCH_BROWSER': (int, 'General', 1),
'LIBRARYSCAN': (int, 'General', 1), 'LIBRARYSCAN': (int, 'General', 1),
@@ -155,6 +167,7 @@ _CONFIG_DEFINITIONS = {
'LOSSLESS_BITRATE_TO': (int, 'General', 0), 'LOSSLESS_BITRATE_TO': (int, 'General', 0),
'LOSSLESS_DESTINATION_DIR': (path, 'General', ''), 'LOSSLESS_DESTINATION_DIR': (path, 'General', ''),
'MB_IGNORE_AGE': (int, 'General', 365), 'MB_IGNORE_AGE': (int, 'General', 365),
'MB_IGNORE_AGE_MISSING': (int, 'General', 0),
'MININOVA': (int, 'Mininova', 0), 'MININOVA': (int, 'Mininova', 0),
'MININOVA_RATIO': (str, 'Mininova', ''), 'MININOVA_RATIO': (str, 'Mininova', ''),
'MIRROR': (str, 'General', 'musicbrainz.org'), 'MIRROR': (str, 'General', 'musicbrainz.org'),
@@ -226,6 +239,10 @@ _CONFIG_DEFINITIONS = {
'PUSHOVER_KEYS': (str, 'Pushover', ''), 'PUSHOVER_KEYS': (str, 'Pushover', ''),
'PUSHOVER_ONSNATCH': (int, 'Pushover', 0), 'PUSHOVER_ONSNATCH': (int, 'Pushover', 0),
'PUSHOVER_PRIORITY': (int, 'Pushover', 0), 'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
'QBITTORRENT_HOST': (str, 'QBitTorrent', ''),
'QBITTORRENT_LABEL': (str, 'QBitTorrent', ''),
'QBITTORRENT_PASSWORD': (str, 'QBitTorrent', ''),
'QBITTORRENT_USERNAME': (str, 'QBitTorrent', ''),
'RENAME_FILES': (int, 'General', 0), 'RENAME_FILES': (int, 'General', 0),
'RENAME_UNPROCESSED': (bool_int, 'General', 1), 'RENAME_UNPROCESSED': (bool_int, 'General', 1),
'RENAME_FROZEN': (bool_int, 'General', 1), 'RENAME_FROZEN': (bool_int, 'General', 1),
@@ -236,6 +253,7 @@ _CONFIG_DEFINITIONS = {
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''), 'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
'RUTRACKER_RATIO': (str, 'Rutracker', ''), 'RUTRACKER_RATIO': (str, 'Rutracker', ''),
'RUTRACKER_USER': (str, 'Rutracker', ''), 'RUTRACKER_USER': (str, 'Rutracker', ''),
'RUTRACKER_COOKIE': (str, 'Rutracker', ''),
'SAB_APIKEY': (str, 'SABnzbd', ''), 'SAB_APIKEY': (str, 'SABnzbd', ''),
'SAB_CATEGORY': (str, 'SABnzbd', ''), 'SAB_CATEGORY': (str, 'SABnzbd', ''),
'SAB_HOST': (str, 'SABnzbd', ''), 'SAB_HOST': (str, 'SABnzbd', ''),
@@ -243,6 +261,11 @@ _CONFIG_DEFINITIONS = {
'SAB_USERNAME': (str, 'SABnzbd', ''), 'SAB_USERNAME': (str, 'SABnzbd', ''),
'SAMPLINGFREQUENCY': (int, 'General', 44100), 'SAMPLINGFREQUENCY': (int, 'General', 44100),
'SEARCH_INTERVAL': (int, 'General', 1440), 'SEARCH_INTERVAL': (int, 'General', 1440),
'SLACK_ENABLED': (int, 'Slack', 0),
'SLACK_URL': (str, 'Slack', ''),
'SLACK_CHANNEL': (str, 'Slack', ''),
'SLACK_EMOJI': (str, 'Slack', ''),
'SLACK_ONSNATCH': (int, 'Slack', 0),
'SOFT_CHROOT': (path, 'General', ''), 'SOFT_CHROOT': (path, 'General', ''),
'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'), 'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'),
'SONGKICK_ENABLED': (int, 'Songkick', 1), 'SONGKICK_ENABLED': (int, 'Songkick', 1),
@@ -274,6 +297,9 @@ _CONFIG_DEFINITIONS = {
'TWITTER_PASSWORD': (str, 'Twitter', ''), 'TWITTER_PASSWORD': (str, 'Twitter', ''),
'TWITTER_PREFIX': (str, 'Twitter', 'Headphones'), 'TWITTER_PREFIX': (str, 'Twitter', 'Headphones'),
'TWITTER_USERNAME': (str, 'Twitter', ''), 'TWITTER_USERNAME': (str, 'Twitter', ''),
'TQUATTRECENTONZE': (int, 'tquattrecentonze', 0),
'TQUATTRECENTONZE_PASSWORD': (str, 'tquattrecentonze', ''),
'TQUATTRECENTONZE_USER': (str, 'tquattrecentonze', ''),
'UPDATE_DB_INTERVAL': (int, 'General', 24), 'UPDATE_DB_INTERVAL': (int, 'General', 24),
'USENET_RETENTION': (int, 'General', '1500'), 'USENET_RETENTION': (int, 'General', '1500'),
'UTORRENT_HOST': (str, 'uTorrent', ''), 'UTORRENT_HOST': (str, 'uTorrent', ''),
@@ -286,10 +312,10 @@ _CONFIG_DEFINITIONS = {
'WAFFLES_PASSKEY': (str, 'Waffles', ''), 'WAFFLES_PASSKEY': (str, 'Waffles', ''),
'WAFFLES_RATIO': (str, 'Waffles', ''), 'WAFFLES_RATIO': (str, 'Waffles', ''),
'WAFFLES_UID': (str, 'Waffles', ''), 'WAFFLES_UID': (str, 'Waffles', ''),
'WHATCD': (int, 'What.cd', 0), 'REDACTED': (int, 'Redacted', 0),
'WHATCD_PASSWORD': (str, 'What.cd', ''), 'REDACTED_USERNAME': (str, 'Redacted', ''),
'WHATCD_RATIO': (str, 'What.cd', ''), 'REDACTED_PASSWORD': (str, 'Redacted', ''),
'WHATCD_USERNAME': (str, 'What.cd', ''), 'REDACTED_RATIO': (str, 'Redacted', ''),
'XBMC_ENABLED': (int, 'XBMC', 0), 'XBMC_ENABLED': (int, 'XBMC', 0),
'XBMC_HOST': (str, 'XBMC', ''), 'XBMC_HOST': (str, 'XBMC', ''),
'XBMC_NOTIFY': (int, 'XBMC', 0), 'XBMC_NOTIFY': (int, 'XBMC', 0),
+36
View File
@@ -0,0 +1,36 @@
import pprint
import sys
import threading
import traceback
from headphones import logger
def cry():
"""
Logs thread traces.
"""
tmap = {}
main_thread = None
# get a map of threads by their ID so we can print their names
# during the traceback dump
for t in threading.enumerate():
if t.ident:
tmap[t.ident] = t
else:
main_thread = t
# Loop over each thread's current frame, writing info about it
for tid, frame in sys._current_frames().iteritems():
thread = tmap.get(tid, main_thread)
lines = []
lines.append('%s\n' % thread.getName())
lines.append('========================================\n')
lines += traceback.format_stack(frame)
lines.append('========================================\n')
lines.append('LOCAL VARIABLES:\n')
lines.append('========================================\n')
lines.append(pprint.pformat(frame.f_locals))
lines.append('\n\n')
logger.info("".join(lines))
+82 -41
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# This file is part of Headphones. # This file is part of Headphones.
# #
# Headphones is free software: you can redistribute it and/or modify # Headphones is free software: you can redistribute it and/or modify
@@ -49,6 +51,7 @@ delugeweb_auth = {}
delugeweb_url = '' delugeweb_url = ''
deluge_verify_cert = False deluge_verify_cert = False
scrub_logs = True scrub_logs = True
headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}
def _scrubber(text): def _scrubber(text):
@@ -80,8 +83,8 @@ def addTorrent(link, data=None, name=None):
result = {} result = {}
retid = False retid = False
url_what = ['https://what.cd/', 'http://what.cd/'] url_apollo = ['https://apollo.rip/', 'http://apollo.rip/']
url_waffles = ['https://waffles.fm/', 'http://waffles.fm/'] url_waffles = ['https://waffles.ch/', 'http://waffles.ch/']
if link.lower().startswith('magnet:'): if link.lower().startswith('magnet:'):
logger.debug('Deluge: Got a magnet link: %s' % _scrubber(link)) logger.debug('Deluge: Got a magnet link: %s' % _scrubber(link))
@@ -94,7 +97,7 @@ def addTorrent(link, data=None, name=None):
if link.lower().startswith(tuple(url_waffles)): if link.lower().startswith(tuple(url_waffles)):
if 'rss=' not in link: if 'rss=' not in link:
link = link + '&rss=1' link = link + '&rss=1'
if link.lower().startswith(tuple(url_what)): if link.lower().startswith(tuple(url_apollo)):
logger.debug('Deluge: Using different User-Agent for this site') logger.debug('Deluge: Using different User-Agent for this site')
user_agent = 'Headphones' user_agent = 'Headphones'
# This method will make Deluge download the file # This method will make Deluge download the file
@@ -104,11 +107,11 @@ def addTorrent(link, data=None, name=None):
# return addTorrent(local_torrent_path) # return addTorrent(local_torrent_path)
else: else:
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' 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} get_headers = {'User-Agent': user_agent}
torrentfile = '' torrentfile = ''
logger.debug('Deluge: Trying to download (GET)') logger.debug('Deluge: Trying to download (GET)')
try: try:
r = requests.get(link, headers=headers) r = requests.get(link, headers=get_headers)
if r.status_code == 200: if r.status_code == 200:
logger.debug('Deluge: 200 OK') logger.debug('Deluge: 200 OK')
# .text will ruin the encoding for some torrents # .text will ruin the encoding for some torrents
@@ -135,7 +138,10 @@ def addTorrent(link, data=None, name=None):
# remove '.torrent' suffix # remove '.torrent' suffix
if name[-len('.torrent'):] == '.torrent': if name[-len('.torrent'):] == '.torrent':
name = name[:-len('.torrent')] name = name[:-len('.torrent')]
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40])) try:
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
except:
logger.debug('Deluge: Sending Deluge torrent with problematic name and some content')
result = {'type': 'torrent', result = {'type': 'torrent',
'name': name, 'name': name,
'content': torrentfile} 'content': torrentfile}
@@ -164,7 +170,10 @@ def addTorrent(link, data=None, name=None):
# remove '.torrent' suffix # remove '.torrent' suffix
if name[-len('.torrent'):] == '.torrent': if name[-len('.torrent'):] == '.torrent':
name = name[:-len('.torrent')] name = name[:-len('.torrent')]
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40])) try:
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
except UnicodeDecodeError:
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name.decode('utf-8'), str(torrentfile)[:40]))
result = {'type': 'torrent', result = {'type': 'torrent',
'name': name, 'name': name,
'content': torrentfile} 'content': torrentfile}
@@ -199,7 +208,7 @@ def getTorrentFolder(result):
], ],
"id": 21}) "id": 21})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
result['total_done'] = json.loads(response.text)['result']['total_done'] result['total_done'] = json.loads(response.text)['result']['total_done']
tries = 0 tries = 0
@@ -207,7 +216,7 @@ def getTorrentFolder(result):
tries += 1 tries += 1
time.sleep(5) time.sleep(5)
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
result['total_done'] = json.loads(response.text)['result']['total_done'] result['total_done'] = json.loads(response.text)['result']['total_done']
post_data = json.dumps({"method": "web.get_torrent_status", post_data = json.dumps({"method": "web.get_torrent_status",
@@ -226,7 +235,7 @@ def getTorrentFolder(result):
"id": 23}) "id": 23})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
result['save_path'] = json.loads(response.text)['result']['save_path'] result['save_path'] = json.loads(response.text)['result']['save_path']
result['name'] = json.loads(response.text)['result']['name'] result['name'] = json.loads(response.text)['result']['name']
@@ -242,18 +251,45 @@ def removeTorrent(torrentid, remove_data=False):
_get_auth() _get_auth()
try: try:
result = False logger.debug('Deluge: Checking if torrent %s finished seeding' % str(torrentid))
post_data = json.dumps({"method": "core.remove_torrent", post_data = json.dumps({"method": "web.get_torrent_status",
"params": [ "params": [
torrentid, torrentid,
remove_data [
], "name",
"id": 25}) "ratio",
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, "state"
verify=deluge_verify_cert) ]
result = json.loads(response.text)['result'] ],
"id": 26})
return result response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert, headers=headers)
try:
state = json.loads(response.text)['result']['state']
except KeyError as e:
logger.debug('Deluge: "state" KeyError when trying to remove torrent %s' % str(torrentid))
return False
not_finished = ["queued", "seeding", "downloading", "checking", "error"]
result = False
if state.lower() in not_finished:
logger.debug('Deluge: Torrent %s is either queued or seeding, not removing yet' % str(torrentid))
return False
else:
logger.debug('Deluge: Removing torrent %s' % str(torrentid))
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,
verify=deluge_verify_cert, headers=headers)
result = json.loads(response.text)['result']
return result
except Exception as e: except Exception as e:
logger.error('Deluge: Removing torrent failed: %s' % str(e)) logger.error('Deluge: Removing torrent failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines() formatted_lines = traceback.format_exc().splitlines()
@@ -269,7 +305,8 @@ def _get_auth():
delugeweb_host = headphones.CONFIG.DELUGE_HOST delugeweb_host = headphones.CONFIG.DELUGE_HOST
delugeweb_cert = headphones.CONFIG.DELUGE_CERT delugeweb_cert = headphones.CONFIG.DELUGE_CERT
delugeweb_password = headphones.CONFIG.DELUGE_PASSWORD delugeweb_password = headphones.CONFIG.DELUGE_PASSWORD
logger.debug('Deluge: Using password %s******%s' % (delugeweb_password[0], delugeweb_password[-1])) if len(delugeweb_password) > 0:
logger.debug('Deluge: Using password %s******%s' % (delugeweb_password[0], delugeweb_password[-1]))
if not delugeweb_host.startswith('http'): if not delugeweb_host.startswith('http'):
delugeweb_host = 'http://%s' % delugeweb_host delugeweb_host = 'http://%s' % delugeweb_host
@@ -292,12 +329,12 @@ def _get_auth():
"id": 1}) "id": 1})
try: try:
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
except requests.ConnectionError: except requests.ConnectionError:
try: try:
logger.debug('Deluge: Connection failed, let\'s try HTTPS just in case') logger.debug('Deluge: Connection failed, let\'s try HTTPS just in case')
response = requests.post(delugeweb_url.replace('http:', 'https:'), data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url.replace('http:', 'https:'), data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
# If the previous line didn't fail, change delugeweb_url for the rest of this session # If the previous line didn't fail, change delugeweb_url for the rest of this session
logger.error('Deluge: Switching to HTTPS, but certificate won\'t be verified because NO CERTIFICATE WAS CONFIGURED!') logger.error('Deluge: Switching to HTTPS, but certificate won\'t be verified because NO CERTIFICATE WAS CONFIGURED!')
delugeweb_url = delugeweb_url.replace('http:', 'https:') delugeweb_url = delugeweb_url.replace('http:', 'https:')
@@ -322,7 +359,7 @@ def _get_auth():
"id": 10}) "id": 10})
try: try:
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
except Exception as e: except Exception as e:
logger.error('Deluge: Authentication failed: %s' % str(e)) logger.error('Deluge: Authentication failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines() formatted_lines = traceback.format_exc().splitlines()
@@ -339,7 +376,7 @@ def _get_auth():
"id": 11}) "id": 11})
try: try:
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
except Exception as e: except Exception as e:
logger.error('Deluge: Authentication failed: %s' % str(e)) logger.error('Deluge: Authentication failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines() formatted_lines = traceback.format_exc().splitlines()
@@ -347,7 +384,8 @@ def _get_auth():
return None return None
delugeweb_hosts = json.loads(response.text)['result'] delugeweb_hosts = json.loads(response.text)['result']
if len(delugeweb_hosts) == 0: # Check if delugeweb_hosts is None before checking its length
if not delugeweb_hosts or len(delugeweb_hosts) == 0:
logger.error('Deluge: WebUI does not contain daemons') logger.error('Deluge: WebUI does not contain daemons')
return None return None
@@ -357,7 +395,7 @@ def _get_auth():
try: try:
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
except Exception as e: except Exception as e:
logger.error('Deluge: Authentication failed: %s' % str(e)) logger.error('Deluge: Authentication failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines() formatted_lines = traceback.format_exc().splitlines()
@@ -370,7 +408,7 @@ def _get_auth():
try: try:
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
except Exception as e: except Exception as e:
logger.error('Deluge: Authentication failed: %s' % str(e)) logger.error('Deluge: Authentication failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines() formatted_lines = traceback.format_exc().splitlines()
@@ -395,7 +433,7 @@ def _add_torrent_magnet(result):
"params": [result['url'], {}], "params": [result['url'], {}],
"id": 2}) "id": 2})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
result['hash'] = json.loads(response.text)['result'] result['hash'] = json.loads(response.text)['result']
logger.debug('Deluge: Response was %s' % str(json.loads(response.text))) logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
return json.loads(response.text)['result'] return json.loads(response.text)['result']
@@ -415,7 +453,7 @@ def _add_torrent_url(result):
"params": [result['url'], {}], "params": [result['url'], {}],
"id": 32}) "id": 32})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
result['location'] = json.loads(response.text)['result'] result['location'] = json.loads(response.text)['result']
logger.debug('Deluge: Response was %s' % str(json.loads(response.text))) logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
return json.loads(response.text)['result'] return json.loads(response.text)['result']
@@ -433,10 +471,11 @@ def _add_torrent_file(result):
try: try:
# content is torrent file contents that needs to be encoded to base64 # content is torrent file contents that needs to be encoded to base64
post_data = json.dumps({"method": "core.add_torrent_file", post_data = json.dumps({"method": "core.add_torrent_file",
"params": [result['name'] + '.torrent', b64encode(result['content'].encode('utf8')), {}], "params": [result['name'] + '.torrent',
b64encode(result['content'].encode('utf8')), {}],
"id": 2}) "id": 2})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
result['hash'] = json.loads(response.text)['result'] result['hash'] = json.loads(response.text)['result']
logger.debug('Deluge: Response was %s' % str(json.loads(response.text))) logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
return json.loads(response.text)['result'] return json.loads(response.text)['result']
@@ -444,11 +483,13 @@ def _add_torrent_file(result):
try: try:
# content is torrent file contents that needs to be encoded to base64 # content is torrent file contents that needs to be encoded to base64
# this time let's try leaving the encoding as is # this time let's try leaving the encoding as is
logger.debug('Deluge: There was a decoding issue, let\'s try again')
post_data = json.dumps({"method": "core.add_torrent_file", post_data = json.dumps({"method": "core.add_torrent_file",
"params": [result['name'] + '.torrent', b64encode(result['content']), {}], "params": [result['name'].decode('utf8') + '.torrent',
b64encode(result['content']), {}],
"id": 22}) "id": 22})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
result['hash'] = json.loads(response.text)['result'] result['hash'] = json.loads(response.text)['result']
logger.debug('Deluge: Response was %s' % str(json.loads(response.text))) logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
return json.loads(response.text)['result'] return json.loads(response.text)['result']
@@ -480,7 +521,7 @@ def setTorrentLabel(result):
"params": [], "params": [],
"id": 3}) "id": 3})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
labels = json.loads(response.text)['result'] labels = json.loads(response.text)['result']
if labels is not None: if labels is not None:
@@ -491,7 +532,7 @@ def setTorrentLabel(result):
"params": [label], "params": [label],
"id": 4}) "id": 4})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
logger.debug('Deluge: %s label added to Deluge' % label) logger.debug('Deluge: %s label added to Deluge' % label)
except Exception as e: except Exception as e:
logger.error('Deluge: Setting label failed: %s' % str(e)) logger.error('Deluge: Setting label failed: %s' % str(e))
@@ -503,7 +544,7 @@ def setTorrentLabel(result):
"params": [result['hash'], label], "params": [result['hash'], label],
"id": 5}) "id": 5})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
logger.debug('Deluge: %s label added to torrent' % label) logger.debug('Deluge: %s label added to torrent' % label)
else: else:
logger.debug('Deluge: Label plugin not detected') logger.debug('Deluge: Label plugin not detected')
@@ -527,12 +568,12 @@ def setSeedRatio(result):
"params": [result['hash'], True], "params": [result['hash'], True],
"id": 5}) "id": 5})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
post_data = json.dumps({"method": "core.set_torrent_stop_ratio", post_data = json.dumps({"method": "core.set_torrent_stop_ratio",
"params": [result['hash'], float(ratio)], "params": [result['hash'], float(ratio)],
"id": 6}) "id": 6})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
return not json.loads(response.text)['error'] return not json.loads(response.text)['error']
@@ -555,7 +596,7 @@ def setTorrentPath(result):
"params": [result['hash'], True], "params": [result['hash'], True],
"id": 7}) "id": 7})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
if headphones.CONFIG.DELUGE_DONE_DIRECTORY: if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY
@@ -569,7 +610,7 @@ def setTorrentPath(result):
"params": [result['hash'], move_to], "params": [result['hash'], move_to],
"id": 8}) "id": 8})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
return not json.loads(response.text)['error'] return not json.loads(response.text)['error']
@@ -592,7 +633,7 @@ def setTorrentPause(result):
"params": [[result['hash']]], "params": [[result['hash']]],
"id": 9}) "id": 9})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth, response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert) verify=deluge_verify_cert, headers=headers)
return not json.loads(response.text)['error'] return not json.loads(response.text)['error']
+120 -17
View File
@@ -20,6 +20,12 @@ import datetime
import shutil import shutil
import time import time
import sys import sys
import tempfile
import glob
from beets import logging as beetslogging
import six
from contextlib import contextmanager
import fnmatch import fnmatch
import re import re
@@ -217,8 +223,7 @@ def replace_illegal_chars(string, type="file"):
if type == "file": if type == "file":
string = re.sub('[\?"*:|<>/]', '_', string) string = re.sub('[\?"*:|<>/]', '_', string)
if type == "folder": if type == "folder":
string = re.sub('[:\?<>"|]', '_', string) string = re.sub('[:\?<>"|*]', '_', string)
return string return string
@@ -251,9 +256,17 @@ _XLATE_SPECIAL = {
# Translation table. # Translation table.
# Cover additional special characters processing normalization. # Cover additional special characters processing normalization.
u"'": '', # replace apostrophe with nothing u"'": '', # replace apostrophe with nothing
u"": '', # replace musicbrainz style apostrophe with nothing
u'&': ' and ', # expand & to ' and ' u'&': ' and ', # expand & to ' and '
} }
_XLATE_MUSICBRAINZ = {
# Translation table for Musicbrainz.
u"": '...', # HORIZONTAL ELLIPSIS (U+2026)
u"": "'", # APOSTROPHE (U+0027)
u"": "-", # EN DASH (U+2013)
}
def _translate(s, dictionary): def _translate(s, dictionary):
# type: (basestring,Mapping[basestring,basestring])->basestring # type: (basestring,Mapping[basestring,basestring])->basestring
@@ -319,9 +332,27 @@ def clean_name(s):
# 6. trim # 6. trim
u = u.strip() u = u.strip()
# 7. lowercase # 7. lowercase
u = u.lower()
return u return u
def clean_musicbrainz_name(s, return_as_string=True):
# type: (basestring)->unicode
"""Substitute special Musicbrainz characters.
:param s: string to clean up, probably unicode.
:return: cleaned-up version of input string.
"""
if not isinstance(s, unicode):
u = unicode(s, 'ascii', 'replace')
else:
u = s
u = _translate(u, _XLATE_MUSICBRAINZ)
if return_as_string:
return u.encode('utf-8')
else:
return u
def cleanTitle(title): def cleanTitle(title):
title = re.sub('[\.\-\/\_]', ' ', title).lower() title = re.sub('[\.\-\/\_]', ' ', title).lower()
@@ -620,26 +651,68 @@ def get_downloaded_track_list(albumpath):
return downloaded_track_list return downloaded_track_list
def preserve_torrent_directory(albumpath): def preserve_torrent_directory(albumpath, forced=False, single=False):
""" """
Copy torrent directory to headphones-modified to keep files for seeding. Copy torrent directory to temp headphones_ directory to keep files for seeding.
""" """
from headphones import logger from headphones import logger
new_folder = os.path.join(albumpath,
'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace')) # Create temp dir
logger.info( if headphones.CONFIG.KEEP_TORRENT_FILES_DIR:
"Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding") tempdir = headphones.CONFIG.KEEP_TORRENT_FILES_DIR
else:
tempdir = tempfile.gettempdir()
logger.info("Preparing to copy to a temporary directory for post processing: " + albumpath.decode(
headphones.SYS_ENCODING, 'replace'))
try: try:
shutil.copytree(albumpath, new_folder) file_name = os.path.basename(os.path.normpath(albumpath))
return new_folder if not single:
prefix = "headphones_" + file_name + "_"
else:
prefix = "headphones_" + os.path.splitext(file_name)[0] + "_"
new_folder = tempfile.mkdtemp(prefix=prefix, dir=tempdir)
except Exception as e: except Exception as e:
logger.warn("Cannot copy/move files to temp folder: " + logger.error("Cannot create temp directory: " + tempdir.decode(
new_folder.decode(headphones.SYS_ENCODING, 'replace') + headphones.SYS_ENCODING, 'replace') + ". Error: " + str(e))
". Not continuing. Error: " + str(e)) return None
# Attempt to stop multiple temp dirs being created for the same albumpath
if not forced:
try:
workdir = os.path.join(tempdir, prefix)
workdir = re.sub(r'\[', '[[]', workdir)
workdir = re.sub(r'(?<!\[)\]', '[]]', workdir)
if len(glob.glob(workdir + '*/')) >= 3:
logger.error(
"Looks like a temp directory has previously been created for this albumpath, not continuing " + workdir.decode(
headphones.SYS_ENCODING, 'replace'))
shutil.rmtree(new_folder)
return None
except Exception as e:
logger.warn("Cannot determine if already copied/processed, will copy anyway: Warning: " + str(e))
# Copy to temp dir
try:
subdir = os.path.join(new_folder, "headphones")
logger.info("Copying files to " + subdir.decode(headphones.SYS_ENCODING, 'replace'))
if not single:
shutil.copytree(albumpath, subdir)
else:
os.makedirs(subdir)
shutil.copy(albumpath, subdir)
# Update the album path with the new location
return subdir
except Exception as e:
logger.warn("Cannot copy/move files to temp directory: " + new_folder.decode(headphones.SYS_ENCODING,
'replace') + ". Not continuing. Error: " + str(
e))
shutil.rmtree(new_folder)
return None return None
def cue_split(albumpath): def cue_split(albumpath, keep_original_folder=False):
""" """
Attempts to check and split audio files by a cue for the given directory. Attempts to check and split audio files by a cue for the given directory.
""" """
@@ -661,6 +734,15 @@ def cue_split(albumpath):
# Split cue # Split cue
if cue_count and cue_count >= count and cue_dirs: if cue_count and cue_count >= count and cue_dirs:
# Copy to temp directory
if keep_original_folder:
temppath = preserve_torrent_directory(albumpath)
if temppath:
cue_dirs = [cue_dir.replace(albumpath, temppath) for cue_dir in cue_dirs]
albumpath = temppath
else:
return None
from headphones import logger, cuesplit from headphones import logger, cuesplit
logger.info("Attempting to split audio files by cue") logger.info("Attempting to split audio files by cue")
@@ -671,12 +753,12 @@ def cue_split(albumpath):
except Exception as e: except Exception as e:
os.chdir(cwd) os.chdir(cwd)
logger.warn("Cue not split: " + str(e)) logger.warn("Cue not split: " + str(e))
return False return None
os.chdir(cwd) os.chdir(cwd)
return True return albumpath
return False return None
def extract_logline(s): def extract_logline(s):
@@ -897,3 +979,24 @@ def create_https_certificates(ssl_cert, ssl_key):
return False return False
return True return True
class BeetsLogCapture(beetslogging.Handler):
def __init__(self):
beetslogging.Handler.__init__(self)
self.messages = []
def emit(self, record):
self.messages.append(six.text_type(record.msg))
@contextmanager
def capture_beets_log(logger='beets'):
capture = BeetsLogCapture()
log = beetslogging.getLogger(logger)
log.addHandler(capture)
try:
yield capture.messages
finally:
log.removeHandler(capture)
+6 -2
View File
@@ -262,8 +262,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
else: else:
if check_release_date is None or check_release_date == u"None": if check_release_date is None or check_release_date == u"None":
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title'])) if headphones.CONFIG.MB_IGNORE_AGE_MISSING is not 1:
new_releases = mb.get_new_releases(rgid, includeExtras, True) logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
new_releases = mb.get_new_releases(rgid, includeExtras, True)
else:
logger.info("[%s] Skipping update of: %s (No Release Date)" % (artist['artist_name'], rg['title']))
new_releases = 0
else: else:
if len(check_release_date) == 10: if len(check_release_date) == 10:
release_date = check_release_date release_date = check_release_date
+5 -2
View File
@@ -55,7 +55,7 @@ def request_lastfm(method, **kwargs):
return return
if "error" in data: if "error" in data:
logger.error("Last.FM returned an error: %s", data["message"]) logger.debug("Last.FM returned an error: %s", data["message"])
return return
return data return data
@@ -149,7 +149,10 @@ def getTagTopArtists(tag, limit=50):
logger.debug("Fetched %d artists from Last.FM", len(artists)) logger.debug("Fetched %d artists from Last.FM", len(artists))
for artist in artists: for artist in artists:
artist_mbid = artist["mbid"] try:
artist_mbid = artist["mbid"]
except KeyError:
continue
if not any(artist_mbid in x for x in results): if not any(artist_mbid in x for x in results):
artistlist.append(artist_mbid) artistlist.append(artist_mbid)
+29 -24
View File
@@ -51,8 +51,8 @@ def startmb():
mbpass = headphones.CONFIG.CUSTOMPASS mbpass = headphones.CONFIG.CUSTOMPASS
sleepytime = int(headphones.CONFIG.CUSTOMSLEEP) sleepytime = int(headphones.CONFIG.CUSTOMSLEEP)
elif headphones.CONFIG.MIRROR == "headphones": elif headphones.CONFIG.MIRROR == "headphones":
mbhost = "codeshy.com" mbhost = "musicbrainz.codeshy.com"
mbport = 8181 mbport = 80
mbuser = headphones.CONFIG.HPUSER mbuser = headphones.CONFIG.HPUSER
mbpass = headphones.CONFIG.HPPASS mbpass = headphones.CONFIG.HPPASS
sleepytime = 0 sleepytime = 0
@@ -91,10 +91,6 @@ def findArtist(name, limit=1):
artistlist = [] artistlist = []
artistResults = None artistResults = None
chars = set('!?*-')
if any((c in chars) for c in name):
name = '"' + name + '"'
criteria = {'artist': name.lower()} criteria = {'artist': name.lower()}
with mb_lock: with mb_lock:
@@ -156,16 +152,13 @@ def findRelease(name, limit=1, artist=None):
if not artist and ':' in name: if not artist and ':' in name:
name, artist = name.rsplit(":", 1) name, artist = name.rsplit(":", 1)
chars = set('!?*-') criteria = {'release': name.lower()}
if any((c in chars) for c in name): if artist:
name = '"' + name + '"' criteria['artist'] = artist.lower()
if artist and any((c in chars) for c in artist):
artist = '"' + artist + '"'
with mb_lock: with mb_lock:
try: try:
releaseResults = musicbrainzngs.search_releases(query=name, limit=limit, artist=artist)[ releaseResults = musicbrainzngs.search_releases(limit=limit, **criteria)['release-list']
'release-list']
except musicbrainzngs.WebServiceError as e: # need to update exceptions except musicbrainzngs.WebServiceError as e: # need to update exceptions
logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e))) logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e)))
mb_lock.snooze(5) mb_lock.snooze(5)
@@ -234,10 +227,6 @@ def findSeries(name, limit=1):
serieslist = [] serieslist = []
seriesResults = None seriesResults = None
chars = set('!?*-')
if any((c in chars) for c in name):
name = '"' + name + '"'
criteria = {'series': name.lower()} criteria = {'series': name.lower()}
with mb_lock: with mb_lock:
@@ -759,19 +748,12 @@ def findArtistbyAlbum(name):
def findAlbumID(artist=None, album=None): def findAlbumID(artist=None, album=None):
results = None results = None
chars = set('!?*-')
try: try:
if album and artist: if album and artist:
if any((c in chars) for c in album):
album = '"' + album + '"'
if any((c in chars) for c in artist):
artist = '"' + artist + '"'
criteria = {'release': album.lower()} criteria = {'release': album.lower()}
criteria['artist'] = artist.lower() criteria['artist'] = artist.lower()
else: else:
if any((c in chars) for c in album):
album = '"' + album + '"'
criteria = {'release': album.lower()} criteria = {'release': album.lower()}
with mb_lock: with mb_lock:
results = musicbrainzngs.search_release_groups(limit=1, **criteria).get( results = musicbrainzngs.search_release_groups(limit=1, **criteria).get(
@@ -788,3 +770,26 @@ def findAlbumID(artist=None, album=None):
return False return False
rgid = unicode(results[0]['id']) rgid = unicode(results[0]['id'])
return rgid return rgid
def getArtistForReleaseGroup(rgid):
"""
Returns artist name for a release group
Used for series where we store the series instead of the artist
"""
releaseGroup = None
try:
with mb_lock:
releaseGroup = musicbrainzngs.get_release_group_by_id(
rgid, ["artists"])
releaseGroup = releaseGroup['release-group']
except musicbrainzngs.WebServiceError as e:
logger.warn(
'Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (
rgid, str(e)))
mb_lock.snooze(5)
if not releaseGroup:
return False
else:
return releaseGroup['artist-credit'][0]['artist']['name']
+2 -1
View File
@@ -27,8 +27,9 @@ def update(artistid, artist_name, release_groups):
# cut down on api calls. If it's ineffective then we'll switch to search # cut down on api calls. If it's ineffective then we'll switch to search
replacements = {" & ": " ", ".": ""} replacements = {" & ": " ", ".": ""}
mc_artist_name = helpers.clean_musicbrainz_name(artist_name, return_as_string=False)
mc_artist_name = mc_artist_name.replace("'", " ")
mc_artist_name = helpers.replace_all(artist_name.lower(), replacements) mc_artist_name = helpers.replace_all(artist_name.lower(), replacements)
mc_artist_name = mc_artist_name.replace(" ", "-") mc_artist_name = mc_artist_name.replace(" ", "-")
headers = { headers = {
+219 -72
View File
@@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from urllib import urlencode from urllib import urlencode, quote_plus
import urllib import urllib
import subprocess import subprocess
import json import json
@@ -148,7 +148,9 @@ class PROWL(object):
http_handler.request("POST", http_handler.request("POST",
"/publicapi/add", "/publicapi/add",
headers={'Content-type': "application/x-www-form-urlencoded"}, headers={
'Content-type':
"application/x-www-form-urlencoded"},
body=urlencode(data)) body=urlencode(data))
response = http_handler.getresponse() response = http_handler.getresponse()
request_status = response.status request_status = response.status
@@ -203,28 +205,34 @@ class XBMC(object):
url = host + '/xbmcCmds/xbmcHttp/?' + url_command url = host + '/xbmcCmds/xbmcHttp/?' + url_command
if self.password: if self.password:
return request.request_content(url, auth=(self.username, self.password)) return request.request_content(url,
auth=(self.username, self.password))
else: else:
return request.request_content(url) return request.request_content(url)
def _sendjson(self, host, method, params={}): def _sendjson(self, host, method, params={}):
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}] data = [
{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
headers = {'Content-Type': 'application/json'} headers = {'Content-Type': 'application/json'}
url = host + '/jsonrpc' url = host + '/jsonrpc'
if self.password: if self.password:
response = request.request_json(url, method="post", data=json.dumps(data), response = request.request_json(
headers=headers, auth=(self.username, self.password)) url, method="post",
data=json.dumps(data),
headers=headers, auth=(
self.username, self.password))
else: else:
response = request.request_json(url, method="post", data=json.dumps(data), response = request.request_json(url, method="post",
data=json.dumps(data),
headers=headers) headers=headers)
if response: if response:
return response[0]['result'] return response[0]['result']
def update(self): def update(self):
# From what I read you can't update the music library on a per directory or per path basis # From what I read you can't update the music library on a per
# so need to update the whole thing # directory or per path basis so need to update the whole thing
hosts = [x.strip() for x in self.hosts.split(',')] hosts = [x.strip() for x in self.hosts.split(',')]
@@ -247,18 +255,23 @@ class XBMC(object):
logger.info('Sending notification command to XMBC @ ' + host) logger.info('Sending notification command to XMBC @ ' + host)
try: try:
version = self._sendjson(host, 'Application.GetProperties', version = self._sendjson(host, 'Application.GetProperties',
{'properties': ['version']})['version']['major'] {'properties': ['version']})[
'version']['major']
if version < 12: # Eden if version < 12: # Eden
notification = header + "," + message + "," + time + "," + albumartpath notification = header + "," + message + "," + time + \
"," + albumartpath
notifycommand = {'command': 'ExecBuiltIn', notifycommand = {'command': 'ExecBuiltIn',
'parameter': 'Notification(' + notification + ')'} 'parameter': 'Notification(' +
notification + ')'}
request = self._sendhttp(host, notifycommand) request = self._sendhttp(host, notifycommand)
else: # Frodo else: # Frodo
params = {'title': header, 'message': message, 'displaytime': int(time), params = {'title': header, 'message': message,
'displaytime': int(time),
'image': albumartpath} 'image': albumartpath}
request = self._sendjson(host, 'GUI.ShowNotification', params) request = self._sendjson(host, 'GUI.ShowNotification',
params)
if not request: if not request:
raise Exception raise Exception
@@ -323,22 +336,28 @@ class Plex(object):
url = host + '/xbmcCmds/xbmcHttp/?' + command url = host + '/xbmcCmds/xbmcHttp/?' + command
if self.password: if self.password:
response = request.request_response(url, auth=(self.username, self.password)) response = request.request_response(url, auth=(
self.username, self.password))
else: else:
response = request.request_response(url) response = request.request_response(url)
return response return response
def _sendjson(self, host, method, params={}): def _sendjson(self, host, method, params={}):
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}] data = [
{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
headers = {'Content-Type': 'application/json'} headers = {'Content-Type': 'application/json'}
url = host + '/jsonrpc' url = host + '/jsonrpc'
if self.password: if self.password:
response = request.request_json(url, method="post", data=json.dumps(data), response = request.request_json(
headers=headers, auth=(self.username, self.password)) url, method="post",
data=json.dumps(data),
headers=headers, auth=(
self.username, self.password))
else: else:
response = request.request_json(url, method="post", data=json.dumps(data), response = request.request_json(url, method="post",
data=json.dumps(data),
headers=headers) headers=headers)
if response: if response:
@@ -346,13 +365,14 @@ class Plex(object):
def update(self): def update(self):
# From what I read you can't update the music library on a per directory or per path basis # From what I read you can't update the music library on a per
# so need to update the whole thing # directory or per path basis so need to update the whole thing
hosts = [x.strip() for x in self.server_hosts.split(',')] hosts = [x.strip() for x in self.server_hosts.split(',')]
for host in hosts: for host in hosts:
logger.info('Sending library update command to Plex Media Server@ ' + host) logger.info(
'Sending library update command to Plex Media Server@ ' + host)
url = "%s/library/sections" % host url = "%s/library/sections" % host
if self.token: if self.token:
params = {'X-Plex-Token': self.token} params = {'X-Plex-Token': self.token}
@@ -369,7 +389,8 @@ class Plex(object):
for s in sections: for s in sections:
if s.getAttribute('type') == "artist": if s.getAttribute('type') == "artist":
url = "%s/library/sections/%s/refresh" % (host, s.getAttribute('key')) url = "%s/library/sections/%s/refresh" % (
host, s.getAttribute('key'))
request.request_response(url, params=params) request.request_response(url, params=params)
def notify(self, artist, album, albumartpath): def notify(self, artist, album, albumartpath):
@@ -381,27 +402,35 @@ class Plex(object):
time = "3000" # in ms time = "3000" # in ms
for host in hosts: for host in hosts:
logger.info('Sending notification command to Plex client @ ' + host) logger.info(
'Sending notification command to Plex client @ ' + host)
try: try:
version = self._sendjson(host, 'Application.GetProperties', version = self._sendjson(host, 'Application.GetProperties',
{'properties': ['version']})['version']['major'] {'properties': ['version']})[
'version']['major']
if version < 12: # Eden if version < 12: # Eden
notification = header + "," + message + "," + time + "," + albumartpath notification = header + "," + message + "," + time + \
"," + albumartpath
notifycommand = {'command': 'ExecBuiltIn', notifycommand = {'command': 'ExecBuiltIn',
'parameter': 'Notification(' + notification + ')'} 'parameter': 'Notification(' +
notification + ')'}
request = self._sendhttp(host, notifycommand) request = self._sendhttp(host, notifycommand)
else: # Frodo else: # Frodo
params = {'title': header, 'message': message, 'displaytime': int(time), params = {'title': header, 'message': message,
'displaytime': int(time),
'image': albumartpath} 'image': albumartpath}
request = self._sendjson(host, 'GUI.ShowNotification', params) request = self._sendjson(host, 'GUI.ShowNotification',
params)
if not request: if not request:
raise Exception raise Exception
except Exception: except Exception:
logger.error('Error sending notification request to Plex client @ ' + host) logger.error(
'Error sending notification request to Plex client @ ' +
host)
class NMA(object): class NMA(object):
@@ -419,7 +448,8 @@ class NMA(object):
message = "Headphones has snatched: " + snatched message = "Headphones has snatched: " + snatched
else: else:
event = artist + ' - ' + album + ' complete!' event = artist + ' - ' + album + ' complete!'
message = "Headphones has downloaded and postprocessed: " + artist + ' [' + album + ']' message = "Headphones has downloaded and postprocessed: " + \
artist + ' [' + album + ']'
logger.debug(u"NMA event: " + event) logger.debug(u"NMA event: " + event)
logger.debug(u"NMA message: " + message) logger.debug(u"NMA message: " + message)
@@ -433,7 +463,8 @@ class NMA(object):
if len(keys) > 1: if len(keys) > 1:
batch = True batch = True
response = p.push(title, event, message, priority=nma_priority, batch_mode=batch) response = p.push(title, event, message, priority=nma_priority,
batch_mode=batch)
if not response[api][u'code'] == u'200': if not response[api][u'code'] == u'200':
logger.error(u'Could not send notification to NotifyMyAndroid') logger.error(u'Could not send notification to NotifyMyAndroid')
@@ -461,9 +492,11 @@ class PUSHBULLET(object):
data['device_iden'] = self.deviceid data['device_iden'] = self.deviceid
headers = {'Content-type': "application/json", headers = {'Content-type': "application/json",
'Authorization': 'Bearer ' + headphones.CONFIG.PUSHBULLET_APIKEY} 'Authorization': 'Bearer ' +
headphones.CONFIG.PUSHBULLET_APIKEY}
response = request.request_json(url, method="post", headers=headers, data=json.dumps(data)) response = request.request_json(url, method="post", headers=headers,
data=json.dumps(data))
if response: if response:
logger.info(u"PushBullet notifications sent.") logger.info(u"PushBullet notifications sent.")
@@ -492,7 +525,9 @@ class PUSHALOT(object):
http_handler.request("POST", http_handler.request("POST",
"/api/sendmessage", "/api/sendmessage",
headers={'Content-type': "application/x-www-form-urlencoded"}, headers={
'Content-type':
"application/x-www-form-urlencoded"},
body=urlencode(data)) body=urlencode(data))
response = http_handler.getresponse() response = http_handler.getresponse()
request_status = response.status request_status = response.status
@@ -512,6 +547,49 @@ class PUSHALOT(object):
return False return False
class JOIN(object):
def __init__(self):
self.enabled = headphones.CONFIG.JOIN_ENABLED
self.apikey = headphones.CONFIG.JOIN_APIKEY
self.deviceid = headphones.CONFIG.JOIN_DEVICEID
self.url = 'https://joinjoaomgcd.appspot.com/_ah/' \
'api/messaging/v1/sendPush?apikey={apikey}' \
'&title={title}&text={text}' \
'&icon={icon}'
def notify(self, message, event):
if not headphones.CONFIG.JOIN_ENABLED or \
not headphones.CONFIG.JOIN_APIKEY:
return
icon = "https://cdn.rawgit.com/Headphones/" \
"headphones/develop/data/images/headphoneslogo.png"
if not self.deviceid:
self.deviceid = "group.all"
l = [x.strip() for x in self.deviceid.split(',')]
if len(l) > 1:
self.url += '&deviceIds={deviceid}'
else:
self.url += '&deviceId={deviceid}'
response = urllib2.urlopen(self.url.format(apikey=self.apikey,
title=quote_plus(event),
text=quote_plus(
message.encode(
"utf-8")),
icon=icon,
deviceid=self.deviceid))
if response:
logger.info(u"Join notifications sent.")
return True
else:
logger.error(u"Join notification failed.")
return False
class Synoindex(object): class Synoindex(object):
def __init__(self, util_loc='/usr/syno/bin/synoindex'): def __init__(self, util_loc='/usr/syno/bin/synoindex'):
self.util_loc = util_loc self.util_loc = util_loc
@@ -524,7 +602,8 @@ class Synoindex(object):
if not self.util_exists(): if not self.util_exists():
logger.warn( logger.warn(
"Error sending notification: synoindex utility not found at %s" % self.util_loc) "Error sending notification: synoindex utility "
"not found at %s" % self.util_loc)
return return
if os.path.isfile(path): if os.path.isfile(path):
@@ -533,16 +612,19 @@ class Synoindex(object):
cmd_arg = '-A' cmd_arg = '-A'
else: else:
logger.warn( logger.warn(
"Error sending notification: Path passed to synoindex was not a file or folder.") "Error sending notification: Path passed to synoindex "
"was not a file or folder.")
return return
cmd = [self.util_loc, cmd_arg, path] cmd = [self.util_loc, cmd_arg, path]
logger.info("Calling synoindex command: %s" % str(cmd)) logger.info("Calling synoindex command: %s" % str(cmd))
try: try:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
cwd=headphones.PROG_DIR) cwd=headphones.PROG_DIR)
out, error = p.communicate() out, error = p.communicate()
# synoindex never returns any codes other than '0', highly irritating # synoindex never returns any codes other than '0',
# highly irritating
except OSError, e: except OSError, e:
logger.warn("Error sending notification: %s" % str(e)) logger.warn("Error sending notification: %s" % str(e))
@@ -580,7 +662,8 @@ class PUSHOVER(object):
headers = {'Content-type': "application/x-www-form-urlencoded"} headers = {'Content-type': "application/x-www-form-urlencoded"}
response = request.request_response(url, method="POST", headers=headers, data=data) response = request.request_response(url, method="POST",
headers=headers, data=data)
if response: if response:
logger.info(u"Pushover notifications sent.") logger.info(u"Pushover notifications sent.")
@@ -614,20 +697,25 @@ class TwitterNotifier(object):
def notify_snatch(self, title): def notify_snatch(self, title):
if headphones.CONFIG.TWITTER_ONSNATCH: if headphones.CONFIG.TWITTER_ONSNATCH:
self._notifyTwitter( self._notifyTwitter(
common.notifyStrings[common.NOTIFY_SNATCH] + ': ' + title + ' at ' + helpers.now()) common.notifyStrings[
common.NOTIFY_SNATCH] + ': ' + title + ' at ' +
helpers.now())
def notify_download(self, title): def notify_download(self, title):
if headphones.CONFIG.TWITTER_ENABLED: if headphones.CONFIG.TWITTER_ENABLED:
self._notifyTwitter(common.notifyStrings[ self._notifyTwitter(common.notifyStrings[
common.NOTIFY_DOWNLOAD] + ': ' + title + ' at ' + helpers.now()) common.NOTIFY_DOWNLOAD] + ': ' +
title + ' at ' + helpers.now())
def test_notify(self): def test_notify(self):
return self._notifyTwitter( return self._notifyTwitter(
"This is a test notification from Headphones at " + helpers.now(), force=True) "This is a test notification from Headphones at " + helpers.now(),
force=True)
def _get_authorization(self): def _get_authorization(self):
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) oauth_consumer = oauth.Consumer(key=self.consumer_key,
secret=self.consumer_secret)
oauth_client = oauth.Client(oauth_consumer) oauth_client = oauth.Client(oauth_consumer)
logger.info('Requesting temp token from Twitter') logger.info('Requesting temp token from Twitter')
@@ -635,32 +723,42 @@ class TwitterNotifier(object):
resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET') resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET')
if resp['status'] != '200': if resp['status'] != '200':
logger.info('Invalid respond from Twitter requesting temp token: %s' % resp['status']) logger.info(
'Invalid respond from Twitter requesting temp token: %s' %
resp['status'])
else: else:
request_token = dict(parse_qsl(content)) request_token = dict(parse_qsl(content))
headphones.CONFIG.TWITTER_USERNAME = request_token['oauth_token'] headphones.CONFIG.TWITTER_USERNAME = request_token['oauth_token']
headphones.CONFIG.TWITTER_PASSWORD = request_token['oauth_token_secret'] headphones.CONFIG.TWITTER_PASSWORD = request_token[
'oauth_token_secret']
return self.AUTHORIZATION_URL + "?oauth_token=" + request_token['oauth_token'] return self.AUTHORIZATION_URL + "?oauth_token=" + request_token[
'oauth_token']
def _get_credentials(self, key): def _get_credentials(self, key):
request_token = {} request_token = {}
request_token['oauth_token'] = headphones.CONFIG.TWITTER_USERNAME request_token['oauth_token'] = headphones.CONFIG.TWITTER_USERNAME
request_token['oauth_token_secret'] = headphones.CONFIG.TWITTER_PASSWORD request_token[
'oauth_token_secret'] = headphones.CONFIG.TWITTER_PASSWORD
request_token['oauth_callback_confirmed'] = 'true' request_token['oauth_callback_confirmed'] = 'true'
token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret']) token = oauth.Token(request_token['oauth_token'],
request_token['oauth_token_secret'])
token.set_verifier(key) token.set_verifier(key)
logger.info('Generating and signing request for an access token using key ' + key) logger.info(
'Generating and signing request for an access token using key ' +
key)
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) oauth_consumer = oauth.Consumer(key=self.consumer_key,
secret=self.consumer_secret)
logger.info('oauth_consumer: ' + str(oauth_consumer)) logger.info('oauth_consumer: ' + str(oauth_consumer))
oauth_client = oauth.Client(oauth_consumer, token) oauth_client = oauth.Client(oauth_consumer, token)
logger.info('oauth_client: ' + str(oauth_client)) logger.info('oauth_client: ' + str(oauth_client))
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', resp, content = oauth_client.request(self.ACCESS_TOKEN_URL,
method='POST',
body='oauth_verifier=%s' % key) body='oauth_verifier=%s' % key)
logger.info('resp, content: ' + str(resp) + ',' + str(content)) logger.info('resp, content: ' + str(resp) + ',' + str(content))
@@ -669,14 +767,18 @@ class TwitterNotifier(object):
logger.info('resp[status] = ' + str(resp['status'])) logger.info('resp[status] = ' + str(resp['status']))
if resp['status'] != '200': if resp['status'] != '200':
logger.info('The request for a token with did not succeed: ' + str(resp['status']), logger.info('The request for a token with did not succeed: ' + str(
resp['status']),
logger.ERROR) logger.ERROR)
return False return False
else: else:
logger.info('Your Twitter Access Token key: %s' % access_token['oauth_token']) logger.info('Your Twitter Access Token key: %s' % access_token[
logger.info('Access Token secret: %s' % access_token['oauth_token_secret']) 'oauth_token'])
logger.info(
'Access Token secret: %s' % access_token['oauth_token_secret'])
headphones.CONFIG.TWITTER_USERNAME = access_token['oauth_token'] headphones.CONFIG.TWITTER_USERNAME = access_token['oauth_token']
headphones.CONFIG.TWITTER_PASSWORD = access_token['oauth_token_secret'] headphones.CONFIG.TWITTER_PASSWORD = access_token[
'oauth_token_secret']
return True return True
def _send_tweet(self, message=None): def _send_tweet(self, message=None):
@@ -688,7 +790,8 @@ class TwitterNotifier(object):
logger.info(u"Sending tweet: " + message) logger.info(u"Sending tweet: " + message)
api = twitter.Api(username, password, access_token_key, access_token_secret) api = twitter.Api(username, password, access_token_key,
access_token_secret)
try: try:
api.PostUpdate(message) api.PostUpdate(message)
@@ -741,7 +844,8 @@ class OSX_NOTIFY(object):
) )
NSUserNotification = self.objc.lookUpClass('NSUserNotification') NSUserNotification = self.objc.lookUpClass('NSUserNotification')
NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter') NSUserNotificationCenter = self.objc.lookUpClass(
'NSUserNotificationCenter')
NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool') NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool')
if not NSUserNotification or not NSUserNotificationCenter: if not NSUserNotification or not NSUserNotificationCenter:
@@ -756,14 +860,17 @@ class OSX_NOTIFY(object):
if text: if text:
notification.setInformativeText_(text) notification.setInformativeText_(text)
if sound: if sound:
notification.setSoundName_("NSUserNotificationDefaultSoundName") notification.setSoundName_(
"NSUserNotificationDefaultSoundName")
if image: if image:
source_img = self.AppKit.NSImage.alloc().initByReferencingFile_(image) source_img = self.AppKit.NSImage.alloc().\
initByReferencingFile_(image)
notification.setContentImage_(source_img) notification.setContentImage_(source_img)
# notification.set_identityImage_(source_img) # notification.set_identityImage_(source_img)
notification.setHasActionButton_(False) notification.setHasActionButton_(False)
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter() notification_center = NSUserNotificationCenter.\
defaultUserNotificationCenter()
notification_center.deliverNotification_(notification) notification_center.deliverNotification_(notification)
del pool del pool
@@ -784,13 +891,16 @@ class BOXCAR(object):
def notify(self, title, message, rgid=None): def notify(self, title, message, rgid=None):
try: try:
if rgid: if rgid:
message += '<br></br><a href="http://musicbrainz.org/release-group/%s">MusicBrainz</a>' % rgid message += '<br></br><a href="http://musicbrainz.org/' \
'release-group/%s">MusicBrainz</a>' % rgid
data = urllib.urlencode({ data = urllib.urlencode({
'user_credentials': headphones.CONFIG.BOXCAR_TOKEN, 'user_credentials': headphones.CONFIG.BOXCAR_TOKEN,
'notification[title]': title.encode('utf-8'), 'notification[title]': title.encode('utf-8'),
'notification[long_message]': message.encode('utf-8'), 'notification[long_message]': message.encode('utf-8'),
'notification[sound]': "done" 'notification[sound]': "done",
'notification[icon_url]': "https://raw.githubusercontent.com/rembo10/headphones/master/data/images"
"/headphoneslogo.png"
}) })
req = urllib2.Request(self.url) req = urllib2.Request(self.url)
@@ -818,8 +928,9 @@ class SubSonicNotifier(object):
self.host = self.host + "/" self.host = self.host + "/"
# Invoke request # Invoke request
request.request_response(self.host + "musicFolderSettings.view?scanNow", request.request_response(
auth=(self.username, self.password)) self.host + "musicFolderSettings.view?scanNow",
auth=(self.username, self.password))
class Email(object): class Email(object):
@@ -827,13 +938,15 @@ class Email(object):
message = MIMEText(message, 'plain', "utf-8") message = MIMEText(message, 'plain', "utf-8")
message['Subject'] = subject message['Subject'] = subject
message['From'] = email.utils.formataddr(('Headphones', headphones.CONFIG.EMAIL_FROM)) message['From'] = email.utils.formataddr(
('Headphones', headphones.CONFIG.EMAIL_FROM))
message['To'] = headphones.CONFIG.EMAIL_TO message['To'] = headphones.CONFIG.EMAIL_TO
try: try:
if headphones.CONFIG.EMAIL_SSL: if headphones.CONFIG.EMAIL_SSL:
mailserver = smtplib.SMTP_SSL(headphones.CONFIG.EMAIL_SMTP_SERVER, mailserver = smtplib.SMTP_SSL(
headphones.CONFIG.EMAIL_SMTP_PORT) headphones.CONFIG.EMAIL_SMTP_SERVER,
headphones.CONFIG.EMAIL_SMTP_PORT)
else: else:
mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER, mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER,
headphones.CONFIG.EMAIL_SMTP_PORT) headphones.CONFIG.EMAIL_SMTP_PORT)
@@ -847,7 +960,8 @@ class Email(object):
mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER, mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER,
headphones.CONFIG.EMAIL_SMTP_PASSWORD) headphones.CONFIG.EMAIL_SMTP_PASSWORD)
mailserver.sendmail(headphones.CONFIG.EMAIL_FROM, headphones.CONFIG.EMAIL_TO, mailserver.sendmail(headphones.CONFIG.EMAIL_FROM,
headphones.CONFIG.EMAIL_TO,
message.as_string()) message.as_string())
mailserver.quit() mailserver.quit()
return True return True
@@ -858,7 +972,6 @@ class Email(object):
class TELEGRAM(object): class TELEGRAM(object):
def notify(self, message, status): def notify(self, message, status):
if not headphones.CONFIG.TELEGRAM_ENABLED: if not headphones.CONFIG.TELEGRAM_ENABLED:
return return
@@ -876,15 +989,49 @@ class TELEGRAM(object):
# Send message to user using Telegram's Bot API # Send message to user using Telegram's Bot API
try: try:
response = requests.post(TELEGRAM_API % (token, "sendMessage"), data=payload) response = requests.post(TELEGRAM_API % (token, "sendMessage"),
data=payload)
except Exception, e: except Exception, e:
logger.info(u'Telegram notify failed: ' + str(e)) logger.info(u'Telegram notify failed: ' + str(e))
# Error logging # Error logging
sent_successfuly = True sent_successfuly = True
if not response.status_code == 200: if not response.status_code == 200:
logger.info(u'Could not send notification to TelegramBot (token=%s). Response: [%s]', (token, response.text)) logger.info(
u'Could not send notification to TelegramBot '
u'(token=%s). Response: [%s]',
(token, response.text))
sent_successfuly = False sent_successfuly = False
logger.info(u"Telegram notifications sent.") logger.info(u"Telegram notifications sent.")
return sent_successfuly return sent_successfuly
class SLACK(object):
def notify(self, message, status):
if not headphones.CONFIG.SLACK_ENABLED:
return
import requests
SLACK_URL = headphones.CONFIG.SLACK_URL
channel = headphones.CONFIG.SLACK_CHANNEL
emoji = headphones.CONFIG.SLACK_EMOJI
payload = {'channel': channel, 'text': status + ': ' + message,
'icon_emoji': emoji}
try:
response = requests.post(SLACK_URL, json=payload)
except Exception, e:
logger.info(u'Slack notify failed: ' + str(e))
sent_successfuly = True
if not response.status_code == 200:
logger.info(
u'Could not send notification to Slack. Response: [%s]',
(response.text))
sent_successfuly = False
logger.info(u"Slack notifications sent.")
return sent_successfuly
+146 -76
View File
@@ -17,7 +17,6 @@ import shutil
import uuid import uuid
import threading import threading
import itertools import itertools
import tempfile
import os import os
import re import re
@@ -25,11 +24,12 @@ import beets
import headphones import headphones
from beets import autotag from beets import autotag
from beets import config as beetsconfig from beets import config as beetsconfig
from beets import logging as beetslogging
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
from beetsplug import lyrics as beetslyrics from beetsplug import lyrics as beetslyrics
from headphones import notifiers, utorrent, transmission, deluge from headphones import notifiers, utorrent, transmission, deluge, qbittorrent
from headphones import db, albumart, librarysync from headphones import db, albumart, librarysync
from headphones import logger, helpers, request, mb, music_encoder from headphones import logger, helpers, mb, music_encoder
from headphones import metadata from headphones import metadata
postprocessor_lock = threading.Lock() postprocessor_lock = threading.Lock()
@@ -44,6 +44,8 @@ def checkFolder():
for album in snatched: for album in snatched:
if album['FolderName']: if album['FolderName']:
folder_name = album['FolderName']
single = False
if album['Kind'] == 'nzb': if album['Kind'] == 'nzb':
download_dir = headphones.CONFIG.DOWNLOAD_DIR download_dir = headphones.CONFIG.DOWNLOAD_DIR
else: else:
@@ -52,21 +54,32 @@ def checkFolder():
else: else:
download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
album_path = os.path.join(download_dir, album['FolderName']).encode( # Get folder from torrent hash
headphones.SYS_ENCODING, 'replace') if album['TorrentHash'] and headphones.CONFIG.TORRENT_DOWNLOADER:
logger.debug("Checking if %s exists" % album_path) torrent_folder_name = None
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
torrent_folder_name, single = transmission.getFolder(album['TorrentHash'])
elif headphones.CONFIG.TORRENT_DOWNLOADER == 4:
torrent_folder_name, single = qbittorrent.getFolder(album['TorrentHash'])
if torrent_folder_name:
folder_name = torrent_folder_name
if os.path.exists(album_path): if folder_name:
logger.info('Found "' + album['FolderName'] + '" in ' + album[ album_path = os.path.join(download_dir, folder_name).encode(
'Kind'] + ' download folder. Verifying....') headphones.SYS_ENCODING, 'replace')
verify(album['AlbumID'], album_path, album['Kind']) logger.debug("Checking if %s exists" % album_path)
if os.path.exists(album_path):
logger.info('Found "' + folder_name + '" in ' + album[
'Kind'] + ' download folder. Verifying....')
verify(album['AlbumID'], album_path, album['Kind'], single=single)
else: else:
logger.info("No folder name found for " + album['Title']) logger.info("No folder name found for " + album['Title'])
logger.debug("Checking download folder finished.") logger.debug("Checking download folder finished.")
def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=False): def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=False, single=False):
myDB = db.DBConnection() myDB = db.DBConnection()
release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone() release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid]) tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
@@ -207,13 +220,38 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
'replace') + " isn't complete yet. Will try again on the next run") 'replace') + " isn't complete yet. Will try again on the next run")
return return
# Split cue # Force single file through
if single and not downloaded_track_list:
downloaded_track_list.append(albumpath)
# Check to see if we're preserving the torrent dir
if (headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent") or headphones.CONFIG.KEEP_ORIGINAL_FOLDER:
keep_original_folder = True
# Split cue before metadata check
if headphones.CONFIG.CUE_SPLIT and downloaded_cuecount and downloaded_cuecount >= len( if headphones.CONFIG.CUE_SPLIT and downloaded_cuecount and downloaded_cuecount >= len(
downloaded_track_list): downloaded_track_list):
if headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent": new_folder = None
albumpath = helpers.preserve_torrent_directory(albumpath) new_albumpath = albumpath
if albumpath and helpers.cue_split(albumpath): if keep_original_folder:
temp_path = helpers.preserve_torrent_directory(new_albumpath, forced)
if not temp_path:
markAsUnprocessed(albumid, new_albumpath, keep_original_folder)
return
else:
new_albumpath = temp_path
new_folder = os.path.split(new_albumpath)[0]
Kind = "cue_split"
cuepath = helpers.cue_split(new_albumpath)
if not cuepath:
if new_folder:
shutil.rmtree(new_folder)
markAsUnprocessed(albumid, albumpath, keep_original_folder)
return
else:
albumpath = cuepath
downloaded_track_list = helpers.get_downloaded_track_list(albumpath) downloaded_track_list = helpers.get_downloaded_track_list(albumpath)
keep_original_folder = False
# test #1: metadata - usually works # test #1: metadata - usually works
logger.debug('Verifying metadata...') logger.debug('Verifying metadata...')
@@ -243,7 +281,7 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
if metaartist == dbartist and metaalbum == dbalbum: if metaartist == dbartist and metaalbum == dbalbum:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind, doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
keep_original_folder) keep_original_folder, forced, single)
return return
# test #2: filenames # test #2: filenames
@@ -262,7 +300,7 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
if dbtrack in filetrack: if dbtrack in filetrack:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind, doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
keep_original_folder) keep_original_folder, forced, single)
return return
# test #3: number of songs and duration # test #3: number of songs and duration
@@ -295,46 +333,49 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
delta = abs(downloaded_track_duration - db_track_duration) delta = abs(downloaded_track_duration - db_track_duration)
if delta < 240: if delta < 240:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind, doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
keep_original_folder) keep_original_folder, forced, single)
return return
logger.warn(u'Could not identify album: %s. It may not be the intended album.', logger.warn(u'Could not identify album: %s. It may not be the intended album.',
albumpath.decode(headphones.SYS_ENCODING, 'replace')) albumpath.decode(headphones.SYS_ENCODING, 'replace'))
markAsUnprocessed(albumid, albumpath, keep_original_folder)
def markAsUnprocessed(albumid, albumpath, keep_original_folder=False):
myDB = db.DBConnection()
myDB.action( myDB.action(
'UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', 'UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid])
[albumid])
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath) processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
if not processed: if not processed:
if headphones.CONFIG.RENAME_UNPROCESSED: if headphones.CONFIG.RENAME_UNPROCESSED and not keep_original_folder:
renameUnprocessedFolder(albumpath, tag="Unprocessed") renameUnprocessedFolder(albumpath, tag="Unprocessed")
else: else:
logger.warn(u"Won't rename %s to mark as 'Unprocessed', because it is disabled.", logger.warn(u"Won't rename %s to mark as 'Unprocessed', because it is disabled or folder is being kept.",
albumpath.decode(headphones.SYS_ENCODING, 'replace')) albumpath.decode(headphones.SYS_ENCODING, 'replace'))
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None, def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None,
keep_original_folder=False): keep_original_folder=False, forced=False, single=False):
logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle'])) logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle']))
new_folder = None 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: # Preserve the torrent dir
new_folder = tempfile.mkdtemp(prefix="headphones_") if keep_original_folder or single:
subdir = os.path.join(new_folder, "headphones") temp_path = helpers.preserve_torrent_directory(albumpath, forced, single)
logger.info("Copying files to " + subdir.decode(headphones.SYS_ENCODING, 'replace') + " subfolder to preserve downloaded files for seeding") if not temp_path:
try: markAsUnprocessed(albumid, albumpath, keep_original_folder)
shutil.copytree(albumpath, subdir)
# Update the album path with the new location
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)
return return
else:
albumpath = temp_path
new_folder = os.path.split(albumpath)[0]
elif Kind == "cue_split":
new_folder = os.path.split(albumpath)[0]
# Need to update the downloaded track list with the new location. # Need to update the downloaded track list with the new location.
# Could probably just throw in the "headphones-modified" folder, # Could probably just throw in the "headphones-modified" folder,
# but this is good to make sure we're not counting files that may have failed to move # but this is good to make sure we're not counting files that may have failed to move
if new_folder:
downloaded_track_list = [] downloaded_track_list = []
for r, d, f in os.walk(albumpath): for r, d, f in os.walk(albumpath):
for files in f: for files in f:
if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
@@ -352,7 +393,8 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
downloaded_track.decode(headphones.SYS_ENCODING, "replace")) downloaded_track.decode(headphones.SYS_ENCODING, "replace"))
return return
except IOError: except IOError:
logger.error("Unable to find media file: %s. Not continuing.") logger.error("Unable to find media file: %s. Not continuing.", downloaded_track.decode(
headphones.SYS_ENCODING, "replace"))
if new_folder: if new_folder:
shutil.rmtree(new_folder) shutil.rmtree(new_folder)
return return
@@ -387,21 +429,13 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
shutil.rmtree(new_folder) shutil.rmtree(new_folder)
return return
# get artwork and path
album_art_path = None
artwork = None artwork = None
album_art_path = albumart.getAlbumArt(albumid) if headphones.CONFIG.EMBED_ALBUM_ART or headphones.CONFIG.ADD_ALBUM_ART or \
if headphones.CONFIG.EMBED_ALBUM_ART or headphones.CONFIG.ADD_ALBUM_ART: (headphones.CONFIG.PLEX_ENABLED and headphones.CONFIG.PLEX_NOTIFY) or \
(headphones.CONFIG.XBMC_ENABLED and headphones.CONFIG.XBMC_NOTIFY):
if album_art_path: album_art_path, artwork = albumart.getAlbumArt(albumid)
artwork = request.request_content(album_art_path)
else:
artwork = None
if not album_art_path or not artwork or len(artwork) < 100:
logger.info("No suitable album art found from Amazon. Checking Last.FM....")
artwork = albumart.getCachedArt(albumid)
if not artwork or len(artwork) < 100:
artwork = False
logger.info("No suitable album art found from Last.FM. Not adding album art")
if headphones.CONFIG.EMBED_ALBUM_ART and artwork: if headphones.CONFIG.EMBED_ALBUM_ART and artwork:
embedAlbumArt(artwork, downloaded_track_list) embedAlbumArt(artwork, downloaded_track_list)
@@ -446,12 +480,12 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
[albumid]) [albumid])
# Check if torrent has finished seeding # Check if torrent has finished seeding
if headphones.CONFIG.TORRENT_DOWNLOADER == 1 or headphones.CONFIG.TORRENT_DOWNLOADER == 2: if headphones.CONFIG.TORRENT_DOWNLOADER != 0:
seed_snatched = myDB.action( seed_snatched = myDB.action(
'SELECT * from snatched WHERE Status="Seed_Snatched" and AlbumID=?', 'SELECT * from snatched WHERE Status="Seed_Snatched" and AlbumID=?',
[albumid]).fetchone() [albumid]).fetchone()
if seed_snatched: if seed_snatched:
hash = seed_snatched['FolderName'] hash = seed_snatched['TorrentHash']
torrent_removed = False torrent_removed = False
logger.info(u'%s - %s. Checking if torrent has finished seeding and can be removed' % ( logger.info(u'%s - %s. Checking if torrent has finished seeding and can be removed' % (
release['ArtistName'], release['AlbumTitle'])) release['ArtistName'], release['AlbumTitle']))
@@ -459,8 +493,10 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
torrent_removed = transmission.removeTorrent(hash, True) torrent_removed = transmission.removeTorrent(hash, True)
elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
torrent_removed = deluge.removeTorrent(hash, True) torrent_removed = deluge.removeTorrent(hash, True)
else: elif headphones.CONFIG.TORRENT_DOWNLOADER == 2:
torrent_removed = utorrent.removeTorrent(hash, True) torrent_removed = utorrent.removeTorrent(hash, True)
else:
torrent_removed = qbittorrent.removeTorrent(hash, True)
# Torrent removed, delete the snatched record, else update Status for scheduled job to check # Torrent removed, delete the snatched record, else update Status for scheduled job to check
if torrent_removed: if torrent_removed:
@@ -538,6 +574,11 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
pushbullet = notifiers.PUSHBULLET() pushbullet = notifiers.PUSHBULLET()
pushbullet.notify(pushmessage, statusmessage) pushbullet.notify(pushmessage, statusmessage)
if headphones.CONFIG.JOIN_ENABLED:
logger.info(u"Join request")
join = notifiers.JOIN()
join.notify(pushmessage, statusmessage)
if headphones.CONFIG.TELEGRAM_ENABLED: if headphones.CONFIG.TELEGRAM_ENABLED:
logger.info(u"Telegram request") logger.info(u"Telegram request")
telegram = notifiers.TELEGRAM() telegram = notifiers.TELEGRAM()
@@ -918,12 +959,29 @@ def correctMetadata(albumid, release, downloaded_track_list):
if not items: if not items:
continue continue
search_ids = []
logger.debug('Getting recommendation from beets. Artist: %s. Album: %s. Tracks: %s', release['ArtistName'],
release['AlbumTitle'], len(items))
# Try with specific release, e.g. alternate release selected from albumPage
if release['ReleaseID'] != release['AlbumID']:
logger.debug('trying beets with specific Release ID: %s', release['ReleaseID'])
search_ids = [release['ReleaseID']]
try: try:
cur_artist, cur_album, candidates, rec = autotag.tag_album(items, beetslog = beetslogging.getLogger('beets')
search_artist=helpers.latinToAscii( beetslog.set_global_level(beetslogging.DEBUG) if headphones.VERBOSE else beetslog.set_global_level(
release['ArtistName']), beetslogging.CRITICAL)
search_album=helpers.latinToAscii( with helpers.capture_beets_log() as logs:
release['AlbumTitle'])) cur_artist, cur_album, prop = autotag.tag_album(items,
search_artist=release['ArtistName'],
search_album=release['AlbumTitle'],
search_ids=search_ids)
candidates = prop.candidates
rec = prop.recommendation
for log in logs:
logger.debug('Beets: %s', log)
beetslog.set_global_level(beetslogging.NOTSET)
except Exception as e: except Exception as e:
logger.error('Error getting recommendation: %s. Not writing metadata', e) logger.error('Error getting recommendation: %s. Not writing metadata', e)
return False return False
@@ -1186,7 +1244,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
'Found a match in the database: %s. Verifying to make sure it is the correct album', 'Found a match in the database: %s. Verifying to make sure it is the correct album',
snatched['Title']) snatched['Title'])
verify(snatched['AlbumID'], folder, snatched['Kind'], verify(snatched['AlbumID'], folder, snatched['Kind'],
keep_original_folder=keep_original_folder) forced=True, keep_original_folder=keep_original_folder)
continue continue
# Attempt 2: strip release group id from filename # Attempt 2: strip release group id from filename
@@ -1213,7 +1271,8 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
else: else:
logger.info( logger.info(
'Found a (possibly) valid Musicbrainz release group id in album folder name.') 'Found a (possibly) valid Musicbrainz release group id in album folder name.')
verify(rgid, folder, forced=True) verify(rgid, folder, forced=True,
keep_original_folder=keep_original_folder)
continue continue
# Attempt 3a: parse the folder name into a valid format # Attempt 3a: parse the folder name into a valid format
@@ -1232,7 +1291,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
logger.info( logger.info(
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album', 'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
release['ArtistName'], release['AlbumTitle']) release['ArtistName'], release['AlbumTitle'])
verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder) verify(release['AlbumID'], folder, forced=True, keep_original_folder=keep_original_folder)
continue continue
else: else:
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, logger.info('Querying MusicBrainz for the release group id for: %s - %s', name,
@@ -1244,7 +1303,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
rgid = None rgid = None
if rgid: if rgid:
verify(rgid, folder, keep_original_folder=keep_original_folder) verify(rgid, folder, forced=True, keep_original_folder=keep_original_folder)
continue continue
else: else:
logger.info('No match found on MusicBrainz for: %s - %s', name, album) logger.info('No match found on MusicBrainz for: %s - %s', name, album)
@@ -1257,12 +1316,23 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
except Exception: except Exception:
name = album = None name = album = None
# Check if there's a cue to split # Not found from meta data, check if there's a cue to split and try meta data again
if headphones.CONFIG.CUE_SPLIT and not name and not album and helpers.cue_split(folder): kind = None
try: if headphones.CONFIG.CUE_SPLIT and not name and not album:
name, album, year = helpers.extract_metadata(folder) cue_folder = helpers.cue_split(folder, keep_original_folder=keep_original_folder)
except Exception: if cue_folder:
name = album = None try:
name, album, year = helpers.extract_metadata(cue_folder)
except Exception:
name = album = None
if name:
folder = cue_folder
if keep_original_folder:
keep_original_folder = False
kind = "cue_split"
elif folder != cue_folder:
cue_folder = os.path.split(cue_folder)[0]
shutil.rmtree(cue_folder)
if name and album: if name and album:
release = myDB.action( release = myDB.action(
@@ -1272,7 +1342,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
logger.info( logger.info(
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album', 'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
release['ArtistName'], release['AlbumTitle']) release['ArtistName'], release['AlbumTitle'])
verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder) verify(release['AlbumID'], folder, Kind=kind, forced=True, keep_original_folder=keep_original_folder)
continue continue
else: else:
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, logger.info('Querying MusicBrainz for the release group id for: %s - %s', name,
@@ -1284,7 +1354,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
rgid = None rgid = None
if rgid: if rgid:
verify(rgid, folder, keep_original_folder=keep_original_folder) verify(rgid, folder, Kind=kind, forced=True, keep_original_folder=keep_original_folder)
continue continue
else: else:
logger.info('No match found on MusicBrainz for: %s - %s', name, album) logger.info('No match found on MusicBrainz for: %s - %s', name, album)
@@ -1301,7 +1371,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
logger.info( logger.info(
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album', 'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
release['ArtistName'], release['AlbumTitle']) release['ArtistName'], release['AlbumTitle'])
verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder) verify(release['AlbumID'], folder, forced=True, keep_original_folder=keep_original_folder)
continue continue
else: else:
logger.info('Querying MusicBrainz for the release group id for: %s', logger.info('Querying MusicBrainz for the release group id for: %s',
@@ -1313,7 +1383,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
rgid = None rgid = None
if rgid: if rgid:
verify(rgid, folder, keep_original_folder=keep_original_folder) verify(rgid, folder, forced=True, keep_original_folder=keep_original_folder)
continue continue
else: else:
logger.info('No match found on MusicBrainz for: %s - %s', name, album) logger.info('No match found on MusicBrainz for: %s - %s', name, album)
+294
View File
@@ -0,0 +1,294 @@
# 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/>.
import urllib
import urllib2
import cookielib
import json
import time
import mimetypes
import random
import string
import os
import headphones
from headphones import logger
from collections import namedtuple
class qbittorrentclient(object):
TOKEN_REGEX = "<div id='token' style='display:none;'>([^<>]+)</div>"
UTSetting = namedtuple("UTSetting", ["name", "int", "str", "access"])
def __init__(self, base_url=None, username=None, password=None,):
host = headphones.CONFIG.QBITTORRENT_HOST
if not host.startswith('http'):
host = 'http://' + host
if host.endswith('/'):
host = host[:-1]
if host.endswith('/gui'):
host = host[:-4]
self.base_url = host
self.username = headphones.CONFIG.QBITTORRENT_USERNAME
self.password = headphones.CONFIG.QBITTORRENT_PASSWORD
self.cookiejar = cookielib.CookieJar()
self.opener = self._make_opener()
self._get_sid(self.base_url, self.username, self.password)
def _make_opener(self):
# create opener with cookie handler to carry QBitTorrent SID cookie
cookie_handler = urllib2.HTTPCookieProcessor(self.cookiejar)
handlers = [cookie_handler]
return urllib2.build_opener(*handlers)
def _get_sid(self, base_url, username, password):
# login so we can capture SID cookie
login_data = urllib.urlencode({'username': username, 'password': password})
try:
self.opener.open(base_url + '/login', login_data)
except urllib2.URLError as err:
logger.debug('Error getting SID. qBittorrent responded with error: ' + str(err.reason))
return
for cookie in self.cookiejar:
logger.debug('login cookie: ' + cookie.name + ', value: ' + cookie.value)
return
def _command(self, command, args=None, content_type=None, files=None):
logger.debug('QBittorrent WebAPI Command: %s' % command)
url = self.base_url + '/' + command
data = None
headers = dict()
if content_type == 'multipart/form-data':
data, headers = encode_multipart(args, files)
else:
if args:
data = urllib.urlencode(args)
if content_type:
headers['Content-Type'] = content_type
logger.debug('%s' % json.dumps(headers, indent=4))
logger.debug('%s' % data)
request = urllib2.Request(url, data, headers)
try:
response = self.opener.open(request)
info = response.info()
if info:
if info.getheader('content-type'):
if info.getheader('content-type') == 'application/json':
resp = ''
for line in response:
resp = resp + line
logger.debug('response code: %s' % str(response.code))
logger.debug('response: %s' % resp)
return response.code, json.loads(resp)
logger.debug('response code: %s' % str(response.code))
return response.code, None
except urllib2.URLError as err:
logger.debug('Failed URL: %s' % url)
logger.debug('QBitTorrent webUI raised the following error: %s' % str(err))
return None, None
def _get_list(self, **args):
return self._command('query/torrents', args)
def _get_settings(self):
status, value = self._command('query/preferences')
logger.debug('get_settings() returned %d items' % len(value))
return value
def get_savepath(self, hash):
logger.debug('qb.get_savepath(%s)' % hash)
status, torrentList = self._get_list()
for torrent in torrentList:
if torrent['hash']:
if torrent['hash'].upper() == hash.upper():
return torrent['save_path']
return None
def start(self, hash):
logger.debug('qb.start(%s)' % hash)
args = {'hash': hash}
return self._command('command/resume', args, 'application/x-www-form-urlencoded')
def pause(self, hash):
logger.debug('qb.pause(%s)' % hash)
args = {'hash': hash}
return self._command('command/pause', args, 'application/x-www-form-urlencoded')
def getfiles(self, hash):
logger.debug('qb.getfiles(%s)' % hash)
return self._command('query/propertiesFiles/' + hash)
def getprops(self, hash):
logger.debug('qb.getprops(%s)' % hash)
return self._command('query/propertiesGeneral/' + hash)
def setprio(self, hash, priority):
logger.debug('qb.setprio(%s,%d)' % (hash, priority))
args = {'hash': hash, 'priority': priority}
return self._command('command/setFilePrio', args, 'application/x-www-form-urlencoded')
def remove(self, hash, remove_data=False):
logger.debug('qb.remove(%s,%s)' % (hash, remove_data))
args = {'hashes': hash}
if remove_data:
command = 'command/deletePerm'
else:
command = 'command/delete'
return self._command(command, args, 'application/x-www-form-urlencoded')
def removeTorrent(hash, remove_data=False):
logger.debug('removeTorrent(%s,%s)' % (hash, remove_data))
qbclient = qbittorrentclient()
status, torrentList = qbclient._get_list()
for torrent in torrentList:
if torrent['hash'].upper() == hash.upper():
if torrent['state'] == 'uploading' or torrent['state'] == 'stalledUP':
logger.info('%s has finished seeding, removing torrent and data' % torrent['name'])
qbclient.remove(hash, remove_data)
return True
else:
logger.info('%s has not finished seeding yet, torrent will not be removed, will try again on next run' % torrent['name'])
return False
return False
def addTorrent(link):
logger.debug('addTorrent(%s)' % link)
qbclient = qbittorrentclient()
args = {'urls': link, 'savepath': headphones.CONFIG.DOWNLOAD_TORRENT_DIR}
if headphones.CONFIG.QBITTORRENT_LABEL:
args['category'] = headphones.CONFIG.QBITTORRENT_LABEL
return qbclient._command('command/download', args, 'multipart/form-data')
def addFile(data):
logger.debug('addFile(data)')
qbclient = qbittorrentclient()
files = {'torrents': {'filename': '', 'content': data}}
return qbclient._command('command/upload', filelist=files)
def getName(hash):
logger.debug('getName(%s)' % hash)
qbclient = qbittorrentclient()
tries = 1
while tries <= 6:
time.sleep(10)
status, torrentlist = qbclient._get_list()
for torrent in torrentlist:
if torrent['hash'].lower() == hash.lower():
return torrent['name']
tries += 1
return None
def getFolder(hash):
logger.debug('getFolder(%s)' % hash)
torrent_folder = None
single_file = False
qbclient = qbittorrentclient()
try:
status, torrent_files = qbclient.getfiles(hash.lower())
if torrent_files:
if len(torrent_files) == 1:
torrent_folder = torrent_files[0]['name']
single_file = True
else:
torrent_folder = os.path.split(torrent_files[0]['name'])[0]
single_file = False
except:
torrent_folder = None
single_file = False
return torrent_folder, single_file
_BOUNDARY_CHARS = string.digits + string.ascii_letters
# Taken from http://code.activestate.com/recipes/578668-encode-multipart-form-data-for-uploading-files-via/
# "MIT License" which is compatible with GPL
def encode_multipart(args, files, boundary=None):
logger.debug('encode_multipart()')
def escape_quote(s):
return s.replace('"', '\\"')
if boundary is None:
boundary = ''.join(random.choice(_BOUNDARY_CHARS) for i in range(30))
lines = []
if args:
for name, value in args.items():
lines.extend((
'--{0}'.format(boundary),
'Content-Disposition: form-data; name="{0}"'.format(escape_quote(name)),
'',
str(value),
))
logger.debug(''.join(lines))
if files:
for name, value in files.items():
filename = value['filename']
if 'mimetype' in value:
mimetype = value['mimetype']
else:
mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
lines.extend((
'--{0}'.format(boundary),
'Content-Disposition: form-data; name="{0}"; filename="{1}"'.format(
escape_quote(name), escape_quote(filename)),
'Content-Type: {0}'.format(mimetype),
'',
value['content'],
))
lines.extend((
'--{0}--'.format(boundary),
'',
))
body = '\r\n'.join(lines)
headers = {
'Content-Type': 'multipart/form-data; boundary={0}'.format(boundary),
'Content-Length': str(len(body)),
}
return (body, headers)
+62 -17
View File
@@ -6,6 +6,7 @@ from urlparse import urlparse
import re import re
import requests as requests import requests as requests
# from requests.auth import HTTPDigestAuth
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import headphones import headphones
@@ -24,7 +25,7 @@ class Rutracker(object):
return self.loggedin return self.loggedin
def still_logged_in(self, html): def still_logged_in(self, html):
if not html or "action=\"http://login.rutracker.org/forum/login.php\">" in html: if not html or "action=\"http://rutracker.org/forum/login.php\">" in html:
return False return False
else: else:
return True return True
@@ -34,7 +35,7 @@ class Rutracker(object):
Logs in user Logs in user
""" """
loginpage = 'http://login.rutracker.org/forum/login.php' loginpage = 'http://rutracker.org/forum/login.php'
post_params = { post_params = {
'login_username': headphones.CONFIG.RUTRACKER_USER, 'login_username': headphones.CONFIG.RUTRACKER_USER,
'login_password': headphones.CONFIG.RUTRACKER_PASSWORD, 'login_password': headphones.CONFIG.RUTRACKER_PASSWORD,
@@ -44,28 +45,34 @@ class Rutracker(object):
logger.info("Attempting to log in to rutracker...") logger.info("Attempting to log in to rutracker...")
try: try:
r = self.session.post(loginpage, data=post_params, timeout=self.timeout) r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
# try again # try again
if 'bb_data' not in r.cookies.keys(): if not self.has_bb_session_cookie(r):
time.sleep(10) time.sleep(10)
r = self.session.post(loginpage, data=post_params, timeout=self.timeout) if headphones.CONFIG.RUTRACKER_COOKIE:
if r.status_code != 200: logger.info("Attempting to log in using predefined cookie...")
logger.error("rutracker login returned status code %s" % r.status_code) r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False, cookies={'bb_session': headphones.CONFIG.RUTRACKER_COOKIE})
self.loggedin = False
else:
if 'bb_data' in r.cookies.keys():
self.loggedin = True
logger.info("Successfully logged in to rutracker")
else: else:
logger.error( r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
"Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later") if self.has_bb_session_cookie(r):
self.loggedin = False self.loggedin = True
logger.info("Successfully logged in to rutracker")
else:
logger.error(
"Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later")
self.loggedin = False
return self.loggedin return self.loggedin
except Exception as e: except Exception as e:
logger.error("Unknown error logging in to rutracker: %s" % e) logger.error("Unknown error logging in to rutracker: %s" % e)
self.loggedin = False self.loggedin = False
return self.loggedin return self.loggedin
def has_bb_session_cookie(self, response):
if 'bb_session' in response.cookies.keys():
return True
# Rutracker randomly send a 302 redirect code, cookie may be present in response history
return next(('bb_session' in r.cookies.keys() for r in response.history), False)
def searchurl(self, artist, album, year, format): def searchurl(self, artist, album, year, format):
""" """
Return the search url Return the search url
@@ -91,7 +98,11 @@ class Rutracker(object):
# sort by size, descending. # sort by size, descending.
sort = '&o=7&s=2' sort = '&o=7&s=2'
searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort) try:
searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
except:
searchterm = searchterm.encode('utf-8')
searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
logger.info("Searching rutracker using term: %s", searchterm) logger.info("Searching rutracker using term: %s", searchterm)
return searchurl return searchurl
@@ -167,7 +178,7 @@ class Rutracker(object):
return the .torrent data return the .torrent data
""" """
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t'] torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
downloadurl = 'http://dl.rutracker.org/forum/dl.php?t=' + torrent_id downloadurl = 'http://rutracker.org/forum/dl.php?t=' + torrent_id
cookie = {'bb_dl': torrent_id} cookie = {'bb_dl': torrent_id}
try: try:
headers = {'Referer': url} headers = {'Referer': url}
@@ -214,3 +225,37 @@ class Rutracker(object):
self.session.post(url, params={'action': 'add-file'}, files=files) self.session.post(url, params={'action': 'add-file'}, files=files)
except Exception as e: except Exception as e:
logger.exception('Error adding file to utorrent %s', e) logger.exception('Error adding file to utorrent %s', e)
# TODO get this working in qbittorrent.py
def qbittorrent_add_file(self, data):
host = headphones.CONFIG.QBITTORRENT_HOST
if not host.startswith('http'):
host = 'http://' + host
if host.endswith('/'):
host = host[:-1]
if host.endswith('/gui'):
host = host[:-4]
base_url = host
# self.session.auth = HTTPDigestAuth(headphones.CONFIG.QBITTORRENT_USERNAME, headphones.CONFIG.QBITTORRENT_PASSWORD)
url = base_url + '/login'
try:
self.session.post(url, data={'username': headphones.CONFIG.QBITTORRENT_USERNAME,
'password': headphones.CONFIG.QBITTORRENT_PASSWORD})
except Exception as e:
logger.exception('Error adding file to qbittorrent %s', e)
return
url = base_url + '/command/upload'
args = {'savepath': headphones.CONFIG.DOWNLOAD_TORRENT_DIR}
if headphones.CONFIG.QBITTORRENT_LABEL:
args['category'] = headphones.CONFIG.QBITTORRENT_LABEL
torrent_files = {'torrents': data}
try:
self.session.post(url, data=args, files=torrent_files)
except Exception as e:
logger.exception('Error adding file to qbittorrent %s', e)
+373 -68
View File
@@ -14,6 +14,7 @@
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi # NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
# t411 support added by a1ex, @likeitneverwentaway on github for maintenance
from base64 import b16encode, b32decode from base64 import b16encode, b32decode
from hashlib import sha1 from hashlib import sha1
@@ -24,29 +25,33 @@ import datetime
import subprocess import subprocess
import unicodedata import unicodedata
import urlparse import urlparse
from json import loads
import os import os
import re import re
from pygazelle import api as gazelleapi from pygazelle import api as gazelleapi
from pygazelle import encoding as gazelleencoding from pygazelle import encoding as gazelleencoding
from pygazelle import format as gazelleformat from pygazelle import format as gazelleformat
from pygazelle import release_type as gazellerelease_type
import headphones import headphones
from headphones.common import USER_AGENT from headphones.common import USER_AGENT
from headphones import logger, db, helpers, classes, sab, nzbget, request from headphones import logger, db, helpers, classes, sab, nzbget, request
from headphones import utorrent, transmission, notifiers, rutracker, deluge from headphones import utorrent, transmission, notifiers, rutracker, deluge, qbittorrent
from bencode import bencode, bdecode from bencode import bencode, bdecode
# Magnet to torrent services, for Black hole. Stolen from CouchPotato. # Magnet to torrent services, for Black hole. Stolen from CouchPotato.
TORRENT_TO_MAGNET_SERVICES = [ TORRENT_TO_MAGNET_SERVICES = [
# 'https://zoink.it/torrent/%s.torrent', # 'https://zoink.it/torrent/%s.torrent',
# 'http://torrage.com/torrent/%s.torrent', # 'http://torrage.com/torrent/%s.torrent',
'https://torcache.net/torrent/%s.torrent', # 'https://torcache.net/torrent/%s.torrent',
'http://itorrents.org/torrent/%s.torrent',
] ]
# Persistent What.cd API object # Persistent Apollo.rip API object
gazelle = None apolloobj = None
ruobj = None ruobj = None
# Persistent RED API object
redobj = None
def fix_url(s, charset="utf-8"): def fix_url(s, charset="utf-8"):
@@ -159,13 +164,15 @@ def get_seed_ratio(provider):
seed_ratio = headphones.CONFIG.RUTRACKER_RATIO seed_ratio = headphones.CONFIG.RUTRACKER_RATIO
elif provider == 'Kick Ass Torrents': elif provider == 'Kick Ass Torrents':
seed_ratio = headphones.CONFIG.KAT_RATIO seed_ratio = headphones.CONFIG.KAT_RATIO
elif provider == 'What.cd': elif provider == 'Apollo.rip':
seed_ratio = headphones.CONFIG.WHATCD_RATIO seed_ratio = headphones.CONFIG.APOLLO_RATIO
elif provider == 'Redacted':
seed_ratio = headphones.CONFIG.REDACTED_RATIO
elif provider == 'The Pirate Bay': elif provider == 'The Pirate Bay':
seed_ratio = headphones.CONFIG.PIRATEBAY_RATIO seed_ratio = headphones.CONFIG.PIRATEBAY_RATIO
elif provider == 'Old Pirate Bay': elif provider == 'Old Pirate Bay':
seed_ratio = headphones.CONFIG.OLDPIRATEBAY_RATIO seed_ratio = headphones.CONFIG.OLDPIRATEBAY_RATIO
elif provider == 'Waffles.fm': elif provider == 'Waffles.ch':
seed_ratio = headphones.CONFIG.WAFFLES_RATIO seed_ratio = headphones.CONFIG.WAFFLES_RATIO
elif provider == 'Mininova': elif provider == 'Mininova':
seed_ratio = headphones.CONFIG.MININOVA_RATIO seed_ratio = headphones.CONFIG.MININOVA_RATIO
@@ -199,14 +206,13 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
continue continue
if headphones.CONFIG.WAIT_UNTIL_RELEASE_DATE and album['ReleaseDate']: if headphones.CONFIG.WAIT_UNTIL_RELEASE_DATE and album['ReleaseDate']:
try: release_date = strptime_musicbrainz(album['ReleaseDate'])
release_date = datetime.datetime.strptime(album['ReleaseDate'], "%Y-%m-%d") if not release_date:
except: logger.warn("No valid date for: %s. Skipping automatic search" %
logger.warn( album['AlbumTitle'])
"No valid date for: %s. Skipping automatic search" % album['AlbumTitle'])
continue continue
if release_date > datetime.datetime.today(): elif release_date > datetime.datetime.today():
logger.info("Skipping: %s. Waiting for release date of: %s" % ( logger.info("Skipping: %s. Waiting for release date of: %s" % (
album['AlbumTitle'], album['ReleaseDate'])) album['AlbumTitle'], album['ReleaseDate']))
continue continue
@@ -235,6 +241,26 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
logger.info('Search for wanted albums complete') logger.info('Search for wanted albums complete')
def strptime_musicbrainz(date_str):
"""
Release date as returned by Musicbrainz may contain the full date (Year-Month-Day)
but it may as well be just Year-Month or even just the year.
Args:
date_str: the date as a string (ex: "2003-05-01", "2003-03", "2003")
Returns:
The more accurate datetime object we can create or None if parse failed
"""
acceptable_formats = ('%Y-%m-%d', '%Y-%m', '%Y')
for date_format in acceptable_formats:
try:
return datetime.datetime.strptime(date_str, date_format)
except:
pass
return None
def do_sorted_search(album, new, losslessOnly, choose_specific_download=False): def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
NZB_PROVIDERS = (headphones.CONFIG.HEADPHONES_INDEXER or NZB_PROVIDERS = (headphones.CONFIG.HEADPHONES_INDEXER or
headphones.CONFIG.NEWZNAB or headphones.CONFIG.NEWZNAB or
@@ -252,8 +278,10 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
headphones.CONFIG.MININOVA or headphones.CONFIG.MININOVA or
headphones.CONFIG.WAFFLES or headphones.CONFIG.WAFFLES or
headphones.CONFIG.RUTRACKER or headphones.CONFIG.RUTRACKER or
headphones.CONFIG.WHATCD or headphones.CONFIG.APOLLO or
headphones.CONFIG.STRIKE) headphones.CONFIG.REDACTED or
headphones.CONFIG.STRIKE or
headphones.CONFIG.TQUATTRECENTONZE)
results = [] results = []
myDB = db.DBConnection() myDB = db.DBConnection()
@@ -714,7 +742,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None,
} }
data = request.request_json( data = request.request_json(
url='http://api.omgwtfnzbs.org/json/', url='http://api.omgwtfnzbs.me/json/',
params=params, headers=headers params=params, headers=headers
) )
@@ -813,6 +841,19 @@ def send_to_downloader(data, bestqual, album):
torrent_name = helpers.replace_illegal_chars(folder_name) + '.torrent' torrent_name = helpers.replace_illegal_chars(folder_name) + '.torrent'
download_path = os.path.join(headphones.CONFIG.TORRENTBLACKHOLE_DIR, torrent_name) download_path = os.path.join(headphones.CONFIG.TORRENTBLACKHOLE_DIR, torrent_name)
# Blackhole for t411
if bestqual[2].lower().startswith("http://api.t411"):
if headphones.CONFIG.MAGNET_LINKS == 2:
try:
url = bestqual[2].split('TOKEN')[0]
token = bestqual[2].split('TOKEN')[1]
data = request.request_content(url, headers={'Authorization': token})
torrent_to_file(download_path, data)
logger.info('Successfully converted magnet to torrent file')
except Exception as e:
logger.error("Error converting magnet link: %s" % str(e))
return
if bestqual[2].lower().startswith("magnet:"): if bestqual[2].lower().startswith("magnet:"):
if headphones.CONFIG.MAGNET_LINKS == 1: if headphones.CONFIG.MAGNET_LINKS == 1:
try: try:
@@ -838,12 +879,11 @@ def send_to_downloader(data, bestqual, album):
services = TORRENT_TO_MAGNET_SERVICES[:] services = TORRENT_TO_MAGNET_SERVICES[:]
random.shuffle(services) random.shuffle(services)
headers = {'User-Agent': USER_AGENT} headers = {'User-Agent': USER_AGENT}
headers['Referer'] = 'https://torcache.net/'
for service in services: for service in services:
data = request.request_content(service % torrent_hash, headers=headers) data = request.request_content(service % torrent_hash, headers=headers)
if data and "torcache" in data: if data:
if not torrent_to_file(download_path, data): if not torrent_to_file(download_path, data):
return return
# Extract folder name from torrent # Extract folder name from torrent
@@ -889,11 +929,11 @@ def send_to_downloader(data, bestqual, album):
logger.error("Error sending torrent to Transmission. Are you sure it's running?") logger.error("Error sending torrent to Transmission. Are you sure it's running?")
return return
folder_name = transmission.getTorrentFolder(torrentid) folder_name = transmission.getName(torrentid)
if folder_name: if folder_name:
logger.info('Torrent folder name: %s' % folder_name) logger.info('Torrent name: %s' % folder_name)
else: else:
logger.error('Torrent folder name could not be determined') logger.error('Torrent name could not be determined')
return return
# Set Seed Ratio # Set Seed Ratio
@@ -943,7 +983,7 @@ def send_to_downloader(data, bestqual, album):
except Exception as e: except Exception as e:
logger.error('Error sending torrent to Deluge: %s' % str(e)) logger.error('Error sending torrent to Deluge: %s' % str(e))
else: # if headphones.CONFIG.TORRENT_DOWNLOADER == 2: elif headphones.CONFIG.TORRENT_DOWNLOADER == 2:
logger.info("Sending torrent to uTorrent") logger.info("Sending torrent to uTorrent")
# Add torrent # Add torrent
@@ -974,19 +1014,42 @@ def send_to_downloader(data, bestqual, album):
seed_ratio = get_seed_ratio(bestqual[3]) seed_ratio = get_seed_ratio(bestqual[3])
if seed_ratio is not None: if seed_ratio is not None:
utorrent.setSeedRatio(torrentid, seed_ratio) utorrent.setSeedRatio(torrentid, seed_ratio)
else: # if headphones.CONFIG.TORRENT_DOWNLOADER == 4:
logger.info("Sending torrent to QBiTorrent")
# Add torrent
if bestqual[3] == 'rutracker.org':
ruobj.qbittorrent_add_file(data)
else:
qbittorrent.addTorrent(bestqual[2])
# Get hash
torrentid = calculate_torrent_hash(bestqual[2], data)
torrentid = torrentid.lower()
if not torrentid:
logger.error('Torrent id could not be determined')
return
# Get name
folder_name = qbittorrent.getName(torrentid)
if folder_name:
logger.info('Torrent name: %s' % folder_name)
else:
logger.error('Torrent name could not be determined')
return
myDB = db.DBConnection() myDB = db.DBConnection()
myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']]) myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']])
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)', myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?, ?)',
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Snatched", folder_name, [album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Snatched", folder_name,
kind]) kind, torrentid])
# Store the torrent id so we can check later if it's finished seeding and can be removed # Store the torrent id so we can check later if it's finished seeding and can be removed
if seed_ratio is not None and seed_ratio != 0 and torrentid: if seed_ratio is not None and seed_ratio != 0 and torrentid:
myDB.action( myDB.action(
'INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)', 'INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?, ?)',
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", torrentid, [album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", folder_name,
kind]) kind, torrentid])
# notify # notify
artist = album[1] artist = album[1]
@@ -1014,6 +1077,14 @@ def send_to_downloader(data, bestqual, album):
logger.info(u"Sending PushBullet notification") logger.info(u"Sending PushBullet notification")
pushbullet = notifiers.PUSHBULLET() pushbullet = notifiers.PUSHBULLET()
pushbullet.notify(name, "Download started") pushbullet.notify(name, "Download started")
if headphones.CONFIG.JOIN_ENABLED and headphones.CONFIG.JOIN_ONSNATCH:
logger.info(u"Sending Join notification")
join = notifiers.JOIN()
join.notify(name, "Download started")
if headphones.CONFIG.SLACK_ENABLED and headphones.CONFIG.SLACK_ONSNATCH:
logger.info(u"Sending Slack notification")
slack = notifiers.SLACK()
slack.notify(name, "Download started")
if headphones.CONFIG.TELEGRAM_ENABLED and headphones.CONFIG.TELEGRAM_ONSNATCH: if headphones.CONFIG.TELEGRAM_ENABLED and headphones.CONFIG.TELEGRAM_ONSNATCH:
logger.info(u"Sending Telegram notification") logger.info(u"Sending Telegram notification")
telegram = notifiers.TELEGRAM() telegram = notifiers.TELEGRAM()
@@ -1140,7 +1211,8 @@ def verifyresult(title, artistterm, term, lossless):
def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
choose_specific_download=False): choose_specific_download=False):
global gazelle # persistent what.cd api object to reduce number of login attempts global apolloobj # persistent apollo.rip api object to reduce number of login attempts
global redobj # persistent redacted api object to reduce number of login attempts
global ruobj # and rutracker global ruobj # and rutracker
reldate = album['ReleaseDate'] reldate = album['ReleaseDate']
@@ -1220,10 +1292,13 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly: if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
categories = "3040" categories = "3040"
maxsize = 10000000000
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
categories = "3040,3010" categories = "3040,3010,3050"
maxsize = 10000000000
else: else:
categories = "3010" categories = "3010,3050"
maxsize = 300000000
if album['Type'] == 'Other': if album['Type'] == 'Other':
categories = "3030" categories = "3030"
@@ -1233,8 +1308,12 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
provider = torznab_host[0] provider = torznab_host[0]
# Format Jackett provider
if "api/v2.0/indexers" in torznab_host[0]:
provider = "Jackett_" + provider.split("/indexers/", 1)[1].split('/', 1)[0]
# Request results # Request results
logger.info('Parsing results from %s using search term: %s' % (torznab_host[0], term)) logger.info('Parsing results from %s using search term: %s' % (provider, term))
headers = {'User-Agent': USER_AGENT} headers = {'User-Agent': USER_AGENT}
params = { params = {
@@ -1245,25 +1324,47 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
"q": term "q": term
} }
data = request.request_feed( data = request.request_soup(
url=torznab_host[0] + '/api?', url=torznab_host[0],
params=params, headers=headers params=params, headers=headers
) )
# Process feed # Process feed
if data: if data:
if not len(data.entries): items = data.find_all('item')
logger.info(u"No results found from %s for %s", torznab_host[0], term) if not items:
logger.info(u"No results found from %s for %s", provider, term)
else: else:
for item in data.entries: for item in items:
try: try:
url = item.link title = item.title.get_text()
title = item.title url = item.find("link").next_sibling.strip()
size = int(item.links[1]['length']) seeders = int(item.find("torznab:attr", attrs={"name": "seeders"}).get('value'))
# Torrentech hack - size currently not returned, make it up
if 'torrentech' in torznab_host[0]:
if albumlength:
if 'Lossless' in title:
size = albumlength / 1000 * 800 * 128
elif 'MP3' in title:
size = albumlength / 1000 * 320 * 128
else:
size = albumlength / 1000 * 256 * 128
else:
logger.info('Skipping %s, could not determine size' % title)
continue
else:
size = int(item.size.string)
if all(word.lower() in title.lower() for word in term.split()): if all(word.lower() in title.lower() for word in term.split()):
logger.info( if size < maxsize and minimumseeders < seeders:
'Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
resultlist.append((title, size, url, provider, 'torrent', True)) resultlist.append((title, size, url, provider, 'torrent', True))
else:
logger.info(
'%s is larger than the maxsize or has too little seeders for this category, '
'skipping. (Size: %i bytes, Seeders: %d)',
title, size, seeders)
else: else:
logger.info('Skipping %s, not all search term words found' % title) logger.info('Skipping %s, not all search term words found' % title)
@@ -1279,7 +1380,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
if headphones.CONFIG.KAT_PROXY_URL: if headphones.CONFIG.KAT_PROXY_URL:
providerurl = fix_url(set_proxy(headphones.CONFIG.KAT_PROXY_URL)) providerurl = fix_url(set_proxy(headphones.CONFIG.KAT_PROXY_URL))
else: else:
providerurl = fix_url("https://kat.cr") providerurl = fix_url("https://katcr.co/new/")
# Build URL # Build URL
providerurl = providerurl + "/usearch/" + ka_term providerurl = providerurl + "/usearch/" + ka_term
@@ -1332,8 +1433,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
logger.exception("Unhandled exception in the KAT parser") logger.exception("Unhandled exception in the KAT parser")
if headphones.CONFIG.WAFFLES: if headphones.CONFIG.WAFFLES:
provider = "Waffles.fm" provider = "Waffles.ch"
providerurl = fix_url("https://www.waffles.fm/browse.php") providerurl = fix_url("https://waffles.ch/browse.php")
bitrate = None bitrate = None
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly: if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
@@ -1355,14 +1456,15 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
query_items = [usersearchterm] query_items = [usersearchterm]
query_items.extend(['format:(%s)' % format, query_items.extend(['format:(%s)' % format,
'size:[0 TO %d]' % maxsize, 'size:[0 TO %d]' % maxsize])
'-seeders:0']) # cut out dead torrents # (25/03/2017 Waffles back up after 5 months, all torrents currently have no seeders, remove for now)
# '-seeders:0']) cut out dead torrents
if bitrate: if bitrate:
query_items.append('bitrate:"%s"' % bitrate) query_items.append('bitrate:"%s"' % bitrate)
# Requesting content # Requesting content
logger.info('Parsing results from Waffles.fm') logger.info('Parsing results from Waffles.ch')
params = { params = {
"uid": headphones.CONFIG.WAFFLES_UID, "uid": headphones.CONFIG.WAFFLES_UID,
@@ -1395,7 +1497,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
logger.info('Found %s. Size: %s', title, helpers.bytes_to_mb(size)) logger.info('Found %s. Size: %s', title, helpers.bytes_to_mb(size))
except Exception as e: except Exception as e:
logger.error( logger.error(
u"An error occurred while trying to parse the response from Waffles.fm: %s", u"An error occurred while trying to parse the response from Waffles.ch: %s",
e) e)
# rutracker.org # rutracker.org
@@ -1432,9 +1534,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
if rulist: if rulist:
resultlist.extend(rulist) resultlist.extend(rulist)
if headphones.CONFIG.WHATCD: if headphones.CONFIG.APOLLO:
provider = "What.cd" provider = "Apollo.rip"
providerurl = "http://what.cd/" providerurl = "http://apollo.rip/"
bitrate = None bitrate = None
bitrate_string = bitrate bitrate_string = bitrate
@@ -1457,7 +1559,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
bitrate_string = encoding_string bitrate_string = encoding_string
if bitrate_string not in gazelleencoding.ALL_ENCODINGS: if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
logger.info( logger.info(
u"Your preferred bitrate is not one of the available What.cd filters, so not using it as a search parameter.") u"Your preferred bitrate is not one of the available Apollo.rip filters, so not using it as a search parameter.")
maxsize = 10000000000 maxsize = 10000000000
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
search_formats = [gazelleformat.FLAC, gazelleformat.MP3] search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
@@ -1466,27 +1568,157 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
search_formats = [gazelleformat.MP3] search_formats = [gazelleformat.MP3]
maxsize = 300000000 maxsize = 300000000
if not gazelle or not gazelle.logged_in(): if not apolloobj or not apolloobj.logged_in():
try: try:
logger.info(u"Attempting to log in to What.cd...") logger.info(u"Attempting to log in to Apollo.rip...")
gazelle = gazelleapi.GazelleAPI(headphones.CONFIG.WHATCD_USERNAME, apolloobj = gazelleapi.GazelleAPI(headphones.CONFIG.APOLLO_USERNAME,
headphones.CONFIG.WHATCD_PASSWORD) headphones.CONFIG.APOLLO_PASSWORD,
gazelle._login() headphones.CONFIG.APOLLO_URL)
apolloobj._login()
except Exception as e: except Exception as e:
gazelle = None apolloobj = None
logger.error(u"What.cd credentials incorrect or site is down. Error: %s %s" % ( logger.error(u"Apollo.rip credentials incorrect or site is down. Error: %s %s" % (
e.__class__.__name__, str(e))) e.__class__.__name__, str(e)))
if gazelle and gazelle.logged_in(): if apolloobj and apolloobj.logged_in():
logger.info(u"Searching %s..." % provider)
all_torrents = []
# Specify release types to filter by
if album['Type'] == 'Album':
album_type = [gazellerelease_type.ALBUM]
if album['Type'] == 'Soundtrack':
album_type = [gazellerelease_type.SOUNDTRACK]
if album['Type'] == 'EP':
album_type = [gazellerelease_type.EP]
# No musicbrainz match for this type
# if album['Type'] == 'Anthology':
# album_type = [gazellerelease_type.ANTHOLOGY]
if album['Type'] == 'Compilation':
album_type = [gazellerelease_type.COMPILATION]
if album['Type'] == 'DJ-mix':
album_type = [gazellerelease_type.DJ_MIX]
if album['Type'] == 'Single':
album_type = [gazellerelease_type.SINGLE]
if album['Type'] == 'Live':
album_type = [gazellerelease_type.LIVE_ALBUM]
if album['Type'] == 'Remix':
album_type = [gazellerelease_type.REMIX]
if album['Type'] == 'Bootleg':
album_type = [gazellerelease_type.BOOTLEG]
if album['Type'] == 'Interview':
album_type = [gazellerelease_type.INTERVIEW]
if album['Type'] == 'Mixtape/Street':
album_type = [gazellerelease_type.MIXTAPE]
for search_format in search_formats:
if usersearchterm:
all_torrents.extend(
apolloobj.search_torrents(searchstr=usersearchterm, format=search_format,
encoding=bitrate_string, releasetype=album_type)['results'])
else:
all_torrents.extend(apolloobj.search_torrents(artistname=semi_clean_artist_term,
groupname=semi_clean_album_term,
format=search_format,
encoding=bitrate_string,
releasetype=album_type)['results'])
# filter on format, size, and num seeders
logger.info(u"Filtering torrents by format, maximum size, and minimum seeders...")
match_torrents = [t for t in all_torrents if
t.size <= maxsize and t.seeders >= minimumseeders]
logger.info(
u"Remaining torrents: %s" % ", ".join(repr(torrent) for torrent in match_torrents))
# sort by times d/l'd
if not len(match_torrents):
logger.info(u"No results found from %s for %s after filtering" % (provider, term))
elif len(match_torrents) > 1:
logger.info(u"Found %d matching releases from %s for %s - %s after filtering" %
(len(match_torrents), provider, artistterm, albumterm))
logger.info(
"Sorting torrents by times snatched and preferred bitrate %s..." % bitrate_string)
match_torrents.sort(key=lambda x: int(x.snatched), reverse=True)
if gazelleformat.MP3 in search_formats:
# sort by size after rounding to nearest 10MB...hacky, but will favor highest quality
match_torrents.sort(key=lambda x: int(10 * round(x.size / 1024. / 1024. / 10.)),
reverse=True)
if search_formats and None not in search_formats:
match_torrents.sort(
key=lambda x: int(search_formats.index(x.format))) # prefer lossless
# if bitrate:
# match_torrents.sort(key=lambda x: re.match("mp3", x.getTorrentDetails(), flags=re.I), reverse=True)
# match_torrents.sort(key=lambda x: str(bitrate) in x.getTorrentFolderName(), reverse=True)
logger.info(
u"New order: %s" % ", ".join(repr(torrent) for torrent in match_torrents))
for torrent in match_torrents:
if not torrent.file_path:
torrent.group.update_group_data() # will load the file_path for the individual torrents
resultlist.append((torrent.file_path,
torrent.size,
apolloobj.generate_torrent_link(torrent.id),
provider,
'torrent', True))
# Redacted - Using same logic as What.CD as it's also Gazelle, so should really make this into something reusable
if headphones.CONFIG.REDACTED:
provider = "Redacted"
providerurl = "https://redacted.ch"
bitrate = None
bitrate_string = bitrate
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly: # Lossless Only mode
search_formats = [gazelleformat.FLAC]
maxsize = 10000000000
elif headphones.CONFIG.PREFERRED_QUALITY == 2: # Preferred quality mode
search_formats = [None] # should return all
bitrate = headphones.CONFIG.PREFERRED_BITRATE
if bitrate:
if 225 <= int(bitrate) < 256:
bitrate = 'V0'
elif 200 <= int(bitrate) < 225:
bitrate = 'V1'
elif 175 <= int(bitrate) < 200:
bitrate = 'V2'
for encoding_string in gazelleencoding.ALL_ENCODINGS:
if re.search(bitrate, encoding_string, flags=re.I):
bitrate_string = encoding_string
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
logger.info(
u"Your preferred bitrate is not one of the available RED filters, so not using it as a search parameter.")
maxsize = 10000000000
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
maxsize = 10000000000
else: # Highest quality excluding lossless
search_formats = [gazelleformat.MP3]
maxsize = 300000000
if not redobj or not redobj.logged_in():
try:
logger.info(u"Attempting to log in to Redacted...")
redobj = gazelleapi.GazelleAPI(headphones.CONFIG.REDACTED_USERNAME,
headphones.CONFIG.REDACTED_PASSWORD,
providerurl)
redobj._login()
except Exception as e:
redobj = None
logger.error(u"Redacted credentials incorrect or site is down. Error: %s %s" % (
e.__class__.__name__, str(e)))
if redobj and redobj.logged_in():
logger.info(u"Searching %s..." % provider) logger.info(u"Searching %s..." % provider)
all_torrents = [] all_torrents = []
for search_format in search_formats: for search_format in search_formats:
if usersearchterm: if usersearchterm:
all_torrents.extend( all_torrents.extend(
gazelle.search_torrents(searchstr=usersearchterm, format=search_format, redobj.search_torrents(searchstr=usersearchterm, format=search_format,
encoding=bitrate_string)['results']) encoding=bitrate_string)['results'])
else: else:
all_torrents.extend(gazelle.search_torrents(artistname=semi_clean_artist_term, all_torrents.extend(redobj.search_torrents(artistname=semi_clean_artist_term,
groupname=semi_clean_album_term, groupname=semi_clean_album_term,
format=search_format, format=search_format,
encoding=bitrate_string)['results']) encoding=bitrate_string)['results'])
@@ -1526,20 +1758,20 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
torrent.group.update_group_data() # will load the file_path for the individual torrents torrent.group.update_group_data() # will load the file_path for the individual torrents
resultlist.append((torrent.file_path, resultlist.append((torrent.file_path,
torrent.size, torrent.size,
gazelle.generate_torrent_link(torrent.id), redobj.generate_torrent_link(torrent.id),
provider, provider,
'torrent', True)) 'torrent', True))
# Pirate Bay # Pirate Bay
if headphones.CONFIG.PIRATEBAY: if headphones.CONFIG.PIRATEBAY:
provider = "The Pirate Bay" provider = "The Pirate Bay"
tpb_term = term.replace("!", "") tpb_term = term.replace("!", "").replace("'", " ")
# Use proxy if specified # Use proxy if specified
if headphones.CONFIG.PIRATEBAY_PROXY_URL: if headphones.CONFIG.PIRATEBAY_PROXY_URL:
providerurl = fix_url(set_proxy(headphones.CONFIG.PIRATEBAY_PROXY_URL)) providerurl = fix_url(set_proxy(headphones.CONFIG.PIRATEBAY_PROXY_URL))
else: else:
providerurl = fix_url("https://thepiratebay.se") providerurl = fix_url("https://thepiratebay.org")
# Build URL # Build URL
providerurl = providerurl + "/search/" + tpb_term + "/0/7/" # 7 is sort by seeders providerurl = providerurl + "/search/" + tpb_term + "/0/7/" # 7 is sort by seeders
@@ -1763,6 +1995,77 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
resultlist.append((title, size, url, provider, 'torrent', match)) resultlist.append((title, size, url, provider, 'torrent', match))
except Exception as e: except Exception as e:
logger.exception("Unhandled exception in Mininova Parser") logger.exception("Unhandled exception in Mininova Parser")
# t411
if headphones.CONFIG.TQUATTRECENTONZE:
username = headphones.CONFIG.TQUATTRECENTONZE_USER
password = headphones.CONFIG.TQUATTRECENTONZE_PASSWORD
API_URL = "http://api.t411.ai"
AUTH_URL = API_URL + '/auth'
DL_URL = API_URL + '/torrents/download/'
provider = "t411"
t411_term = term.replace(" ", "%20")
SEARCH_URL = API_URL + '/torrents/search/' + t411_term + "?limit=15&cid=395&subcat=623"
headers_login = {'username': username, 'password': password}
# Requesting content
logger.info('Parsing results from t411 using search term: %s' % term)
req = request.request_content(AUTH_URL, method='post', data=headers_login)
if len(req.split('"')) == 9:
token = req.split('"')[7]
headers_auth = {'Authorization': token}
logger.info('t411 - User %s logged in' % username)
else:
logger.info('t411 - Login error : %s' % req.split('"')[3])
# Quality
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
providerurl = fix_url(SEARCH_URL + "&term[16][]=529&term[16][]=1184")
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
providerurl = fix_url(SEARCH_URL + "&term[16][]=685&term[16][]=527&term[16][]=1070&term[16][]=528&term[16][]=1167&term[16][]=1166&term[16][]=530&term[16][]=529&term[16][]=1184&term[16][]=532&term[16][]=533&term[16][]=1085&term[16][]=534&term[16][]=535&term[16][]=1069&term[16][]=537&term[16][]=538")
elif headphones.CONFIG.PREFERRED_QUALITY == 0:
providerurl = fix_url(SEARCH_URL + "&term[16][]=685&term[16][]=527&term[16][]=1070&term[16][]=528&term[16][]=1167&term[16][]=1166&term[16][]=530&term[16][]=532&term[16][]=533&term[16][]=1085&term[16][]=534&term[16][]=535&term[16][]=1069&term[16][]=537&term[16][]=538")
else:
providerurl = fix_url(SEARCH_URL)
# Tracker search
req = request.request_content(providerurl, headers=headers_auth)
req = loads(req)
total = req['total']
# Process feed
if total == '0':
logger.info("No results found from t411 for %s" % term)
else:
logger.info('Found %s results from t411' % total)
torrents = req['torrents']
for torrent in torrents:
try:
title = torrent['name']
if torrent['seeders'] < minimumseeders:
logger.info('Skipping torrent %s : seeders below minimum set' % title)
continue
id = torrent['id']
size = int(torrent['size'])
data = request.request_content(DL_URL + id, headers=headers_auth)
# Blackhole
if headphones.CONFIG.TORRENT_DOWNLOADER == 0 and headphones.CONFIG.MAGNET_LINKS == 2:
url = DL_URL + id + 'TOKEN' + token
resultlist.append((title, size, url, provider, 'torrent', True))
# Build magnet
else:
metadata = bdecode(data)
hashcontents = bencode(metadata['info'])
digest = sha1(hashcontents).hexdigest()
trackers = [metadata["announce"]][0]
url = 'magnet:?xt=urn:btih:%s&tr=%s' % (digest, trackers)
resultlist.append((title, size, url, provider, 'torrent', True))
except Exception as e:
logger.error("Error converting magnet link: %s" % str(e))
return
# attempt to verify that this isn't a substring result # attempt to verify that this isn't a substring result
# when looking for "Foo - Foo" we don't want "Foobar" # when looking for "Foo - Foo" we don't want "Foobar"
@@ -1800,7 +2103,9 @@ def preprocess(resultlist):
if result[3] == 'Kick Ass Torrents': if result[3] == 'Kick Ass Torrents':
headers['Referer'] = 'https://torcache.net/' headers['Referer'] = 'https://torcache.net/'
headers['User-Agent'] = USER_AGENT headers['User-Agent'] = USER_AGENT
elif result[3] == 'What.cd': elif result[3] == 'Apollo.rip':
headers['User-Agent'] = 'Headphones'
elif result[3] == 'Redacted':
headers['User-Agent'] = 'Headphones' headers['User-Agent'] = 'Headphones'
elif result[3] == "The Pirate Bay" or result[3] == "Old Pirate Bay": elif result[3] == "The Pirate Bay" or result[3] == "Old Pirate Bay":
headers[ headers[
+19 -18
View File
@@ -13,13 +13,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
import threading
from headphones import db, utorrent, transmission, logger from headphones import db, utorrent, transmission, deluge, qbittorrent, logger
import headphones import headphones
postprocessor_lock = threading.Lock()
def checkTorrentFinished(): def checkTorrentFinished():
""" """
@@ -28,21 +25,25 @@ def checkTorrentFinished():
logger.info("Checking if any torrents have finished seeding and can be removed") logger.info("Checking if any torrents have finished seeding and can be removed")
with postprocessor_lock: myDB = db.DBConnection()
myDB = db.DBConnection() results = myDB.select('SELECT * from snatched WHERE Status="Seed_Processed"')
results = myDB.select('SELECT * from snatched WHERE Status="Seed_Processed"')
for album in results: for album in results:
hash = album['FolderName'] hash = album['TorrentHash']
albumid = album['AlbumID'] albumid = album['AlbumID']
torrent_removed = False torrent_removed = False
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
torrent_removed = transmission.removeTorrent(hash, True)
else:
torrent_removed = utorrent.removeTorrent(hash, True)
if torrent_removed: if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?', torrent_removed = transmission.removeTorrent(hash, True)
[albumid]) elif headphones.CONFIG.TORRENT_DOWNLOADER == 2:
torrent_removed = utorrent.removeTorrent(hash, True)
elif headphones.CONFIG.TORRENT_DOWNLOADER == 3:
torrent_removed = deluge.removeTorrent(hash, True)
else:
torrent_removed = qbittorrent.removeTorrent(hash, True)
if torrent_removed:
myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?',
[albumid])
logger.info("Checking finished torrents completed") logger.info("Checking finished torrents completed")
+27 -2
View File
@@ -17,6 +17,7 @@ import time
import json import json
import base64 import base64
import urlparse import urlparse
import os
from headphones import logger, request from headphones import logger, request
import headphones import headphones
@@ -33,7 +34,7 @@ _session_id = None
def addTorrent(link, data=None): def addTorrent(link, data=None):
method = 'torrent-add' method = 'torrent-add'
if link.endswith('.torrent') and not link.startswith('http') or data: if link.endswith('.torrent') and not link.startswith(('http', 'magnet')) or data:
if data: if data:
metainfo = str(base64.b64encode(data)) metainfo = str(base64.b64encode(data))
else: else:
@@ -64,7 +65,31 @@ def addTorrent(link, data=None):
return False return False
def getTorrentFolder(torrentid): def getFolder(torrentid):
torrent_folder = None
single_file = False
method = 'torrent-get'
arguments = {'ids': torrentid, 'fields': ['files']}
response = torrentAction(method, arguments)
try:
torrent_files = response['arguments']['torrents'][0]['files']
if torrent_files:
if len(torrent_files) == 1:
torrent_folder = torrent_files[0]['name']
single_file = True
else:
torrent_folder = os.path.split(torrent_files[0]['name'])[0]
single_file = False
except:
torrent_folder = None
single_file = False
return torrent_folder, single_file
def getName(torrentid):
method = 'torrent-get' method = 'torrent-get'
arguments = {'ids': torrentid, 'fields': ['name', 'percentDone']} arguments = {'ids': torrentid, 'fields': ['name', 'percentDone']}
+4 -3
View File
@@ -39,14 +39,15 @@ def runGit(args):
try: try:
logger.debug('Trying to execute: "' + cmd + '" with shell in ' + headphones.PROG_DIR) logger.debug('Trying to execute: "' + cmd + '" with shell in ' + headphones.PROG_DIR)
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
shell=True,
cwd=headphones.PROG_DIR) cwd=headphones.PROG_DIR)
output, err = p.communicate() output, err = p.communicate()
output = output.strip() output = output.strip()
logger.debug('Git output: ' + output) logger.debug('Git output: ' + output)
except OSError: except OSError as e:
logger.debug('Command failed: %s', cmd) logger.debug('Command failed: %s. Error: %s' % (cmd, e))
continue continue
if 'not found' in output or "not recognized as an internal or external command" in output: if 'not found' in output or "not recognized as an internal or external command" in output:
+77 -22
View File
@@ -14,6 +14,7 @@
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi # NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
# t411 support added by a1ex, @likeitneverwentaway on github for maintenance
from operator import itemgetter from operator import itemgetter
import threading import threading
@@ -28,7 +29,7 @@ import urllib2
import os import os
import re import re
from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers, notifiers from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers, notifiers, crier
from headphones.helpers import checked, radio, today, clean_name from headphones.helpers import checked, radio, today, clean_name
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
from mako import exceptions from mako import exceptions
@@ -69,6 +70,11 @@ class WebInterface(object):
artists = myDB.select('SELECT * from artists order by ArtistSortName COLLATE NOCASE') artists = myDB.select('SELECT * from artists order by ArtistSortName COLLATE NOCASE')
return serve_template(templatename="index.html", title="Home", artists=artists) return serve_template(templatename="index.html", title="Home", artists=artists)
@cherrypy.expose
def threads(self):
crier.cry()
raise cherrypy.HTTPRedirect("home")
@cherrypy.expose @cherrypy.expose
def artistPage(self, ArtistID): def artistPage(self, ArtistID):
myDB = db.DBConnection() myDB = db.DBConnection()
@@ -246,7 +252,10 @@ class WebInterface(object):
namecheck = myDB.select('SELECT ArtistName from artists where ArtistID=?', [ArtistID]) namecheck = myDB.select('SELECT ArtistName from artists where ArtistID=?', [ArtistID])
for name in namecheck: for name in namecheck:
artistname = name['ArtistName'] artistname = name['ArtistName']
logger.info(u"Deleting all traces of artist: " + artistname) try:
logger.info(u"Deleting all traces of artist: " + artistname)
except TypeError:
logger.info(u"Deleting all traces of artist: null")
myDB.action('DELETE from artists WHERE ArtistID=?', [ArtistID]) myDB.action('DELETE from artists WHERE ArtistID=?', [ArtistID])
from headphones import cache from headphones import cache
@@ -339,14 +348,18 @@ class WebInterface(object):
dirs = set(dirs) dirs = set(dirs)
for dir in dirs: try:
artistfolder = os.path.join(dir, folder) for dir in dirs:
if not os.path.isdir(artistfolder.encode(headphones.SYS_ENCODING)): artistfolder = os.path.join(dir, folder)
logger.debug("Cannot find directory: " + artistfolder) if not os.path.isdir(artistfolder.encode(headphones.SYS_ENCODING)):
continue logger.debug("Cannot find directory: " + artistfolder)
threading.Thread(target=librarysync.libraryScan, continue
kwargs={"dir": artistfolder, "artistScan": True, "ArtistID": ArtistID, threading.Thread(target=librarysync.libraryScan,
"ArtistName": artist_name}).start() kwargs={"dir": artistfolder, "artistScan": True, "ArtistID": ArtistID,
"ArtistName": artist_name}).start()
except Exception as e:
logger.error('Unable to complete the scan: %s' % e)
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
@cherrypy.expose @cherrypy.expose
@@ -528,7 +541,7 @@ class WebInterface(object):
myDB = db.DBConnection() myDB = db.DBConnection()
upcoming = myDB.select( upcoming = myDB.select(
"SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate ASC") "SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate ASC")
wanted = myDB.select("SELECT * from albums WHERE Status='Wanted'") wanted = myDB.select("SELECT * from albums WHERE Status='Wanted' order by ReleaseDate ASC")
return serve_template(templatename="upcoming.html", title="Upcoming", upcoming=upcoming, return serve_template(templatename="upcoming.html", title="Upcoming", upcoming=upcoming,
wanted=wanted) wanted=wanted)
@@ -865,11 +878,19 @@ class WebInterface(object):
def musicScan(self, path, scan=0, redirect=None, autoadd=0, libraryscan=0): def musicScan(self, path, scan=0, redirect=None, autoadd=0, libraryscan=0):
headphones.CONFIG.LIBRARYSCAN = libraryscan headphones.CONFIG.LIBRARYSCAN = libraryscan
headphones.CONFIG.AUTO_ADD_ARTISTS = autoadd headphones.CONFIG.AUTO_ADD_ARTISTS = autoadd
headphones.CONFIG.MUSIC_DIR = path
headphones.CONFIG.write() try:
params = {}
headphones.CONFIG.MUSIC_DIR = path
headphones.CONFIG.write()
except Exception as e:
logger.warn("Cannot save scan directory to config: %s", e)
if scan:
params = {"dir": path}
if scan: if scan:
try: try:
threading.Thread(target=librarysync.libraryScan).start() threading.Thread(target=librarysync.libraryScan, kwargs=params).start()
except Exception as e: except Exception as e:
logger.error('Unable to complete the scan: %s' % e) logger.error('Unable to complete the scan: %s' % e)
if redirect: if redirect:
@@ -1139,6 +1160,7 @@ class WebInterface(object):
"download_scan_interval": headphones.CONFIG.DOWNLOAD_SCAN_INTERVAL, "download_scan_interval": headphones.CONFIG.DOWNLOAD_SCAN_INTERVAL,
"update_db_interval": headphones.CONFIG.UPDATE_DB_INTERVAL, "update_db_interval": headphones.CONFIG.UPDATE_DB_INTERVAL,
"mb_ignore_age": headphones.CONFIG.MB_IGNORE_AGE, "mb_ignore_age": headphones.CONFIG.MB_IGNORE_AGE,
"mb_ignore_age_missing": headphones.CONFIG.MB_IGNORE_AGE_MISSING,
"search_interval": headphones.CONFIG.SEARCH_INTERVAL, "search_interval": headphones.CONFIG.SEARCH_INTERVAL,
"libraryscan_interval": headphones.CONFIG.LIBRARYSCAN_INTERVAL, "libraryscan_interval": headphones.CONFIG.LIBRARYSCAN_INTERVAL,
"sab_host": headphones.CONFIG.SAB_HOST, "sab_host": headphones.CONFIG.SAB_HOST,
@@ -1151,6 +1173,10 @@ class WebInterface(object):
"nzbget_password": headphones.CONFIG.NZBGET_PASSWORD, "nzbget_password": headphones.CONFIG.NZBGET_PASSWORD,
"nzbget_category": headphones.CONFIG.NZBGET_CATEGORY, "nzbget_category": headphones.CONFIG.NZBGET_CATEGORY,
"nzbget_priority": headphones.CONFIG.NZBGET_PRIORITY, "nzbget_priority": headphones.CONFIG.NZBGET_PRIORITY,
"qbittorrent_host": headphones.CONFIG.QBITTORRENT_HOST,
"qbittorrent_username": headphones.CONFIG.QBITTORRENT_USERNAME,
"qbittorrent_password": headphones.CONFIG.QBITTORRENT_PASSWORD,
"qbittorrent_label": headphones.CONFIG.QBITTORRENT_LABEL,
"transmission_host": headphones.CONFIG.TRANSMISSION_HOST, "transmission_host": headphones.CONFIG.TRANSMISSION_HOST,
"transmission_username": headphones.CONFIG.TRANSMISSION_USERNAME, "transmission_username": headphones.CONFIG.TRANSMISSION_USERNAME,
"transmission_password": headphones.CONFIG.TRANSMISSION_PASSWORD, "transmission_password": headphones.CONFIG.TRANSMISSION_PASSWORD,
@@ -1171,6 +1197,7 @@ class WebInterface(object):
"torrent_downloader_transmission": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 1), "torrent_downloader_transmission": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 1),
"torrent_downloader_utorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 2), "torrent_downloader_utorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 2),
"torrent_downloader_deluge": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 3), "torrent_downloader_deluge": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 3),
"torrent_downloader_qbittorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 4),
"download_dir": headphones.CONFIG.DOWNLOAD_DIR, "download_dir": headphones.CONFIG.DOWNLOAD_DIR,
"use_blackhole": checked(headphones.CONFIG.BLACKHOLE), "use_blackhole": checked(headphones.CONFIG.BLACKHOLE),
"blackhole_dir": headphones.CONFIG.BLACKHOLE_DIR, "blackhole_dir": headphones.CONFIG.BLACKHOLE_DIR,
@@ -1218,12 +1245,21 @@ class WebInterface(object):
"rutracker_user": headphones.CONFIG.RUTRACKER_USER, "rutracker_user": headphones.CONFIG.RUTRACKER_USER,
"rutracker_password": headphones.CONFIG.RUTRACKER_PASSWORD, "rutracker_password": headphones.CONFIG.RUTRACKER_PASSWORD,
"rutracker_ratio": headphones.CONFIG.RUTRACKER_RATIO, "rutracker_ratio": headphones.CONFIG.RUTRACKER_RATIO,
"use_whatcd": checked(headphones.CONFIG.WHATCD), "rutracker_cookie": headphones.CONFIG.RUTRACKER_COOKIE,
"whatcd_username": headphones.CONFIG.WHATCD_USERNAME, "use_apollo": checked(headphones.CONFIG.APOLLO),
"whatcd_password": headphones.CONFIG.WHATCD_PASSWORD, "apollo_username": headphones.CONFIG.APOLLO_USERNAME,
"whatcd_ratio": headphones.CONFIG.WHATCD_RATIO, "apollo_password": headphones.CONFIG.APOLLO_PASSWORD,
"apollo_ratio": headphones.CONFIG.APOLLO_RATIO,
"apollo_url": headphones.CONFIG.APOLLO_URL,
"use_redacted": checked(headphones.CONFIG.REDACTED),
"redacted_username": headphones.CONFIG.REDACTED_USERNAME,
"redacted_password": headphones.CONFIG.REDACTED_PASSWORD,
"redacted_ratio": headphones.CONFIG.REDACTED_RATIO,
"use_strike": checked(headphones.CONFIG.STRIKE), "use_strike": checked(headphones.CONFIG.STRIKE),
"strike_ratio": headphones.CONFIG.STRIKE_RATIO, "strike_ratio": headphones.CONFIG.STRIKE_RATIO,
"use_tquattrecentonze": checked(headphones.CONFIG.TQUATTRECENTONZE),
"tquattrecentonze_user": headphones.CONFIG.TQUATTRECENTONZE_USER,
"tquattrecentonze_password": headphones.CONFIG.TQUATTRECENTONZE_PASSWORD,
"pref_qual_0": radio(headphones.CONFIG.PREFERRED_QUALITY, 0), "pref_qual_0": radio(headphones.CONFIG.PREFERRED_QUALITY, 0),
"pref_qual_1": radio(headphones.CONFIG.PREFERRED_QUALITY, 1), "pref_qual_1": radio(headphones.CONFIG.PREFERRED_QUALITY, 1),
"pref_qual_2": radio(headphones.CONFIG.PREFERRED_QUALITY, 2), "pref_qual_2": radio(headphones.CONFIG.PREFERRED_QUALITY, 2),
@@ -1247,6 +1283,8 @@ class WebInterface(object):
"keep_nfo": checked(headphones.CONFIG.KEEP_NFO), "keep_nfo": checked(headphones.CONFIG.KEEP_NFO),
"add_album_art": checked(headphones.CONFIG.ADD_ALBUM_ART), "add_album_art": checked(headphones.CONFIG.ADD_ALBUM_ART),
"album_art_format": headphones.CONFIG.ALBUM_ART_FORMAT, "album_art_format": headphones.CONFIG.ALBUM_ART_FORMAT,
"album_art_min_width": headphones.CONFIG.ALBUM_ART_MIN_WIDTH,
"album_art_max_width": headphones.CONFIG.ALBUM_ART_MAX_WIDTH,
"embed_album_art": checked(headphones.CONFIG.EMBED_ALBUM_ART), "embed_album_art": checked(headphones.CONFIG.EMBED_ALBUM_ART),
"embed_lyrics": checked(headphones.CONFIG.EMBED_LYRICS), "embed_lyrics": checked(headphones.CONFIG.EMBED_LYRICS),
"replace_existing_folders": checked(headphones.CONFIG.REPLACE_EXISTING_FOLDERS), "replace_existing_folders": checked(headphones.CONFIG.REPLACE_EXISTING_FOLDERS),
@@ -1273,6 +1311,7 @@ class WebInterface(object):
"magnet_links_3": radio(headphones.CONFIG.MAGNET_LINKS, 3), "magnet_links_3": radio(headphones.CONFIG.MAGNET_LINKS, 3),
"log_dir": headphones.CONFIG.LOG_DIR, "log_dir": headphones.CONFIG.LOG_DIR,
"cache_dir": headphones.CONFIG.CACHE_DIR, "cache_dir": headphones.CONFIG.CACHE_DIR,
"keep_torrent_files_dir": headphones.CONFIG.KEEP_TORRENT_FILES_DIR,
"interface_list": interface_list, "interface_list": interface_list,
"music_encoder": checked(headphones.CONFIG.MUSIC_ENCODER), "music_encoder": checked(headphones.CONFIG.MUSIC_ENCODER),
"encoder": headphones.CONFIG.ENCODER, "encoder": headphones.CONFIG.ENCODER,
@@ -1373,7 +1412,16 @@ class WebInterface(object):
"email_ssl": checked(headphones.CONFIG.EMAIL_SSL), "email_ssl": checked(headphones.CONFIG.EMAIL_SSL),
"email_tls": checked(headphones.CONFIG.EMAIL_TLS), "email_tls": checked(headphones.CONFIG.EMAIL_TLS),
"email_onsnatch": checked(headphones.CONFIG.EMAIL_ONSNATCH), "email_onsnatch": checked(headphones.CONFIG.EMAIL_ONSNATCH),
"idtag": checked(headphones.CONFIG.IDTAG) "idtag": checked(headphones.CONFIG.IDTAG),
"slack_enabled": checked(headphones.CONFIG.SLACK_ENABLED),
"slack_url": headphones.CONFIG.SLACK_URL,
"slack_channel": headphones.CONFIG.SLACK_CHANNEL,
"slack_emoji": headphones.CONFIG.SLACK_EMOJI,
"slack_onsnatch": checked(headphones.CONFIG.SLACK_ONSNATCH),
"join_enabled": checked(headphones.CONFIG.JOIN_ENABLED),
"join_onsnatch": checked(headphones.CONFIG.JOIN_ONSNATCH),
"join_apikey": headphones.CONFIG.JOIN_APIKEY,
"join_deviceid": headphones.CONFIG.JOIN_DEVICEID
} }
for k, v in config.iteritems(): for k, v in config.iteritems():
@@ -1420,8 +1468,8 @@ class WebInterface(object):
"use_newznab", "newznab_enabled", "use_torznab", "torznab_enabled", "use_newznab", "newznab_enabled", "use_torznab", "torznab_enabled",
"use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay", "use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay",
"use_mininova", "use_waffles", "use_rutracker", "use_mininova", "use_waffles", "use_rutracker",
"use_whatcd", "use_strike", "preferred_bitrate_allow_lossless", "detect_bitrate", "use_apollo", "use_redacted", "use_strike", "use_tquattrecentonze", "preferred_bitrate_allow_lossless",
"ignore_clean_releases", "freeze_db", "cue_split", "move_files", "detect_bitrate", "ignore_clean_releases", "freeze_db", "cue_split", "move_files",
"rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art", "rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art",
"embed_album_art", "embed_lyrics", "embed_album_art", "embed_lyrics",
"replace_existing_folders", "keep_original_folder", "file_underscores", "replace_existing_folders", "keep_original_folder", "file_underscores",
@@ -1441,7 +1489,8 @@ class WebInterface(object):
"osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch", "osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch",
"songkick_enabled", "songkick_filter_enabled", "songkick_enabled", "songkick_filter_enabled",
"mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch", "mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch",
"customauth", "idtag", "deluge_paused" "customauth", "idtag", "deluge_paused",
"join_enabled", "join_onsnatch"
] ]
for checked_config in checked_configs: for checked_config in checked_configs:
if checked_config not in kwargs: if checked_config not in kwargs:
@@ -1710,6 +1759,12 @@ class WebInterface(object):
telegram = notifiers.TELEGRAM() telegram = notifiers.TELEGRAM()
telegram.notify("it works!", "lazers pew pew") telegram.notify("it works!", "lazers pew pew")
@cherrypy.expose
def testJoin(self):
logger.info("Testing Join notifications")
join = notifiers.JOIN()
join.notify("it works!", "Test message")
class Artwork(object): class Artwork(object):
@cherrypy.expose @cherrypy.expose
+16 -36
View File
@@ -1,7 +1,8 @@
#!/bin/sh #!/bin/sh
# #
# PROVIDE: headphones # PROVIDE: headphones
# REQUIRE: DAEMON sabnzbd # REQUIRE: DAEMON
# BEFORE: LOGIN
# KEYWORD: shutdown # KEYWORD: shutdown
# #
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf # Add the following lines to /etc/rc.conf.local or /etc/rc.conf
@@ -15,56 +16,35 @@
# as root. # as root.
# headphones_dir: Directory where Headphones lives. # headphones_dir: Directory where Headphones lives.
# Default: /usr/local/headphones # Default: /usr/local/headphones
# headphones_chdir: Change to this directory before running Headphones.
# Default is same as headphones_dir.
# headphones_pid: The name of the pidfile to create. # headphones_pid: The name of the pidfile to create.
# Default is headphones.pid in headphones_dir. # Default is headphones.pid in headphones_dir.
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
. /etc/rc.subr . /etc/rc.subr
name="headphones" name="headphones"
rcvar=${name}_enable rcvar=${name}_enable
load_rc_config ${name} load_rc_config ${name}
: "${headphones_enable:="NO"}" : "${headphones_enable:="NO"}"
: "${headphones_user:="_sabnzbd"}" : "${headphones_user:="_sabnzbd"}"
: "${headphones_dir:="/usr/local/headphones"}" : "${headphones_dir:="/usr/local/headphones"}"
: "${headphones_chdir:="${headphones_dir}"}" : "${headphones_conf:="/usr/local/headphones/config.ini"}"
: "${headphones_pid:="${headphones_dir}/headphones.pid"}"
status_cmd="${name}_status" command="${headphones_dir}/Headphones.py"
stop_cmd="${name}_stop" command_interpreter="/usr/local/bin/python"
pidfile="/var/run/headphones/headphones.pid"
start_precmd="headphones_start_precmd"
headphones_flags="--daemon --nolaunch --pidfile $pidfile --config $headphones_conf $headphones_flags"
command="/usr/sbin/daemon" headphones_start_precmd() {
command_args="-f -p ${headphones_pid} python ${headphones_dir}/Headphones.py ${headphones_flags} --quiet --nolaunch" if [ $($ID -u) != 0 ]; then
err 1 "Must be root."
fi
# Ensure user is root when running this script. if [ ! -d /var/run/headphones ]; then
if [ "$(id -u)" != "0" ]; then install -do $headphones_user /var/run/headphones
echo "Oops, you should be root before running this!" fi
exit 1
fi
verify_headphones_pid() {
# Make sure the pid corresponds to the Headphones process.
pid=$(cat "${headphones_pid}" 2>/dev/null)
pgrep -F "${headphones_pid}" -q "python ${headphones_dir}/Headphones.py"
return $?
}
# Try to stop Headphones cleanly by calling shutdown over http.
headphones_stop() {
echo "Stopping $name"
verify_headphones_pid
if [ -n "${pid}" ]; then
wait_for_pids "${pid}"
echo "Stopped"
fi
}
headphones_status() {
verify_headphones_pid && echo "$name is running as ${pid}" || echo "$name is not running"
} }
load_rc_config ${name}
run_rc_command "$1" run_rc_command "$1"
+7 -1
View File
@@ -33,6 +33,7 @@
## PYTHON_BIN= #$DAEMON, the location of the python binary, the default is /usr/bin/python ## PYTHON_BIN= #$DAEMON, the location of the python binary, the default is /usr/bin/python
## HP_OPTS= #$EXTRA_DAEMON_OPTS, extra cli option for headphones, i.e. " --config=/home/headphones/config.ini" ## HP_OPTS= #$EXTRA_DAEMON_OPTS, extra cli option for headphones, i.e. " --config=/home/headphones/config.ini"
## HP_PORT= #$PORT_OPTS, hardcoded port for the webserver, overrides value in config.ini ## HP_PORT= #$PORT_OPTS, hardcoded port for the webserver, overrides value in config.ini
## HP_HOST= #$HOST_OPTS, host for the webserver, overrides value in config.ini
## ##
## EXAMPLE if want to run as different user ## EXAMPLE if want to run as different user
## add HP_USER=username to /etc/default/headphones ## add HP_USER=username to /etc/default/headphones
@@ -105,7 +106,12 @@ load_settings() {
PORT_OPTS=" --port=${HP_PORT} " PORT_OPTS=" --port=${HP_PORT} "
} }
DAEMON_OPTS=" Headphones.py --quiet --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS}${EXTRA_DAEMON_OPTS}" # Host config
[ -n "$HP_HOST" ] && {
HOST_OPTS=" --host=${HP_HOST} "
}
DAEMON_OPTS=" Headphones.py --quiet --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS} ${HOST_OPTS} ${EXTRA_DAEMON_OPTS}"
SETTINGS_LOADED=TRUE SETTINGS_LOADED=TRUE
fi fi
-21
View File
@@ -1,21 +0,0 @@
The MIT License
Copyright (c) 2010-2014 Adrian Sampson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-94
View File
@@ -1,94 +0,0 @@
.. image:: https://travis-ci.org/sampsyo/beets.svg?branch=master
:target: https://travis-ci.org/sampsyo/beets
.. image:: http://img.shields.io/coveralls/sampsyo/beets.svg
:target: https://coveralls.io/r/sampsyo/beets
.. image:: http://img.shields.io/pypi/v/beets.svg
:target: https://pypi.python.org/pypi/beets
Beets is the media library management system for obsessive-compulsive music
geeks.
The purpose of beets is to get your music collection right once and for all.
It catalogs your collection, automatically improving its metadata as it goes.
It then provides a bouquet of tools for manipulating and accessing your music.
Here's an example of beets' brainy tag corrector doing its thing::
$ beet import ~/music/ladytron
Tagging:
Ladytron - Witching Hour
(Similarity: 98.4%)
* Last One Standing -> The Last One Standing
* Beauty -> Beauty*2
* White Light Generation -> Whitelightgenerator
* All the Way -> All the Way...
Because beets is designed as a library, it can do almost anything you can
imagine for your music collection. Via `plugins`_, beets becomes a panacea:
- Fetch or calculate all the metadata you could possibly need: `album art`_,
`lyrics`_, `genres`_, `tempos`_, `ReplayGain`_ levels, or `acoustic
fingerprints`_.
- Get metadata from `MusicBrainz`_, `Discogs`_, or `Beatport`_. Or guess
metadata using songs' filenames or their acoustic fingerprints.
- `Transcode audio`_ to any format you like.
- Check your library for `duplicate tracks and albums`_ or for `albums that
are missing tracks`_.
- Clean up crufty tags left behind by other, less-awesome tools.
- Embed and extract album art from files' metadata.
- Browse your music library graphically through a Web browser and play it in any
browser that supports `HTML5 Audio`_.
- Analyze music files' metadata from the command line.
- Listen to your library with a music player that speaks the `MPD`_ protocol
and works with a staggering variety of interfaces.
If beets doesn't do what you want yet, `writing your own plugin`_ is
shockingly simple if you know a little Python.
.. _plugins: http://beets.readthedocs.org/page/plugins/
.. _MPD: http://www.musicpd.org/
.. _MusicBrainz music collection: http://musicbrainz.org/doc/Collections/
.. _writing your own plugin:
http://beets.readthedocs.org/page/dev/plugins.html
.. _HTML5 Audio:
http://www.w3.org/TR/html-markup/audio.html
.. _albums that are missing tracks:
http://beets.readthedocs.org/page/plugins/missing.html
.. _duplicate tracks and albums:
http://beets.readthedocs.org/page/plugins/duplicates.html
.. _Transcode audio:
http://beets.readthedocs.org/page/plugins/convert.html
.. _Beatport: http://www.beatport.com/
.. _Discogs: http://www.discogs.com/
.. _acoustic fingerprints:
http://beets.readthedocs.org/page/plugins/chroma.html
.. _ReplayGain: http://beets.readthedocs.org/page/plugins/replaygain.html
.. _tempos: http://beets.readthedocs.org/page/plugins/echonest.html
.. _genres: http://beets.readthedocs.org/page/plugins/lastgenre.html
.. _album art: http://beets.readthedocs.org/page/plugins/fetchart.html
.. _lyrics: http://beets.readthedocs.org/page/plugins/lyrics.html
.. _MusicBrainz: http://musicbrainz.org/
Read More
---------
Learn more about beets at `its Web site`_. Follow `@b33ts`_ on Twitter for
news and updates.
You can install beets by typing ``pip install beets``. Then check out the
`Getting Started`_ guide.
.. _its Web site: http://beets.radbox.org/
.. _Getting Started: http://beets.readthedocs.org/page/guides/main.html
.. _@b33ts: http://twitter.com/b33ts/
Authors
-------
Beets is by `Adrian Sampson`_ with a supporting cast of thousands. For help,
please contact the `mailing list`_.
.. _mailing list: https://groups.google.com/forum/#!forum/beets-users
.. _Adrian Sampson: http://homes.cs.washington.edu/~asampson/
Regular → Executable
+26 -9
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# This file is part of beets. # This file is part of beets.
# Copyright 2014, Adrian Sampson. # Copyright 2016, Adrian Sampson.
# #
# Permission is hereby granted, free of charge, to any person obtaining # Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the # a copy of this software and associated documentation files (the
@@ -12,17 +13,33 @@
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software. # included in all copies or substantial portions of the Software.
# This particular version has been slightly modified to work with Headphones from __future__ import division, absolute_import, print_function
# https://github.com/rembo10/headphones
__version__ = '1.3.10-headphones'
__author__ = 'Adrian Sampson <adrian@radbox.org>'
import os import os
import beets.library
from beets.util import confit from beets.util import confit
Library = beets.library.Library # This particular version has been slightly modified to work with Headphones
# https://github.com/rembo10/headphones
__version__ = u'1.4.4-headphones'
__author__ = u'Adrian Sampson <adrian@radbox.org>'
config = confit.LazyConfig(os.path.dirname(__file__), __name__)
class IncludeLazyConfig(confit.LazyConfig):
"""A version of Confit's LazyConfig that also merges in data from
YAML files specified in an `include` setting.
"""
def read(self, user=True, defaults=True):
super(IncludeLazyConfig, self).read(user, defaults)
try:
for view in self['include']:
filename = view.as_filename()
if os.path.isfile(filename):
self.set_file(filename)
except confit.NotFoundError:
pass
# headphones
#config = IncludeLazyConfig('beets', __name__)
config = IncludeLazyConfig(os.path.dirname(__file__), __name__)
+26
View File
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2017, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""The __main__ module lets you run the beets CLI interface by typing
`python -m beets`.
"""
from __future__ import division, absolute_import, print_function
import sys
from .ui import main
if __name__ == "__main__":
main(sys.argv[1:])

Some files were not shown because too many files have changed in this diff Show More