mirror of
https://github.com/rembo10/headphones.git
synced 2026-09-10 00:32:52 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3199605be | ||
|
|
9a7006ad14 | ||
|
|
38197a5871 | ||
|
|
ee58f62b2b | ||
|
|
58edc604b3 | ||
|
|
ba33a5e655 | ||
|
|
379fd3d0b8 | ||
|
|
374b106522 | ||
|
|
bf74f57535 | ||
|
|
f1a03e5fb6 | ||
|
|
50cbe2e943 | ||
|
|
219f855dfc | ||
|
|
f18334d87c | ||
|
|
72fce3cdeb | ||
|
|
5283b48736 | ||
|
|
507d8aa7d2 | ||
|
|
4bce833c1e | ||
|
|
f7a23bb4d0 | ||
|
|
5d145b6f8a | ||
|
|
3ab31f1da7 | ||
|
|
cf85d94a37 | ||
|
|
a8a53353ee | ||
|
|
476ee7b312 | ||
|
|
14bef1e63d | ||
|
|
dc22bb006d | ||
|
|
b49cd8a813 | ||
|
|
d7275e5a99 | ||
|
|
aa1867cf06 | ||
|
|
19b766a7ce | ||
|
|
b45f3565da | ||
|
|
22525668fd | ||
|
|
af79bc47e8 | ||
|
|
6208fe0e69 | ||
|
|
f66a37abbd | ||
|
|
4b52ae445d | ||
|
|
8a2c3169e2 | ||
|
|
805eb7dd13 | ||
|
|
81486d2cd2 | ||
|
|
d1237b052a | ||
|
|
a4ef757d12 | ||
|
|
bb8eb19dbf | ||
|
|
602e115a29 | ||
|
|
e2c335db2d | ||
|
|
44583d42dd | ||
|
|
43d465ca22 | ||
|
|
bd60935493 | ||
|
|
0264b1fb0d | ||
|
|
24f22834ff | ||
|
|
aed2e63984 | ||
|
|
694c17a40b | ||
|
|
6c34bc46e9 | ||
|
|
0b2600b783 | ||
|
|
693e5b1624 | ||
|
|
287278c049 | ||
|
|
496560909f | ||
|
|
22b1bf36ab | ||
|
|
b506a1797d | ||
|
|
ff8417b410 | ||
|
|
741cb466ef | ||
|
|
a99a6abde2 | ||
|
|
4631b3cbbf | ||
|
|
7230428a31 | ||
|
|
2b5b2bd506 | ||
|
|
99c3e8498d | ||
|
|
472f657c88 | ||
|
|
368e30067b | ||
|
|
053fd40e76 | ||
|
|
7af7f6be1e | ||
|
|
5fd20e50fd | ||
|
|
43df2956fb | ||
|
|
0c96997005 | ||
|
|
81651b119a | ||
|
|
ecad4d39eb | ||
|
|
0d3be4866f | ||
|
|
eeed89811b | ||
|
|
d9bff5dffe | ||
|
|
e3983d615d |
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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); })
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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.")
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
@@ -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':
|
||||
|
||||
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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']:
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
from qbittorrentv2.client import Client
|
||||
Executable
+800
@@ -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
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+2574
-3238
File diff suppressed because it is too large
Load Diff
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user