Compare commits

...
77 Commits
Author SHA1 Message Date
rembo10 b3199605be v0.5.20 2021-10-15 09:56:59 +05:30
rembo10 9a7006ad14 Update changelog for v0.5.20 release 2021-10-15 09:56:49 +05:30
rembo10 38197a5871 Merge branch 'ssl-fix' into develop 2021-10-15 08:14:19 +05:30
rembo10 ee58f62b2b Update root certificates for the requests lib 2021-10-15 08:13:49 +05:30
AdeHub 58edc604b3 Merge branch 'develop' 2021-07-08 19:57:08 +12:00
AdeHub ba33a5e655 last.fm api key 2021-07-08 19:52:46 +12:00
AdeHub 379fd3d0b8 Merge branch 'develop' 2020-10-17 11:06:38 +13:00
AdeHub 374b106522 last.fm api
last.fm api
2020-10-17 11:02:00 +13:00
AdeHub bf74f57535 Merge branch 'develop' 2020-05-28 10:49:51 +12:00
AdeHub f1a03e5fb6 pep 2020-04-15 07:22:31 +12:00
AdeHub 50cbe2e943 qBit logging 2020-04-15 07:10:51 +12:00
AdeHub 219f855dfc qBittorrent v2 api 2020-04-13 09:50:40 +12:00
AdeHub f18334d87c Merge branch 'develop' 2020-03-07 10:30:57 +13:00
AdeHub 72fce3cdeb last.fm api key
new key
2020-03-07 10:25:45 +13:00
AdeHub 5283b48736 Merge branch 'develop' 2019-09-08 17:38:17 +12:00
AdeHub 507d8aa7d2 Telegram 2019-09-08 17:27:51 +12:00
AdeHub 4bce833c1e pep 2019-08-17 11:48:50 +12:00
AdeHub f7a23bb4d0 Fix/Enhance Telegram 2019-08-17 11:36:55 +12:00
AdeHub 5d145b6f8a revert to older requests version 2019-07-15 13:54:34 +12:00
AdeHub 3ab31f1da7 urllib3
requests supported version
2019-07-14 21:02:40 +12:00
AdeHub cf85d94a37 requests packages 2019-07-14 20:46:19 +12:00
AdeHub a8a53353ee pep 2019-07-14 20:13:00 +12:00
AdeHub 476ee7b312 mb artist relationships
Get Artist Relationship urls including Discogs, Wikepedia etc.

Currently not used
2019-07-14 19:59:48 +12:00
AdeHub 14bef1e63d Use fanart.tv for artist image
last.fm no longer allows access to artist images. Get artist image from fanart.tv  instead
2019-07-14 19:57:40 +12:00
Ade dc22bb006d Hotfix index creation from develop
Fixes #3175
2019-01-08 21:09:41 +13:00
Ade b49cd8a813 Attempt to fix Index creation error 2019-01-06 16:52:39 +13:00
Ade d7275e5a99 Update Requests lib 2019-01-06 12:12:32 +13:00
Ade aa1867cf06 More Jackett redirect stuff 2019-01-01 12:45:59 +13:00
Ade 19b766a7ce Jackett redirect update
Moved the redirect for Transmission, Deluge
2018-12-31 10:22:37 +13:00
Ade b45f3565da Orpheus.network issues 2018-12-21 18:51:48 +13:00
Ade 22525668fd Remove Mininova 2018-12-21 18:21:33 +13:00
Ade af79bc47e8 Escape sql artist string 2018-12-19 20:25:49 +13:00
AdeHub 6208fe0e69 Merge pull request #3165 from hypsometric/gazelle_freeleech_tokens
Redacted: use a FL token when available
2018-12-14 08:38:25 +13:00
Hypsometric f66a37abbd Redacted: use a FL token when available
- Use a FL token when available
- Added a configuration (default off) to enable the feature
- Skip prefetching the data with Deluge as with Transmission in
`preprocess()`

- Note: because QBittorrent & uTorrent need the data to compute the
torrenthash but still require the url to download the file, it will
result in 2 FL tokens used instead of one because the .torrent will be
downloaded twice from the server.

Fixes #3163
2018-12-13 16:41:22 +01:00
AdeHub 4b52ae445d Merge pull request #3160 from doucheymcdoucherson/develop1
Replace apollo.rip with orpheus.network
2018-12-08 17:52:11 +13:00
doucheymcdoucherson 8a2c3169e2 fix whitespace on blank line 2018-11-08 12:12:03 -08:00
doucheymcdoucherson 805eb7dd13 tabs to spaces 2018-11-08 12:08:41 -08:00
doucheymcdoucherson 81486d2cd2 fix for seeder sort with Orpheus 2018-11-08 11:41:33 -08:00
doucheymcdoucherson d1237b052a replace apollo.rip with orpheus.network 2018-11-08 09:58:09 -08:00
AdeHub a4ef757d12 Merge pull request #3144 from cheese1/develop
fix typo
2018-10-14 17:15:03 +13:00
AdeHub bb8eb19dbf Merge pull request #3151 from permster/develop
Workaround for issue #2674
2018-10-14 17:14:33 +13:00
Ade 602e115a29 Disable torznab tests 2018-10-14 15:28:39 +13:00
Ade e2c335db2d more pep 2018-10-13 18:22:54 +13:00
Ade 44583d42dd pep 2018-10-13 18:18:01 +13:00
Ade 43d465ca22 OSX notify import fix 2018-10-13 18:13:53 +13:00
Ade bd60935493 Torrent folder fix 2018-10-13 12:17:58 +13:00
Travis Golliher 0264b1fb0d Workaround for issue #2674 2018-10-06 02:46:13 -07:00
Ade 24f22834ff Torznab/Jackett Seed Ratio 2018-09-07 18:41:47 +12:00
cheese1 aed2e63984 fix missing space 2018-09-03 11:31:39 +02:00
cheese1 694c17a40b fix typo 2018-09-02 20:38:54 +02:00
Ade 6c34bc46e9 Fix Ignore Undated MB Releases not saving 2018-08-25 17:39:30 +12:00
Ade 0b2600b783 Fix helpers.replace_all
- Renamed post processing replace_all to pattern_substitute
- Resurrected replace_all
2018-08-17 18:43:43 +12:00
Ade 693e5b1624 Improve database locking issues
- Added new indexes
- Retry if locked
- Fix upsert race condition
- Various changes to track matching
2018-08-15 14:51:27 +12:00
Ade 287278c049 Merge branch 'develop' of https://github.com/rembo10/headphones into develop 2018-08-14 20:08:59 +12:00
AdeHub 496560909f Merge pull request #3128 from doucheymcdoucherson/develop1
fixes #3110
2018-08-14 20:05:39 +12:00
AdeHub 22b1bf36ab Merge pull request #3124 from doucheymcdoucherson/develop
sort Apollo.rip results by number of seeders
2018-08-14 20:04:40 +12:00
Ade b506a1797d Merge branch 'master' of https://github.com/rembo10/headphones into develop 2018-07-12 12:51:20 +12:00
Ade ff8417b410 Merge branch 'master' into develop 2018-07-12 12:45:19 +12:00
doucheymcdoucherson 741cb466ef fixes #3110 2018-07-06 14:09:12 -07:00
doucheymcdoucherson a99a6abde2 sort Apollo.rip results by number of seeders 2018-07-03 11:46:51 -07:00
Ade 4631b3cbbf Plex update fix 2018-06-14 10:53:24 +12:00
Ade 7230428a31 Speed up albumPage 2018-06-10 10:37:59 +12:00
Ade 2b5b2bd506 rutracker hires
get more lossless results
2018-06-06 18:56:18 +12:00
Ade 99c3e8498d More Jackett magnet 2018-06-03 15:22:16 +12:00
Ade 472f657c88 Jackett magnet links 2018-05-28 20:58:27 +12:00
Ade 368e30067b Remove t411, kick ass 2018-05-10 10:33:52 +12:00
Ade 053fd40e76 pep 2018-04-25 21:03:16 +12:00
Ade 7af7f6be1e Remove Kick Ass 2018-04-25 12:05:21 +12:00
Ade 5fd20e50fd Remove Strike Search 2018-04-25 11:55:14 +12:00
Ade 43df2956fb Remove t411 pt2 2018-04-25 11:48:54 +12:00
Ade 0c96997005 Remove t411 2018-04-25 11:33:39 +12:00
Ade 81651b119a Stop Metacritic 404
Fixes #2715
2018-04-24 20:14:11 +12:00
Ade ecad4d39eb Get Original Folder from Temp directory
Fixes #3109
2018-04-21 11:02:21 +12:00
Ade 0d3be4866f Email date
Fixes #2703
2018-04-14 13:59:18 +12:00
AdeHub eeed89811b Merge pull request #3104 from mirwan/torznab_size_fix
Items from torznab are not returned when size is defined as torznab:attr
2018-04-09 19:58:17 +12:00
Erwan Miran d9bff5dffe size may be set as torznab:attr 2018-04-08 23:43:42 +02:00
Ade e3983d615d Merge branches 'develop' and 'master' of https://github.com/rembo10/headphones 2017-10-22 09:51:39 +13:00
112 changed files with 4440 additions and 4082 deletions
+15
View File
@@ -1,5 +1,20 @@
# Changelog
## v0.5.20
Released 15 October 2021
Highlights:
* Improved: Last.fm & metacrtic api fixes
* Improved: Various database fixes
* Added: qBitTorrent
* Added: Telegram notifications
* Improved: Many more since the last release. Check the list
Many thanks to @AdeHub for keeping the ship afloat
and to everyone else who contributed.
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.19...v0.5.20).
## v0.5.19
Released 27 March 2018
+1 -1
View File
@@ -324,7 +324,7 @@
if (++x === 10) {
clearInterval(refreshInterval);
}
var sts = $("#artistname").text();
var sts = $("#artistname").text().trim();
if (wasLoading == true || sts == "Loading"){
location.reload();
$('#albumnamespinner').remove()
+19 -68
View File
@@ -137,7 +137,7 @@
<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']}">
<input type="checkbox" id="mb_ignore_age_missing" name="mb_ignore_age_missing" value="1" ${config['mb_ignore_age_missing']}
</div>
</fieldset>
</td>
@@ -601,23 +601,6 @@
</div>
</fieldset>
<fieldset>
<div class="row checkbox left">
<input id="use_kat" type="checkbox" class="bigcheck" name="use_kat" value="1" ${config['use_kat']} /><label for="use_kat"><span class="option">Kick Ass Torrents</span></label>
</div>
<div class="config">
<div class="row">
<label>Proxy URL</label>
<input type="text" name="kat_proxy_url" value="${config['kat_proxy_url']}" size="36">
<small>Optional. Leave empty for default.</small>
</div>
<div class="row">
<label>Seed Ratio</label>
<input type="text" class="override-float" name="kat_ratio" value="${config['kat_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_waffles" type="checkbox" class="bigcheck" name="use_waffles" value="1" ${config['use_waffles']} /><label for="use_waffles"><span class="option">Waffles.ch</span></label>
@@ -664,24 +647,24 @@
<fieldset>
<div class="row checkbox left">
<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>
<input id="use_orpheus" type="checkbox" class="bigcheck" name="use_orpheus" value="1" ${config['use_orpheus']} /><label for="use_orpheus"><span class="option">Orpheus.network</span></label>
</div>
<div class="config">
<div class="row">
<label>Username</label>
<input type="text" name="apollo_username" value="${config['apollo_username']}" size="36">
<input type="text" name="orpheus_username" value="${config['orpheus_username']}" size="36">
</div>
<div class="row">
<label>Password</label>
<input type="password" name="apollo_password" value="${config['apollo_password'] | h}" size="36">
<input type="password" name="orpheus_password" value="${config['orpheus_password'] | h}" size="36">
</div>
<div class="row">
<label>URL</label>
<input type="text" name="apollo_url" value="${config['apollo_url']}" size="36">
<input type="text" name="orpheus_url" value="${config['orpheus_url']}" size="36">
</div>
<div class="row">
<label>Seed Ratio</label>
<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">
<input type="text" class="override-float" name="orpheus_ratio" value="${config['orpheus_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>
@@ -703,17 +686,9 @@
<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>
<fieldset>
<div class="row checkbox left">
<input id="use_strike" type="checkbox" class="bigcheck" name="use_strike" value="1" ${config['use_strike']} /><label for="use_strike"><span class="option">Strike Search</span></label>
</div>
<div class="config">
<div class="row">
<label>Seed Ratio</label>
<input type="text" class="override-float" name="strike_ratio" value="${config['strike_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding.">
<label>Use freeleech tokens when available</label>
<input type="checkbox" name="redacted_use_fltoken" value="1" ${config['redacted_use_fltoken']}>
</div>
</div>
</fieldset>
@@ -733,6 +708,10 @@
<label>Torznab API</label>
<input type="text" name="torznab_apikey" value="${config['torznab_apikey']}" size="36">
</div>
<div class="row">
<label>Torznab Seed Ratio</label>
<input type="text" class="override-float" name="torznab_ratio" value="${config['torznab_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 checkbox">
<input id="torznab_enabled" type="checkbox" name="torznab_enabled" value="1" ${config['torznab_enabled']} /><label>Enabled</label>
</div>
@@ -742,7 +721,7 @@
%>
%for torznab in config['extra_torznabs']:
<%
if torznab[2] == '1' or torznab[2] == 1:
if torznab[3] == '1' or torznab[3] == 1:
torznab_enabled = "checked"
else:
torznab_enabled = ""
@@ -756,6 +735,10 @@
<label>Torznab API</label>
<input type="text" name="torznab_api${torznab_number}" value="${torznab[1]}" size="36">
</div>
<div class="row">
<label>Torznab Seed Ratio</label>
<input type="text" class="override-float" name="torznab_ratio${torznab_number}" value="${torznab[2]}" 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 checkbox">
<input id="torznab_enabled" type="checkbox" name="torznab_enabled${torznab_number}" value="1" ${torznab_enabled} /><label>Enabled</label>
</div>
@@ -787,34 +770,6 @@
</div>
</fieldset>
<fieldset>
<div class="row checkbox left">
<input id="use_mininova" type="checkbox" class="bigcheck" name="use_mininova" value="1" ${config['use_mininova']} /><label for="use_mininova"><span class="option">Mininova</span></label>
</div>
<div class="config">
<div class="row">
<label>Seed Ratio</label>
<input type="text" class="override-float" name="mininova_ratio" value="${config['mininova_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_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>
</tr>
@@ -2566,7 +2521,7 @@
$("#add_torznab").click(function() {
var intId = $("#torznab_providers > div").size() + deletedTorznabs + 1;
var formfields = $("<div class=\"config\" id=\"torznab" + intId + "\"><div class=\"row\"><label>Torznab Host</label><input type=\"text\" name=\"torznab_host" + intId + "\" size=\"30\"></div><div class=\"row\"><label>Torznab API</label><input type=\"text\" name=\"torznab_api" + intId + "\" size=\"36\"></div><div class=\"row checkbox\"><input type=\"checkbox\" name=\"torznab_enabled" + intId + "\" value=\"1\" checked /><label>Enabled</label></div>");
var formfields = $("<div class=\"config\" id=\"torznab" + intId + "\"><div class=\"row\"><label>Torznab Host</label><input type=\"text\" name=\"torznab_host" + intId + "\" size=\"30\"></div><div class=\"row\"><label>Torznab API</label><input type=\"text\" name=\"torznab_api" + intId + "\" size=\"36\"></div><div class=\"row\"><label>Torznab Seed Ratio</label><input type=\"text\" class=\"override-float\" name=\"torznab_ratio" + intId + "\" size=\"10\"></div><div class=\"row checkbox\"><input type=\"checkbox\" name=\"torznab_enabled" + intId + "\" value=\"1\" checked /><label>Enabled</label></div>");
var removeTorznabButton = $("<div class=\"row\"><input type=\"button\" class=\"remove_torznab\" value=\"Remove\" /></div>");
removeTorznabButton.click(function() {
$(this).parent().remove();
@@ -2595,20 +2550,16 @@
initConfigCheckbox("#use_nzbsorg");
initConfigCheckbox("#use_omgwtfnzbs");
initConfigCheckbox("#use_torznab");
initConfigCheckbox("#use_kat");
initConfigCheckbox("#use_piratebay");
initConfigCheckbox("#use_oldpiratebay");
initConfigCheckbox("#use_mininova");
initConfigCheckbox("#use_waffles");
initConfigCheckbox("#use_rutracker");
initConfigCheckbox("#use_apollo");
initConfigCheckbox("#use_orpheus");
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); })
+1 -1
View File
@@ -36,7 +36,7 @@
"aTargets": [0],
"mData":"ArtistID",
"mRender": function ( data, type, full ) {
return '<div id="artistImg"><img class="albumArt" alt="" id="'+ data + '" data-src="artwork/thumbs/artist/' + data + '"/></div>';
return '<div id="artistImg"><img class="albumArt" height="50" width="50" alt="" id="'+ data + '" data-src="artwork/thumbs/artist/' + data + '"/></div>';
}
},
{
+27
View File
@@ -360,6 +360,8 @@ def sig_handler(signum=None, frame=None):
def dbcheck():
logger.debug("SQLite Version: %s", sqlite3.sqlite_version)
logger.debug("DB-API Version: %s", sqlite3.version)
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()
c.execute(
@@ -420,6 +422,31 @@ def dbcheck():
c.execute(
'CREATE INDEX IF NOT EXISTS tracks_artistid ON tracks(ArtistID ASC)')
# Speed up album page
c.execute('CREATE INDEX IF NOT EXISTS allalbums_albumid ON allalbums(AlbumID ASC)')
c.execute('CREATE INDEX IF NOT EXISTS alltracks_albumid ON alltracks(AlbumID ASC)')
c.execute('CREATE INDEX IF NOT EXISTS releases_albumid ON releases(ReleaseGroupID ASC)')
c.execute('CREATE INDEX IF NOT EXISTS descriptions_albumid ON descriptions(ReleaseGroupID ASC)')
# Speed up artist deletion
c.execute('CREATE INDEX IF NOT EXISTS allalbums_artistid ON allalbums(ArtistID ASC)')
c.execute('CREATE INDEX IF NOT EXISTS alltracks_artistid ON alltracks(ArtistID ASC)')
c.execute('CREATE INDEX IF NOT EXISTS descriptions_artistid ON descriptions(ArtistID ASC)')
# Speed up Artist refresh hybrid release
c.execute('CREATE INDEX IF NOT EXISTS albums_releaseid ON albums(ReleaseID ASC)')
c.execute('CREATE INDEX IF NOT EXISTS tracks_releaseid ON tracks(ReleaseID ASC)')
# Speed up scanning and track matching
c.execute('CREATE INDEX IF NOT EXISTS artist_artistname ON artists(ArtistName COLLATE NOCASE ASC)')
# General speed up
c.execute('CREATE INDEX IF NOT EXISTS artist_artistsortname ON artists(ArtistSortName COLLATE NOCASE ASC)')
c.execute(
"""CREATE INDEX IF NOT EXISTS have_matched_artist_album ON have(Matched ASC, ArtistName COLLATE NOCASE ASC, AlbumTitle COLLATE NOCASE ASC)""")
c.execute('DROP INDEX IF EXISTS have_matched')
try:
c.execute('SELECT IncludeExtras from artists')
except sqlite3.OperationalError:
+1 -1
View File
@@ -82,7 +82,7 @@ class Api(object):
def fetchData(self):
if self.data == 'OK':
logger.info('Recieved API command: %s', self.cmd)
logger.info('Received API command: %s', self.cmd)
methodToCall = getattr(self, "_" + self.cmd)
methodToCall(**self.kwargs)
if 'callback' not in self.kwargs:
+107 -26
View File
@@ -14,11 +14,16 @@
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
import os
from six.moves.urllib.parse import urlencode
import headphones
from headphones import db, helpers, logger, lastfm, request, mb
LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
LASTFM_API_KEY = "8d983789c771afaeb7412ac358d4bad0"
FANART_URL = 'https://webservice.fanart.tv/v3/music/'
FANART_PROJECT_KEY = '22b73c9603eba09d0c855f2d2bdba31c'
FANART_CLIENT_KEY = '919b389a18a3f0b2c916090022ab3c7a'
class Cache(object):
@@ -96,6 +101,11 @@ class Cache(object):
def _is_current(self, filename=None, date=None):
if filename:
# 2019 last.fm no longer allows access to artist images, for now we'll keep the cached artist image if it exists and get new from fanart.tv
if self.id_type == 'artist' and 'fanart' not in filename:
return True
base_filename = os.path.basename(filename)
date = base_filename.split('.')[1]
@@ -211,20 +221,34 @@ class Cache(object):
if ArtistID:
self.id_type = 'artist'
data = lastfm.request_lastfm("artist.getinfo", mbid=ArtistID, api_key=LASTFM_API_KEY)
# 2019 last.fm no longer allows access to artist images, try fanart.tv instead
image_url = None
thumb_url = None
data = request.request_json(FANART_URL + ArtistID, whitelist_status_code=404,
headers={'api-key': FANART_PROJECT_KEY, 'client-key': FANART_CLIENT_KEY})
if not data:
return
try:
image_url = data['artist']['image'][-1]['#text']
except (KeyError, IndexError):
logger.debug('No artist image found')
image_url = None
if data.get('artistthumb'):
image_url = data['artistthumb'][0]['url']
elif data.get('artistbackground'):
image_url = data['artistbackground'][0]['url']
# elif data.get('hdmusiclogo'):
# image_url = data['hdmusiclogo'][0]['url']
thumb_url = self._get_thumb_url(data)
if not thumb_url:
logger.debug('No artist thumbnail image found')
# fallback to 1st album cover if none of the above
elif 'albums' in data:
for mbid, art in data.get('albums', dict()).items():
if 'albumcover' in art:
image_url = art['albumcover'][0]['url']
break
if image_url:
thumb_url = image_url
else:
logger.debug('No artist image found on fanart.tv for Artist Id: %s', self.id)
else:
@@ -283,6 +307,7 @@ class Cache(object):
"""
myDB = db.DBConnection()
fanart = False
# Since lastfm uses release ids rather than release group ids for albums, we have to do a artist + album search for albums
# Exception is when adding albums manually, then we should use release id
@@ -311,15 +336,42 @@ class Cache(object):
except KeyError:
logger.debug('No artist bio found')
self.info_content = None
try:
image_url = data['artist']['image'][-1]['#text']
except KeyError:
logger.debug('No artist image found')
image_url = None
thumb_url = self._get_thumb_url(data)
if not thumb_url:
logger.debug('No artist thumbnail image found')
# 2019 last.fm no longer allows access to artist images, try fanart.tv instead
image_url = None
thumb_url = None
data = request.request_json(FANART_URL + self.id, whitelist_status_code=404,
headers={'api-key': FANART_PROJECT_KEY, 'client-key': FANART_CLIENT_KEY})
if data.get('artistthumb'):
image_url = data['artistthumb'][0]['url']
elif data.get('artistbackground'):
image_url = data['artistbackground'][0]['url']
# elif data.get('hdmusiclogo'):
# image_url = data['hdmusiclogo'][0]['url']
# fallback to 1st album cover if none of the above
elif 'albums' in data:
for mbid, art in data.get('albums', dict()).items():
if 'albumcover' in art:
image_url = art['albumcover'][0]['url']
break
# finally, use 1st album cover from last.fm
if image_url:
fanart = True
thumb_url = image_url
else:
dbalbum = myDB.action(
'SELECT ArtworkURL, ThumbURL FROM albums WHERE ArtworkURL IS NOT NULL AND ArtistID=?',
[self.id]).fetchone()
if dbalbum:
fanart = True
image_url = dbalbum['ArtworkURL']
thumb_url = dbalbum['ThumbURL']
if not image_url:
logger.debug('No artist image found on fanart.tv for Artist Id: %s', self.id)
else:
dbalbum = myDB.action(
@@ -427,7 +479,11 @@ class Cache(object):
ext = os.path.splitext(image_url)[1]
artwork_path = os.path.join(self.path_to_art_cache,
if fanart:
artwork_path = os.path.join(self.path_to_art_cache,
self.id + '_fanart_' + '.' + helpers.today() + ext)
else:
artwork_path = os.path.join(self.path_to_art_cache,
self.id + '.' + helpers.today() + ext)
try:
with open(artwork_path, 'wb') as f:
@@ -443,7 +499,9 @@ class Cache(object):
# as it's missing/outdated.
if thumb_url and self.query_type in ['thumb', 'artwork'] and not (
self.thumb_files and self._is_current(self.thumb_files[0])):
artwork = request.request_content(thumb_url, timeout=20)
if not (self.query_type == 'artwork' and 'fanart' in thumb_url and artwork):
artwork = request.request_content(thumb_url, timeout=20)
if artwork:
# Make sure the artwork dir exists:
@@ -466,11 +524,34 @@ class Cache(object):
ext = os.path.splitext(image_url)[1]
thumb_path = os.path.join(self.path_to_art_cache,
'T_' + self.id + '.' + helpers.today() + ext)
if fanart:
thumb_path = os.path.join(self.path_to_art_cache,
'T_' + self.id + '_fanart_' + '.' + helpers.today() + ext)
else:
thumb_path = os.path.join(self.path_to_art_cache,
'T_' + self.id + '.' + helpers.today() + ext)
try:
with open(thumb_path, 'wb') as f:
f.write(artwork)
if self.id_type != 'artist':
with open(thumb_path, 'wb') as f:
f.write(artwork)
else:
# 2019 last.fm no longer allows access to artist images, use the fanart.tv image to create a thumb
artwork_thumb = None
if 'fanart' in thumb_url:
# Create thumb using image resizing service
artwork_path = '{0}?{1}'.format('http://images.weserv.nl/', urlencode({
'url': thumb_url.replace('http://', ''),
'w': 300,
}))
artwork_thumb = request.request_content(artwork_path, timeout=20, whitelist_status_code=404)
if artwork_thumb:
with open(thumb_path, 'wb') as f:
f.write(artwork_thumb)
else:
with open(thumb_path, 'wb') as f:
f.write(artwork)
os.chmod(thumb_path, int(headphones.CONFIG.FILE_PERMISSIONS, 8))
except (OSError, IOError) as e:
@@ -486,7 +567,7 @@ def getArtwork(ArtistID=None, AlbumID=None):
if not artwork_path:
return None
if artwork_path.startswith('http://'):
if artwork_path.startswith(('http://', 'https://')):
return artwork_path
else:
artwork_file = os.path.basename(artwork_path)
@@ -500,7 +581,7 @@ def getThumb(ArtistID=None, AlbumID=None):
if not artwork_path:
return None
if artwork_path.startswith('http://'):
if artwork_path.startswith(('http://', 'https://')):
return artwork_path
else:
thumbnail_file = os.path.basename(artwork_path)
+23 -18
View File
@@ -41,11 +41,11 @@ _CONFIG_DEFINITIONS = {
'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'),
'ORPHEUS': (int, 'Orpheus.network', 0),
'ORPHEUS_PASSWORD': (str, 'Orpheus.network', ''),
'ORPHEUS_RATIO': (str, 'Orpheus.network', ''),
'ORPHEUS_USERNAME': (str, 'Orpheus.network', ''),
'ORPHEUS_URL': (str, 'Orpheus.network', 'https://orpheus.network'),
'AUTOWANT_ALL': (int, 'General', 0),
'AUTOWANT_MANUALLY_ADDED': (int, 'General', 1),
'AUTOWANT_UPCOMING': (int, 'General', 1),
@@ -150,9 +150,6 @@ _CONFIG_DEFINITIONS = {
'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', ''),
@@ -168,8 +165,6 @@ _CONFIG_DEFINITIONS = {
'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'),
'MOVE_FILES': (int, 'General', 0),
'MPC_ENABLED': (bool_int, 'MPC', False),
@@ -271,8 +266,6 @@ _CONFIG_DEFINITIONS = {
'SONGKICK_ENABLED': (int, 'Songkick', 1),
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
'SONGKICK_LOCATION': (str, 'Songkick', ''),
'STRIKE': (int, 'Strike', 0),
'STRIKE_RATIO': (str, 'Strike', ''),
'SUBSONIC_ENABLED': (int, 'Subsonic', 0),
'SUBSONIC_HOST': (str, 'Subsonic', ''),
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
@@ -289,6 +282,7 @@ _CONFIG_DEFINITIONS = {
'TORZNAB_APIKEY': (str, 'Torznab', ''),
'TORZNAB_ENABLED': (int, 'Torznab', 1),
'TORZNAB_HOST': (str, 'Torznab', ''),
'TORZNAB_RATIO': (str, 'Torznab', ''),
'TRANSMISSION_HOST': (str, 'Transmission', ''),
'TRANSMISSION_PASSWORD': (str, 'Transmission', ''),
'TRANSMISSION_USERNAME': (str, 'Transmission', ''),
@@ -297,9 +291,6 @@ _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', ''),
@@ -316,6 +307,7 @@ _CONFIG_DEFINITIONS = {
'REDACTED_USERNAME': (str, 'Redacted', ''),
'REDACTED_PASSWORD': (str, 'Redacted', ''),
'REDACTED_RATIO': (str, 'Redacted', ''),
'REDACTED_USE_FLTOKEN': (int, 'Redacted', 0),
'XBMC_ENABLED': (int, 'XBMC', 0),
'XBMC_HOST': (str, 'XBMC', ''),
'XBMC_NOTIFY': (int, 'XBMC', 0),
@@ -420,8 +412,8 @@ class Config(object):
def get_extra_torznabs(self):
""" Return the extra torznab tuples """
extra_torznabs = list(
itertools.izip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 3)
for i in range(3)])
itertools.izip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 4)
for i in range(4)])
)
return extra_torznabs
@@ -492,4 +484,17 @@ class Config(object):
if self.CONFIG_VERSION == '5':
if self.OPEN_MAGNET_LINKS:
self.MAGNET_LINKS = 2
self.CONFIG_VERSION = '5'
# Add Seed Ratio to Torznabs
if self.EXTRA_TORZNABS:
extra_torznabs = list(
itertools.izip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 3)
for i in range(3)])
)
new_torznabs = []
for torznab in extra_torznabs:
new_torznabs.extend([torznab[0], torznab[1], u'', torznab[2]])
if new_torznabs:
self.EXTRA_TORZNABS = new_torznabs
self.CONFIG_VERSION = '6'
+86 -83
View File
@@ -355,86 +355,89 @@ class ConfigApiTest(TestCase):
# TORZNABS
# TODO : here is copypaste from of NEZNABS tests. Make tests better, plz refactor them
#
@TestArgs(
('', []),
('ABCDEF', [('A', 'B', 'C'), ('D', 'E', 'F')]),
(['ABC', 'DEF'], []),
([1], []),
([1, 2], []),
([1, 2, 3], [(1, 2, 3)]),
([1, 2, 3, 'Aaa'], [(1, 2, 3)]),
([1, 2, 3, 'Aaa', 'Bbba'], [(1, 2, 3)]),
([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc', 'Ddddda'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
)
def test_get_extra_torznabs(self, conf_value, expected):
""" Config: get_extra_torznabs """
path = '/tmp/notexist'
# itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
# set up mocks:
# 'EXTRA_TORZNABS': (list, '', ''),
self.config_mock["Torznab"] = {"extra_torznabs": conf_value}
# call methods
c = headphones.config.Config(path)
res = c.get_extra_torznabs()
# assertions:
self.assertEqual(res, expected)
def test_clear_extra_torznabs(self):
""" Config: clear_extra_torznabs """
path = '/tmp/notexist'
random_value = -1292721
self.config_mock["Torznab"] = {"extra_torznabs": [1, 2, 3]}
self.config_mock["Torznab"] = {"do_not_touch": random_value}
# call methods
c = headphones.config.Config(path)
res = c.clear_extra_torznabs()
# assertions:
self.assertIsNone(res)
self.assertEqual(self.config_mock["Torznab"]["extra_torznabs"], [])
self.assertEqual(self.config_mock["Torznab"]["do_not_touch"], random_value)
@TestArgs(
([], [''], ['']),
([], 'ABCDEF', ['A', 'B', 'C', 'D', 'E', 'F']),
([1, 2, [False, True]], ['3', [0, 0]], [1, 2, [False, True], '3', [0, 0]]),
)
def test_add_extra_torznab(self, initial, added, expected):
""" Config: add_extra_torznab """
path = '/tmp/notexist'
self.config_mock["Torznab"] = {"extra_torznabs": initial}
# call methods
c = headphones.config.Config(path)
c.add_extra_torznab(added)
act = self.config_mock["Torznab"]["extra_torznabs"]
# assertions:
self.assertEqual(act, expected)
@TestArgs(
(None),
([]),
([1, 2, 3]),
([True]),
)
def test_add_extra_torznab_raise_on_none(self, initial):
""" Config: add_extra_torznab should raise on None adding"""
path = '/tmp/notexist'
self.config_mock["Torznab"] = {"extra_torznabs": initial}
# call methods
c = headphones.config.Config(path)
with self.assertRaises(TypeError):
c.add_extra_torznab(None)
pass
# TODO: Fix tests for following:
# CONFIG_VERSION == '5' each entry = 'host, api, enabled'
# CONFIG_VERSION > '5' each entry = 'host, api, seed ratio, enabled'
# @TestArgs(
# ('', []),
# ('ABCDEF', [('A', 'B', 'C'), ('D', 'E', 'F')]),
# (['ABC', 'DEF'], []),
# ([1], []),
# ([1, 2], []),
# ([1, 2, 3], [(1, 2, 3)]),
#
# ([1, 2, 3, 'Aaa'], [(1, 2, 3)]),
# ([1, 2, 3, 'Aaa', 'Bbba'], [(1, 2, 3)]),
# ([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
# ([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc', 'Ddddda'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
# )
# def test_get_extra_torznabs(self, conf_value, expected):
# """ Config: get_extra_torznabs """
# path = '/tmp/notexist'
#
# # itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
# # set up mocks:
# # 'EXTRA_TORZNABS': (list, '', ''),
# self.config_mock["Torznab"] = {"extra_torznabs": conf_value}
#
# # call methods
# c = headphones.config.Config(path)
# res = c.get_extra_torznabs()
#
# # assertions:
# self.assertEqual(res, expected)
#
# def test_clear_extra_torznabs(self):
# """ Config: clear_extra_torznabs """
# path = '/tmp/notexist'
#
# random_value = -1292721
# self.config_mock["Torznab"] = {"extra_torznabs": [1, 2, 3]}
# self.config_mock["Torznab"] = {"do_not_touch": random_value}
#
# # call methods
# c = headphones.config.Config(path)
# res = c.clear_extra_torznabs()
#
# # assertions:
# self.assertIsNone(res)
# self.assertEqual(self.config_mock["Torznab"]["extra_torznabs"], [])
# self.assertEqual(self.config_mock["Torznab"]["do_not_touch"], random_value)
#
# @TestArgs(
# ([], [''], ['']),
# ([], 'ABCDEF', ['A', 'B', 'C', 'D', 'E', 'F']),
#
# ([1, 2, [False, True]], ['3', [0, 0]], [1, 2, [False, True], '3', [0, 0]]),
# )
# def test_add_extra_torznab(self, initial, added, expected):
# """ Config: add_extra_torznab """
# path = '/tmp/notexist'
#
# self.config_mock["Torznab"] = {"extra_torznabs": initial}
#
# # call methods
# c = headphones.config.Config(path)
# c.add_extra_torznab(added)
# act = self.config_mock["Torznab"]["extra_torznabs"]
#
# # assertions:
# self.assertEqual(act, expected)
#
# @TestArgs(
# (None),
# ([]),
# ([1, 2, 3]),
# ([True]),
# )
# def test_add_extra_torznab_raise_on_none(self, initial):
# """ Config: add_extra_torznab should raise on None adding"""
# path = '/tmp/notexist'
#
# self.config_mock["Torznab"] = {"extra_torznabs": initial}
#
# # call methods
# c = headphones.config.Config(path)
# with self.assertRaises(TypeError):
# c.add_extra_torznab(None)
# pass
+90 -32
View File
@@ -19,6 +19,8 @@
from __future__ import with_statement
import time
import sqlite3
import os
@@ -45,36 +47,97 @@ class DBConnection:
self.connection = sqlite3.connect(dbFilename(filename), timeout=20)
# don't wait for the disk to finish writing
self.connection.execute("PRAGMA synchronous = OFF")
# journal disabled since we never do rollbacks
# default set to Write-Ahead Logging WAL
self.connection.execute("PRAGMA journal_mode = %s" % headphones.CONFIG.JOURNAL_MODE)
# 64mb of cache memory,probably need to make it user configurable
self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize() * 1024))
self.connection.row_factory = sqlite3.Row
def action(self, query, args=None):
def action(self, query, args=None, upsert_insert_qry=None):
if query is None:
return
sqlResult = None
attempts = 0
dberror = None
try:
with self.connection as c:
if args is None:
sqlResult = c.execute(query)
while attempts < 10:
try:
with self.connection as c:
# log that previous attempt was locked and we're trying again
if dberror:
if args is None:
logger.debug('SQL: Database was previously locked, trying again. Attempt number %i. Query: %s', attempts + 1, query)
else:
logger.debug('SQL: Database was previously locked, trying again. Attempt number %i. Query: %s. Args: %s', attempts + 1, query, args)
# debugging
# try:
# explain_query = 'EXPLAIN QUERY PLAN ' + query
# if not args:
# sql_results = c.execute(explain_query)
# else:
# sql_results = c.execute(explain_query, args)
# if not args:
# print(explain_query)
# else:
# print(explain_query + ' ' + str(args))
# explain_results = sql_results
# for row in explain_results:
# print row
# except Exception as e:
# print(e)
# Execute query
# time0 = time.time()
if args is None:
sqlResult = c.execute(query)
# logger.debug('SQL: ' + query)
else:
sqlResult = c.execute(query, args)
# logger.debug('SQL: %s. Args: %s', query, args)
# INSERT part of upsert query
if upsert_insert_qry:
sqlResult = c.execute(upsert_insert_qry, args)
# logger.debug('SQL: %s. Args: %s', upsert_insert_qry, args)
# debugging: loose test to log queries taking longer than 5 seconds
# seconds = time.time() - time0
# if seconds > 5:
# if args is None:
# logger.debug("SQL: Query ran for %s seconds: %s", seconds, query)
# else:
# logger.debug("SQL: Query ran for %s seconds: %s with args %s", seconds, query, args)
break
except sqlite3.OperationalError, e:
if "unable to open database file" in e.message or "database is locked" in e.message:
dberror = e
if args is None:
logger.debug('Database error: %s. Query: %s', e, query)
else:
logger.debug('Database error: %s. Query: %s. Args: %s', e, query, args)
attempts += 1
time.sleep(1)
else:
sqlResult = c.execute(query, args)
except sqlite3.OperationalError, e:
if "unable to open database file" in e.message or "database is locked" in e.message:
logger.warn('Database Error: %s', e)
else:
logger.error('Database error: %s', e)
logger.error('Database error: %s', e)
raise
except sqlite3.DatabaseError, e:
logger.error('Fatal Error executing %s :: %s', query, e)
raise
except sqlite3.DatabaseError, e:
logger.error('Fatal Error executing %s :: %s', query, e)
raise
# log if no results returned due to lock
if not sqlResult and attempts:
if args is None:
logger.warn('SQL: Query failed due to database error: %s. Query: %s', dberror, query)
else:
logger.warn('SQL: Query failed due to database error: %s. Query: %s. Args: %s', dberror, query, args)
return sqlResult
@@ -88,24 +151,19 @@ class DBConnection:
return sqlResults
def upsert(self, tableName, valueDict, keyDict):
"""
Transactions an Update or Insert to a table based on key.
If the table is not updated then the 'WHERE changes' will be 0 and the table inserted
"""
def genParams(myDict):
return [x + " = ?" for x in myDict.keys()]
changesBefore = self.connection.total_changes
update_query = "UPDATE " + tableName + " SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
update_query = "UPDATE " + tableName + " SET " + ", ".join(
genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
insert_query = ("INSERT INTO " + tableName + " (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + " SELECT " + ", ".join(
["?"] * len(valueDict.keys() + keyDict.keys())) + " WHERE changes()=0")
self.action(update_query, valueDict.values() + keyDict.values())
if self.connection.total_changes == changesBefore:
insert_query = (
"INSERT INTO " + tableName + " (" + ", ".join(
valueDict.keys() + keyDict.keys()) + ")" +
" VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")"
)
try:
self.action(insert_query, valueDict.values() + keyDict.values())
except sqlite3.IntegrityError:
logger.info('Queries failed: %s and %s', update_query, insert_query)
try:
self.action(update_query, valueDict.values() + keyDict.values(), upsert_insert_qry=insert_query)
except sqlite3.IntegrityError:
logger.info('Queries failed: %s and %s', update_query, insert_query)
+2 -2
View File
@@ -83,7 +83,7 @@ def addTorrent(link, data=None, name=None):
result = {}
retid = False
url_apollo = ['https://apollo.rip/', 'http://apollo.rip/']
url_orpheus = ['https://orpheus.network/', 'http://orpheus.network/']
url_waffles = ['https://waffles.ch/', 'http://waffles.ch/']
if link.lower().startswith('magnet:'):
@@ -97,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_apollo)):
if link.lower().startswith(tuple(url_orpheus)):
logger.debug('Deluge: Using different User-Agent for this site')
user_agent = 'Headphones'
# This method will make Deluge download the file
+46 -15
View File
@@ -197,9 +197,15 @@ def piratesize(size):
return size
def replace_all(text, dic, normalize=False):
def pattern_substitute(pattern, dic, normalize=False):
"""
Execute path rendering/substitution based on replacement dictionary
e.g. pattern = $Artist/$Album
dic = {Artist: 'My artist', Album: 'My album'}
returns My artist/My album
"""
from headphones import pathrender
if not text:
if not pattern:
return ''
if normalize:
@@ -216,7 +222,16 @@ def replace_all(text, dic, normalize=False):
j.decode(headphones.SYS_ENCODING, 'replace'))
new_dic[i] = j
dic = new_dic
return pathrender.render(text, dic)[0]
return pathrender.render(pattern, dic)[0]
def replace_all(text, dic):
if not text:
return ''
for i, j in dic.iteritems():
text = text.replace(i, j)
return text
def replace_illegal_chars(string, type="file"):
@@ -669,9 +684,9 @@ def preserve_torrent_directory(albumpath, forced=False, single=False):
try:
file_name = os.path.basename(os.path.normpath(albumpath))
if not single:
prefix = "headphones_" + file_name + "_"
prefix = "headphones_" + file_name + "_@hp@_"
else:
prefix = "headphones_" + os.path.splitext(file_name)[0] + "_"
prefix = "headphones_" + os.path.splitext(file_name)[0] + "_@hp@_"
new_folder = tempfile.mkdtemp(prefix=prefix, dir=tempdir)
except Exception as e:
logger.error("Cannot create temp directory: " + tempdir.decode(
@@ -810,9 +825,11 @@ def smartMove(src, dest, delete=True):
source_dir = os.path.dirname(src)
filename = os.path.basename(src)
source_path = os.path.join(source_dir, filename)
dest_path = os.path.join(dest, filename)
if os.path.isfile(os.path.join(dest, filename)):
logger.info('Destination file exists: %s', os.path.join(dest, filename))
if os.path.isfile(dest_path):
logger.info('Destination file exists: %s', dest_path)
title = os.path.splitext(filename)[0]
ext = os.path.splitext(filename)[1]
i = 1
@@ -830,15 +847,29 @@ def smartMove(src, dest, delete=True):
src.decode(headphones.SYS_ENCODING, 'replace'), e)
break
try:
if delete:
shutil.move(os.path.join(source_dir, filename), os.path.join(dest, filename))
else:
shutil.copy(os.path.join(source_dir, filename), os.path.join(dest, filename))
if delete:
try:
logger.info('Moving "%s" to "%s"', source_path, dest_path)
shutil.move(source_path, dest_path)
except Exception as e:
exists = os.path.exists(dest_path)
if exists and os.path.getsize(source_path) == os.path.getsize(dest_path):
logger.warn('Successfully moved file "%s", but something went wrong: %s',
filename.decode(headphones.SYS_ENCODING, 'replace'), e)
os.unlink(source_path)
else:
# remove faultly copied file
if exists:
os.unlink(dest_path)
raise
else:
try:
logger.info('Copying "%s" to "%s"', source_path, dest_path)
shutil.copy(source_path, dest_path)
return True
except Exception as e:
logger.warn('Error moving file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'),
e)
except Exception as e:
logger.warn('Error copying file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'),
e)
def walk_directory(basedir, followlinks=True):
+21 -10
View File
@@ -89,11 +89,13 @@ def artistlist_to_mbids(artistlist, forced=False):
addArtisttoDB(artistid)
# Just update the tracks if it does
else:
havetracks = len(
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) + len(
myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist]))
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, artistid])
# not sure this is correct and we're updating during scanning in librarysync
# else:
# havetracks = len(
# myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) + len(
# myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist]))
# myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, artistid])
# Delete it from the New Artists if the request came from there
if forced:
@@ -105,7 +107,7 @@ def artistlist_to_mbids(artistlist, forced=False):
try:
lastfm.getSimilar()
except Exception as e:
logger.warn('Failed to update arist information from Last.fm: %s' % e)
logger.warn('Failed to update artist information from Last.fm: %s' % e)
def addArtistIDListToDB(artistidlist):
@@ -309,7 +311,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
# This will be used later to build a hybrid release
fullreleaselist = []
# Search for releases within a release group
find_hybrid_releases = myDB.action("SELECT * from allalbums WHERE AlbumID=?",
find_hybrid_releases = myDB.select("SELECT * from allalbums WHERE AlbumID=?",
[rg['id']])
# Build the dictionary for the fullreleaselist
@@ -516,7 +518,10 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
logger.info(
u"[%s] Seeing if we need album art for %s" % (artist['artist_name'], rg['title']))
cache.getThumb(AlbumID=rg['id'])
try:
cache.getThumb(AlbumID=rg['id'])
except Exception as e:
logger.error("Error getting album art: %s", e)
# Start a search for the album if it's new, hasn't been marked as
# downloaded and autowant_all is selected. This search is deferred,
@@ -532,10 +537,16 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
finalize_update(artistid, artist['artist_name'], errors)
logger.info(u"Seeing if we need album art for: %s" % artist['artist_name'])
cache.getThumb(ArtistID=artistid)
try:
cache.getThumb(ArtistID=artistid)
except Exception as e:
logger.error("Error getting album art: %s", e)
logger.info(u"Fetching Metacritic reviews for: %s" % artist['artist_name'])
metacritic.update(artistid, artist['artist_name'], artist['releasegroups'])
try:
metacritic.update(artistid, artist['artist_name'], artist['releasegroups'])
except Exception as e:
logger.error("Error getting Metacritic reviews: %s", e)
if errors:
logger.info(
+248 -153
View File
@@ -50,27 +50,43 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
logger.info('Scanning music directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
if not append:
# Clean up bad filepaths
tracks = myDB.select(
'SELECT Location from alltracks WHERE Location IS NOT NULL UNION SELECT Location from tracks WHERE Location IS NOT NULL')
# Clean up bad filepaths. Queries can take some time, ensure all results are loaded before processing
if ArtistID:
tracks = myDB.action(
'SELECT Location FROM alltracks WHERE ArtistID = ? AND Location IS NOT NULL UNION SELECT Location FROM tracks WHERE ArtistID = ? AND Location '
'IS NOT NULL',
[ArtistID, ArtistID])
else:
tracks = myDB.action(
'SELECT Location FROM alltracks WHERE Location IS NOT NULL UNION SELECT Location FROM tracks WHERE Location IS NOT NULL')
locations = []
for track in tracks:
encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING, 'replace')
locations.append(track['Location'])
for location in locations:
encoded_track_string = location.encode(headphones.SYS_ENCODING, 'replace')
if not os.path.isfile(encoded_track_string):
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
[None, None, None, track['Location']])
[None, None, None, location])
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
[None, None, None, track['Location']])
[None, None, None, location])
del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName from have')
if ArtistName:
del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName FROM have WHERE ArtistName = ? COLLATE NOCASE', [ArtistName])
else:
del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName FROM have')
locations = []
for track in del_have_tracks:
encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING, 'replace')
locations.append([track['Location'], track['ArtistName']])
for location in locations:
encoded_track_string = location[0].encode(headphones.SYS_ENCODING, 'replace')
if not os.path.isfile(encoded_track_string):
if track['ArtistName']:
if location[1]:
# Make sure deleted files get accounted for when updating artist track counts
new_artists.append(track['ArtistName'])
myDB.action('DELETE FROM have WHERE Location=?', [track['Location']])
new_artists.append(location[1])
myDB.action('DELETE FROM have WHERE Location=?', [location[0]])
logger.info(
'File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(
headphones.SYS_ENCODING, 'replace'))
@@ -216,6 +232,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
song_count = 0
latest_artist = []
last_completion_percentage = 0
prev_artist_name = None
artistid = None
for song in song_list:
@@ -237,87 +255,72 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
# ARE NEVER FOUND, AND THE OTHER CLAUSES WERE NEVER HIT. FURTHERMORE, OTHER MATCHING FUNCTIONS IN THIS PROGRAM
# (IMPORTER.PY, MB.PY) SIMPLY DO A [ARTIST/ALBUM/TRACK] OR CLEANNAME MATCH, SO IT'S ALL CONSISTENT.
if song['ArtistName'] and song['AlbumTitle'] and song['TrackTitle']:
albumid = None
track = myDB.action(
'SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
[song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
have_updated = False
if track:
controlValueDict = {'ArtistName': track['ArtistName'],
'AlbumTitle': track['AlbumTitle'],
'TrackTitle': track['TrackTitle']}
newValueDict = {'Location': song['Location'],
'BitRate': song['BitRate'],
'Format': song['Format']}
myDB.upsert("tracks", newValueDict, controlValueDict)
if song['ArtistName'] and song['CleanName']:
artist_name = song['ArtistName']
clean_name = song['CleanName']
controlValueDict2 = {'Location': song['Location']}
newValueDict2 = {'Matched': track['AlbumID']}
myDB.upsert("have", newValueDict2, controlValueDict2)
have_updated = True
else:
track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?',
[song['CleanName']]).fetchone()
# Only update if artist is in the db
if artist_name != prev_artist_name:
prev_artist_name = artist_name
artistid = None
artist_lookup = "\"" + artist_name.replace("\"", "\"\"") + "\""
try:
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM artists WHERE ArtistName LIKE ' + artist_lookup + '')
except:
dbartist = None
if not dbartist:
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM tracks WHERE CleanName = ?', [clean_name])
if not dbartist:
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM alltracks WHERE CleanName = ?', [clean_name])
if not dbartist:
clean_artist = helpers.clean_name(artist_name)
if clean_artist:
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM tracks WHERE CleanName >= ? and CleanName < ?',
[clean_artist, clean_artist + '{'])
if not dbartist:
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM alltracks WHERE CleanName >= ? and CleanName < ?',
[clean_artist, clean_artist + '{'])
if dbartist:
artistid = dbartist[0][0]
if artistid:
# This was previously using Artist, Album, Title with a SELECT LIKE ? and was not using an index
# (Possible issue: https://stackoverflow.com/questions/37845854/python-sqlite3-not-using-index-with-like)
# Now selects/updates using CleanName index (may have to revert if not working)
# matching on CleanName should be enough, ensure it's the same artist just in case
# Update tracks
track = myDB.action('SELECT AlbumID, ArtistName FROM tracks WHERE CleanName = ? AND ArtistID = ?', [clean_name, artistid]).fetchone()
if track:
controlValueDict = {'CleanName': track['CleanName']}
newValueDict = {'Location': song['Location'],
'BitRate': song['BitRate'],
'Format': song['Format']}
myDB.upsert("tracks", newValueDict, controlValueDict)
albumid = track['AlbumID']
myDB.action(
'UPDATE tracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ? AND ArtistID = ?',
[song['Location'], song['BitRate'], song['Format'], clean_name, artistid])
controlValueDict2 = {'Location': song['Location']}
newValueDict2 = {'Matched': track['AlbumID']}
myDB.upsert("have", newValueDict2, controlValueDict2)
have_updated = True
else:
controlValueDict2 = {'Location': song['Location']}
newValueDict2 = {'Matched': "Failed"}
myDB.upsert("have", newValueDict2, controlValueDict2)
have_updated = True
alltrack = myDB.action(
'SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
[song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
if alltrack:
controlValueDict = {'ArtistName': alltrack['ArtistName'],
'AlbumTitle': alltrack['AlbumTitle'],
'TrackTitle': alltrack['TrackTitle']}
newValueDict = {'Location': song['Location'],
'BitRate': song['BitRate'],
'Format': song['Format']}
myDB.upsert("alltracks", newValueDict, controlValueDict)
controlValueDict2 = {'Location': song['Location']}
newValueDict2 = {'Matched': alltrack['AlbumID']}
myDB.upsert("have", newValueDict2, controlValueDict2)
else:
alltrack = myDB.action(
'SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?',
[song['CleanName']]).fetchone()
# Update alltracks
alltrack = myDB.action('SELECT AlbumID, ArtistName FROM alltracks WHERE CleanName = ? AND ArtistID = ?', [clean_name, artistid]).fetchone()
if alltrack:
controlValueDict = {'CleanName': alltrack['CleanName']}
newValueDict = {'Location': song['Location'],
'BitRate': song['BitRate'],
'Format': song['Format']}
myDB.upsert("alltracks", newValueDict, controlValueDict)
controlValueDict2 = {'Location': song['Location']}
newValueDict2 = {'Matched': alltrack['AlbumID']}
myDB.upsert("have", newValueDict2, controlValueDict2)
else:
# alltracks may not exist if adding album manually, have should only be set to failed if not already updated in tracks
if not have_updated:
controlValueDict2 = {'Location': song['Location']}
newValueDict2 = {'Matched': "Failed"}
myDB.upsert("have", newValueDict2, controlValueDict2)
albumid = alltrack['AlbumID']
myDB.action(
'UPDATE alltracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ? AND ArtistID = ?',
[song['Location'], song['BitRate'], song['Format'], clean_name, artistid])
# Update have
controlValueDict2 = {'Location': song['Location']}
if albumid:
newValueDict2 = {'Matched': albumid}
else:
controlValueDict2 = {'Location': song['Location']}
newValueDict2 = {'Matched': "Failed"}
myDB.upsert("have", newValueDict2, controlValueDict2)
myDB.upsert("have", newValueDict2, controlValueDict2)
# myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']])
# myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']])
logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING,
'replace'))
@@ -327,49 +330,110 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
# Clean up the new artist list
unique_artists = {}.fromkeys(new_artists).keys()
current_artists = myDB.select('SELECT ArtistName, ArtistID from artists')
# There was a bug where artists with special characters (-,') would show up in new artists.
artist_list = [
x for x in unique_artists
if helpers.clean_name(x).lower() not in [
helpers.clean_name(y[0]).lower()
for y in current_artists
]
]
artists_checked = [
x for x in unique_artists
if helpers.clean_name(x).lower() in [
helpers.clean_name(y[0]).lower()
for y in current_artists
]
]
# # Don't think we need to do this, check the db instead below
#
# # artist scan
# if ArtistName:
# current_artists = [[ArtistName]]
# # directory scan
# else:
# current_artists = myDB.select('SELECT ArtistName, ArtistID FROM artists WHERE ArtistName IS NOT NULL')
#
# # There was a bug where artists with special characters (-,') would show up in new artists.
#
# # artist_list = scanned artists not in the db
# artist_list = [
# x for x in unique_artists
# if helpers.clean_name(x).lower() not in [
# helpers.clean_name(y[0]).lower()
# for y in current_artists
# ]
# ]
#
# # artists_checked = scanned artists that exist in the db
# artists_checked = [
# x for x in unique_artists
# if helpers.clean_name(x).lower() in [
# helpers.clean_name(y[0]).lower()
# for y in current_artists
# ]
# ]
# Update track counts
for artist in artists_checked:
# Have tracks are selected from tracks table and not all tracks because of duplicates
# We update the track count upon an album switch to compliment this
havetracks = (
len(myDB.select(
'SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL',
[artist])) + len(myDB.select(
'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
[artist]))
)
# Note: some people complain about having "artist have tracks" > # of tracks total in artist official releases
# (can fix by getting rid of second len statement)
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistName=?', [havetracks, artist])
new_artist_list = []
logger.info('Found %i new artists' % len(artist_list))
for artist in unique_artists:
if artist_list:
if not artist:
continue
logger.info('Processing artist: %s' % artist)
# check if artist is already in the db
artist_lookup = "\"" + artist.replace("\"", "\"\"") + "\""
try:
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM artists WHERE ArtistName LIKE ' + artist_lookup + '')
except:
dbartist = None
if not dbartist:
clean_artist = helpers.clean_name(artist)
if clean_artist:
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM tracks WHERE CleanName >= ? and CleanName < ?',
[clean_artist, clean_artist + '{'])
if not dbartist:
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM alltracks WHERE CleanName >= ? and CleanName < ?',
[clean_artist, clean_artist + '{'])
# new artist not in db, add to list
if not dbartist:
new_artist_list.append(artist)
else:
# artist in db, update have track counts
artistid = dbartist[0][0]
# Have tracks are selected from tracks table and not all tracks because of duplicates
# We update the track count upon an album switch to compliment this
# havetracks = (
# len(myDB.select(
# 'SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL',
# [artist])) + len(myDB.select(
# 'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
# [artist]))
# )
try:
havetracks = (
len(myDB.select(
'SELECT ArtistID From tracks WHERE ArtistID = ? AND Location IS NOT NULL',
[artistid])) + len(myDB.select(
'SELECT ArtistName FROM have WHERE ArtistName LIKE ' + artist_lookup + ' AND Matched = "Failed"'))
)
except Exception as e:
logger.warn('Error updating counts for artist: %s: %s' % (artist, e))
# Note: some people complain about having "artist have tracks" > # of tracks total in artist official releases
# (can fix by getting rid of second len statement)
if havetracks:
myDB.action('UPDATE artists SET HaveTracks = ? WHERE ArtistID = ?', [havetracks, artistid])
# Update albums to downloaded
update_album_status(ArtistID=artistid)
logger.info('Found %i new artists' % len(new_artist_list))
# Add scanned artists not in the db
if new_artist_list:
if headphones.CONFIG.AUTO_ADD_ARTISTS:
logger.info('Importing %i new artists' % len(artist_list))
importer.artistlist_to_mbids(artist_list)
logger.info('Importing %i new artists' % len(new_artist_list))
importer.artistlist_to_mbids(new_artist_list)
else:
logger.info('To add these artists, go to Manage->Manage New Artists')
# myDB.action('DELETE from newartists')
for artist in artist_list:
for artist in new_artist_list:
myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist])
if headphones.CONFIG.DETECT_BITRATE and bitrates:
@@ -379,50 +443,78 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
# If we're appending a new album to the database, update the artists total track counts
logger.info('Updating artist track counts')
havetracks = len(
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
[ArtistID])) + len(myDB.select(
'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
[ArtistName]))
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
artist_lookup = "\"" + ArtistName.replace("\"", "\"\"") + "\""
try:
havetracks = len(
myDB.select('SELECT ArtistID FROM tracks WHERE ArtistID = ? AND Location IS NOT NULL',
[ArtistID])) + len(myDB.select(
'SELECT ArtistName FROM have WHERE ArtistName LIKE ' + artist_lookup + ' AND Matched = "Failed"'))
except Exception as e:
logger.warn('Error updating counts for artist: %s: %s' % (ArtistName, e))
if not append:
update_album_status()
if havetracks:
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
# Moved above to call for each artist
# if not append:
# update_album_status()
if not append and not artistScan:
lastfm.getSimilar()
logger.info('Library scan complete')
if ArtistName:
logger.info('Scanning complete for artist: %s', ArtistName)
else:
logger.info('Library scan complete')
# ADDED THIS SECTION TO MARK ALBUMS AS DOWNLOADED IF ARTISTS ARE ADDED EN MASSE BEFORE LIBRARY IS SCANNED
# Think the above comment relates to calling from Manage Unmatched
def update_album_status(AlbumID=None):
# This used to select and update all albums and would clobber the db, changed to run by ArtistID.
def update_album_status(AlbumID=None, ArtistID=None):
myDB = db.DBConnection()
logger.info('Counting matched tracks to mark albums as skipped/downloaded')
# logger.info('Counting matched tracks to mark albums as skipped/downloaded')
if AlbumID:
album_status_updater = myDB.action(
'SELECT AlbumID, AlbumTitle, Status from albums WHERE AlbumID=?', [AlbumID])
'SELECT'
' a.AlbumID, a.ArtistName, a.AlbumTitle, a.Status, AVG(t.Location IS NOT NULL) * 100 AS album_completion '
'FROM'
' albums AS a '
'JOIN tracks AS t ON t.AlbumID = a.AlbumID '
'WHERE'
' a.AlbumID = ? AND a.Status != "Downloaded" '
'GROUP BY'
' a.AlbumID '
'HAVING'
' AVG(t.Location IS NOT NULL) * 100 >= ?',
[AlbumID, headphones.CONFIG.ALBUM_COMPLETION_PCT]
)
else:
album_status_updater = myDB.action('SELECT AlbumID, AlbumTitle, Status from albums')
for album in album_status_updater:
track_counter = myDB.action('SELECT Location from tracks where AlbumID=?',
[album['AlbumID']])
total_tracks = 0
have_tracks = 0
for track in track_counter:
total_tracks += 1
if track['Location']:
have_tracks += 1
if total_tracks != 0:
album_completion = float(float(have_tracks) / float(total_tracks)) * 100
else:
album_completion = 0
logger.info('Album %s does not have any tracks in database' % album['AlbumTitle'])
album_status_updater = myDB.action(
'SELECT'
' a.AlbumID, a.ArtistID, a.ArtistName, a.AlbumTitle, a.Status, AVG(t.Location IS NOT NULL) * 100 AS album_completion '
'FROM'
' albums AS a '
'JOIN tracks AS t ON t.AlbumID = a.AlbumID '
'WHERE'
' a.ArtistID = ? AND a.Status != "Downloaded" '
'GROUP BY'
' a.AlbumID '
'HAVING'
' AVG(t.Location IS NOT NULL) * 100 >= ?',
[ArtistID, headphones.CONFIG.ALBUM_COMPLETION_PCT]
)
if album_completion >= headphones.CONFIG.ALBUM_COMPLETION_PCT:
new_album_status = "Downloaded"
new_album_status = "Downloaded"
albums = []
for album in album_status_updater:
albums.append([album['AlbumID'], album['ArtistName'], album['AlbumTitle']])
for album in albums:
# I don't think we want to change Downloaded->Skipped.....
# I think we can only automatically change Skipped->Downloaded when updating
@@ -433,10 +525,13 @@ def update_album_status(AlbumID=None):
# new_album_status = "Skipped"
# else:
# new_album_status = album['Status']
else:
new_album_status = album['Status']
# else:
# new_album_status = album['Status']
#
# myDB.upsert("albums", {'Status': new_album_status}, {'AlbumID': album['AlbumID']})
# if new_album_status != album['Status']:
# logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status))
# logger.info('Album status update complete')
myDB.upsert("albums", {'Status': new_album_status}, {'AlbumID': album['AlbumID']})
if new_album_status != album['Status']:
logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status))
logger.info('Album status update complete')
myDB.action('UPDATE albums SET Status = ? WHERE AlbumID = ?', [new_album_status, album[0]])
logger.info('Album: %s - %s. Status updated to %s' % (album[1], album[2], new_album_status))
+24
View File
@@ -793,3 +793,27 @@ def getArtistForReleaseGroup(rgid):
return False
else:
return releaseGroup['artist-credit'][0]['artist']['name']
def getArtistRelationships(artistid):
"""
Returns list of relationship urls. e.g. Discogs, Wikipedia etc.
"""
urls = []
artist = None
try:
with mb_lock:
info = musicbrainzngs.get_artist_by_id(artistid, includes='url-rels')
except musicbrainzngs.WebServiceError as e:
logger.warn(
'Attempt to query MusicBrainz for %s failed "%s"' % (artistid, str(e)))
mb_lock.snooze(5)
if 'artist' in info:
artist = info['artist']
if 'url-relation-list' in artist:
for l in artist['url-relation-list']:
urls.append({
'type': l['type'],
'url': l['target']
})
return urls
+1 -1
View File
@@ -37,7 +37,7 @@ def update(artistid, artist_name, release_groups):
url = "http://www.metacritic.com/person/" + mc_artist_name + "?filter-options=music&sort_options=date&num_items=100"
res = request.request_soup(url, headers=headers)
res = request.request_soup(url, headers=headers, whitelist_status_code=404)
rows = None
+13 -7
View File
@@ -289,13 +289,19 @@ def album_metadata(path, release, common_tags):
first_char = sort_name[0]
orig_folder = u''
for r, d, f in os.walk(path):
try:
orig_folder = os.path.basename(
os.path.normpath(r).decode(headphones.SYS_ENCODING, 'replace'))
break
except:
pass
# Get from temp path
if "_@hp@_" in path:
orig_folder = path.rsplit("headphones_", 1)[1].split("_@hp@_")[0]
orig_folder = orig_folder.decode(headphones.SYS_ENCODING, 'replace')
else:
for r, d, f in os.walk(path):
try:
orig_folder = os.path.basename(
os.path.normpath(r).decode(headphones.SYS_ENCODING, 'replace'))
break
except:
pass
override_values = {
Vars.ARTIST: artist,
+2 -2
View File
@@ -168,8 +168,8 @@ class MetadataTest(TestCase):
if not _h.SYS_ENCODING:
_h.SYS_ENCODING = 'UTF-8'
res = _hp.replace_all(
res = _hp.pattern_substitute(
"/music/$First/$Artist/$Artist - $Album{ [$Year]}", md, True)
self.assertEqual(res, u"/music/A/artist/artist - Album",
"check correct rendering of None via replace_all()")
"check correct rendering of None via pattern_substitute()")
+81 -24
View File
@@ -23,6 +23,7 @@ import email.utils
from httplib import HTTPSConnection
from urlparse import parse_qsl
import urllib2
import requests as requests
import os.path
from headphones import logger, helpers, common, request
@@ -365,6 +366,41 @@ class Plex(object):
def update(self):
# Get token from user credentials
if not self.token:
loginpage = 'https://plex.tv/users/sign_in.json'
post_params = {
'user[login]': self.username,
'user[password]': self.password
}
headers = {
'X-Plex-Device-Name': 'Headphones',
'X-Plex-Product': 'Headphones',
'X-Plex-Client-Identifier': common.USER_AGENT,
'X-Plex-Version': ''
}
logger.info("Getting plex.tv credentials for user %s", self.username)
try:
r = requests.post(loginpage, data=post_params, headers=headers)
r.raise_for_status()
except requests.RequestException as e:
logger.error("Error getting plex.tv credentials, check settings: %s", e)
return False
try:
data = r.json()
except ValueError as e:
logger.error("Error getting plex.tv credentials: %s", e)
return False
try:
self.token = data['user']['authentication_token']
except KeyError as e:
logger.error("Error getting plex.tv credentials: %s", e)
return False
# 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
@@ -379,20 +415,28 @@ class Plex(object):
else:
params = False
r = request.request_minidom(url, params=params)
try:
r = request.request_minidom(url, params=params)
if not r:
logger.warn("Error getting Plex Media Server details, check settings (possibly incorrect token)")
return False
sections = r.getElementsByTagName('Directory')
sections = r.getElementsByTagName('Directory')
if not sections:
logger.info(u"Plex Media Server not running on: " + host)
if not sections:
logger.info(u"Plex Media Server not running on: " + host)
return False
for s in sections:
if s.getAttribute('type') == "artist":
url = "%s/library/sections/%s/refresh" % (
host, s.getAttribute('key'))
request.request_response(url, params=params)
except Exception as e:
logger.error("Error getting Plex Media Server details: %s" % e)
return False
for s in sections:
if s.getAttribute('type') == "artist":
url = "%s/library/sections/%s/refresh" % (
host, s.getAttribute('key'))
request.request_response(url, params=params)
def notify(self, artist, album, albumartpath):
hosts = [x.strip() for x in self.client_hosts.split(',')]
@@ -817,7 +861,7 @@ class OSX_NOTIFY(object):
self.AppKit = __import__("AppKit")
except:
logger.warn('OS X Notification: Cannot import objc or AppKit')
return False
pass
def swizzle(self, cls, SEL, func):
old_IMP = getattr(cls, SEL, None)
@@ -941,6 +985,7 @@ class Email(object):
message['From'] = email.utils.formataddr(
('Headphones', headphones.CONFIG.EMAIL_FROM))
message['To'] = headphones.CONFIG.EMAIL_TO
message['Date'] = email.utils.formatdate(localtime=True)
try:
if headphones.CONFIG.EMAIL_SSL:
@@ -972,7 +1017,7 @@ class Email(object):
class TELEGRAM(object):
def notify(self, message, status):
def notify(self, message, status, rgid=None, image=None):
if not headphones.CONFIG.TELEGRAM_ENABLED:
return
@@ -985,22 +1030,34 @@ class TELEGRAM(object):
userid = headphones.CONFIG.TELEGRAM_USERID
# Construct message
payload = {'chat_id': userid, 'text': status + ': ' + message}
message = '\n\n' + message
# Send message to user using Telegram's Bot API
try:
response = requests.post(TELEGRAM_API % (token, "sendMessage"),
data=payload)
except Exception, e:
logger.info(u'Telegram notify failed: ' + str(e))
# MusicBrainz link
if rgid:
message += '\n\n <a href="http://musicbrainz.org/' \
'release-group/%s">MusicBrainz</a>' % rgid
# Send image
response = None
if image:
image_file = {'photo': (image, open(image, "rb"))}
payload = {'chat_id': userid, 'parse_mode': "HTML", 'caption': status + message}
try:
response = requests.post(TELEGRAM_API % (token, "sendPhoto"), data=payload, files=image_file)
except Exception, e:
logger.info(u'Telegram notify failed: ' + str(e))
# Sent text
else:
payload = {'chat_id': userid, 'parse_mode': "HTML", 'text': status + message}
try:
response = requests.post(TELEGRAM_API % (token, "sendMessage"), data=payload)
except Exception, e:
logger.info(u'Telegram notify failed: ' + str(e))
# Error logging
sent_successfuly = True
if not response.status_code == 200:
logger.info(
u'Could not send notification to TelegramBot '
u'(token=%s). Response: [%s]',
(token, response.text))
if response and not response.status_code == 200:
logger.info("Could not send notification to TelegramBot (token=%s). Response: [%s]", token, response.text)
sent_successfuly = False
logger.info(u"Telegram notifications sent.")
+9 -5
View File
@@ -183,13 +183,17 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
controlValueDict = {"TrackID": track['id'],
"AlbumID": albumid}
clean_name = helpers.clean_name(
release_dict['artist_name'] + ' ' + release_dict['title'] + ' ' + track['title'])
newValueDict = {"ArtistID": release_dict['artist_id'],
"ArtistName": release_dict['artist_name'],
"AlbumTitle": release_dict['title'],
"AlbumASIN": release_dict['asin'],
"TrackTitle": track['title'],
"TrackDuration": track['duration'],
"TrackNumber": track['number']
"TrackNumber": track['number'],
"CleanName": clean_name
}
myDB.upsert("tracks", newValueDict, controlValueDict)
@@ -582,7 +586,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
if headphones.CONFIG.TELEGRAM_ENABLED:
logger.info(u"Telegram request")
telegram = notifiers.TELEGRAM()
telegram.notify(pushmessage, statusmessage)
telegram.notify(statusmessage, pushmessage)
if headphones.CONFIG.TWITTER_ENABLED:
logger.info(u"Sending Twitter notification")
@@ -656,7 +660,7 @@ def addAlbumArt(artwork, albumpath, release, metadata_dict):
if artwork[:4] == '\x89PNG':
ext = ".png"
album_art_name = helpers.replace_all(
album_art_name = helpers.pattern_substitute(
headphones.CONFIG.ALBUM_ART_FORMAT.strip(), md) + ext
album_art_name = helpers.replace_illegal_chars(album_art_name).encode(
@@ -712,7 +716,7 @@ def moveFiles(albumpath, release, metadata_dict):
logger.info("Moving files: %s" % albumpath)
md = metadata.album_metadata(albumpath, release, metadata_dict)
folder = helpers.replace_all(
folder = helpers.pattern_substitute(
headphones.CONFIG.FOLDER_FORMAT.strip(), md, normalize=True)
if headphones.CONFIG.FILE_UNDERSCORES:
@@ -1091,7 +1095,7 @@ def renameFiles(albumpath, downloaded_track_list, release):
title = md[metadata.Vars.TITLE]
new_file_name = helpers.cleanTitle(title) + ext
else:
new_file_name = helpers.replace_all(
new_file_name = helpers.pattern_substitute(
headphones.CONFIG.FILE_FORMAT.strip(), md
).replace('/', '_') + ext
Regular → Executable
+84 -20
View File
@@ -28,6 +28,8 @@ import headphones
from headphones import logger
from collections import namedtuple
from qbittorrentv2 import Client
class qbittorrentclient(object):
@@ -49,9 +51,20 @@ class qbittorrentclient(object):
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)
# Try new v2 api
try:
self.qb = Client(self.base_url)
login_text = self.qb.login(self.username, self.password)
if login_text:
logger.warning("Could not login to qBittorrent v2 api, check credentials: %s", login_text)
self.version = 2
except Exception as e:
logger.warning("Error with qBittorrent v2 api, check settings or update, will try v1: %s" % e)
self.cookiejar = cookielib.CookieJar()
self.opener = self._make_opener()
self._get_sid(self.base_url, self.username, self.password)
self.version = 1
def _make_opener(self):
# create opener with cookie handler to carry QBitTorrent SID cookie
@@ -161,19 +174,32 @@ class qbittorrentclient(object):
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)
if qbclient.version == 2:
torrentlist = qbclient.qb.torrents(hashes=hash.lower())
else:
status, torrentlist = qbclient._get_list()
for torrent in torrentlist:
if torrent['hash'].lower() == hash.lower():
if torrent['ratio'] >= torrent['ratio_limit'] and torrent['ratio_limit'] >= 0:
if qbclient.version == 2:
if remove_data:
logger.info(
'%s has finished seeding, removing torrent and data. '
'Ratio: %s, Ratio Limit: %s' % (torrent['name'], torrent['ratio'], torrent['ratio_limit']))
qbclient.qb.delete_permanently(hash)
else:
logger.info('%s has finished seeding, removing torrent' % torrent['name'])
qbclient.qb.delete(hash)
else:
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'])
logger.info(
'%s has not finished seeding yet, torrent will not be removed, will try again on next run. '
'Ratio: %s, Ratio Limit: %s' % (torrent['name'], torrent['ratio'], torrent['ratio_limit']))
return False
return False
@@ -182,20 +208,27 @@ 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
if qbclient.version == 2:
return qbclient.qb.download_from_link(link, savepath=headphones.CONFIG.DOWNLOAD_TORRENT_DIR,
category=headphones.CONFIG.QBITTORRENT_LABEL)
else:
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')
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)
if qbclient.version == 2:
return qbclient.qb.download_from_file(data, savepath=headphones.CONFIG.DOWNLOAD_TORRENT_DIR,
category=headphones.CONFIG.QBITTORRENT_LABEL)
else:
files = {'torrents': {'filename': '', 'content': data}}
return qbclient._command('command/upload', filelist=files)
def getName(hash):
@@ -206,7 +239,10 @@ def getName(hash):
tries = 1
while tries <= 6:
time.sleep(10)
status, torrentlist = qbclient._get_list()
if qbclient.version == 2:
torrentlist = qbclient.qb.torrents(hashes=hash.lower())
else:
status, torrentlist = qbclient._get_list()
for torrent in torrentlist:
if torrent['hash'].lower() == hash.lower():
return torrent['name']
@@ -224,13 +260,17 @@ def getFolder(hash):
qbclient = qbittorrentclient()
try:
status, torrent_files = qbclient.getfiles(hash.lower())
if qbclient.version == 2:
torrent_files = qbclient.qb.get_torrent_files(hash.lower())
else:
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]
torrent_folder = torrent_folder.split(os.sep)[0]
single_file = False
except:
torrent_folder = None
@@ -239,6 +279,30 @@ def getFolder(hash):
return torrent_folder, single_file
def setSeedRatio(hash, ratio):
logger.debug('setSeedRatio(%s)' % hash)
qbclient = qbittorrentclient()
if qbclient.version == 2:
ratio = -1 if ratio == 0 else ratio
return qbclient.qb.set_share_ratio(hash.lower(), ratio)
else:
logger.warn('setSeedRatio only available with qBittorrent v2 api')
return
def apiVersion2():
logger.debug('getApiVersion')
qbclient = qbittorrentclient()
if qbclient.version == 2:
return True
else:
return False
_BOUNDARY_CHARS = string.digits + string.ascii_letters
+1 -1
View File
@@ -93,7 +93,7 @@ def request_response(url, method="get", auto_raise=True,
if kwargs["verify"]:
logger.error(
"Unable to connect to remote host because of a SSL error. "
"It is likely that your system cannot verify the validity"
"It is likely that your system cannot verify the validity "
"of the certificate. The remote certificate is either "
"self-signed, or the remote server uses SNI. See the wiki for "
"more information on this topic.")
+3 -2
View File
@@ -87,10 +87,10 @@ class Rutracker(object):
searchterm = searchterm + year
if format == 'lossless':
format = '+lossless'
format = '+lossless||TR24'
self.maxsize = 10000000000
elif format == 'lossless+mp3':
format = '+lossless||mp3||aac'
format = '+lossless||TR24||mp3||aac'
self.maxsize = 10000000000
else:
format = '+mp3||aac'
@@ -104,6 +104,7 @@ class Rutracker(object):
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
def search(self, searchurl):
+96 -307
View File
@@ -14,7 +14,6 @@
# 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
@@ -25,7 +24,6 @@ import datetime
import subprocess
import unicodedata
import urlparse
from json import loads
import os
import re
@@ -41,14 +39,13 @@ 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',
'http://itorrents.org/torrent/%s.torrent',
'https://cache.torrentgalaxy.org/get/%s',
'https://www.seedpeer.me/torrent/%s'
]
# Persistent Apollo.rip API object
apolloobj = None
# Persistent Orpheus.network API object
orpheusobj = None
ruobj = None
# Persistent RED API object
redobj = None
@@ -156,16 +153,14 @@ def calculate_torrent_hash(link, data=None):
def get_seed_ratio(provider):
"""
Return the seed ratio for the specified provider, if applicable. Defaults to
Return the seed ratio for the specified provider if applicable. Defaults to
None in case of an error.
"""
if provider == 'rutracker.org':
seed_ratio = headphones.CONFIG.RUTRACKER_RATIO
elif provider == 'Kick Ass Torrents':
seed_ratio = headphones.CONFIG.KAT_RATIO
elif provider == 'Apollo.rip':
seed_ratio = headphones.CONFIG.APOLLO_RATIO
elif provider == 'Orpheus.network':
seed_ratio = headphones.CONFIG.ORPHEUS_RATIO
elif provider == 'Redacted':
seed_ratio = headphones.CONFIG.REDACTED_RATIO
elif provider == 'The Pirate Bay':
@@ -174,10 +169,15 @@ def get_seed_ratio(provider):
seed_ratio = headphones.CONFIG.OLDPIRATEBAY_RATIO
elif provider == 'Waffles.ch':
seed_ratio = headphones.CONFIG.WAFFLES_RATIO
elif provider == 'Mininova':
seed_ratio = headphones.CONFIG.MININOVA_RATIO
elif provider == 'Strike':
seed_ratio = headphones.CONFIG.STRIKE_RATIO
elif provider.startswith("Jackett_"):
provider = provider.split("Jackett_")[1]
if provider in headphones.CONFIG.TORZNAB_HOST:
seed_ratio = headphones.CONFIG.TORZNAB_RATIO
else:
for torznab in headphones.CONFIG.get_extra_torznabs():
if provider in torznab[0]:
seed_ratio = torznab[2]
break
else:
seed_ratio = None
@@ -272,16 +272,12 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
headphones.CONFIG.NZBGET_HOST)
TORRENT_PROVIDERS = (headphones.CONFIG.TORZNAB or
headphones.CONFIG.KAT or
headphones.CONFIG.PIRATEBAY or
headphones.CONFIG.OLDPIRATEBAY or
headphones.CONFIG.MININOVA or
headphones.CONFIG.WAFFLES or
headphones.CONFIG.RUTRACKER or
headphones.CONFIG.APOLLO or
headphones.CONFIG.REDACTED or
headphones.CONFIG.STRIKE or
headphones.CONFIG.TQUATTRECENTONZE)
headphones.CONFIG.ORPHEUS or
headphones.CONFIG.REDACTED)
results = []
myDB = db.DBConnection()
@@ -440,6 +436,7 @@ def sort_search_results(resultlist, album, new, albumlength):
resultlist = temp_list
# if headphones.CONFIG.PREFERRED_QUALITY == 2 and headphones.CONFIG.PREFERRED_BITRATE and result[3] != 'Orpheus.network':
if headphones.CONFIG.PREFERRED_QUALITY == 2 and headphones.CONFIG.PREFERRED_BITRATE:
try:
@@ -487,6 +484,10 @@ def sort_search_results(resultlist, album, new, albumlength):
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True)
# keep number of seeders order for Orpheus.network
# if result[3] == 'Orpheus.network':
# finallist = resultlist
if not len(finallist):
logger.info('No appropriate matches found for %s - %s', album['ArtistName'],
album['AlbumTitle'])
@@ -841,19 +842,6 @@ 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:
@@ -1019,7 +1007,10 @@ def send_to_downloader(data, bestqual, album):
# Add torrent
if bestqual[3] == 'rutracker.org':
ruobj.qbittorrent_add_file(data)
if qbittorrent.apiVersion2:
qbittorrent.addFile(data)
else:
ruobj.qbittorrent_add_file(data)
else:
qbittorrent.addTorrent(bestqual[2])
@@ -1038,6 +1029,11 @@ def send_to_downloader(data, bestqual, album):
logger.error('Torrent name could not be determined')
return
# Set Seed Ratio
seed_ratio = get_seed_ratio(bestqual[3])
if seed_ratio is not None:
qbittorrent.setSeedRatio(torrentid, seed_ratio)
myDB = db.DBConnection()
myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']])
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?, ?)',
@@ -1087,8 +1083,12 @@ def send_to_downloader(data, bestqual, album):
slack.notify(name, "Download started")
if headphones.CONFIG.TELEGRAM_ENABLED and headphones.CONFIG.TELEGRAM_ONSNATCH:
logger.info(u"Sending Telegram notification")
from headphones import cache
c = cache.Cache()
album_art = c.get_artwork_from_cache(None, rgid)
telegram = notifiers.TELEGRAM()
telegram.notify(name, "Download started")
message = 'Snatched from ' + provider + '. ' + name
telegram.notify(message, "Snatched: " + title, rgid, image=album_art)
if headphones.CONFIG.TWITTER_ENABLED and headphones.CONFIG.TWITTER_ONSNATCH:
logger.info(u"Sending Twitter notification")
twitter = notifiers.TwitterNotifier()
@@ -1211,7 +1211,7 @@ def verifyresult(title, artistterm, term, lossless):
def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
choose_specific_download=False):
global apolloobj # persistent apollo.rip api object to reduce number of login attempts
global orpheusobj # persistent orpheus.network api object to reduce number of login attempts
global redobj # persistent redacted api object to reduce number of login attempts
global ruobj # and rutracker
@@ -1284,10 +1284,10 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
if headphones.CONFIG.TORZNAB_HOST and headphones.CONFIG.TORZNAB_ENABLED:
torznab_hosts.append((headphones.CONFIG.TORZNAB_HOST, headphones.CONFIG.TORZNAB_APIKEY,
headphones.CONFIG.TORZNAB_ENABLED))
headphones.CONFIG.TORZNAB_RATIO, headphones.CONFIG.TORZNAB_ENABLED))
for torznab_host in headphones.CONFIG.get_extra_torznabs():
if torznab_host[2] == '1' or torznab_host[2] == 1:
if torznab_host[3] == '1' or torznab_host[3] == 1:
torznab_hosts.append(torznab_host)
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
@@ -1353,8 +1353,10 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
else:
logger.info('Skipping %s, could not determine size' % title)
continue
else:
elif item.size:
size = int(item.size.string)
else:
size = int(item.find("torznab:attr", attrs={"name": "size"}).get('value'))
if all(word.lower() in title.lower() for word in term.split()):
if size < maxsize and minimumseeders < seeders:
@@ -1372,66 +1374,6 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
logger.exception(
"An unknown error occurred trying to parse the feed: %s" % e)
if headphones.CONFIG.KAT:
provider = "Kick Ass Torrents"
ka_term = term.replace("!", "")
# Use proxy if specified
if headphones.CONFIG.KAT_PROXY_URL:
providerurl = fix_url(set_proxy(headphones.CONFIG.KAT_PROXY_URL))
else:
providerurl = fix_url("https://katcr.co/new/")
# Build URL
providerurl = providerurl + "/usearch/" + ka_term
# Set max size and category
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
maxsize = 10000000000
providerurl += " category:lossless/"
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
maxsize = 10000000000
providerurl += " category:music/"
else:
maxsize = 300000000
providerurl += " category:music/"
# Requesting content
logger.info("Searching %s using term: %s" % (provider, ka_term))
params = {
"field": "seeders",
"sorder": "desc",
"rss": "1"
}
data = request.request_feed(url=providerurl, params=params,
whitelist_status_code=[404])
# Process feed
if data:
if not len(data.entries):
logger.info("No results found on %s using search term: %s" % (provider, ka_term))
else:
for item in data.entries:
try:
title = item['title']
seeders = item['torrent_seeds']
if headphones.CONFIG.TORRENT_DOWNLOADER == 0:
url = item['links'][1]['href']
else:
url = item['torrent_magneturi']
size = int(item['links'][1]['length'])
if size < maxsize and minimumseeders < int(seeders):
resultlist.append((title, size, url, provider, 'torrent', True))
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
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, int(seeders))
except Exception as e:
logger.exception("Unhandled exception in the KAT parser")
if headphones.CONFIG.WAFFLES:
provider = "Waffles.ch"
providerurl = fix_url("https://waffles.ch/browse.php")
@@ -1534,9 +1476,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
if rulist:
resultlist.extend(rulist)
if headphones.CONFIG.APOLLO:
provider = "Apollo.rip"
providerurl = "http://apollo.rip/"
if headphones.CONFIG.ORPHEUS:
provider = "Orpheus.network"
providerurl = "http://orpheus.network/"
bitrate = None
bitrate_string = bitrate
@@ -1559,7 +1501,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 Apollo.rip filters, so not using it as a search parameter.")
u"Your preferred bitrate is not one of the available Orpheus.network 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]
@@ -1568,22 +1510,24 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
search_formats = [gazelleformat.MP3]
maxsize = 300000000
if not apolloobj or not apolloobj.logged_in():
if not orpheusobj or not orpheusobj.logged_in():
try:
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()
logger.info(u"Attempting to log in to Orpheus.network...")
orpheusobj = gazelleapi.GazelleAPI(headphones.CONFIG.ORPHEUS_USERNAME,
headphones.CONFIG.ORPHEUS_PASSWORD,
headphones.CONFIG.ORPHEUS_URL)
orpheusobj._login()
except Exception as e:
apolloobj = None
logger.error(u"Apollo.rip credentials incorrect or site is down. Error: %s %s" % (
orpheusobj = None
logger.error(u"Orpheus.network credentials incorrect or site is down. Error: %s %s" % (
e.__class__.__name__, str(e)))
if apolloobj and apolloobj.logged_in():
if orpheusobj and orpheusobj.logged_in():
logger.info(u"Searching %s..." % provider)
all_torrents = []
album_type = ""
# Specify release types to filter by
if album['Type'] == 'Album':
album_type = [gazellerelease_type.ALBUM]
@@ -1610,14 +1554,16 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
album_type = [gazellerelease_type.INTERVIEW]
if album['Type'] == 'Mixtape/Street':
album_type = [gazellerelease_type.MIXTAPE]
if album['Type'] == 'Other':
album_type = [gazellerelease_type.UNKNOWN]
for search_format in search_formats:
if usersearchterm:
all_torrents.extend(
apolloobj.search_torrents(searchstr=usersearchterm, format=search_format,
orpheusobj.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,
all_torrents.extend(orpheusobj.search_torrents(artistname=semi_clean_artist_term,
groupname=semi_clean_album_term,
format=search_format,
encoding=bitrate_string,
@@ -1637,13 +1583,11 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
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)
logger.info('Sorting torrents by number of seeders...')
match_torrents.sort(key=lambda x: int(x.seeders), 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)
logger.info('Sorting torrents by seeders...')
match_torrents.sort(key=lambda x: int(x.seeders), 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
@@ -1658,7 +1602,7 @@ 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,
apolloobj.generate_torrent_link(torrent.id),
orpheusobj.generate_torrent_link(torrent.id),
provider,
'torrent', True))
@@ -1756,9 +1700,10 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
for torrent in match_torrents:
if not torrent.file_path:
torrent.group.update_group_data() # will load the file_path for the individual torrents
use_token = headphones.CONFIG.REDACTED_USE_FLTOKEN and torrent.can_use_token
resultlist.append((torrent.file_path,
torrent.size,
redobj.generate_torrent_link(torrent.id),
redobj.generate_torrent_link(torrent.id, use_token),
provider,
'torrent', True))
@@ -1804,7 +1749,6 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
for item in rows:
try:
url = None
rightformat = True
title = ''.join(item.find("a", {"class": "detLink"}))
seeds = int(''.join(item.find("td", {"align": "right"})))
@@ -1873,7 +1817,6 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
try:
links = item.select("td.title-row a")
rightformat = True
title = links[1].text
seeds = int(item.select("td.seeders-row")[0].text)
url = links[0][
@@ -1895,184 +1838,13 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
logger.error(
u"An unknown error occurred in the Old Pirate Bay parser: %s" % e)
# Strike
if headphones.CONFIG.STRIKE:
provider = "Strike"
s_term = term.replace("!", "")
providerurl = fix_url("https://getstrike.net/api/v2/torrents/search/?phrase=")
providerurl = providerurl + s_term + "&category=Music"
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
providerurl = providerurl + "&subcategory=Lossless"
maxsize = 10000000000
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
maxsize = 10000000000
else:
maxsize = 300000000
logger.info("Searching %s using term: %s" % (provider, s_term))
data = request.request_json(url=providerurl,
whitelist_status_code=[404])
if not data or not data.get('torrents'):
logger.info("No results found on %s using search term: %s" % (provider, s_term))
else:
for item in data['torrents']:
try:
title = item['torrent_title']
seeders = item['seeds']
url = item['magnet_uri']
size = int(item['size'])
if size < maxsize and minimumseeders < int(seeders):
resultlist.append((title, size, url, provider, 'torrent', True))
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
else:
logger.info(
'%s is larger than the maxsize, the wrong format or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %d)',
title, size, int(seeders))
except Exception as e:
logger.exception("Unhandled exception in the Strike parser")
# Mininova
if headphones.CONFIG.MININOVA:
provider = "Mininova"
providerurl = fix_url("http://www.mininova.org/rss/" + term + "/5")
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
# categories = "7" #music
format = "2" # flac
maxsize = 10000000000
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
# categories = "7" #music
format = "10" # mp3+flac
maxsize = 10000000000
else:
# categories = "7" #music
format = "8" # mp3
maxsize = 300000000
# Requesting content
logger.info('Parsing results from Mininova')
data = request.request_feed(
url=providerurl,
timeout=20
)
# Process feed
if data:
if not len(data.entries):
logger.info(u"No results found from %s for %s" % (provider, term))
else:
for item in data.entries:
try:
rightformat = True
title = item.title
sxstart = item.description.find("Ratio: ") + 7
seeds = ""
while item.description[sxstart:sxstart + 1] != " ":
seeds = seeds + item.description[sxstart:sxstart + 1]
sxstart = sxstart + 1
url = item.links[1]['url']
size = int(item.links[1]['length'])
if format == "2":
torrent = request.request_content(url)
if not torrent or (int(torrent.find(".mp3")) > 0 and int(
torrent.find(".flac")) < 1):
rightformat = False
if rightformat and size < maxsize and minimumseeders < seeds:
match = True
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
else:
match = False
logger.info('%s is larger than the maxsize, the wrong format or has too little seeders'
' for this category, skipping. (Size: %i bytes, Seeders: %i, Format: %s)' % (
title, size, int(seeds), rightformat))
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"
# this should be less of an issue when it isn't a self-titled album so we'll only check vs artist
results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)]
# Additional filtering for size etc
# if results and not choose_specific_download and result[3] != 'Orpheus.network':
if results and not choose_specific_download:
results = more_filtering(results, album, albumlength, new)
@@ -2086,30 +1858,47 @@ def preprocess(resultlist):
for result in resultlist:
if result[4] == 'torrent':
headers = {}
# rutracker always needs the torrent data
if result[3] == 'rutracker.org':
return ruobj.get_torrent_data(result[2]), result
# Get out of here if we're using Transmission
if headphones.CONFIG.TORRENT_DOWNLOADER == 1: # if not a magnet link still need the .torrent to generate hash... uTorrent support labeling
# Jackett sometimes redirects
jackett_content = None
if result[3].startswith('Jackett_') or 'torznab' in result[3].lower():
r = request.request_response(url=result[2], headers=headers, allow_redirects=False)
if r:
jackett_content = r.content
link = r.headers.get('Location')
if link and link != result[2]:
if link.startswith('magnet:'):
result = (result[0], result[1], link, result[3], "magnet", result[5])
return "d10:magnet-uri%d:%se" % (len(link), link), result
else:
result = (result[0], result[1], link, result[3], result[4], result[5])
return True, result
# Get out of here if we're using Transmission or Deluge
# if not a magnet link still need the .torrent to generate hash... uTorrent support labeling
if headphones.CONFIG.TORRENT_DOWNLOADER in [1, 3]:
return True, result
# Get out of here if it's a magnet link
if result[2].lower().startswith("magnet:"):
return True, result
# Download the torrent file
headers = {}
if result[3] == 'Kick Ass Torrents':
headers['Referer'] = 'https://torcache.net/'
headers['User-Agent'] = USER_AGENT
elif result[3] == 'Apollo.rip':
if result[3] == 'Orpheus.network':
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[
'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'] = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
elif jackett_content:
return jackett_content, result
return request.request_content(url=result[2], headers=headers), result
if result[4] == 'magnet':
+1
View File
@@ -81,6 +81,7 @@ def getFolder(torrentid):
single_file = True
else:
torrent_folder = os.path.split(torrent_files[0]['name'])[0]
torrent_folder = torrent_folder.split(os.sep)[0]
single_file = False
except:
torrent_folder = None
+55 -59
View File
@@ -14,7 +14,6 @@
# 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
@@ -327,7 +326,7 @@ class WebInterface(object):
'$first': firstchar.lower(),
}
folder = helpers.replace_all(folder_format.strip(), values, normalize=True)
folder = helpers.pattern_substitute(folder_format.strip(), values, normalize=True)
folder = helpers.replace_illegal_chars(folder, type="folder")
folder = folder.replace('./', '_/').replace('/.', '/_')
@@ -641,6 +640,7 @@ class WebInterface(object):
'SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=?',
[existing_artist])
update_count = 0
artist_id = None
for entry in have_tracks:
old_clean_filename = entry['CleanName']
if old_clean_filename.startswith(existing_artist_clean):
@@ -649,31 +649,30 @@ class WebInterface(object):
myDB.action(
'UPDATE have SET CleanName=? WHERE ArtistName=? AND CleanName=?',
[new_clean_filename, existing_artist, old_clean_filename])
controlValueDict = {"CleanName": new_clean_filename}
newValueDict = {"Location": entry['Location'],
"BitRate": entry['BitRate'],
"Format": entry['Format']
}
# Attempt to match tracks with new CleanName
match_alltracks = myDB.action(
'SELECT CleanName from alltracks WHERE CleanName=?',
'SELECT CleanName FROM alltracks WHERE CleanName = ?',
[new_clean_filename]).fetchone()
if match_alltracks:
myDB.upsert("alltracks", newValueDict, controlValueDict)
myDB.action(
'UPDATE alltracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ?',
[entry['Location'], entry['BitRate'], entry['Format'], new_clean_filename])
match_tracks = myDB.action(
'SELECT CleanName, AlbumID from tracks WHERE CleanName=?',
'SELECT ArtistID, CleanName, AlbumID FROM tracks WHERE CleanName = ?',
[new_clean_filename]).fetchone()
if match_tracks:
myDB.upsert("tracks", newValueDict, controlValueDict)
myDB.action(
'UPDATE tracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ?',
[entry['Location'], entry['BitRate'], entry['Format'], new_clean_filename])
myDB.action('UPDATE have SET Matched="Manual" WHERE CleanName=?',
[new_clean_filename])
update_count += 1
# This was throwing errors and I don't know why, but it seems to be working fine.
# else:
# logger.info("There was an error modifying Artist %s. This should not have happened" % existing_artist)
artist_id = match_tracks['Artist_ID']
logger.info("Manual matching yielded %s new matches for Artist: %s" % (update_count, new_artist))
if update_count > 0:
librarysync.update_album_status()
if artist_id:
librarysync.update_album_status(ArtistID=artist_id)
else:
logger.info(
"Artist %s already named appropriately; nothing to modify" % existing_artist)
@@ -699,29 +698,28 @@ class WebInterface(object):
'UPDATE have SET CleanName=? WHERE ArtistName=? AND AlbumTitle=? AND CleanName=?',
[new_clean_filename, existing_artist, existing_album,
old_clean_filename])
controlValueDict = {"CleanName": new_clean_filename}
newValueDict = {"Location": entry['Location'],
"BitRate": entry['BitRate'],
"Format": entry['Format']
}
# Attempt to match tracks with new CleanName
match_alltracks = myDB.action(
'SELECT CleanName from alltracks WHERE CleanName=?',
'SELECT CleanName FROM alltracks WHERE CleanName = ?',
[new_clean_filename]).fetchone()
if match_alltracks:
myDB.upsert("alltracks", newValueDict, controlValueDict)
myDB.action(
'UPDATE alltracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ?',
[entry['Location'], entry['BitRate'], entry['Format'], new_clean_filename])
match_tracks = myDB.action(
'SELECT CleanName, AlbumID from tracks WHERE CleanName=?',
'SELECT CleanName, AlbumID FROM tracks WHERE CleanName = ?',
[new_clean_filename]).fetchone()
if match_tracks:
myDB.upsert("tracks", newValueDict, controlValueDict)
myDB.action(
'UPDATE tracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ?',
[entry['Location'], entry['BitRate'], entry['Format'], new_clean_filename])
myDB.action('UPDATE have SET Matched="Manual" WHERE CleanName=?',
[new_clean_filename])
album_id = match_tracks['AlbumID']
update_count += 1
# This was throwing errors and I don't know why, but it seems to be working fine.
# else:
# logger.info("There was an error modifying Artist %s / Album %s with clean name %s" % (existing_artist, existing_album, existing_clean_string))
logger.info("Manual matching yielded %s new matches for Artist: %s / Album: %s" % (
update_count, new_artist, new_album))
if update_count > 0:
@@ -786,25 +784,29 @@ class WebInterface(object):
album = tracks['AlbumTitle']
track_title = tracks['TrackTitle']
if tracks['CleanName'] != original_clean:
artist_id_check = myDB.action('SELECT ArtistID FROM tracks WHERE CleanName = ?',
[tracks['CleanName']]).fetchone()
if artist_id_check:
artist_id = artist_id_check[0]
myDB.action(
'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?',
'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName = ?',
[None, None, None, tracks['CleanName']])
myDB.action(
'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?',
'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE CleanName = ?',
[None, None, None, tracks['CleanName']])
myDB.action(
'UPDATE have SET CleanName=?, Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND TrackTitle=?',
(original_clean, artist, album, track_title))
update_count += 1
if update_count > 0:
librarysync.update_album_status()
librarysync.update_album_status(ArtistID=artist_id)
logger.info("Artist: %s successfully restored to unmatched list" % artist)
elif action == "unmatchAlbum":
artist = existing_artist
album = existing_album
update_clean = myDB.select(
'SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have WHERE ArtistName=? AND AlbumTitle=?',
'SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched FROM have WHERE ArtistName=? AND AlbumTitle=?',
(artist, album))
update_count = 0
for tracks in update_clean:
@@ -813,15 +815,15 @@ class WebInterface(object):
'TrackTitle']).lower()
track_title = tracks['TrackTitle']
if tracks['CleanName'] != original_clean:
album_id_check = myDB.action('SELECT AlbumID from tracks WHERE CleanName=?',
album_id_check = myDB.action('SELECT AlbumID FROM tracks WHERE CleanName = ?',
[tracks['CleanName']]).fetchone()
if album_id_check:
album_id = album_id_check[0]
myDB.action(
'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?',
'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName = ?',
[None, None, None, tracks['CleanName']])
myDB.action(
'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?',
'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE CleanName = ?',
[None, None, None, tracks['CleanName']])
myDB.action(
'UPDATE have SET CleanName=?, Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND TrackTitle=?',
@@ -1160,7 +1162,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,
"mb_ignore_age_missing": checked(headphones.CONFIG.MB_IGNORE_AGE_MISSING),
"search_interval": headphones.CONFIG.SEARCH_INTERVAL,
"libraryscan_interval": headphones.CONFIG.LIBRARYSCAN_INTERVAL,
"sab_host": headphones.CONFIG.SAB_HOST,
@@ -1211,6 +1213,7 @@ class WebInterface(object):
"use_torznab": checked(headphones.CONFIG.TORZNAB),
"torznab_host": headphones.CONFIG.TORZNAB_HOST,
"torznab_apikey": headphones.CONFIG.TORZNAB_APIKEY,
"torznab_ratio": headphones.CONFIG.TORZNAB_RATIO,
"torznab_enabled": checked(headphones.CONFIG.TORZNAB_ENABLED),
"extra_torznabs": headphones.CONFIG.get_extra_torznabs(),
"use_nzbsorg": checked(headphones.CONFIG.NZBSORG),
@@ -1226,17 +1229,12 @@ class WebInterface(object):
"torrentblackhole_dir": headphones.CONFIG.TORRENTBLACKHOLE_DIR,
"download_torrent_dir": headphones.CONFIG.DOWNLOAD_TORRENT_DIR,
"numberofseeders": headphones.CONFIG.NUMBEROFSEEDERS,
"use_kat": checked(headphones.CONFIG.KAT),
"kat_proxy_url": headphones.CONFIG.KAT_PROXY_URL,
"kat_ratio": headphones.CONFIG.KAT_RATIO,
"use_piratebay": checked(headphones.CONFIG.PIRATEBAY),
"piratebay_proxy_url": headphones.CONFIG.PIRATEBAY_PROXY_URL,
"piratebay_ratio": headphones.CONFIG.PIRATEBAY_RATIO,
"use_oldpiratebay": checked(headphones.CONFIG.OLDPIRATEBAY),
"oldpiratebay_url": headphones.CONFIG.OLDPIRATEBAY_URL,
"oldpiratebay_ratio": headphones.CONFIG.OLDPIRATEBAY_RATIO,
"use_mininova": checked(headphones.CONFIG.MININOVA),
"mininova_ratio": headphones.CONFIG.MININOVA_RATIO,
"use_waffles": checked(headphones.CONFIG.WAFFLES),
"waffles_uid": headphones.CONFIG.WAFFLES_UID,
"waffles_passkey": headphones.CONFIG.WAFFLES_PASSKEY,
@@ -1246,20 +1244,16 @@ class WebInterface(object):
"rutracker_password": headphones.CONFIG.RUTRACKER_PASSWORD,
"rutracker_ratio": headphones.CONFIG.RUTRACKER_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_orpheus": checked(headphones.CONFIG.ORPHEUS),
"orpheus_username": headphones.CONFIG.ORPHEUS_USERNAME,
"orpheus_password": headphones.CONFIG.ORPHEUS_PASSWORD,
"orpheus_ratio": headphones.CONFIG.ORPHEUS_RATIO,
"orpheus_url": headphones.CONFIG.ORPHEUS_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,
"redacted_use_fltoken": checked(headphones.CONFIG.REDACTED_USE_FLTOKEN),
"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),
@@ -1466,9 +1460,9 @@ class WebInterface(object):
checked_configs = [
"launch_browser", "enable_https", "api_enabled", "use_blackhole", "headphones_indexer",
"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_apollo", "use_redacted", "use_strike", "use_tquattrecentonze", "preferred_bitrate_allow_lossless",
"use_nzbsorg", "use_omgwtfnzbs", "use_piratebay", "use_oldpiratebay",
"use_waffles", "use_rutracker",
"use_orpheus", "use_redacted", "redacted_use_fltoken", "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",
@@ -1476,7 +1470,7 @@ class WebInterface(object):
"include_extras", "official_releases_only",
"wait_until_release_date", "autowant_upcoming", "autowant_all",
"autowant_manually_added", "do_not_process_unmatched", "keep_torrent_files",
"music_encoder",
"music_encoder", "mb_ignore_age_missing",
"encoderlossless", "encoder_multicore", "delete_lossless_files", "growl_enabled",
"growl_onsnatch", "prowl_enabled",
"prowl_onsnatch", "xbmc_enabled", "xbmc_update", "xbmc_notify", "lms_enabled",
@@ -1543,13 +1537,15 @@ class WebInterface(object):
if len(torznab_number):
torznab_api_key = 'torznab_api' + torznab_number
torznab_enabled_key = 'torznab_enabled' + torznab_number
torznab_ratio_key = 'torznab_ratio' + torznab_number
torznab_host = kwargs.get(torznab_host_key, '')
torznab_api = kwargs.get(torznab_api_key, '')
torznab_enabled = int(kwargs.get(torznab_enabled_key, 0))
for key in [torznab_host_key, torznab_api_key, torznab_enabled_key]:
torznab_ratio = kwargs.get(torznab_ratio_key, '')
for key in [torznab_host_key, torznab_api_key, torznab_enabled_key, torznab_ratio_key]:
if key in kwargs:
del kwargs[key]
extra_torznabs.append((torznab_host, torznab_api, torznab_enabled))
extra_torznabs.append((torznab_host, torznab_api, torznab_ratio, torznab_enabled))
# Convert the extras to list then string. Coming in as 0 or 1 (append new extras to the end)
temp_extras_list = []
@@ -1743,9 +1739,9 @@ class WebInterface(object):
@cherrypy.expose
def testPlex(self):
logger.info(u"Testing plex notifications")
logger.info(u"Testing plex update")
plex = notifiers.Plex()
plex.notify("hellooooo", "test album!", "")
plex.update()
@cherrypy.expose
def testPushbullet(self):
+1 -1
View File
@@ -145,7 +145,7 @@ class _GNTPBase(object):
pointerEnd = pointerStart + dataLength
data = self.raw[pointerStart:pointerEnd]
if not len(data) == dataLength:
raise errors.ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data)))
raise errors.ParseError('INVALID_DATA_LENGTH Expected: %s Received %s' % (dataLength, len(data)))
return data
def _validate_password(self, password):
+6 -5
View File
@@ -401,14 +401,15 @@ class GazelleAPI(object):
return {'curr_page': curr_page, 'pages': pages, 'results': matching_torrents}
def generate_torrent_link(self, id):
url = "%storrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s" %\
(self.site, id, self.logged_in_user.authkey, self.logged_in_user.passkey)
def generate_torrent_link(self, id, use_token=False):
url = "%storrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s&usetoken=%d" %\
(self.site, id, self.logged_in_user.authkey, self.logged_in_user.passkey, use_token)
return url
def save_torrent_file(self, id, dest):
def save_torrent_file(self, id, dest, use_token=False):
file_data = self.unparsed_request("torrents.php", 'download',
id=id, authkey=self.logged_in_user.authkey, torrent_pass=self.logged_in_user.passkey)
id=id, authkey=self.logged_in_user.authkey, torrent_pass=self.logged_in_user.passkey,
usetoken=int(use_token))
with open(dest, 'w+') as dest_file:
dest_file.write(file_data)
+1
View File
@@ -152,6 +152,7 @@ class Torrent(object):
self.snatched = search_torrent_json_response['snatches']
self.free_torrent = search_torrent_json_response['isFreeleech'] or search_torrent_json_response['isPersonalFreeleech']
self.time = search_torrent_json_response['time']
self.can_use_token = search_torrent_json_response.get('canUseToken', False)
def set_torrent_top_10_data(self, top_10_json_response):
if self.id != top_10_json_response['torrentId']:
+1
View File
@@ -0,0 +1 @@
from qbittorrentv2.client import Client
+800
View File
@@ -0,0 +1,800 @@
import requests
import json
class LoginRequired(Exception):
def __str__(self):
return 'Please login first.'
class Client(object):
"""class to interact with qBittorrent WEB API"""
def __init__(self, url, verify=True):
"""
Initialize the client
:param url: Base URL of the qBittorrent WEB API
:param verify: Boolean to specify if SSL verification should be done.
Defaults to True.
"""
if not url.endswith('/'):
url += '/'
self.url = url + 'api/v2/'
self.verify = verify
session = requests.Session()
prefs_url = self.url + 'app/preferences'
check_prefs = session.get(prefs_url, verify=self.verify)
if check_prefs.status_code == 200:
self._is_authenticated = True
self.session = session
elif check_prefs.status_code == 404:
self._is_authenticated = False
raise RuntimeError("""
This wrapper only supports qBittorrent applications
with version higher than 4.1.x.
Please use the latest qBittorrent release.
""")
else:
self._is_authenticated = False
def _get(self, endpoint, **kwargs):
"""
Method to perform GET request on the API.
:param endpoint: Endpoint of the API.
:param kwargs: Other keyword arguments for requests.
:return: Response of the GET request.
"""
return self._request(endpoint, 'get', **kwargs)
def _post(self, endpoint, data, **kwargs):
"""
Method to perform POST request on the API.
:param endpoint: Endpoint of the API.
:param data: POST DATA for the request.
:param kwargs: Other keyword arguments for requests.
:return: Response of the POST request.
"""
return self._request(endpoint, 'post', data, **kwargs)
def _request(self, endpoint, method, data=None, **kwargs):
"""
Method to hanle both GET and POST requests.
:param endpoint: Endpoint of the API.
:param method: Method of HTTP request.
:param data: POST DATA for the request.
:param kwargs: Other keyword arguments.
:return: Response for the request.
"""
final_url = self.url + endpoint
if not self._is_authenticated:
raise LoginRequired
kwargs['verify'] = self.verify
if method == 'get':
request = self.session.get(final_url, **kwargs)
else:
request = self.session.post(final_url, data, **kwargs)
request.raise_for_status()
request.encoding = 'utf_8'
if len(request.text) == 0:
data = json.loads('{}')
else:
try:
data = json.loads(request.text)
except ValueError:
data = request.text
return data
def login(self, username='admin', password='admin'):
"""
Method to authenticate the qBittorrent Client.
Declares a class attribute named ``session`` which
stores the authenticated session if the login is correct.
Else, shows the login error.
:param username: Username.
:param password: Password.
:return: Response to login request to the API.
"""
self.session = requests.Session()
login = self.session.post(self.url + 'auth/login',
data={'username': username,
'password': password},
verify=self.verify)
if login.text == 'Ok.':
self._is_authenticated = True
else:
return login.text
def logout(self):
"""
Logout the current session.
"""
response = self._get('auth/logout')
self._is_authenticated = False
return response
@property
def qbittorrent_version(self):
"""
Get qBittorrent version.
"""
return self._get('app/version')
@property
def api_version(self):
"""
Get WEB API version.
"""
return self._get('app/webapiVersion')
def shutdown(self):
"""
Shutdown qBittorrent.
"""
return self._get('app/shutdown')
def get_default_save_path(self):
"""
Get default save path.
"""
return self._get('app/defaultSavePath')
def get_log(self, **params):
"""
Returns a list of log entries matching the supplied params.
:param normal: Include normal messages (default: true).
:param info: Include info messages (default: true).
:param warning: Include warning messages (default: true).
:param critical: Include critical messages (default: true).
:param last_known_id: Exclude messages with "message id" <= last_known_id (default: -1).
:return: list().
For example: qb.get_log(normal='true', info='true')
"""
return self._get('log/main', params=params)
def torrents(self, **filters):
"""
Returns a list of torrents matching the supplied filters.
:param filter: Current status of the torrents.
:param category: Fetch all torrents with the supplied label.
:param sort: Sort torrents by.
:param reverse: Enable reverse sorting.
:param limit: Limit the number of torrents returned.
:param offset: Set offset (if less than 0, offset from end).
:return: list() of torrent with matching filter.
For example: qb.torrents(filter='downloading', sort='ratio').
"""
params = {}
for name, value in filters.items():
# make sure that old 'status' argument still works
name = 'filter' if name == 'status' else name
params[name] = value
return self._get('torrents/info', params=params)
def get_torrent(self, infohash):
"""
Get details of the torrent.
:param infohash: INFO HASH of the torrent.
"""
return self._get('torrents/properties?hash=' + infohash.lower())
def get_torrent_trackers(self, infohash):
"""
Get trackers for the torrent.
:param infohash: INFO HASH of the torrent.
"""
return self._get('torrents/trackers?hash=' + infohash.lower())
def get_torrent_webseeds(self, infohash):
"""
Get webseeds for the torrent.
:param infohash: INFO HASH of the torrent.
"""
return self._get('torrents/webseeds?hash=' + infohash.lower())
def get_torrent_files(self, infohash):
"""
Get list of files for the torrent.
:param infohash: INFO HASH of the torrent.
"""
return self._get('torrents/files?hash=' + infohash.lower())
def get_torrent_piece_states(self, infohash):
"""
Get list of all pieces (in order) of a specific torrent.
:param infohash: INFO HASH of the torrent.
:return: array of states (integers).
"""
return self._get('torrents/pieceStates?hash=' + infohash.lower())
def get_torrent_piece_hashes(self, infohash):
"""
Get list of all hashes (in order) of a specific torrent.
:param infohash: INFO HASH of the torrent.
:return: array of hashes (strings).
"""
return self._get('torrents/pieceHashes?hash=' + infohash.lower())
@property
def global_transfer_info(self):
"""
:return: dict{} of the global transfer info of qBittorrent.
"""
return self._get('transfer/info')
@property
def preferences(self):
"""
Get the current qBittorrent preferences.
Can also be used to assign individual preferences.
For setting multiple preferences at once,
see ``set_preferences`` method.
Note: Even if this is a ``property``,
to fetch the current preferences dict, you are required
to call it like a bound method.
Wrong::
qb.preferences
Right::
qb.preferences()
"""
prefs = self._get('app/preferences')
class Proxy(Client):
"""
Proxy class to to allow assignment of individual preferences.
this class overrides some methods to ease things.
Because of this, settings can be assigned like::
In [5]: prefs = qb.preferences()
In [6]: prefs['autorun_enabled']
Out[6]: True
In [7]: prefs['autorun_enabled'] = False
In [8]: prefs['autorun_enabled']
Out[8]: False
"""
def __init__(self, url, prefs, auth, session):
self.url = url
self.prefs = prefs
self._is_authenticated = auth
self.session = session
def __getitem__(self, key):
return self.prefs[key]
def __setitem__(self, key, value):
kwargs = {key: value}
return self.set_preferences(**kwargs)
def __call__(self):
return self.prefs
return Proxy(self.url, prefs, self._is_authenticated, self.session)
def sync_main_data(self, rid=0):
"""
Sync the torrents main data by supplied LAST RESPONSE ID.
Read more @ https://git.io/fxgB8
:param rid: Response ID of last request.
"""
return self._get('sync/maindata', params={'rid': rid})
def sync_peers_data(self, infohash, rid=0):
"""
Sync the torrent peers data by supplied LAST RESPONSE ID.
Read more @ https://git.io/fxgBg
:param infohash: INFO HASH of torrent.
:param rid: Response ID of last request.
"""
params = {'hash': infohash.lower(), 'rid': rid}
return self._get('sync/torrentPeers', params=params)
def download_from_link(self, link, **kwargs):
"""
Download torrent using a link.
:param link: URL Link or list of.
:param savepath: Path to download the torrent.
:param category: Label or Category of the torrent(s).
:return: Empty JSON data.
"""
# old:new format
old_arg_map = {'save_path': 'savepath'} # , 'label': 'category'}
# convert old option names to new option names
options = kwargs.copy()
for old_arg, new_arg in old_arg_map.items():
if options.get(old_arg) and not options.get(new_arg):
options[new_arg] = options[old_arg]
if isinstance(link, list):
options['urls'] = "\n".join(link)
else:
options['urls'] = link
# workaround to send multipart/formdata request
# http://stackoverflow.com/a/23131823/4726598
dummy_file = {'_dummy': (None, '_dummy')}
return self._post('torrents/add', data=options, files=dummy_file)
def download_from_file(self, file_buffer, **kwargs):
"""
Download torrent using a file.
:param file_buffer: Single file() buffer or list of.
:param save_path: Path to download the torrent.
:param label: Label of the torrent(s).
:return: Empty JSON data.
"""
if isinstance(file_buffer, list):
torrent_files = {}
for i, f in enumerate(file_buffer):
torrent_files.update({'torrents%s' % i: f})
else:
torrent_files = {'torrents': file_buffer}
data = kwargs.copy()
if data.get('save_path'):
data.update({'savepath': data['save_path']})
return self._post('torrents/add', data=data, files=torrent_files)
def add_trackers(self, infohash, trackers):
"""
Add trackers to a torrent.
:param infohash: INFO HASH of torrent.
:param trackers: Trackers.
:note %0A (aka LF newline) between trackers. Ampersand in tracker urls MUST be escaped.
"""
data = {'hash': infohash.lower(),
'urls': trackers}
return self._post('torrents/addTrackers', data=data)
def set_torrent_location(self, infohash_list, location):
"""
Set the location for the torrent
:param infohash: INFO HASH of torrent.
:param location: /mnt/nfs/media.
"""
data = self._process_infohash_list(infohash_list)
data['location'] = location
return self._post('torrents/setLocation', data=data)
def set_torrent_name(self, infohash, name):
"""
Set the name for the torrent
:param infohash: INFO HASH of torrent.
:param name: Whatever_name_you_want.
"""
data = {'hash': infohash.lower(),
'name': name}
return self._post('torrents/rename', data=data)
@staticmethod
def _process_infohash_list(infohash_list):
"""
Method to convert the infohash_list to qBittorrent API friendly values.
:param infohash_list: List of infohash.
"""
if isinstance(infohash_list, list):
data = {'hashes': '|'.join([h.lower() for h in infohash_list])}
else:
data = {'hashes': infohash_list.lower()}
return data
def pause(self, infohash):
"""
Pause a torrent.
:param infohash: INFO HASH of torrent.
"""
return self._post('torrents/pause', data={'hashes': infohash.lower()})
def pause_all(self):
"""
Pause all torrents.
"""
return self._post('torrents/pause', data={'hashes': 'all'})
def pause_multiple(self, infohash_list):
"""
Pause multiple torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self._process_infohash_list(infohash_list)
return self._post('torrents/pause', data=data)
def set_category(self, infohash_list, category):
"""
Set the category on multiple torrents.
The category must exist before using set_category. As of v2.1.0,the API
returns a 409 Client Error for any valid category name that doesn't
already exist.
:param infohash_list: Single or list() of infohashes.
:param category: If category is set to empty string '',
the torrent(s) specified is/are removed from all categories.
"""
data = self._process_infohash_list(infohash_list)
data['category'] = category
return self._post('torrents/setCategory', data=data)
def create_category(self, category):
"""
Create a new category
:param category: category to create
"""
return self._post('torrents/createCategory', data={'category': category.lower()})
def remove_category(self, categories):
"""
Remove categories
:param categories: can contain multiple cateogies separated by \n (%0A urlencoded).
"""
return self._post('torrents/removeCategories', data={'categories': categories})
def resume(self, infohash):
"""
Resume a paused torrent.
:param infohash: INFO HASH of torrent.
"""
return self._post('torrents/resume', data={'hashes': infohash.lower()})
def resume_all(self):
"""
Resume all torrents.
"""
return self._post('torrents/resume', data={'hashes': 'all'})
def resume_multiple(self, infohash_list):
"""
Resume multiple paused torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self._process_infohash_list(infohash_list)
return self._post('torrents/resume', data=data)
def delete(self, infohash_list):
"""
Delete torrents. Does not remove files.
:param infohash_list: Single or list() of infohashes.
"""
return self._delete(infohash_list)
def delete_all(self):
"""
Delete all torrents. Does not remove files.
"""
return self._delete('all')
def delete_permanently(self, infohash_list):
"""
Permanently delete torrents. Removes files.
:param infohash_list: Single or list() of infohashes.
"""
return self._delete(infohash_list, True)
def delete_all_permanently(self):
"""
Permanently delete torrents.
"""
return self._delete('all', True)
def _delete(self, infohash_list, delete_files=False):
"""
Delete torrents.
:param infohash_list: Single or list() of infohashes.
:param delete_files: Whether to delete files along with torrent.
"""
data = self._process_infohash_list(infohash_list)
data['deleteFiles'] = json.dumps(delete_files)
return self._post('torrents/delete', data=data)
def recheck(self, infohash_list):
"""
Recheck torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self._process_infohash_list(infohash_list)
return self._post('torrents/recheck', data=data)
def recheck_all(self):
"""
Recheck all torrents.
"""
return self._post('torrents/recheck', data={'hashes': 'all'})
def reannounce(self, infohash_list):
"""
Recheck all torrents.
:param infohash_list: Single or list() of infohashes; pass 'all' for all torrents.
"""
data = self._process_infohash_list(infohash_list)
return self._post('torrents/reannounce', data=data)
def increase_priority(self, infohash_list):
"""
Increase priority of torrents.
:param infohash_list: Single or list() of infohashes; pass 'all' for all torrents.
"""
data = self._process_infohash_list(infohash_list)
return self._post('torrents/increasePrio', data=data)
def decrease_priority(self, infohash_list):
"""
Decrease priority of torrents.
:param infohash_list: Single or list() of infohashes; pass 'all' for all torrents.
"""
data = self._process_infohash_list(infohash_list)
return self._post('torrents/decreasePrio', data=data)
def set_max_priority(self, infohash_list):
"""
Set torrents to maximum priority level.
:param infohash_list: Single or list() of infohashes; pass 'all' for all torrents.
"""
data = self._process_infohash_list(infohash_list)
return self._post('torrents/topPrio', data=data)
def set_min_priority(self, infohash_list):
"""
Set torrents to minimum priority level.
:param infohash_list: Single or list() of infohashes; pass 'all' for all torrents.
"""
data = self._process_infohash_list(infohash_list)
return self._post('torrents/bottomPrio', data=data)
def set_file_priority(self, infohash, file_id, priority):
"""
Set file of a torrent to a supplied priority level.
:param infohash: INFO HASH of torrent.
:param file_id: ID of the file to set priority.
:param priority: Priority level of the file.
:note priority 4 is no priority set
"""
if priority not in [0, 1, 2, 4, 7]:
raise ValueError("Invalid priority, refer WEB-UI docs for info.")
elif not isinstance(file_id, int):
raise TypeError("File ID must be an int")
data = {'hash': infohash.lower(),
'id': file_id,
'priority': priority}
return self._post('torrents/filePrio', data=data)
def set_automatic_torrent_management(self, infohash_list, enable='false'):
"""
Set the category on multiple torrents.
:param infohash_list: Single or list() of infohashes.
:param enable: is a boolean, affects the torrents listed in infohash_list, default is 'false'
"""
data = self._process_infohash_list(infohash_list)
data['enable'] = enable
return self._post('torrents/setAutoManagement', data=data)
# Get-set global download and upload speed limits.
def get_global_download_limit(self):
"""
Get global download speed limit.
"""
return self._get('transfer/downloadLimit')
def set_global_download_limit(self, limit):
"""
Set global download speed limit.
:param limit: Speed limit in bytes.
"""
return self._post('transfer/setDownloadLimit', data={'limit': limit})
global_download_limit = property(get_global_download_limit,
set_global_download_limit)
def get_global_upload_limit(self):
"""
Get global upload speed limit.
"""
return self._get('transfer/uploadLimit')
def set_global_upload_limit(self, limit):
"""
Set global upload speed limit.
:param limit: Speed limit in bytes.
"""
return self._post('transfer/setUploadLimit', data={'limit': limit})
global_upload_limit = property(get_global_upload_limit,
set_global_upload_limit)
# Get-set download and upload speed limits of the torrents.
def get_torrent_download_limit(self, infohash_list):
"""
Get download speed limit of the supplied torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self._process_infohash_list(infohash_list)
return self._post('torrents/downloadLimit', data=data)
def set_torrent_download_limit(self, infohash_list, limit):
"""
Set download speed limit of the supplied torrents.
:param infohash_list: Single or list() of infohashes.
:param limit: Speed limit in bytes.
"""
data = self._process_infohash_list(infohash_list)
data.update({'limit': limit})
return self._post('torrents/setDownloadLimit', data=data)
def get_torrent_upload_limit(self, infohash_list):
"""
Get upoload speed limit of the supplied torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self._process_infohash_list(infohash_list)
return self._post('torrents/uploadLimit', data=data)
def set_torrent_upload_limit(self, infohash_list, limit):
"""
Set upload speed limit of the supplied torrents.
:param infohash_list: Single or list() of infohashes.
:param limit: Speed limit in bytes.
"""
data = self._process_infohash_list(infohash_list)
data.update({'limit': limit})
return self._post('torrents/setUploadLimit', data=data)
# setting preferences
def set_preferences(self, **kwargs):
"""
Set preferences of qBittorrent.
Read all possible preferences @ https://git.io/fx2Y9
:param kwargs: set preferences in kwargs form.
"""
json_data = "json={}".format(json.dumps(kwargs))
headers = {'content-type': 'application/x-www-form-urlencoded'}
return self._post('app/setPreferences', data=json_data,
headers=headers)
def get_alternative_speed_status(self):
"""
Get Alternative speed limits. (1/0)
"""
return self._get('transfer/speedLimitsMode')
alternative_speed_status = property(get_alternative_speed_status)
def toggle_alternative_speed(self):
"""
Toggle alternative speed limits.
"""
return self._get('transfer/toggleSpeedLimitsMode')
def toggle_sequential_download(self, infohash_list):
"""
Toggle sequential download in supplied torrents.
:param infohash_list: Single or list() of infohashes; pass 'all' for all torrents.
"""
data = self._process_infohash_list(infohash_list)
return self._post('torrents/toggleSequentialDownload', data=data)
def toggle_first_last_piece_priority(self, infohash_list):
"""
Toggle first/last piece priority of supplied torrents.
:param infohash_list: Single or list() of infohashes; pass 'all' for all torrents.
"""
data = self._process_infohash_list(infohash_list)
return self._post('torrents/toggleFirstLastPiecePrio', data=data)
def force_start(self, infohash_list, value):
"""
Force start selected torrents.
:param infohash_list: Single or list() of infohashes; pass 'all' for all torrents.
:param value: Force start value (bool)
"""
data = self._process_infohash_list(infohash_list)
data.update({'value': json.dumps(value)})
return self._post('torrents/setForceStart', data=data)
def set_super_seeding(self, infohash_list, value):
"""
Set super seeding for selected torrents.
:param infohash_list: Single or list() of infohashes; pass 'all' for all torrents.
:param value: Force start value (bool)
"""
data = self._process_infohash_list(infohash_list)
data.update({'value': json.dumps(value)})
return self._post('torrents/setSuperSeeding', data=data)
def set_share_ratio(self, infohash_list, ratio_limit=-2, seeding_time_limit=-2):
"""
Set the share ratio limit of the supplied torrents.
:param infohash_list: Single or list() of infohashes.
:param ratio_limit: Ratio limit (optional 2 decimals). -2 means the global limit, -1 means no limit.
:param seeding_time_limit: Time limit in minutes. -2 means the global limit, -1 means no limit.
"""
data = self._process_infohash_list(infohash_list)
data.update({'ratioLimit': ratio_limit,
'seedingTimeLimit': seeding_time_limit})
return self._post('torrents/setShareLimits', data=data)
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
+2574 -3238
View File
File diff suppressed because it is too large Load Diff
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File

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