Compare commits

..
216 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
208 changed files with 19076 additions and 6865 deletions
-1
View File
@@ -11,7 +11,6 @@ cache:
- lib
python:
- "2.6"
- "2.7"
install:
+33
View File
@@ -1,5 +1,38 @@
# 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
+14 -2
View File
@@ -54,7 +54,10 @@ def main():
try:
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):
pass
@@ -81,6 +84,8 @@ def main():
help='Prevent browser from launching on startup')
parser.add_argument(
'--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()
@@ -170,6 +175,13 @@ def main():
else:
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
# and for CherryPy.
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.
web_config = {
'http_port': http_port,
'http_host': headphones.CONFIG.HTTP_HOST,
'http_host': http_host,
'http_root': headphones.CONFIG.HTTP_ROOT,
'http_proxy': headphones.CONFIG.HTTP_PROXY,
'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://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

+252 -44
View File
@@ -56,7 +56,7 @@
<label title="Password for web server authentication. Leave empty to disable.">
HTTP Password
</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 class="row checkbox">
<input type="checkbox" name="launch_browser" value="1" ${config['launch_browser']} />
@@ -133,6 +133,12 @@
</label>
<input type="text" name="mb_ignore_age" value="${config['mb_ignore_age']}" size="4">days
</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>
</td>
</tr>
@@ -173,7 +179,7 @@
<label title="SABnzbd password. Leave empty if not applicable.">
SABnzbd Password
</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 class="row">
<label title="SABnzbd API key. Can be found in SABnzbd settings.">
@@ -207,7 +213,7 @@
<label title="NZBGet password. Leave empty if not applicable">
NZBget Password
</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 class="row">
<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_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_qbittorrent" value="4" ${config['torrent_downloader_qbittorrent']}> QBitTorrent
</fieldset>
<fieldset id="torrent_blackhole_options">
<div class="row">
@@ -359,7 +366,7 @@
</div>
<div class="row">
<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 class="row">
<small>Note: With Transmission, you can specify a different download directory for downloads sent from Headphones.
@@ -379,13 +386,33 @@
</div>
<div class="row">
<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 class="row">
<label>uTorrent Label</label>
<input type="text" name="utorrent_label" value="${config['utorrent_label']}" size="30">
</div>
</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">
<div class="row">
<label>Deluge WebUI Host and Port</label>
@@ -400,7 +427,7 @@
</div>
<div class="row">
<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 class="row">
<small>Note: With Deluge, you can specify a different download directory for downloads sent from Headphones.
@@ -466,7 +493,7 @@
</div>
<div class="row">
<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 class="row">
<a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Don't have an account? Sign up!</a>
@@ -622,31 +649,59 @@
</div>
<div class="row">
<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 class="row">
<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">
</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>
</fieldset>
<fieldset>
<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 class="config">
<div class="row">
<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 class="row">
<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 class="row">
<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>
</fieldset>
@@ -671,8 +726,8 @@
<div class="config" id="torznab1">
<div class="row">
<label>Torznab Host</label>
<input type="text" name="torznab_host" value="${config['torznab_host']}" size="30">
<small>e.g. http://localhost:9117/torznab/iptorrents</small>
<input type="text" name="torznab_host" value="${config['torznab_host']}" size="36">
<small>e.g. http://localhost:9117/api/v2.0/indexers/demonoid/results/torznab/</small>
</div>
<div class="row">
<label>Torznab API</label>
@@ -695,7 +750,7 @@
<div class="config" id="torznab${torznab_number}">
<div class="row">
<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 class="row">
<label>Torznab API</label>
@@ -743,6 +798,22 @@
</div>
</div>
</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>
</td>
@@ -884,30 +955,42 @@
<input type="checkbox" name="keep_nfo" value="1" ${config['keep_nfo']} />
</label>
</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">
<label>
Add album art jpeg to album folder
<input type="checkbox" name="add_album_art" id="add_album_art" value="1" ${config['add_album_art']}>
</label>
</div>
<div id="album_art_options" style="padding-left: 20px">
<div class="row">
as <input type="text" class="override-float" name="album_art_format" value="${config['album_art_format']}" size="10">.jpg
</div>
<small>Use $Artist/$artist, $Album/$album, $Year/$year, put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}').</small>
</div>
<div class="row checkbox left clearfix nopad">
<label>
Embed album art in each file
<input type="checkbox" name="embed_album_art" value="1" ${config['embed_album_art']}>
</label>
</div>
<div class="row checkbox left clearfix nopad">
<label>
Embed lyrics
<input type="checkbox" name="embed_lyrics" value="1" ${config['embed_lyrics']}>
</label>
<div id="album_art_size_options" style="padding-left: 20px">
<div class="row">
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.">\
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">
<label>
Destination Directory:
@@ -964,7 +1047,7 @@
<label>SMTP User</label><input type="text" name="email_smtp_user" value="${config['email_smtp_user']}" size="254">
</div>
<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 class="row checkbox">
<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">
</div>
<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 class="row checkbox">
<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">
</div>
<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 class="checkbox row">
<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>
</div>
<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>
</div>
<div class="row">
@@ -1226,7 +1309,7 @@
<label>Subsonic Username</label><input type="text" name="subsonic_username" value="${config['subsonic_username']}" size="30">
</div>
<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>
</fieldset>
@@ -1258,6 +1341,26 @@
</div>
</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>
<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>
@@ -1275,6 +1378,23 @@
</div>
</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>
</tr>
</table>
@@ -1570,6 +1690,10 @@
<label>Cache Directory</label>
<input type="text" name="cache_dir" value="${config['cache_dir']}" size="50">
</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>
@@ -1626,7 +1750,7 @@
<label>Username</label><input type="text" class="customuser" name="customuser" value="${config['customuser']}" size="20">
</div>
<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 class="row">
@@ -1639,7 +1763,7 @@
<label>Username</label><input type="text" class="hpuser" name="hpuser" value="${config['hpuser']}" size="20">
</div>
<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>
</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"))
{
$("#album_art_options").show();
$("#album_art_size_options").show();
}
else
{
@@ -1827,10 +1978,15 @@
if ($("#add_album_art").is(":checked"))
{
$("#album_art_options").slideDown();
$("#album_art_size_options").slideDown();
}
else
{
$("#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"))
{
$("#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"))
{
$("#telegramoptions").show();
@@ -2231,26 +2429,30 @@
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();
}
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();
}
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();
}
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"))
{
$("#torrent_blackhole_options,#transmission_options,#utorrent_options").hide();
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#qbittorrent_options").hide();
$("#deluge_options").show();
}
$('input[type=radio]').change(function(){
if ($("#preferred_bitrate").is(":checked"))
{
@@ -2286,19 +2488,23 @@
}
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"))
{
$("#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"))
{
$("#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"))
{
$("#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_waffles");
initConfigCheckbox("#use_rutracker");
initConfigCheckbox("#use_whatcd");
initConfigCheckbox("#use_apollo");
initConfigCheckbox("#use_redacted");
initConfigCheckbox("#use_strike");
initConfigCheckbox("#api_enabled");
initConfigCheckbox("#enable_https");
initConfigCheckbox("#customauth");
initConfigCheckbox("#mb_ignore_age_missing");
initConfigCheckbox("#use_tquattrecentonze");
$('#twitterStep1').click(function () {
$.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({
url: url,
data: dataString,
type: 'POST',
beforeSend: function(jqXHR, settings) {
// Start loader etc.
feedback.prepend(loader);
+23 -6
View File
@@ -86,8 +86,8 @@ CURRENT_VERSION = None
LATEST_VERSION = None
COMMITS_BEHIND = None
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma"]
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff"]
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma", "opus"]
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff", "aif"]
MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS
MIRRORLIST = ["musicbrainz.org", "headphones", "custom"]
@@ -306,9 +306,10 @@ def initialize_scheduler():
minutes=minutes)
# Remove Torrent + data if Post Processed and finished Seeding
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
minutes=minutes)
if headphones.CONFIG.TORRENT_DOWNLOADER != 0:
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
minutes=minutes)
# Start scheduler
if start_jobs and len(SCHED.get_jobs()):
@@ -376,7 +377,7 @@ def dbcheck():
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)')
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
# alltracks
c.execute(
@@ -416,6 +417,8 @@ def dbcheck():
'CREATE INDEX IF NOT EXISTS tracks_Location ON tracks(Location ASC)')
c.execute(
'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:
c.execute('SELECT IncludeExtras from artists')
@@ -613,6 +616,20 @@ def dbcheck():
except sqlite3.OperationalError:
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()
c.close()
+154 -6
View File
@@ -13,16 +13,164 @@
# You should have received a copy of the GNU General Public License
# 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):
myDB = db.DBConnection()
asin = myDB.action(
'SELECT AlbumASIN from albums WHERE AlbumID=?', [albumid]).fetchone()[0]
if asin:
return 'http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % asin
artwork_path = None
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):
+2 -2
View File
@@ -85,8 +85,8 @@ def switch(AlbumID, ReleaseID):
# Update have track counts on index
totaltracks = len(myDB.select(
'SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")',
[newalbumdata['ArtistID']]))
'SELECT TrackTitle from tracks AS tr INNER JOIN albums AS al ON al.AlbumID = tr.AlbumID WHERE al.ArtistID=? '
'AND al.Status != "Ignored"', [newalbumdata['ArtistID']]))
havetracks = len(myDB.select(
'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
[newalbumdata['ArtistID']]))
+27 -6
View File
@@ -16,7 +16,7 @@
import os
import headphones
from headphones import db, helpers, logger, lastfm, request
from headphones import db, helpers, logger, lastfm, request, mb
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)
# 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:
return
@@ -315,18 +323,31 @@ class Cache(object):
else:
dbalbum = myDB.action(
'SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?',
'SELECT ArtistName, AlbumTitle, ReleaseID, Type FROM albums WHERE AlbumID=?',
[self.id]).fetchone()
if dbalbum['ReleaseID'] != self.id:
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'],
api_key=LASTFM_API_KEY)
if not data:
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
album=dbalbum['AlbumTitle'],
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:
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
if dbalbum['Type'] != "part of":
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:
return
+30 -4
View File
@@ -34,11 +34,18 @@ _CONFIG_DEFINITIONS = {
'ADD_ALBUM_ART': (int, 'General', 0),
'ADVANCEDENCODER': (str, 'General', ''),
'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
# be - to be considered "downloaded". Percentage from 0-100
'ALBUM_COMPLETION_PCT': (int, 'Advanced', 80),
'API_ENABLED': (int, 'General', 0),
'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_MANUALLY_ADDED': (int, 'General', 1),
'AUTOWANT_UPCOMING': (int, 'General', 1),
@@ -138,12 +145,17 @@ _CONFIG_DEFINITIONS = {
'IGNORED_FILES': (list, 'Advanced', []), # path
'INCLUDE_EXTRAS': (int, 'General', 0),
'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'),
'KAT': (int, 'Kat', 0),
'KAT_PROXY_URL': (str, 'Kat', ''),
'KAT_RATIO': (str, 'Kat', ''),
'KEEP_NFO': (int, 'General', 0),
'KEEP_TORRENT_FILES': (int, 'General', 0),
'KEEP_TORRENT_FILES_DIR': (path, 'General', ''),
'LASTFM_USERNAME': (str, 'General', ''),
'LAUNCH_BROWSER': (int, 'General', 1),
'LIBRARYSCAN': (int, 'General', 1),
@@ -155,6 +167,7 @@ _CONFIG_DEFINITIONS = {
'LOSSLESS_BITRATE_TO': (int, 'General', 0),
'LOSSLESS_DESTINATION_DIR': (path, 'General', ''),
'MB_IGNORE_AGE': (int, 'General', 365),
'MB_IGNORE_AGE_MISSING': (int, 'General', 0),
'MININOVA': (int, 'Mininova', 0),
'MININOVA_RATIO': (str, 'Mininova', ''),
'MIRROR': (str, 'General', 'musicbrainz.org'),
@@ -226,6 +239,10 @@ _CONFIG_DEFINITIONS = {
'PUSHOVER_KEYS': (str, 'Pushover', ''),
'PUSHOVER_ONSNATCH': (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_UNPROCESSED': (bool_int, 'General', 1),
'RENAME_FROZEN': (bool_int, 'General', 1),
@@ -236,6 +253,7 @@ _CONFIG_DEFINITIONS = {
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
'RUTRACKER_RATIO': (str, 'Rutracker', ''),
'RUTRACKER_USER': (str, 'Rutracker', ''),
'RUTRACKER_COOKIE': (str, 'Rutracker', ''),
'SAB_APIKEY': (str, 'SABnzbd', ''),
'SAB_CATEGORY': (str, 'SABnzbd', ''),
'SAB_HOST': (str, 'SABnzbd', ''),
@@ -243,6 +261,11 @@ _CONFIG_DEFINITIONS = {
'SAB_USERNAME': (str, 'SABnzbd', ''),
'SAMPLINGFREQUENCY': (int, 'General', 44100),
'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', ''),
'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'),
'SONGKICK_ENABLED': (int, 'Songkick', 1),
@@ -274,6 +297,9 @@ _CONFIG_DEFINITIONS = {
'TWITTER_PASSWORD': (str, 'Twitter', ''),
'TWITTER_PREFIX': (str, 'Twitter', 'Headphones'),
'TWITTER_USERNAME': (str, 'Twitter', ''),
'TQUATTRECENTONZE': (int, 'tquattrecentonze', 0),
'TQUATTRECENTONZE_PASSWORD': (str, 'tquattrecentonze', ''),
'TQUATTRECENTONZE_USER': (str, 'tquattrecentonze', ''),
'UPDATE_DB_INTERVAL': (int, 'General', 24),
'USENET_RETENTION': (int, 'General', '1500'),
'UTORRENT_HOST': (str, 'uTorrent', ''),
@@ -286,10 +312,10 @@ _CONFIG_DEFINITIONS = {
'WAFFLES_PASSKEY': (str, 'Waffles', ''),
'WAFFLES_RATIO': (str, 'Waffles', ''),
'WAFFLES_UID': (str, 'Waffles', ''),
'WHATCD': (int, 'What.cd', 0),
'WHATCD_PASSWORD': (str, 'What.cd', ''),
'WHATCD_RATIO': (str, 'What.cd', ''),
'WHATCD_USERNAME': (str, 'What.cd', ''),
'REDACTED': (int, 'Redacted', 0),
'REDACTED_USERNAME': (str, 'Redacted', ''),
'REDACTED_PASSWORD': (str, 'Redacted', ''),
'REDACTED_RATIO': (str, 'Redacted', ''),
'XBMC_ENABLED': (int, 'XBMC', 0),
'XBMC_HOST': (str, 'XBMC', ''),
'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))
+81 -40
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# This file is part of Headphones.
#
# Headphones is free software: you can redistribute it and/or modify
@@ -49,6 +51,7 @@ delugeweb_auth = {}
delugeweb_url = ''
deluge_verify_cert = False
scrub_logs = True
headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}
def _scrubber(text):
@@ -80,7 +83,7 @@ def addTorrent(link, data=None, name=None):
result = {}
retid = False
url_what = ['https://what.cd/', 'http://what.cd/']
url_apollo = ['https://apollo.rip/', 'http://apollo.rip/']
url_waffles = ['https://waffles.ch/', 'http://waffles.ch/']
if link.lower().startswith('magnet:'):
@@ -94,7 +97,7 @@ def addTorrent(link, data=None, name=None):
if link.lower().startswith(tuple(url_waffles)):
if 'rss=' not in link:
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')
user_agent = 'Headphones'
# This method will make Deluge download the file
@@ -104,11 +107,11 @@ def addTorrent(link, data=None, name=None):
# return addTorrent(local_torrent_path)
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'
headers = {'User-Agent': user_agent}
get_headers = {'User-Agent': user_agent}
torrentfile = ''
logger.debug('Deluge: Trying to download (GET)')
try:
r = requests.get(link, headers=headers)
r = requests.get(link, headers=get_headers)
if r.status_code == 200:
logger.debug('Deluge: 200 OK')
# .text will ruin the encoding for some torrents
@@ -135,7 +138,10 @@ def addTorrent(link, data=None, name=None):
# remove '.torrent' suffix
if name[-len('.torrent'):] == '.torrent':
name = name[:-len('.torrent')]
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, 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',
'name': name,
'content': torrentfile}
@@ -164,7 +170,10 @@ def addTorrent(link, data=None, name=None):
# remove '.torrent' suffix
if name[-len('.torrent'):] == '.torrent':
name = name[:-len('.torrent')]
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, 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',
'name': name,
'content': torrentfile}
@@ -199,7 +208,7 @@ def getTorrentFolder(result):
],
"id": 21})
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']
tries = 0
@@ -207,7 +216,7 @@ def getTorrentFolder(result):
tries += 1
time.sleep(5)
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']
post_data = json.dumps({"method": "web.get_torrent_status",
@@ -226,7 +235,7 @@ def getTorrentFolder(result):
"id": 23})
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['name'] = json.loads(response.text)['result']['name']
@@ -242,18 +251,45 @@ def removeTorrent(torrentid, remove_data=False):
_get_auth()
try:
result = False
post_data = json.dumps({"method": "core.remove_torrent",
logger.debug('Deluge: Checking if torrent %s finished seeding' % str(torrentid))
post_data = json.dumps({"method": "web.get_torrent_status",
"params": [
torrentid,
remove_data
],
"id": 25})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert)
result = json.loads(response.text)['result']
[
"name",
"ratio",
"state"
]
],
"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:
logger.error('Deluge: Removing torrent failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines()
@@ -269,7 +305,8 @@ def _get_auth():
delugeweb_host = headphones.CONFIG.DELUGE_HOST
delugeweb_cert = headphones.CONFIG.DELUGE_CERT
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'):
delugeweb_host = 'http://%s' % delugeweb_host
@@ -292,12 +329,12 @@ def _get_auth():
"id": 1})
try:
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:
try:
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,
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
logger.error('Deluge: Switching to HTTPS, but certificate won\'t be verified because NO CERTIFICATE WAS CONFIGURED!')
delugeweb_url = delugeweb_url.replace('http:', 'https:')
@@ -322,7 +359,7 @@ def _get_auth():
"id": 10})
try:
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:
logger.error('Deluge: Authentication failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines()
@@ -339,7 +376,7 @@ def _get_auth():
"id": 11})
try:
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:
logger.error('Deluge: Authentication failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines()
@@ -347,7 +384,8 @@ def _get_auth():
return None
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')
return None
@@ -357,7 +395,7 @@ def _get_auth():
try:
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:
logger.error('Deluge: Authentication failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines()
@@ -370,7 +408,7 @@ def _get_auth():
try:
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:
logger.error('Deluge: Authentication failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines()
@@ -395,7 +433,7 @@ def _add_torrent_magnet(result):
"params": [result['url'], {}],
"id": 2})
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']
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
return json.loads(response.text)['result']
@@ -415,7 +453,7 @@ def _add_torrent_url(result):
"params": [result['url'], {}],
"id": 32})
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']
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
return json.loads(response.text)['result']
@@ -433,10 +471,11 @@ def _add_torrent_file(result):
try:
# content is torrent file contents that needs to be encoded to base64
post_data = json.dumps({"method": "core.add_torrent_file",
"params": [result['name'] + '.torrent', b64encode(result['content'].encode('utf8')), {}],
"params": [result['name'] + '.torrent',
b64encode(result['content'].encode('utf8')), {}],
"id": 2})
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
verify=deluge_verify_cert)
verify=deluge_verify_cert, headers=headers)
result['hash'] = json.loads(response.text)['result']
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
return json.loads(response.text)['result']
@@ -444,11 +483,13 @@ def _add_torrent_file(result):
try:
# content is torrent file contents that needs to be encoded to base64
# 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",
"params": [result['name'] + '.torrent', b64encode(result['content']), {}],
"params": [result['name'].decode('utf8') + '.torrent',
b64encode(result['content']), {}],
"id": 22})
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']
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
return json.loads(response.text)['result']
@@ -480,7 +521,7 @@ def setTorrentLabel(result):
"params": [],
"id": 3})
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']
if labels is not None:
@@ -491,7 +532,7 @@ def setTorrentLabel(result):
"params": [label],
"id": 4})
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)
except Exception as e:
logger.error('Deluge: Setting label failed: %s' % str(e))
@@ -503,7 +544,7 @@ def setTorrentLabel(result):
"params": [result['hash'], label],
"id": 5})
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)
else:
logger.debug('Deluge: Label plugin not detected')
@@ -527,12 +568,12 @@ def setSeedRatio(result):
"params": [result['hash'], True],
"id": 5})
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",
"params": [result['hash'], float(ratio)],
"id": 6})
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']
@@ -555,7 +596,7 @@ def setTorrentPath(result):
"params": [result['hash'], True],
"id": 7})
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:
move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY
@@ -569,7 +610,7 @@ def setTorrentPath(result):
"params": [result['hash'], move_to],
"id": 8})
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']
@@ -592,7 +633,7 @@ def setTorrentPause(result):
"params": [[result['hash']]],
"id": 9})
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']
+120 -17
View File
@@ -20,6 +20,12 @@ import datetime
import shutil
import time
import sys
import tempfile
import glob
from beets import logging as beetslogging
import six
from contextlib import contextmanager
import fnmatch
import re
@@ -217,8 +223,7 @@ def replace_illegal_chars(string, type="file"):
if type == "file":
string = re.sub('[\?"*:|<>/]', '_', string)
if type == "folder":
string = re.sub('[:\?<>"|]', '_', string)
string = re.sub('[:\?<>"|*]', '_', string)
return string
@@ -251,9 +256,17 @@ _XLATE_SPECIAL = {
# Translation table.
# Cover additional special characters processing normalization.
u"'": '', # replace apostrophe with nothing
u"": '', # replace musicbrainz style apostrophe with nothing
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):
# type: (basestring,Mapping[basestring,basestring])->basestring
@@ -319,9 +332,27 @@ def clean_name(s):
# 6. trim
u = u.strip()
# 7. lowercase
u = u.lower()
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):
title = re.sub('[\.\-\/\_]', ' ', title).lower()
@@ -620,26 +651,68 @@ def get_downloaded_track_list(albumpath):
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
new_folder = os.path.join(albumpath,
'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
logger.info(
"Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
# Create temp dir
if headphones.CONFIG.KEEP_TORRENT_FILES_DIR:
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:
shutil.copytree(albumpath, new_folder)
return new_folder
file_name = os.path.basename(os.path.normpath(albumpath))
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:
logger.warn("Cannot copy/move files to temp folder: " +
new_folder.decode(headphones.SYS_ENCODING, 'replace') +
". Not continuing. Error: " + str(e))
logger.error("Cannot create temp directory: " + tempdir.decode(
headphones.SYS_ENCODING, 'replace') + ". 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
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.
"""
@@ -661,6 +734,15 @@ def cue_split(albumpath):
# Split cue
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
logger.info("Attempting to split audio files by cue")
@@ -671,12 +753,12 @@ def cue_split(albumpath):
except Exception as e:
os.chdir(cwd)
logger.warn("Cue not split: " + str(e))
return False
return None
os.chdir(cwd)
return True
return albumpath
return False
return None
def extract_logline(s):
@@ -897,3 +979,24 @@ def create_https_certificates(ssl_cert, ssl_key):
return False
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:
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']))
new_releases = mb.get_new_releases(rgid, includeExtras, True)
if headphones.CONFIG.MB_IGNORE_AGE_MISSING is not 1:
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:
if len(check_release_date) == 10:
release_date = check_release_date
+5 -2
View File
@@ -55,7 +55,7 @@ def request_lastfm(method, **kwargs):
return
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 data
@@ -149,7 +149,10 @@ def getTagTopArtists(tag, limit=50):
logger.debug("Fetched %d artists from Last.FM", len(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):
artistlist.append(artist_mbid)
+27 -22
View File
@@ -91,10 +91,6 @@ def findArtist(name, limit=1):
artistlist = []
artistResults = None
chars = set('!?*-')
if any((c in chars) for c in name):
name = '"' + name + '"'
criteria = {'artist': name.lower()}
with mb_lock:
@@ -156,16 +152,13 @@ def findRelease(name, limit=1, artist=None):
if not artist and ':' in name:
name, artist = name.rsplit(":", 1)
chars = set('!?*-')
if any((c in chars) for c in name):
name = '"' + name + '"'
if artist and any((c in chars) for c in artist):
artist = '"' + artist + '"'
criteria = {'release': name.lower()}
if artist:
criteria['artist'] = artist.lower()
with mb_lock:
try:
releaseResults = musicbrainzngs.search_releases(query=name, limit=limit, artist=artist)[
'release-list']
releaseResults = musicbrainzngs.search_releases(limit=limit, **criteria)['release-list']
except musicbrainzngs.WebServiceError as e: # need to update exceptions
logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e)))
mb_lock.snooze(5)
@@ -234,10 +227,6 @@ def findSeries(name, limit=1):
serieslist = []
seriesResults = None
chars = set('!?*-')
if any((c in chars) for c in name):
name = '"' + name + '"'
criteria = {'series': name.lower()}
with mb_lock:
@@ -759,19 +748,12 @@ def findArtistbyAlbum(name):
def findAlbumID(artist=None, album=None):
results = None
chars = set('!?*-')
try:
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['artist'] = artist.lower()
else:
if any((c in chars) for c in album):
album = '"' + album + '"'
criteria = {'release': album.lower()}
with mb_lock:
results = musicbrainzngs.search_release_groups(limit=1, **criteria).get(
@@ -788,3 +770,26 @@ def findAlbumID(artist=None, album=None):
return False
rgid = unicode(results[0]['id'])
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
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 = mc_artist_name.replace(" ", "-")
headers = {
+219 -72
View File
@@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from urllib import urlencode
from urllib import urlencode, quote_plus
import urllib
import subprocess
import json
@@ -148,7 +148,9 @@ class PROWL(object):
http_handler.request("POST",
"/publicapi/add",
headers={'Content-type': "application/x-www-form-urlencoded"},
headers={
'Content-type':
"application/x-www-form-urlencoded"},
body=urlencode(data))
response = http_handler.getresponse()
request_status = response.status
@@ -203,28 +205,34 @@ class XBMC(object):
url = host + '/xbmcCmds/xbmcHttp/?' + url_command
if self.password:
return request.request_content(url, auth=(self.username, self.password))
return request.request_content(url,
auth=(self.username, self.password))
else:
return request.request_content(url)
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'}
url = host + '/jsonrpc'
if self.password:
response = request.request_json(url, method="post", data=json.dumps(data),
headers=headers, auth=(self.username, self.password))
response = request.request_json(
url, method="post",
data=json.dumps(data),
headers=headers, auth=(
self.username, self.password))
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)
if response:
return response[0]['result']
def update(self):
# From what I read you can't update the music library on a per directory or per path basis
# so need to update the whole thing
# From what I read you can't update the music library on a per
# directory or per path basis so need to update the whole thing
hosts = [x.strip() for x in self.hosts.split(',')]
@@ -247,18 +255,23 @@ class XBMC(object):
logger.info('Sending notification command to XMBC @ ' + host)
try:
version = self._sendjson(host, 'Application.GetProperties',
{'properties': ['version']})['version']['major']
{'properties': ['version']})[
'version']['major']
if version < 12: # Eden
notification = header + "," + message + "," + time + "," + albumartpath
notification = header + "," + message + "," + time + \
"," + albumartpath
notifycommand = {'command': 'ExecBuiltIn',
'parameter': 'Notification(' + notification + ')'}
'parameter': 'Notification(' +
notification + ')'}
request = self._sendhttp(host, notifycommand)
else: # Frodo
params = {'title': header, 'message': message, 'displaytime': int(time),
params = {'title': header, 'message': message,
'displaytime': int(time),
'image': albumartpath}
request = self._sendjson(host, 'GUI.ShowNotification', params)
request = self._sendjson(host, 'GUI.ShowNotification',
params)
if not request:
raise Exception
@@ -323,22 +336,28 @@ class Plex(object):
url = host + '/xbmcCmds/xbmcHttp/?' + command
if self.password:
response = request.request_response(url, auth=(self.username, self.password))
response = request.request_response(url, auth=(
self.username, self.password))
else:
response = request.request_response(url)
return response
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'}
url = host + '/jsonrpc'
if self.password:
response = request.request_json(url, method="post", data=json.dumps(data),
headers=headers, auth=(self.username, self.password))
response = request.request_json(
url, method="post",
data=json.dumps(data),
headers=headers, auth=(
self.username, self.password))
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)
if response:
@@ -346,13 +365,14 @@ class Plex(object):
def update(self):
# From what I read you can't update the music library on a per directory or per path basis
# so need to update the whole thing
# From what I read you can't update the music library on a per
# directory or per path basis so need to update the whole thing
hosts = [x.strip() for x in self.server_hosts.split(',')]
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
if self.token:
params = {'X-Plex-Token': self.token}
@@ -369,7 +389,8 @@ class Plex(object):
for s in sections:
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)
def notify(self, artist, album, albumartpath):
@@ -381,27 +402,35 @@ class Plex(object):
time = "3000" # in ms
for host in hosts:
logger.info('Sending notification command to Plex client @ ' + host)
logger.info(
'Sending notification command to Plex client @ ' + host)
try:
version = self._sendjson(host, 'Application.GetProperties',
{'properties': ['version']})['version']['major']
{'properties': ['version']})[
'version']['major']
if version < 12: # Eden
notification = header + "," + message + "," + time + "," + albumartpath
notification = header + "," + message + "," + time + \
"," + albumartpath
notifycommand = {'command': 'ExecBuiltIn',
'parameter': 'Notification(' + notification + ')'}
'parameter': 'Notification(' +
notification + ')'}
request = self._sendhttp(host, notifycommand)
else: # Frodo
params = {'title': header, 'message': message, 'displaytime': int(time),
params = {'title': header, 'message': message,
'displaytime': int(time),
'image': albumartpath}
request = self._sendjson(host, 'GUI.ShowNotification', params)
request = self._sendjson(host, 'GUI.ShowNotification',
params)
if not request:
raise 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):
@@ -419,7 +448,8 @@ class NMA(object):
message = "Headphones has snatched: " + snatched
else:
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 message: " + message)
@@ -433,7 +463,8 @@ class NMA(object):
if len(keys) > 1:
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':
logger.error(u'Could not send notification to NotifyMyAndroid')
@@ -461,9 +492,11 @@ class PUSHBULLET(object):
data['device_iden'] = self.deviceid
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:
logger.info(u"PushBullet notifications sent.")
@@ -492,7 +525,9 @@ class PUSHALOT(object):
http_handler.request("POST",
"/api/sendmessage",
headers={'Content-type': "application/x-www-form-urlencoded"},
headers={
'Content-type':
"application/x-www-form-urlencoded"},
body=urlencode(data))
response = http_handler.getresponse()
request_status = response.status
@@ -512,6 +547,49 @@ class PUSHALOT(object):
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):
def __init__(self, util_loc='/usr/syno/bin/synoindex'):
self.util_loc = util_loc
@@ -524,7 +602,8 @@ class Synoindex(object):
if not self.util_exists():
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
if os.path.isfile(path):
@@ -533,16 +612,19 @@ class Synoindex(object):
cmd_arg = '-A'
else:
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
cmd = [self.util_loc, cmd_arg, path]
logger.info("Calling synoindex command: %s" % str(cmd))
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)
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:
logger.warn("Error sending notification: %s" % str(e))
@@ -580,7 +662,8 @@ class PUSHOVER(object):
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:
logger.info(u"Pushover notifications sent.")
@@ -614,20 +697,25 @@ class TwitterNotifier(object):
def notify_snatch(self, title):
if headphones.CONFIG.TWITTER_ONSNATCH:
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):
if headphones.CONFIG.TWITTER_ENABLED:
self._notifyTwitter(common.notifyStrings[
common.NOTIFY_DOWNLOAD] + ': ' + title + ' at ' + helpers.now())
common.NOTIFY_DOWNLOAD] + ': ' +
title + ' at ' + helpers.now())
def test_notify(self):
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):
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)
logger.info('Requesting temp token from Twitter')
@@ -635,32 +723,42 @@ class TwitterNotifier(object):
resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET')
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:
request_token = dict(parse_qsl(content))
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):
request_token = {}
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'
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)
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))
oauth_client = oauth.Client(oauth_consumer, token)
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)
logger.info('resp, content: ' + str(resp) + ',' + str(content))
@@ -669,14 +767,18 @@ class TwitterNotifier(object):
logger.info('resp[status] = ' + str(resp['status']))
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)
return False
else:
logger.info('Your Twitter Access Token key: %s' % access_token['oauth_token'])
logger.info('Access Token secret: %s' % access_token['oauth_token_secret'])
logger.info('Your Twitter Access Token key: %s' % access_token[
'oauth_token'])
logger.info(
'Access Token secret: %s' % access_token['oauth_token_secret'])
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
def _send_tweet(self, message=None):
@@ -688,7 +790,8 @@ class TwitterNotifier(object):
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:
api.PostUpdate(message)
@@ -741,7 +844,8 @@ class OSX_NOTIFY(object):
)
NSUserNotification = self.objc.lookUpClass('NSUserNotification')
NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter')
NSUserNotificationCenter = self.objc.lookUpClass(
'NSUserNotificationCenter')
NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool')
if not NSUserNotification or not NSUserNotificationCenter:
@@ -756,14 +860,17 @@ class OSX_NOTIFY(object):
if text:
notification.setInformativeText_(text)
if sound:
notification.setSoundName_("NSUserNotificationDefaultSoundName")
notification.setSoundName_(
"NSUserNotificationDefaultSoundName")
if image:
source_img = self.AppKit.NSImage.alloc().initByReferencingFile_(image)
source_img = self.AppKit.NSImage.alloc().\
initByReferencingFile_(image)
notification.setContentImage_(source_img)
# notification.set_identityImage_(source_img)
notification.setHasActionButton_(False)
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
notification_center = NSUserNotificationCenter.\
defaultUserNotificationCenter()
notification_center.deliverNotification_(notification)
del pool
@@ -784,13 +891,16 @@ class BOXCAR(object):
def notify(self, title, message, rgid=None):
try:
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({
'user_credentials': headphones.CONFIG.BOXCAR_TOKEN,
'notification[title]': title.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)
@@ -818,8 +928,9 @@ class SubSonicNotifier(object):
self.host = self.host + "/"
# Invoke request
request.request_response(self.host + "musicFolderSettings.view?scanNow",
auth=(self.username, self.password))
request.request_response(
self.host + "musicFolderSettings.view?scanNow",
auth=(self.username, self.password))
class Email(object):
@@ -827,13 +938,15 @@ class Email(object):
message = MIMEText(message, 'plain', "utf-8")
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
try:
if headphones.CONFIG.EMAIL_SSL:
mailserver = smtplib.SMTP_SSL(headphones.CONFIG.EMAIL_SMTP_SERVER,
headphones.CONFIG.EMAIL_SMTP_PORT)
mailserver = smtplib.SMTP_SSL(
headphones.CONFIG.EMAIL_SMTP_SERVER,
headphones.CONFIG.EMAIL_SMTP_PORT)
else:
mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER,
headphones.CONFIG.EMAIL_SMTP_PORT)
@@ -847,7 +960,8 @@ class Email(object):
mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER,
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())
mailserver.quit()
return True
@@ -858,7 +972,6 @@ class Email(object):
class TELEGRAM(object):
def notify(self, message, status):
if not headphones.CONFIG.TELEGRAM_ENABLED:
return
@@ -876,15 +989,49 @@ class TELEGRAM(object):
# Send message to user using Telegram's Bot API
try:
response = requests.post(TELEGRAM_API % (token, "sendMessage"), data=payload)
response = requests.post(TELEGRAM_API % (token, "sendMessage"),
data=payload)
except Exception, e:
logger.info(u'Telegram notify failed: ' + str(e))
# Error logging
sent_successfuly = True
if not response.status_code == 200:
logger.info(u'Could not send notification to TelegramBot (token=%s). Response: [%s]', (token, response.text))
logger.info(
u'Could not send notification to TelegramBot '
u'(token=%s). Response: [%s]',
(token, response.text))
sent_successfuly = False
logger.info(u"Telegram notifications sent.")
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 threading
import itertools
import tempfile
import os
import re
@@ -25,11 +24,12 @@ import beets
import headphones
from beets import autotag
from beets import config as beetsconfig
from beets import logging as beetslogging
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
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 logger, helpers, request, mb, music_encoder
from headphones import logger, helpers, mb, music_encoder
from headphones import metadata
postprocessor_lock = threading.Lock()
@@ -44,6 +44,8 @@ def checkFolder():
for album in snatched:
if album['FolderName']:
folder_name = album['FolderName']
single = False
if album['Kind'] == 'nzb':
download_dir = headphones.CONFIG.DOWNLOAD_DIR
else:
@@ -52,21 +54,32 @@ def checkFolder():
else:
download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
album_path = os.path.join(download_dir, album['FolderName']).encode(
headphones.SYS_ENCODING, 'replace')
logger.debug("Checking if %s exists" % album_path)
# Get folder from torrent hash
if album['TorrentHash'] and headphones.CONFIG.TORRENT_DOWNLOADER:
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):
logger.info('Found "' + album['FolderName'] + '" in ' + album[
'Kind'] + ' download folder. Verifying....')
verify(album['AlbumID'], album_path, album['Kind'])
if folder_name:
album_path = os.path.join(download_dir, folder_name).encode(
headphones.SYS_ENCODING, 'replace')
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:
logger.info("No folder name found for " + album['Title'])
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()
release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
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")
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(
downloaded_track_list):
if headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent":
albumpath = helpers.preserve_torrent_directory(albumpath)
if albumpath and helpers.cue_split(albumpath):
new_folder = None
new_albumpath = 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)
keep_original_folder = False
# test #1: metadata - usually works
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:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
keep_original_folder)
keep_original_folder, forced, single)
return
# test #2: filenames
@@ -262,7 +300,7 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
if dbtrack in filetrack:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
keep_original_folder)
keep_original_folder, forced, single)
return
# 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)
if delta < 240:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
keep_original_folder)
keep_original_folder, forced, single)
return
logger.warn(u'Could not identify album: %s. It may not be the intended album.',
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(
'UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?',
[albumid])
'UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid])
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
if not processed:
if headphones.CONFIG.RENAME_UNPROCESSED:
if headphones.CONFIG.RENAME_UNPROCESSED and not keep_original_folder:
renameUnprocessedFolder(albumpath, tag="Unprocessed")
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'))
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']))
new_folder = None
# Check to see if we're preserving the torrent dir
if (headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent" and 'headphones-modified' not in albumpath) or headphones.CONFIG.KEEP_ORIGINAL_FOLDER or keep_original_folder:
new_folder = tempfile.mkdtemp(prefix="headphones_")
subdir = os.path.join(new_folder, "headphones")
logger.info("Copying files to " + subdir.decode(headphones.SYS_ENCODING, 'replace') + " subfolder to preserve downloaded files for seeding")
try:
shutil.copytree(albumpath, 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)
# Preserve the torrent dir
if keep_original_folder or single:
temp_path = helpers.preserve_torrent_directory(albumpath, forced, single)
if not temp_path:
markAsUnprocessed(albumid, albumpath, keep_original_folder)
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.
# 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
# Need to update the downloaded track list with the new location.
# 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
if new_folder:
downloaded_track_list = []
for r, d, f in os.walk(albumpath):
for files in f:
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"))
return
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:
shutil.rmtree(new_folder)
return
@@ -387,21 +429,13 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
shutil.rmtree(new_folder)
return
# get artwork and path
album_art_path = None
artwork = None
album_art_path = albumart.getAlbumArt(albumid)
if headphones.CONFIG.EMBED_ALBUM_ART or headphones.CONFIG.ADD_ALBUM_ART:
if album_art_path:
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 or headphones.CONFIG.ADD_ALBUM_ART or \
(headphones.CONFIG.PLEX_ENABLED and headphones.CONFIG.PLEX_NOTIFY) or \
(headphones.CONFIG.XBMC_ENABLED and headphones.CONFIG.XBMC_NOTIFY):
album_art_path, artwork = albumart.getAlbumArt(albumid)
if headphones.CONFIG.EMBED_ALBUM_ART and artwork:
embedAlbumArt(artwork, downloaded_track_list)
@@ -446,12 +480,12 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
[albumid])
# 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(
'SELECT * from snatched WHERE Status="Seed_Snatched" and AlbumID=?',
[albumid]).fetchone()
if seed_snatched:
hash = seed_snatched['FolderName']
hash = seed_snatched['TorrentHash']
torrent_removed = False
logger.info(u'%s - %s. Checking if torrent has finished seeding and can be removed' % (
release['ArtistName'], release['AlbumTitle']))
@@ -459,8 +493,10 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
torrent_removed = transmission.removeTorrent(hash, True)
elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
torrent_removed = deluge.removeTorrent(hash, True)
else:
elif headphones.CONFIG.TORRENT_DOWNLOADER == 2:
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
if torrent_removed:
@@ -538,6 +574,11 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
pushbullet = notifiers.PUSHBULLET()
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:
logger.info(u"Telegram request")
telegram = notifiers.TELEGRAM()
@@ -918,12 +959,29 @@ def correctMetadata(albumid, release, downloaded_track_list):
if not items:
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:
cur_artist, cur_album, candidates, rec = autotag.tag_album(items,
search_artist=helpers.latinToAscii(
release['ArtistName']),
search_album=helpers.latinToAscii(
release['AlbumTitle']))
beetslog = beetslogging.getLogger('beets')
beetslog.set_global_level(beetslogging.DEBUG) if headphones.VERBOSE else beetslog.set_global_level(
beetslogging.CRITICAL)
with helpers.capture_beets_log() as logs:
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:
logger.error('Error getting recommendation: %s. Not writing metadata', e)
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',
snatched['Title'])
verify(snatched['AlbumID'], folder, snatched['Kind'],
keep_original_folder=keep_original_folder)
forced=True, keep_original_folder=keep_original_folder)
continue
# 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:
logger.info(
'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
# 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(
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
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
else:
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
if rgid:
verify(rgid, folder, keep_original_folder=keep_original_folder)
verify(rgid, folder, forced=True, keep_original_folder=keep_original_folder)
continue
else:
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:
name = album = None
# Check if there's a cue to split
if headphones.CONFIG.CUE_SPLIT and not name and not album and helpers.cue_split(folder):
try:
name, album, year = helpers.extract_metadata(folder)
except Exception:
name = album = None
# Not found from meta data, check if there's a cue to split and try meta data again
kind = None
if headphones.CONFIG.CUE_SPLIT and not name and not album:
cue_folder = helpers.cue_split(folder, keep_original_folder=keep_original_folder)
if cue_folder:
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:
release = myDB.action(
@@ -1272,7 +1342,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
logger.info(
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
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
else:
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
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
else:
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(
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
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
else:
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
if rgid:
verify(rgid, folder, keep_original_folder=keep_original_folder)
verify(rgid, folder, forced=True, keep_original_folder=keep_original_folder)
continue
else:
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 requests as requests
# from requests.auth import HTTPDigestAuth
from bs4 import BeautifulSoup
import headphones
@@ -24,7 +25,7 @@ class Rutracker(object):
return self.loggedin
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
else:
return True
@@ -34,7 +35,7 @@ class Rutracker(object):
Logs in user
"""
loginpage = 'http://login.rutracker.org/forum/login.php'
loginpage = 'http://rutracker.org/forum/login.php'
post_params = {
'login_username': headphones.CONFIG.RUTRACKER_USER,
'login_password': headphones.CONFIG.RUTRACKER_PASSWORD,
@@ -44,28 +45,34 @@ class Rutracker(object):
logger.info("Attempting to log in to rutracker...")
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
if 'bb_data' not in r.cookies.keys():
if not self.has_bb_session_cookie(r):
time.sleep(10)
r = self.session.post(loginpage, data=post_params, timeout=self.timeout)
if r.status_code != 200:
logger.error("rutracker login returned status code %s" % r.status_code)
self.loggedin = False
else:
if 'bb_data' in r.cookies.keys():
self.loggedin = True
logger.info("Successfully logged in to rutracker")
if headphones.CONFIG.RUTRACKER_COOKIE:
logger.info("Attempting to log in using predefined cookie...")
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False, cookies={'bb_session': headphones.CONFIG.RUTRACKER_COOKIE})
else:
logger.error(
"Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later")
self.loggedin = False
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
if self.has_bb_session_cookie(r):
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
except Exception as e:
logger.error("Unknown error logging in to rutracker: %s" % e)
self.loggedin = False
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):
"""
Return the search url
@@ -91,7 +98,11 @@ class Rutracker(object):
# sort by size, descending.
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)
return searchurl
@@ -167,7 +178,7 @@ class Rutracker(object):
return the .torrent data
"""
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}
try:
headers = {'Referer': url}
@@ -214,3 +225,37 @@ class Rutracker(object):
self.session.post(url, params={'action': 'add-file'}, files=files)
except Exception as 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)
+368 -63
View File
@@ -14,6 +14,7 @@
# 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
# t411 support added by a1ex, @likeitneverwentaway on github for maintenance
from base64 import b16encode, b32decode
from hashlib import sha1
@@ -24,29 +25,33 @@ import datetime
import subprocess
import unicodedata
import urlparse
from json import loads
import os
import re
from pygazelle import api as gazelleapi
from pygazelle import encoding as gazelleencoding
from pygazelle import format as gazelleformat
from pygazelle import release_type as gazellerelease_type
import headphones
from headphones.common import USER_AGENT
from headphones import logger, db, helpers, classes, sab, nzbget, request
from headphones import utorrent, transmission, notifiers, rutracker, deluge
from headphones import utorrent, transmission, notifiers, rutracker, deluge, qbittorrent
from bencode import bencode, bdecode
# Magnet to torrent services, for Black hole. Stolen from CouchPotato.
TORRENT_TO_MAGNET_SERVICES = [
# 'https://zoink.it/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
gazelle = None
# Persistent Apollo.rip API object
apolloobj = None
ruobj = None
# Persistent RED API object
redobj = None
def fix_url(s, charset="utf-8"):
@@ -159,8 +164,10 @@ def get_seed_ratio(provider):
seed_ratio = headphones.CONFIG.RUTRACKER_RATIO
elif provider == 'Kick Ass Torrents':
seed_ratio = headphones.CONFIG.KAT_RATIO
elif provider == 'What.cd':
seed_ratio = headphones.CONFIG.WHATCD_RATIO
elif provider == 'Apollo.rip':
seed_ratio = headphones.CONFIG.APOLLO_RATIO
elif provider == 'Redacted':
seed_ratio = headphones.CONFIG.REDACTED_RATIO
elif provider == 'The Pirate Bay':
seed_ratio = headphones.CONFIG.PIRATEBAY_RATIO
elif provider == 'Old Pirate Bay':
@@ -199,14 +206,13 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
continue
if headphones.CONFIG.WAIT_UNTIL_RELEASE_DATE and album['ReleaseDate']:
try:
release_date = datetime.datetime.strptime(album['ReleaseDate'], "%Y-%m-%d")
except:
logger.warn(
"No valid date for: %s. Skipping automatic search" % album['AlbumTitle'])
release_date = strptime_musicbrainz(album['ReleaseDate'])
if not release_date:
logger.warn("No valid date for: %s. Skipping automatic search" %
album['AlbumTitle'])
continue
if release_date > datetime.datetime.today():
elif release_date > datetime.datetime.today():
logger.info("Skipping: %s. Waiting for release date of: %s" % (
album['AlbumTitle'], album['ReleaseDate']))
continue
@@ -235,6 +241,26 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
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):
NZB_PROVIDERS = (headphones.CONFIG.HEADPHONES_INDEXER 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.WAFFLES or
headphones.CONFIG.RUTRACKER or
headphones.CONFIG.WHATCD or
headphones.CONFIG.STRIKE)
headphones.CONFIG.APOLLO or
headphones.CONFIG.REDACTED or
headphones.CONFIG.STRIKE or
headphones.CONFIG.TQUATTRECENTONZE)
results = []
myDB = db.DBConnection()
@@ -714,7 +742,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None,
}
data = request.request_json(
url='http://api.omgwtfnzbs.org/json/',
url='http://api.omgwtfnzbs.me/json/',
params=params, headers=headers
)
@@ -813,6 +841,19 @@ def send_to_downloader(data, bestqual, album):
torrent_name = helpers.replace_illegal_chars(folder_name) + '.torrent'
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 headphones.CONFIG.MAGNET_LINKS == 1:
try:
@@ -838,12 +879,11 @@ def send_to_downloader(data, bestqual, album):
services = TORRENT_TO_MAGNET_SERVICES[:]
random.shuffle(services)
headers = {'User-Agent': USER_AGENT}
headers['Referer'] = 'https://torcache.net/'
for service in services:
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):
return
# 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?")
return
folder_name = transmission.getTorrentFolder(torrentid)
folder_name = transmission.getName(torrentid)
if folder_name:
logger.info('Torrent folder name: %s' % folder_name)
logger.info('Torrent name: %s' % folder_name)
else:
logger.error('Torrent folder name could not be determined')
logger.error('Torrent name could not be determined')
return
# Set Seed Ratio
@@ -943,7 +983,7 @@ def send_to_downloader(data, bestqual, album):
except Exception as 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")
# Add torrent
@@ -974,19 +1014,42 @@ def send_to_downloader(data, bestqual, album):
seed_ratio = get_seed_ratio(bestqual[3])
if seed_ratio is not None:
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.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,
kind])
kind, torrentid])
# 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:
myDB.action(
'INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)',
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", torrentid,
kind])
'INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?, ?)',
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", folder_name,
kind, torrentid])
# notify
artist = album[1]
@@ -1014,6 +1077,14 @@ def send_to_downloader(data, bestqual, album):
logger.info(u"Sending PushBullet notification")
pushbullet = notifiers.PUSHBULLET()
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:
logger.info(u"Sending Telegram notification")
telegram = notifiers.TELEGRAM()
@@ -1140,7 +1211,8 @@ def verifyresult(title, artistterm, term, lossless):
def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
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
reldate = album['ReleaseDate']
@@ -1220,10 +1292,13 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
categories = "3040"
maxsize = 10000000000
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
categories = "3040,3010"
categories = "3040,3010,3050"
maxsize = 10000000000
else:
categories = "3010"
categories = "3010,3050"
maxsize = 300000000
if album['Type'] == 'Other':
categories = "3030"
@@ -1233,8 +1308,12 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
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
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}
params = {
@@ -1245,25 +1324,47 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
"q": term
}
data = request.request_feed(
url=torznab_host[0] + '/api?',
data = request.request_soup(
url=torznab_host[0],
params=params, headers=headers
)
# Process feed
if data:
if not len(data.entries):
logger.info(u"No results found from %s for %s", torznab_host[0], term)
items = data.find_all('item')
if not items:
logger.info(u"No results found from %s for %s", provider, term)
else:
for item in data.entries:
for item in items:
try:
url = item.link
title = item.title
size = int(item.links[1]['length'])
title = item.title.get_text()
url = item.find("link").next_sibling.strip()
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()):
logger.info(
'Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
resultlist.append((title, size, url, provider, 'torrent', True))
if size < maxsize and minimumseeders < seeders:
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
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:
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:
providerurl = fix_url(set_proxy(headphones.CONFIG.KAT_PROXY_URL))
else:
providerurl = fix_url("https://kat.cr")
providerurl = fix_url("https://katcr.co/new/")
# Build URL
providerurl = providerurl + "/usearch/" + ka_term
@@ -1355,8 +1456,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
query_items = [usersearchterm]
query_items.extend(['format:(%s)' % format,
'size:[0 TO %d]' % maxsize,
'-seeders:0']) # cut out dead torrents
'size:[0 TO %d]' % maxsize])
# (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:
query_items.append('bitrate:"%s"' % bitrate)
@@ -1432,9 +1534,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
if rulist:
resultlist.extend(rulist)
if headphones.CONFIG.WHATCD:
provider = "What.cd"
providerurl = "http://what.cd/"
if headphones.CONFIG.APOLLO:
provider = "Apollo.rip"
providerurl = "http://apollo.rip/"
bitrate = None
bitrate_string = bitrate
@@ -1457,7 +1559,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
bitrate_string = encoding_string
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
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
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
@@ -1466,27 +1568,157 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
search_formats = [gazelleformat.MP3]
maxsize = 300000000
if not gazelle or not gazelle.logged_in():
if not apolloobj or not apolloobj.logged_in():
try:
logger.info(u"Attempting to log in to What.cd...")
gazelle = gazelleapi.GazelleAPI(headphones.CONFIG.WHATCD_USERNAME,
headphones.CONFIG.WHATCD_PASSWORD)
gazelle._login()
logger.info(u"Attempting to log in to Apollo.rip...")
apolloobj = gazelleapi.GazelleAPI(headphones.CONFIG.APOLLO_USERNAME,
headphones.CONFIG.APOLLO_PASSWORD,
headphones.CONFIG.APOLLO_URL)
apolloobj._login()
except Exception as e:
gazelle = None
logger.error(u"What.cd credentials incorrect or site is down. Error: %s %s" % (
apolloobj = None
logger.error(u"Apollo.rip credentials incorrect or site is down. Error: %s %s" % (
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)
all_torrents = []
for search_format in search_formats:
if usersearchterm:
all_torrents.extend(
gazelle.search_torrents(searchstr=usersearchterm, format=search_format,
redobj.search_torrents(searchstr=usersearchterm, format=search_format,
encoding=bitrate_string)['results'])
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,
format=search_format,
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
resultlist.append((torrent.file_path,
torrent.size,
gazelle.generate_torrent_link(torrent.id),
redobj.generate_torrent_link(torrent.id),
provider,
'torrent', True))
# Pirate Bay
if headphones.CONFIG.PIRATEBAY:
provider = "The Pirate Bay"
tpb_term = term.replace("!", "")
tpb_term = term.replace("!", "").replace("'", " ")
# Use proxy if specified
if headphones.CONFIG.PIRATEBAY_PROXY_URL:
providerurl = fix_url(set_proxy(headphones.CONFIG.PIRATEBAY_PROXY_URL))
else:
providerurl = fix_url("https://thepiratebay.se")
providerurl = fix_url("https://thepiratebay.org")
# Build URL
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))
except Exception as e:
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
# when looking for "Foo - Foo" we don't want "Foobar"
@@ -1800,7 +2103,9 @@ def preprocess(resultlist):
if result[3] == 'Kick Ass Torrents':
headers['Referer'] = 'https://torcache.net/'
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'
elif result[3] == "The Pirate Bay" or result[3] == "Old Pirate Bay":
headers[
+19 -18
View File
@@ -13,13 +13,10 @@
# You should have received a copy of the GNU General Public License
# 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
postprocessor_lock = threading.Lock()
def checkTorrentFinished():
"""
@@ -28,21 +25,25 @@ def checkTorrentFinished():
logger.info("Checking if any torrents have finished seeding and can be removed")
with postprocessor_lock:
myDB = db.DBConnection()
results = myDB.select('SELECT * from snatched WHERE Status="Seed_Processed"')
myDB = db.DBConnection()
results = myDB.select('SELECT * from snatched WHERE Status="Seed_Processed"')
for album in results:
hash = album['FolderName']
albumid = album['AlbumID']
torrent_removed = False
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
torrent_removed = transmission.removeTorrent(hash, True)
else:
torrent_removed = utorrent.removeTorrent(hash, True)
for album in results:
hash = album['TorrentHash']
albumid = album['AlbumID']
torrent_removed = False
if torrent_removed:
myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?',
[albumid])
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
torrent_removed = transmission.removeTorrent(hash, True)
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")
+27 -2
View File
@@ -17,6 +17,7 @@ import time
import json
import base64
import urlparse
import os
from headphones import logger, request
import headphones
@@ -33,7 +34,7 @@ _session_id = None
def addTorrent(link, data=None):
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:
metainfo = str(base64.b64encode(data))
else:
@@ -64,7 +65,31 @@ def addTorrent(link, data=None):
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'
arguments = {'ids': torrentid, 'fields': ['name', 'percentDone']}
+4 -3
View File
@@ -39,14 +39,15 @@ def runGit(args):
try:
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)
output, err = p.communicate()
output = output.strip()
logger.debug('Git output: ' + output)
except OSError:
logger.debug('Command failed: %s', cmd)
except OSError as e:
logger.debug('Command failed: %s. Error: %s' % (cmd, e))
continue
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/>.
# 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
import threading
@@ -28,7 +29,7 @@ import urllib2
import os
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 mako.lookup import TemplateLookup
from mako import exceptions
@@ -69,6 +70,11 @@ class WebInterface(object):
artists = myDB.select('SELECT * from artists order by ArtistSortName COLLATE NOCASE')
return serve_template(templatename="index.html", title="Home", artists=artists)
@cherrypy.expose
def threads(self):
crier.cry()
raise cherrypy.HTTPRedirect("home")
@cherrypy.expose
def artistPage(self, ArtistID):
myDB = db.DBConnection()
@@ -246,7 +252,10 @@ class WebInterface(object):
namecheck = myDB.select('SELECT ArtistName from artists where ArtistID=?', [ArtistID])
for name in namecheck:
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])
from headphones import cache
@@ -339,14 +348,18 @@ class WebInterface(object):
dirs = set(dirs)
for dir in dirs:
artistfolder = os.path.join(dir, folder)
if not os.path.isdir(artistfolder.encode(headphones.SYS_ENCODING)):
logger.debug("Cannot find directory: " + artistfolder)
continue
threading.Thread(target=librarysync.libraryScan,
kwargs={"dir": artistfolder, "artistScan": True, "ArtistID": ArtistID,
"ArtistName": artist_name}).start()
try:
for dir in dirs:
artistfolder = os.path.join(dir, folder)
if not os.path.isdir(artistfolder.encode(headphones.SYS_ENCODING)):
logger.debug("Cannot find directory: " + artistfolder)
continue
threading.Thread(target=librarysync.libraryScan,
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)
@cherrypy.expose
@@ -528,7 +541,7 @@ class WebInterface(object):
myDB = db.DBConnection()
upcoming = myDB.select(
"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,
wanted=wanted)
@@ -865,11 +878,19 @@ class WebInterface(object):
def musicScan(self, path, scan=0, redirect=None, autoadd=0, libraryscan=0):
headphones.CONFIG.LIBRARYSCAN = libraryscan
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:
try:
threading.Thread(target=librarysync.libraryScan).start()
threading.Thread(target=librarysync.libraryScan, kwargs=params).start()
except Exception as e:
logger.error('Unable to complete the scan: %s' % e)
if redirect:
@@ -1139,6 +1160,7 @@ class WebInterface(object):
"download_scan_interval": headphones.CONFIG.DOWNLOAD_SCAN_INTERVAL,
"update_db_interval": headphones.CONFIG.UPDATE_DB_INTERVAL,
"mb_ignore_age": headphones.CONFIG.MB_IGNORE_AGE,
"mb_ignore_age_missing": headphones.CONFIG.MB_IGNORE_AGE_MISSING,
"search_interval": headphones.CONFIG.SEARCH_INTERVAL,
"libraryscan_interval": headphones.CONFIG.LIBRARYSCAN_INTERVAL,
"sab_host": headphones.CONFIG.SAB_HOST,
@@ -1151,6 +1173,10 @@ class WebInterface(object):
"nzbget_password": headphones.CONFIG.NZBGET_PASSWORD,
"nzbget_category": headphones.CONFIG.NZBGET_CATEGORY,
"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_username": headphones.CONFIG.TRANSMISSION_USERNAME,
"transmission_password": headphones.CONFIG.TRANSMISSION_PASSWORD,
@@ -1171,6 +1197,7 @@ class WebInterface(object):
"torrent_downloader_transmission": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 1),
"torrent_downloader_utorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 2),
"torrent_downloader_deluge": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 3),
"torrent_downloader_qbittorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 4),
"download_dir": headphones.CONFIG.DOWNLOAD_DIR,
"use_blackhole": checked(headphones.CONFIG.BLACKHOLE),
"blackhole_dir": headphones.CONFIG.BLACKHOLE_DIR,
@@ -1218,12 +1245,21 @@ class WebInterface(object):
"rutracker_user": headphones.CONFIG.RUTRACKER_USER,
"rutracker_password": headphones.CONFIG.RUTRACKER_PASSWORD,
"rutracker_ratio": headphones.CONFIG.RUTRACKER_RATIO,
"use_whatcd": checked(headphones.CONFIG.WHATCD),
"whatcd_username": headphones.CONFIG.WHATCD_USERNAME,
"whatcd_password": headphones.CONFIG.WHATCD_PASSWORD,
"whatcd_ratio": headphones.CONFIG.WHATCD_RATIO,
"rutracker_cookie": headphones.CONFIG.RUTRACKER_COOKIE,
"use_apollo": checked(headphones.CONFIG.APOLLO),
"apollo_username": headphones.CONFIG.APOLLO_USERNAME,
"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),
"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_1": radio(headphones.CONFIG.PREFERRED_QUALITY, 1),
"pref_qual_2": radio(headphones.CONFIG.PREFERRED_QUALITY, 2),
@@ -1247,6 +1283,8 @@ class WebInterface(object):
"keep_nfo": checked(headphones.CONFIG.KEEP_NFO),
"add_album_art": checked(headphones.CONFIG.ADD_ALBUM_ART),
"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_lyrics": checked(headphones.CONFIG.EMBED_LYRICS),
"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),
"log_dir": headphones.CONFIG.LOG_DIR,
"cache_dir": headphones.CONFIG.CACHE_DIR,
"keep_torrent_files_dir": headphones.CONFIG.KEEP_TORRENT_FILES_DIR,
"interface_list": interface_list,
"music_encoder": checked(headphones.CONFIG.MUSIC_ENCODER),
"encoder": headphones.CONFIG.ENCODER,
@@ -1373,7 +1412,16 @@ class WebInterface(object):
"email_ssl": checked(headphones.CONFIG.EMAIL_SSL),
"email_tls": checked(headphones.CONFIG.EMAIL_TLS),
"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():
@@ -1420,8 +1468,8 @@ class WebInterface(object):
"use_newznab", "newznab_enabled", "use_torznab", "torznab_enabled",
"use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay",
"use_mininova", "use_waffles", "use_rutracker",
"use_whatcd", "use_strike", "preferred_bitrate_allow_lossless", "detect_bitrate",
"ignore_clean_releases", "freeze_db", "cue_split", "move_files",
"use_apollo", "use_redacted", "use_strike", "use_tquattrecentonze", "preferred_bitrate_allow_lossless",
"detect_bitrate", "ignore_clean_releases", "freeze_db", "cue_split", "move_files",
"rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art",
"embed_album_art", "embed_lyrics",
"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",
"songkick_enabled", "songkick_filter_enabled",
"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:
if checked_config not in kwargs:
@@ -1710,6 +1759,12 @@ class WebInterface(object):
telegram = notifiers.TELEGRAM()
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):
@cherrypy.expose
+16 -36
View File
@@ -1,7 +1,8 @@
#!/bin/sh
#
# PROVIDE: headphones
# REQUIRE: DAEMON sabnzbd
# REQUIRE: DAEMON
# BEFORE: LOGIN
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
@@ -15,56 +16,35 @@
# as root.
# headphones_dir: Directory where Headphones lives.
# 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.
# Default is headphones.pid in headphones_dir.
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
. /etc/rc.subr
name="headphones"
rcvar=${name}_enable
load_rc_config ${name}
: "${headphones_enable:="NO"}"
: "${headphones_user:="_sabnzbd"}"
: "${headphones_dir:="/usr/local/headphones"}"
: "${headphones_chdir:="${headphones_dir}"}"
: "${headphones_pid:="${headphones_dir}/headphones.pid"}"
: "${headphones_conf:="/usr/local/headphones/config.ini"}"
status_cmd="${name}_status"
stop_cmd="${name}_stop"
command="${headphones_dir}/Headphones.py"
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"
command_args="-f -p ${headphones_pid} python ${headphones_dir}/Headphones.py ${headphones_flags} --quiet --nolaunch"
headphones_start_precmd() {
if [ $($ID -u) != 0 ]; then
err 1 "Must be root."
fi
# Ensure user is root when running this script.
if [ "$(id -u)" != "0" ]; then
echo "Oops, you should be root before running this!"
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"
if [ ! -d /var/run/headphones ]; then
install -do $headphones_user /var/run/headphones
fi
}
load_rc_config ${name}
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
## 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_HOST= #$HOST_OPTS, host for the webserver, overrides value in config.ini
##
## EXAMPLE if want to run as different user
## add HP_USER=username to /etc/default/headphones
@@ -105,7 +106,12 @@ load_settings() {
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
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.
# Copyright 2014, Adrian Sampson.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -12,17 +13,33 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# This particular version has been slightly modified to work with Headphones
# https://github.com/rembo10/headphones
__version__ = '1.3.10-headphones'
__author__ = 'Adrian Sampson <adrian@radbox.org>'
from __future__ import division, absolute_import, print_function
import os
import beets.library
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